diff options
author | David Heinemeier Hansson <david@loudthinking.com> | 2005-01-01 19:22:16 +0000 |
---|---|---|
committer | David Heinemeier Hansson <david@loudthinking.com> | 2005-01-01 19:22:16 +0000 |
commit | daf3e92a316c041b6b2bf331c8d2a51e710fe7bf (patch) | |
tree | ca492afaf76db59ec1ab99105c07b54ffb28cc6a /activerecord/lib/active_record | |
parent | a6fefad354d36f3e9a91f8582659ffcda1a35855 (diff) | |
download | rails-daf3e92a316c041b6b2bf331c8d2a51e710fe7bf.tar.gz rails-daf3e92a316c041b6b2bf331c8d2a51e710fe7bf.tar.bz2 rails-daf3e92a316c041b6b2bf331c8d2a51e710fe7bf.zip |
Added a db2 adapter that only depends on the Ruby/DB2 bindings (http://raa.ruby-lang.org/project/ruby-db2/) #386 [Maik Schmidt]. Converted all the fixtures to YAML style ones.
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@303 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activerecord/lib/active_record')
-rwxr-xr-x | activerecord/lib/active_record/associations.rb | 2 | ||||
-rwxr-xr-x | activerecord/lib/active_record/base.rb | 6 | ||||
-rw-r--r-- | activerecord/lib/active_record/connection_adapters/db2_adapter.rb | 127 | ||||
-rwxr-xr-x | activerecord/lib/active_record/fixtures.rb | 2 | ||||
-rw-r--r-- | activerecord/lib/active_record/vendor/db2.rb | 357 |
5 files changed, 489 insertions, 5 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 019de942b9..742491ae48 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -577,4 +577,4 @@ module ActiveRecord end end end -end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 849fb1578b..284f06c694 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -311,7 +311,7 @@ module ActiveRecord #:nodoc: sql << "ORDER BY #{orderings} " unless orderings.nil? connection.add_limit!(sql, 1) - + record = connection.select_one(sql, "#{name} Load First") instantiate(record) unless record.nil? end @@ -919,7 +919,7 @@ module ActiveRecord #:nodoc: # table with a master_id foreign key can instantiate master through Client#master. def method_missing(method_id, *arguments) method_name = method_id.id2name - + if method_name =~ read_method? && @attributes.include?($1) return read_attribute($1) elsif method_name =~ read_untyped_method? && @attributes.include?($1) @@ -959,7 +959,7 @@ module ActiveRecord #:nodoc: # Returns true if the attribute is of a text column and marked for serialization. def unserializable_attribute?(attr_name, column) - @attributes[attr_name] && column.send(:type) == :text && @attributes[attr_name].is_a?(String) && self.class.serialized_attributes[attr_name] + @attributes[attr_name] && [:text, :string].include?(column.send(:type)) && @attributes[attr_name].is_a?(String) && self.class.serialized_attributes[attr_name] end # Returns the unserialized object of the attribute. diff --git a/activerecord/lib/active_record/connection_adapters/db2_adapter.rb b/activerecord/lib/active_record/connection_adapters/db2_adapter.rb new file mode 100644 index 0000000000..b4f4f24ca4 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/db2_adapter.rb @@ -0,0 +1,127 @@ +# db2_adapter.rb +# author: Maik Schmidt <contact@maik-schmidt.de> + +require 'active_record/connection_adapters/abstract_adapter' + +begin + require 'db2/db2cli' unless self.class.const_defined?(:DB2CLI) + require 'active_record/vendor/db2' + + module ActiveRecord + class Base + # Establishes a connection to the database that's used by + # all Active Record objects + def self.db2_connection(config) # :nodoc: + symbolize_strings_in_hash(config) + usr = config[:username] + pwd = config[:password] + + if config.has_key?(:database) + database = config[:database] + else + raise ArgumentError, "No database specified. Missing argument: database." + end + + connection = DB2::Connection.new(DB2::Environment.new) + connection.connect(database, usr, pwd) + ConnectionAdapters::DB2Adapter.new(connection) + end + end + + module ConnectionAdapters + class DB2Adapter < AbstractAdapter # :nodoc: + def select_all(sql, name = nil) + select(sql, name) + end + + def select_one(sql, name = nil) + select(sql, name).first + end + + def insert(sql, name = nil, pk = nil, id_value = nil) + execute(sql, name = nil) + id_value || last_insert_id + end + + def execute(sql, name = nil) + rows_affected = 0 + log(sql, name, @connection) do |connection| + stmt = DB2::Statement.new(connection) + stmt.exec_direct(sql) + rows_affected = stmt.row_count + stmt.free + end + rows_affected + end + + alias_method :update, :execute + alias_method :delete, :execute + + def begin_db_transaction + @connection.set_auto_commit_off + end + + def commit_db_transaction + @connection.commit + @connection.set_auto_commit_on + end + + def rollback_db_transaction + @connection.rollback + @connection.set_auto_commit_on + end + + def quote_column_name(name) name; end + + def quote_string(s) + s.gsub(/'/, "''") # ' (for ruby-mode) + end + + def add_limit!(sql, limit) + sql << " FETCH FIRST #{limit} ROWS ONLY" + end + + def columns(table_name, name = nil) + stmt = DB2::Statement.new(@connection) + result = [] + stmt.columns(table_name.upcase).each do |c| + c_name = c[3].downcase + c_default = c[12] == 'NULL' ? nil : c[12] + c_type = c[5].downcase + c_type += "(#{c[6]})" if !c[6].nil? && c[6] != '' + result << Column.new(c_name, c_default, c_type) + end + stmt.free + result + end + + private + + def last_insert_id + row = select_one(<<-GETID.strip) + with temp(id) as (values (identity_val_local())) select * from temp + GETID + row['id'].to_i + end + + def select(sql, name = nil) + stmt = nil + log(sql, name, @connection) do |connection| + stmt = DB2::Statement.new(connection) + stmt.exec_direct(sql) + end + + rows = [] + while row = stmt.fetch_as_hash + rows << row + end + stmt.free + rows + end + end + end + end +rescue LoadError + retry if require('rubygems') rescue LoadError + # DB2 driver is unavailable. +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 21d0615dfa..1db5d29bd7 100755 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -364,4 +364,4 @@ module Test#:nodoc: end end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/vendor/db2.rb b/activerecord/lib/active_record/vendor/db2.rb new file mode 100644 index 0000000000..f0ec847d72 --- /dev/null +++ b/activerecord/lib/active_record/vendor/db2.rb @@ -0,0 +1,357 @@ +require 'db2/db2cli.rb' + +module DB2 + module DB2Util + include DB2CLI + + def free() SQLFreeHandle(@handle_type, @handle); end + def handle() @handle; end + + def check_rc(rc) + if rc != SQL_SUCCESS and rc != SQL_SUCCESS_WITH_INFO and rc != SQL_NO_DATA_FOUND + rec = 1 + msg = '' + loop do + a = SQLGetDiagRec(@handle_type, @handle, rec, 500) + break if a[0] != SQL_SUCCESS + msg << a[3] if !a[3].nil? and a[3] != '' # Create message. + rec += 1 + end + raise "DB2 error: #{msg}" + end + end + end + + class Environment + include DB2Util + + def initialize + @handle_type = SQL_HANDLE_ENV + rc, @handle = SQLAllocHandle(@handle_type, SQL_NULL_HANDLE) + check_rc(rc) + end + + def data_sources(buffer_length = 1024) + retval = [] + max_buffer_length = buffer_length + + a = SQLDataSources(@handle, SQL_FETCH_FIRST, SQL_MAX_DSN_LENGTH + 1, buffer_length) + retval << [a[1], a[3]] + max_buffer_length = [max_buffer_length, a[4]].max + + loop do + a = SQLDataSources(@handle, SQL_FETCH_NEXT, SQL_MAX_DSN_LENGTH + 1, buffer_length) + break if a[0] == SQL_NO_DATA_FOUND + + retval << [a[1], a[3]] + max_buffer_length = [max_buffer_length, a[4]].max + end + + if max_buffer_length > buffer_length + get_data_sources(max_buffer_length) + else + retval + end + end + end + + class Connection + include DB2Util + + def initialize(environment) + @env = environment + @handle_type = SQL_HANDLE_DBC + rc, @handle = SQLAllocHandle(@handle_type, @env.handle) + check_rc(rc) + end + + def connect(server_name, user_name = '', auth = '') + check_rc(SQLConnect(@handle, server_name, user_name, auth)) + end + + def set_connect_attr(attr, value) + value += "\0" if value.class == String + check_rc(SQLSetConnectAttr(@handle, attr, value)) + end + + def set_auto_commit_on + set_connect_attr(SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_ON) + end + + def set_auto_commit_off + set_connect_attr(SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF) + end + + def disconnect + check_rc(SQLDisconnect(@handle)) + end + + def rollback + check_rc(SQLEndTran(@handle_type, @handle, SQL_ROLLBACK)) + end + + def commit + check_rc(SQLEndTran(@handle_type, @handle, SQL_COMMIT)) + end + end + + class Statement + include DB2Util + + def initialize(connection) + @conn = connection + @handle_type = SQL_HANDLE_STMT + @parms = [] #yun + @sql = '' #yun + @numParms = 0 #yun + @prepared = false #yun + @parmArray = [] #yun. attributes of the parameter markers + rc, @handle = SQLAllocHandle(@handle_type, @conn.handle) + check_rc(rc) + end + + def columns(table_name) + check_rc(SQLColumns(@handle, "", "%", table_name, "%")) + fetch_all + end + + def tables + check_rc(SQLTables(@handle, "", "%", "%", "TABLE")) + fetch_all + end + + def prepare(sql) + @sql = sql + check_rc(SQLPrepare(@handle, sql)) + rc, @numParms = SQLNumParams(@handle) #number of question marks + check_rc(rc) + #-------------------------------------------------------------------------- + # parameter attributes are stored in instance variable @parmArray so that + # they are available when execute method is called. + #-------------------------------------------------------------------------- + if @numParms > 0 # get parameter marker attributes + 1.upto(@numParms) do |i| # parameter number starts from 1 + rc, type, size, decimalDigits = SQLDescribeParam(@handle, i) + check_rc(rc) + @parmArray << Parameter.new(type, size, decimalDigits) + end + end + @prepared = true + self + end + + def execute(*parms) + raise "The statement was not prepared" if @prepared == false + + if parms.size == 1 and parms[0].class == Array + parms = parms[0] + end + + if @numParms != parms.size + raise "Number of parameters supplied does not match with the SQL statement" + end + + if @numParms > 0 #need to bind parameters + #-------------------------------------------------------------------- + #calling bindParms may not be safe. Look comment below. + #-------------------------------------------------------------------- + #bindParms(parms) + + valueArray = [] + 1.upto(@numParms) do |i| # parameter number starts from 1 + type = @parmArray[i - 1].class + size = @parmArray[i - 1].size + decimalDigits = @parmArray[i - 1].decimalDigits + + if parms[i - 1].class == String + valueArray << parms[i - 1] + else + valueArray << parms[i - 1].to_s + end + + rc = SQLBindParameter(@handle, i, type, size, decimalDigits, valueArray[i - 1]) + check_rc(rc) + end + end + + check_rc(SQLExecute(@handle)) + + if @numParms != 0 + check_rc(SQLFreeStmt(@handle, SQL_RESET_PARAMS)) # Reset parameters + end + + self + end + + #------------------------------------------------------------------------------- + # The last argument(value) to SQLBindParameter is a deferred argument, that is, + # it should be available when SQLExecute is called. Even though "value" is + # local to bindParms method, it seems that it is available when SQLExecute + # is called. I am not sure whether it would still work if garbage collection + # is done between bindParms call and SQLExecute call inside the execute method + # above. + #------------------------------------------------------------------------------- + def bindParms(parms) # This is the real thing. It uses SQLBindParms + 1.upto(@numParms) do |i| # parameter number starts from 1 + rc, dataType, parmSize, decimalDigits = SQLDescribeParam(@handle, i) + check_rc(rc) + if parms[i - 1].class == String + value = parms[i - 1] + else + value = parms[i - 1].to_s + end + rc = SQLBindParameter(@handle, i, dataType, parmSize, decimalDigits, value) + check_rc(rc) + end + end + + #------------------------------------------------------------------------------ + # bind method does not use DB2's SQLBindParams, but replaces "?" in the + # SQL statement with the value before passing the SQL statement to DB2. + # It is not efficient and can handle only strings since it puts everything in + # quotes. + #------------------------------------------------------------------------------ + def bind(sql, args) #does not use SQLBindParams + arg_index = 0 + result = "" + tokens(sql).each do |part| + case part + when '?' + result << "'" + (args[arg_index]) + "'" #put it into quotes + arg_index += 1 + when '??' + result << "?" + else + result << part + end + end + if arg_index < args.size + raise "Too many SQL parameters" + elsif arg_index > args.size + raise "Not enough SQL parameters" + end + result + end + + ## Break the sql string into parts. + # + # This is NOT a full lexer for SQL. It just breaks up the SQL + # string enough so that question marks, double question marks and + # quoted strings are separated. This is used when binding + # arguments to "?" in the SQL string. Note: comments are not + # handled. + # + def tokens(sql) + toks = sql.scan(/('([^'\\]|''|\\.)*'|"([^"\\]|""|\\.)*"|\?\??|[^'"?]+)/) + toks.collect { |t| t[0] } + end + + def exec_direct(sql) + check_rc(SQLExecDirect(@handle, sql)) + self + end + + def set_cursor_name(name) + check_rc(SQLSetCursorName(@handle, name)) + self + end + + def get_cursor_name + rc, name = SQLGetCursorName(@handle) + check_rc(rc) + name + end + + def row_count + rc, rowcount = SQLRowCount(@handle) + check_rc(rc) + rowcount + end + + def num_result_cols + rc, cols = SQLNumResultCols(@handle) + check_rc(rc) + cols + end + + def fetch_all + if block_given? + while row = fetch do + yield row + end + else + res = [] + while row = fetch do + res << row + end + res + end + end + + def fetch + cols = get_col_desc + rc = SQLFetch(@handle) + if rc == SQL_NO_DATA_FOUND + SQLFreeStmt(@handle, SQL_CLOSE) # Close cursor + SQLFreeStmt(@handle, SQL_RESET_PARAMS) # Reset parameters + return nil + end + raise "ERROR" unless rc == SQL_SUCCESS + + retval = [] + cols.each_with_index do |c, i| + rc, content = SQLGetData(@handle, i + 1, c[1], c[2] + 1) #yun added 1 to c[2] + retval << adjust_content(content) + end + retval + end + + def fetch_as_hash + cols = get_col_desc + rc = SQLFetch(@handle) + if rc == SQL_NO_DATA_FOUND + SQLFreeStmt(@handle, SQL_CLOSE) # Close cursor + SQLFreeStmt(@handle, SQL_RESET_PARAMS) # Reset parameters + return nil + end + raise "ERROR" unless rc == SQL_SUCCESS + + retval = {} + cols.each_with_index do |c, i| + rc, content = SQLGetData(@handle, i + 1, c[1], c[2] + 1) #yun added 1 to c[2] + retval[c[0]] = adjust_content(content) + end + retval + end + + def get_col_desc + rc, nr_cols = SQLNumResultCols(@handle) + cols = (1..nr_cols).collect do |c| + rc, name, bl, type, col_sz = SQLDescribeCol(@handle, c, 1024) + [name.downcase, type, col_sz] + end + end + + def adjust_content(c) + case c.class.to_s + when 'DB2CLI::NullClass' + return nil + when 'DB2CLI::Time' + "%02d:%02d:%02d" % [c.hour, c.minute, c.second] + when 'DB2CLI::Date' + "%04d-%02d-%02d" % [c.year, c.month, c.day] + when 'DB2CLI::Timestamp' + "%04d-%02d-%02d %02d:%02d:%02d" % [c.year, c.month, c.day, c.hour, c.minute, c.second] + else + return c + end + end + end + + class Parameter + attr_reader :type, :size, :decimalDigits + def initialize(type, size, decimalDigits) + @type, @size, @decimalDigits = type, size, decimalDigits + end + end +end |