ibm_db 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGES +23 -0
- data/LICENSE +18 -0
- data/MANIFEST +14 -0
- data/README +219 -0
- data/ext/extconf.rb +50 -0
- data/ext/ibm_db.c +6259 -0
- data/ext/ruby_ibm_db.h +164 -0
- data/init.rb +42 -0
- data/lib/IBM_DB.rb +2 -0
- data/lib/active_record/connection_adapters/ibm_db_adapter.rb +1053 -0
- data/lib/active_record/vendor/db2-i5-zOS.yaml +328 -0
- data/lib/linux32/ibm_db.so +0 -0
- data/test/activerecord/native_ibm_db/connection.rb +39 -0
- data/test/ibm_db_test.rb +25 -0
- metadata +78 -0
data/ext/ruby_ibm_db.h
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
/*
|
2
|
+
+----------------------------------------------------------------------+
|
3
|
+
| Licensed Materials - Property of IBM |
|
4
|
+
| |
|
5
|
+
| (C) Copyright IBM Corporation 2007. |
|
6
|
+
+----------------------------------------------------------------------+
|
7
|
+
| Authors: Sushant Koduru, Lynh Nguyen, Kanchana Padmanabhan, |
|
8
|
+
| Dan Scott, Helmut Tessarek, Kellen Bombardier, Sam Ruby |
|
9
|
+
+----------------------------------------------------------------------+
|
10
|
+
*/
|
11
|
+
|
12
|
+
#ifndef RUBY_IBM_DB_H
|
13
|
+
#define RUBY_IBM_DB_H
|
14
|
+
|
15
|
+
#include <stdio.h>
|
16
|
+
#include <string.h>
|
17
|
+
#include <stdlib.h>
|
18
|
+
#include <sqlcli1.h>
|
19
|
+
|
20
|
+
#ifndef SQL_XML
|
21
|
+
#define SQL_XML -370
|
22
|
+
#endif
|
23
|
+
|
24
|
+
#ifndef SQL_ATTR_REPLACE_QUOTED_LITERALS
|
25
|
+
#define SQL_ATTR_REPLACE_QUOTED_LITERALS 116
|
26
|
+
#endif
|
27
|
+
|
28
|
+
#ifdef _WIN32
|
29
|
+
#define RUBY_IBM_DB_API __declspec(dllexport)
|
30
|
+
#else
|
31
|
+
#define RUBY_IBM_DB_API
|
32
|
+
#endif
|
33
|
+
|
34
|
+
/* strlen(" SQLCODE=") added in */
|
35
|
+
#define DB2_MAX_ERR_MSG_LEN (SQL_MAX_MESSAGE_LENGTH + SQL_SQLSTATE_SIZE + 10)
|
36
|
+
|
37
|
+
/* Used in _ruby_parse_options */
|
38
|
+
#define DB2_ERRMSG 1
|
39
|
+
#define DB2_ERR 2
|
40
|
+
|
41
|
+
/* DB2 instance environment variable */
|
42
|
+
#define DB2_VAR_INSTANCE "DB2INSTANCE="
|
43
|
+
|
44
|
+
/******** Makes code compatible with the options used by the user */
|
45
|
+
#define BINARY 1
|
46
|
+
#define CONVERT 2
|
47
|
+
#define PASSTHRU 3
|
48
|
+
#define PARAM_FILE 11
|
49
|
+
|
50
|
+
#ifdef PASE
|
51
|
+
#define SQL_IS_INTEGER 0
|
52
|
+
#define SQL_BEST_ROWID 0
|
53
|
+
#define SQLLEN long
|
54
|
+
#define SQLFLOAT double
|
55
|
+
#endif
|
56
|
+
|
57
|
+
/*fetch*/
|
58
|
+
#define FETCH_INDEX 0x01
|
59
|
+
#define FETCH_ASSOC 0x02
|
60
|
+
#define FETCH_BOTH 0x03
|
61
|
+
|
62
|
+
/* Change column case */
|
63
|
+
#define ATTR_CASE 3271982
|
64
|
+
#define CASE_NATURAL 0
|
65
|
+
#define CASE_LOWER 1
|
66
|
+
#define CASE_UPPER 2
|
67
|
+
|
68
|
+
/* maximum sizes */
|
69
|
+
#define USERID_LEN 16
|
70
|
+
#define ACCTSTR_LEN 200
|
71
|
+
#define APPLNAME_LEN 32
|
72
|
+
#define WRKSTNNAME_LEN 18
|
73
|
+
|
74
|
+
void Init_ibm_db();
|
75
|
+
|
76
|
+
VALUE ibm_db_connect(int argc, VALUE *argv, VALUE self);
|
77
|
+
VALUE ibm_db_commit(int argc, VALUE *argv, VALUE self);
|
78
|
+
VALUE ibm_db_pconnect(int argc, VALUE *argv, VALUE self);
|
79
|
+
VALUE ibm_db_autocommit(int argc, VALUE *argv, VALUE self);
|
80
|
+
VALUE ibm_db_bind_param(int argc, VALUE *argv, VALUE self);
|
81
|
+
VALUE ibm_db_close(int argc, VALUE *argv, VALUE self);
|
82
|
+
VALUE ibm_db_columnprivileges(int argc, VALUE *argv, VALUE self);
|
83
|
+
VALUE ibm_db_column_privileges(int argc, VALUE *argv, VALUE self);
|
84
|
+
VALUE ibm_db_columns(int argc, VALUE *argv, VALUE self);
|
85
|
+
VALUE ibm_db_foreignkeys(int argc, VALUE *argv, VALUE self);
|
86
|
+
VALUE ibm_db_foreign_keys(int argc, VALUE *argv, VALUE self);
|
87
|
+
VALUE ibm_db_primarykeys(int argc, VALUE *argv, VALUE self);
|
88
|
+
VALUE ibm_db_primary_keys(int argc, VALUE *argv, VALUE self);
|
89
|
+
VALUE ibm_db_procedure_columns(int argc, VALUE *argv, VALUE self);
|
90
|
+
VALUE ibm_db_procedures(int argc, VALUE *argv, VALUE self);
|
91
|
+
VALUE ibm_db_specialcolumns(int argc, VALUE *argv, VALUE self);
|
92
|
+
VALUE ibm_db_special_columns(int argc, VALUE *argv, VALUE self);
|
93
|
+
VALUE ibm_db_statistics(int argc, VALUE *argv, VALUE self);
|
94
|
+
VALUE ibm_db_tableprivileges(int argc, VALUE *argv, VALUE self);
|
95
|
+
VALUE ibm_db_table_privileges(int argc, VALUE *argv, VALUE self);
|
96
|
+
VALUE ibm_db_tables(int argc, VALUE *argv, VALUE self);
|
97
|
+
VALUE ibm_db_commit(int argc, VALUE *argv, VALUE self);
|
98
|
+
VALUE ibm_db_exec(int argc, VALUE *argv, VALUE self);
|
99
|
+
VALUE ibm_db_prepare(int argc, VALUE *argv, VALUE self);
|
100
|
+
VALUE ibm_db_execute(int argc, VALUE *argv, VALUE self);
|
101
|
+
VALUE ibm_db_conn_errormsg(int argc, VALUE *argv, VALUE self);
|
102
|
+
VALUE ibm_db_stmt_errormsg(int argc, VALUE *argv, VALUE self);
|
103
|
+
VALUE ibm_db_conn_error(int argc, VALUE *argv, VALUE self);
|
104
|
+
VALUE ibm_db_stmt_error(int argc, VALUE *argv, VALUE self);
|
105
|
+
VALUE ibm_db_next_result(int argc, VALUE *argv, VALUE self);
|
106
|
+
VALUE ibm_db_num_fields(int argc, VALUE *argv, VALUE self);
|
107
|
+
VALUE ibm_db_num_rows(int argc, VALUE *argv, VALUE self);
|
108
|
+
VALUE ibm_db_field_name(int argc, VALUE *argv, VALUE self);
|
109
|
+
VALUE ibm_db_field_display_size(int argc, VALUE *argv, VALUE self);
|
110
|
+
VALUE ibm_db_field_num(int argc, VALUE *argv, VALUE self);
|
111
|
+
VALUE ibm_db_field_precision(int argc, VALUE *argv, VALUE self);
|
112
|
+
VALUE ibm_db_field_scale(int argc, VALUE *argv, VALUE self);
|
113
|
+
VALUE ibm_db_field_type(int argc, VALUE *argv, VALUE self);
|
114
|
+
VALUE ibm_db_field_width(int argc, VALUE *argv, VALUE self);
|
115
|
+
VALUE ibm_db_cursor_type(int argc, VALUE *argv, VALUE self);
|
116
|
+
VALUE ibm_db_rollback(int argc, VALUE *argv, VALUE self);
|
117
|
+
VALUE ibm_db_free_stmt(int argc, VALUE *argv, VALUE self);
|
118
|
+
VALUE ibm_db_result(int argc, VALUE *argv, VALUE self);
|
119
|
+
VALUE ibm_db_fetch_row(int argc, VALUE *argv, VALUE self);
|
120
|
+
VALUE ibm_db_fetch_assoc(int argc, VALUE *argv, VALUE self);
|
121
|
+
VALUE ibm_db_fetch_array(int argc, VALUE *argv, VALUE self);
|
122
|
+
VALUE ibm_db_fetch_both(int argc, VALUE *argv, VALUE self);
|
123
|
+
VALUE ibm_db_result_all(int argc, VALUE *argv, VALUE self);
|
124
|
+
VALUE ibm_db_free_result(int argc, VALUE *argv, VALUE self);
|
125
|
+
VALUE ibm_db_set_option(int argc, VALUE *argv, VALUE self);
|
126
|
+
VALUE ibm_db_setoption(int argc, VALUE *argv, VALUE self);
|
127
|
+
VALUE ibm_db_get_option(int argc, VALUE *argv, VALUE self);
|
128
|
+
VALUE ibm_db_getoption(int argc, VALUE *argv, VALUE self);
|
129
|
+
VALUE ibm_db_fetch_object(int argc, VALUE *argv, VALUE self);
|
130
|
+
VALUE ibm_db_server_info(int argc, VALUE *argv, VALUE self);
|
131
|
+
VALUE ibm_db_client_info(int argc, VALUE *argv, VALUE self);
|
132
|
+
VALUE ibm_db_active(int argc, VALUE *argv, VALUE self);
|
133
|
+
|
134
|
+
/*
|
135
|
+
Declare any global variables you may need between the BEGIN
|
136
|
+
and END macros here:
|
137
|
+
*/
|
138
|
+
struct _ibm_db_globals {
|
139
|
+
int bin_mode;
|
140
|
+
char __ruby_conn_err_msg[DB2_MAX_ERR_MSG_LEN];
|
141
|
+
char __ruby_conn_err_state[SQL_SQLSTATE_SIZE + 1];
|
142
|
+
char __ruby_stmt_err_msg[DB2_MAX_ERR_MSG_LEN];
|
143
|
+
char __ruby_stmt_err_state[SQL_SQLSTATE_SIZE + 1];
|
144
|
+
#ifdef PASE /* i5/OS ease of use turn off commit */
|
145
|
+
long i5_allow_commit;
|
146
|
+
#endif /* PASE */
|
147
|
+
};
|
148
|
+
|
149
|
+
/*
|
150
|
+
TODO: make this threadsafe
|
151
|
+
*/
|
152
|
+
|
153
|
+
#define IBM_DB_G(v) (ibm_db_globals->v)
|
154
|
+
|
155
|
+
#endif /* RUBY_IBM_DB_H */
|
156
|
+
|
157
|
+
|
158
|
+
/*
|
159
|
+
* Local variables:
|
160
|
+
* tab-width: 4
|
161
|
+
* c-basic-offset: 4
|
162
|
+
* indent-tabs-mode: t
|
163
|
+
* End:
|
164
|
+
*/
|
data/init.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# +----------------------------------------------------------------------+
|
2
|
+
# | Licensed Materials - Property of IBM |
|
3
|
+
# | |
|
4
|
+
# | (C) Copyright IBM Corporation 2006, 2007. |
|
5
|
+
# +----------------------------------------------------------------------+
|
6
|
+
|
7
|
+
require 'pathname'
|
8
|
+
|
9
|
+
begin
|
10
|
+
puts ".. Attempt to load IBM_DB Ruby driver for IBM Data Servers for this platform: #{RUBY_PLATFORM}"
|
11
|
+
unless defined? IBM_DB
|
12
|
+
# find IBM_DB driver path relative init.rb
|
13
|
+
drv_path = Pathname.new(File.dirname(__FILE__)) + 'lib'
|
14
|
+
drv_path += (RUBY_PLATFORM =~ /mswin32/) ? 'mswin32' : 'linux32'
|
15
|
+
puts ".. Locate IBM_DB Ruby driver path: #{drv_path}"
|
16
|
+
drv_lib = drv_path + 'ibm_db.so'
|
17
|
+
require "#{drv_lib.to_s}"
|
18
|
+
puts ".. Successfuly loaded IBM_DB Ruby driver: #{drv_lib}"
|
19
|
+
end
|
20
|
+
rescue
|
21
|
+
raise LoadError, "Failed to load IBM_DB Driver !?"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Include IBM_DB in the list of supported adapters
|
25
|
+
RAILS_CONNECTION_ADAPTERS << 'ibm_db'
|
26
|
+
# load IBM_DB Adapter provided by the plugin
|
27
|
+
require 'active_record/connection_adapters/ibm_db_adapter'
|
28
|
+
|
29
|
+
# Override the frameworks initialization to re-enable ActiveRecord after being
|
30
|
+
# disabled during plugin install (i.e. config.frameworks -= [ :active_record ])
|
31
|
+
[:load_environment,\
|
32
|
+
:initialize_database,\
|
33
|
+
:initialize_logger,\
|
34
|
+
:initialize_framework_logging,\
|
35
|
+
:initialize_framework_settings,\
|
36
|
+
:initialize_framework_views,\
|
37
|
+
:initialize_dependency_mechanism,\
|
38
|
+
:load_environment ].each do |routine|
|
39
|
+
Rails::Initializer.run(routine) do |config|
|
40
|
+
config.frameworks = [:active_record]
|
41
|
+
end
|
42
|
+
end
|
data/lib/IBM_DB.rb
ADDED
@@ -0,0 +1,1053 @@
|
|
1
|
+
# +----------------------------------------------------------------------+
|
2
|
+
# | Licensed Materials - Property of IBM |
|
3
|
+
# | |
|
4
|
+
# | (C) Copyright IBM Corporation 2006, 2007. |
|
5
|
+
# +----------------------------------------------------------------------+
|
6
|
+
# | Author: Antonio Cangiano <cangiano@ca.ibm.com> |
|
7
|
+
# +----------------------------------------------------------------------+
|
8
|
+
|
9
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
10
|
+
|
11
|
+
module ActiveRecord
|
12
|
+
class Base
|
13
|
+
# Method required to handle LOBs and XML fields. An after save callback checks if a marker
|
14
|
+
# has been inserted by the calling insert or update method, and if so it proceeds to update that record
|
15
|
+
# with the real large object through query binding.
|
16
|
+
after_save :handle_lobs
|
17
|
+
def handle_lobs()
|
18
|
+
if connection.kind_of?(ConnectionAdapters::IBM_DBAdapter)
|
19
|
+
# Checks that the insert or update had at least a BLOB, CLOB or XML field
|
20
|
+
if connection.sql =~ /BLOB\('(.*)'\)/i || connection.sql =~ /@@@IBMTEXT@@@/i || connection.sql =~ /@@@IBMXML@@@/i
|
21
|
+
update_query = "UPDATE #{self.class.table_name} SET ("
|
22
|
+
counter = 0
|
23
|
+
values = []
|
24
|
+
params = []
|
25
|
+
# Selects only binary, text and xml columns
|
26
|
+
self.class.columns.select{|col| col.type == :binary || col.type == :text || col.type == :xml}.each do |col|
|
27
|
+
# Adds the selected columns to the query
|
28
|
+
if counter == 0
|
29
|
+
update_query << "#{col.name}"
|
30
|
+
else
|
31
|
+
update_query << ",#{col.name}"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Add a '?' for the parameter or a NULL if the value is nil or empty (except for a CLOB field where '' can be a value)
|
35
|
+
if self[col.name].nil? || self[col.name] == {} || (self[col.name] == '' && col.type != :text)
|
36
|
+
params << 'NULL'
|
37
|
+
else
|
38
|
+
values << self[col.name]
|
39
|
+
params << '?'
|
40
|
+
end
|
41
|
+
counter += 1
|
42
|
+
end
|
43
|
+
|
44
|
+
update_query << ") = "
|
45
|
+
# IBM DB accepts 'SET (column) = NULL' but not (NULL),
|
46
|
+
# therefore the sql needs to be changed for a single NULL field.
|
47
|
+
if params.size==1 && params[0] == 'NULL'
|
48
|
+
update_query << "NULL"
|
49
|
+
else
|
50
|
+
update_query << "(" + params.join(',') + ")"
|
51
|
+
end
|
52
|
+
update_query << " WHERE #{self.class.primary_key} = #{id}"
|
53
|
+
|
54
|
+
stmt = IBM_DB::prepare(connection.connection, update_query)
|
55
|
+
connection.log_query(update_query,'update of LOB/XML field(s)in handle_lobs')
|
56
|
+
|
57
|
+
# rollback any failed LOB/XML field updates (and remove associated marker)
|
58
|
+
unless IBM_DB::execute(stmt, values)
|
59
|
+
connection.execute("ROLLBACK")
|
60
|
+
raise "Failed to insert/update LOB/XML field(s) due to: #{IBM_DB::stmt_errormsg(stmt)}"
|
61
|
+
end
|
62
|
+
end # if connection.sql
|
63
|
+
end # if connection.kind_of?
|
64
|
+
end # handle_lobs
|
65
|
+
private :handle_lobs
|
66
|
+
|
67
|
+
# Establishes a connection to a specified database using the credentials provided
|
68
|
+
# with the +config+ argument. All the ActiveRecord objects will use this connection
|
69
|
+
def self.ibm_db_connection(config)
|
70
|
+
# Attempts to load the Ruby driver IBM databases
|
71
|
+
# while not already loaded or raises LoadError in case of failure.
|
72
|
+
begin
|
73
|
+
require 'ibm_db' unless defined? IBM_DB
|
74
|
+
rescue LoadError
|
75
|
+
raise LoadError, "Failed to load IBM DB Ruby driver."
|
76
|
+
end
|
77
|
+
|
78
|
+
# Converts all +config+ keys to symbols
|
79
|
+
config = config.symbolize_keys
|
80
|
+
|
81
|
+
# Retrieves the database alias (local catalog name) or remote name
|
82
|
+
# (for remote TCP/IP connections) from the +config+ hash
|
83
|
+
# or raises ArgumentError in case of failure.
|
84
|
+
if config.has_key?(:database)
|
85
|
+
database = config[:database].to_s
|
86
|
+
else
|
87
|
+
raise ArgumentError, "Missing argument: a database name needs to be specified."
|
88
|
+
end
|
89
|
+
|
90
|
+
# Retrieves database user credentials from the +config+ hash
|
91
|
+
# or raises ArgumentError in case of failure.
|
92
|
+
if !config.has_key?(:username) || !config.has_key?(:password)
|
93
|
+
raise ArgumentError, "Missing argument(s): Database configuration #{config} requires credentials: username and password"
|
94
|
+
else
|
95
|
+
username = config[:username].to_s
|
96
|
+
password = config[:password].to_s
|
97
|
+
end
|
98
|
+
|
99
|
+
# Providing default schema (username) when not specified
|
100
|
+
config[:schema] = config.has_key?(:schema) ? config[:schema].to_s : config[:username].to_s
|
101
|
+
|
102
|
+
# Extract connection options from the database configuration
|
103
|
+
# (in support to formatting, audit and billing purposes):
|
104
|
+
# Retrieve database objects fields in lowercase
|
105
|
+
conn_options = {IBM_DB::ATTR_CASE => IBM_DB::CASE_LOWER}
|
106
|
+
# Set connection's user info
|
107
|
+
conn_options[:user] = config[:user].to_s if config.has_key?(:user)
|
108
|
+
# Set connection's account info
|
109
|
+
conn_options[:account] = config[:account].to_s if config.has_key?(:account)
|
110
|
+
# Set connection's application info
|
111
|
+
conn_options[:application] = config[:application].to_s if config.has_key?(:application)
|
112
|
+
# Set connection's workstation info
|
113
|
+
conn_options[:workstation] = config[:workstation].to_s if config.has_key?(:workstation)
|
114
|
+
|
115
|
+
# Checks if a host name or address has been specified. If so, this implies a TCP/IP connection
|
116
|
+
# Returns IBM_DB::Connection object upon succesful DB connection to the database
|
117
|
+
# If otherwise the connection fails, +false+ is returned
|
118
|
+
if config.has_key?(:host)
|
119
|
+
# Retrieves the host address/name
|
120
|
+
host = config[:host]
|
121
|
+
# A net address connection requires a port. If no port has been specified, 50000 is used by default
|
122
|
+
port = config[:port] || 50000
|
123
|
+
# Connects to the database using the database, host, port, username and password specified
|
124
|
+
connection = IBM_DB::connect "DRIVER={IBM DB2 ODBC DRIVER};DATABASE=#{database};HOSTNAME=#{host};\
|
125
|
+
PORT=#{port};PROTOCOL=TCPIP;UID=#{username};PWD=#{password};", '', '', conn_options
|
126
|
+
|
127
|
+
else
|
128
|
+
# No host implies a local catalog-based connection: +database+ represents catalog alias
|
129
|
+
connection = IBM_DB::connect( database, username, password, conn_options )
|
130
|
+
end
|
131
|
+
|
132
|
+
# Verifies that the connection was succesfull
|
133
|
+
if connection
|
134
|
+
# Creates an instance of *IBM_DBAdapter* based on the +connection+
|
135
|
+
# and credentials provided in +config+
|
136
|
+
ConnectionAdapters::IBM_DBAdapter.new(connection, logger, config, conn_options)
|
137
|
+
else
|
138
|
+
# If the connection failed, it raises a Runtime error
|
139
|
+
raise "Failed to connect to the #{database} due to: #{IBM_DB::conn_errormsg}"
|
140
|
+
end
|
141
|
+
end # method self.ibm_db_connection
|
142
|
+
end # class Base
|
143
|
+
|
144
|
+
module ConnectionAdapters
|
145
|
+
class IBM_DBColumn < Column
|
146
|
+
|
147
|
+
# Casts value (which is a String) to an appropriate instance
|
148
|
+
def type_cast(value)
|
149
|
+
# Casts the database NULL value to nil
|
150
|
+
return nil if value == 'NULL'
|
151
|
+
# Invokes parent's method for default casts
|
152
|
+
super
|
153
|
+
end
|
154
|
+
|
155
|
+
# Used to convert from BLOBs to Strings
|
156
|
+
def self.binary_to_string(value)
|
157
|
+
# Returns a string removing the eventual BLOB scalar function
|
158
|
+
value.to_s.gsub(/"SYSIBM"."BLOB"\('(.*)'\)/i,'\1')
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
# Mapping IBM DB SQL datatypes to Ruby data types
|
163
|
+
def simplified_type(field_type)
|
164
|
+
case field_type
|
165
|
+
# if +field_type+ contains 'for bit data' handle it as a binary
|
166
|
+
when /for bit data/i
|
167
|
+
:binary
|
168
|
+
when /smallint/i
|
169
|
+
:boolean
|
170
|
+
when /int/i
|
171
|
+
:integer
|
172
|
+
when /float|double|real/i
|
173
|
+
:float
|
174
|
+
when /decimal|numeric/i
|
175
|
+
:decimal
|
176
|
+
when /timestamp/i
|
177
|
+
:timestamp
|
178
|
+
when /time/i
|
179
|
+
:time
|
180
|
+
when /date/i
|
181
|
+
:date
|
182
|
+
when /clob|text|graphic/i
|
183
|
+
:text
|
184
|
+
when /xml/i
|
185
|
+
:xml
|
186
|
+
when /blob|binary/i
|
187
|
+
:binary
|
188
|
+
when /char/i
|
189
|
+
:string
|
190
|
+
when /boolean/i
|
191
|
+
:boolean
|
192
|
+
end
|
193
|
+
|
194
|
+
end # method simplified_type
|
195
|
+
end #class IBM_DBColumn
|
196
|
+
|
197
|
+
|
198
|
+
# The IBM_DB Adapter requires the native Ruby Driver for IBM Databases (ibm_db.so).
|
199
|
+
# +config+ is a hash passed as an argument to the class initializer.
|
200
|
+
# Connection parameters can be specified in 2 different ways:
|
201
|
+
# Local connection (through a database catalog alias):
|
202
|
+
# ==== Example
|
203
|
+
# adapter: ibm_db
|
204
|
+
# database: SAMPLE
|
205
|
+
# username: user
|
206
|
+
# password: pass
|
207
|
+
# schema: myschema
|
208
|
+
#
|
209
|
+
# Remote TCP/IP connection:
|
210
|
+
# ==== Example
|
211
|
+
# adapter: ibm_db
|
212
|
+
# database: SAMPLE
|
213
|
+
# username: user
|
214
|
+
# password: pass
|
215
|
+
# schema: myschema
|
216
|
+
# host: my_hostname_or_address
|
217
|
+
# port: 50000
|
218
|
+
#
|
219
|
+
# 'SAMPLE' is the database alias as it appears in the local IBM DB catalog
|
220
|
+
# Once you pick the type of connection you want to use, you need to fill-in all the fields above, as shown in the example.
|
221
|
+
# The current version allows you to set only one schema. If you don't specify a schema, the username is used as a schema.
|
222
|
+
class IBM_DBAdapter < AbstractAdapter
|
223
|
+
attr_reader :connection
|
224
|
+
attr_accessor :sql
|
225
|
+
attr_reader :schema, :user, :account, :application, :workstation
|
226
|
+
|
227
|
+
# Name of the adapter
|
228
|
+
def adapter_name
|
229
|
+
'IBM_DB'
|
230
|
+
end
|
231
|
+
|
232
|
+
def initialize(connection, logger, config, conn_options)
|
233
|
+
# Caching database connection configuration (+connect+ or +reconnect+ support)
|
234
|
+
@database = config[:database]
|
235
|
+
@username = config[:username]
|
236
|
+
@password = config[:password]
|
237
|
+
if config.has_key?(:host)
|
238
|
+
@host = config[:host]
|
239
|
+
@port = config[:port] || 50000 # default port
|
240
|
+
end
|
241
|
+
@schema = config[:schema]
|
242
|
+
|
243
|
+
# Caching database connection options (auditing and billing support)
|
244
|
+
@user = conn_options[:user] if conn_options.has_key?(:user)
|
245
|
+
@account = conn_options[:account] if conn_options.has_key?(:account)
|
246
|
+
@application = conn_options[:application] if conn_options.has_key?(:application)
|
247
|
+
@workstation = conn_options[:workstation] if conn_options.has_key?(:workstation)
|
248
|
+
|
249
|
+
# Calls the parent class +ConnectionAdapters+' initializer
|
250
|
+
# which sets @connection, @logger, @runtime and @last_verification
|
251
|
+
super(connection, logger)
|
252
|
+
|
253
|
+
if @connection
|
254
|
+
server_info = IBM_DB::server_info( @connection )
|
255
|
+
case server_info.DBMS_NAME
|
256
|
+
# DB2 on Linux, Unix and Windows (LUW)
|
257
|
+
when /DB2\//i
|
258
|
+
@servertype = IBM_DB2_LUW.new(@connection, self)
|
259
|
+
# DB2 on zOS
|
260
|
+
#
|
261
|
+
when /DB2/i
|
262
|
+
case server_info.DBMS_VER
|
263
|
+
when /09/
|
264
|
+
@servertype = IBM_DB2_ZOS.new(@connection, self)
|
265
|
+
when /08/
|
266
|
+
@servertype = IBM_DB2_ZOS_8.new(@connection, self)
|
267
|
+
when /07/
|
268
|
+
@servertype = IBM_DB2_ZOS_7.new(@connection, self)
|
269
|
+
else
|
270
|
+
raise "Only DB2 z/OS version 7 and above are currently supported"
|
271
|
+
end
|
272
|
+
# DB2 on i5
|
273
|
+
when /AS/i
|
274
|
+
@servertype = IBM_DB2_I5.new(@connection, self)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Executes the +set schema+ statement using the schema identifier provided
|
279
|
+
stmt = execute("SET SCHEMA #{@schema}") if @schema && @schema != @username
|
280
|
+
end
|
281
|
+
|
282
|
+
def schema=(name)
|
283
|
+
unless name == @schema
|
284
|
+
@schema = name
|
285
|
+
stmt = execute("SET SCHEMA #{@schema}")
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def user=(name)
|
290
|
+
unless name == @user
|
291
|
+
option = {IBM_DB::SQL_ATTR_INFO_USERID => "#{name}"}
|
292
|
+
if IBM_DB::set_option( @connection, option, 1 )
|
293
|
+
@user = IBM_DB::get_option( @connection, IBM_DB::SQL_ATTR_INFO_USERID )
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def account=(name)
|
299
|
+
unless name == @account
|
300
|
+
option = {IBM_DB::SQL_ATTR_INFO_ACCTSTR => "#{name}"}
|
301
|
+
if IBM_DB::set_option( @connection, option, 1 )
|
302
|
+
@account = IBM_DB::get_option( @connection, IBM_DB::SQL_ATTR_INFO_ACCTSTR )
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def application=(name)
|
308
|
+
unless name == @application
|
309
|
+
option = {IBM_DB::SQL_ATTR_INFO_APPLNAME => "#{name}"}
|
310
|
+
if IBM_DB::set_option( @connection, option, 1 )
|
311
|
+
@application = IBM_DB::get_option( @connection, IBM_DB::SQL_ATTR_INFO_APPLNAME )
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def workstation=(name)
|
317
|
+
unless name == @workstation
|
318
|
+
option = {IBM_DB::SQL_ATTR_INFO_WRKSTNNAME => "#{name}"}
|
319
|
+
if IBM_DB::set_option( @connection, option, 1 )
|
320
|
+
@workstation = IBM_DB::get_option( @connection, IBM_DB::SQL_ATTR_INFO_WRKSTNNAME )
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# This adapter supports migrations
|
326
|
+
# Some limits applies, for example +remove_column+ and +change_column+
|
327
|
+
# are not currently implemented
|
328
|
+
def supports_migrations?
|
329
|
+
true
|
330
|
+
end
|
331
|
+
|
332
|
+
def log_query(sql, name) #:nodoc:
|
333
|
+
# Used by handle_lobs
|
334
|
+
log(sql,name){}
|
335
|
+
end
|
336
|
+
|
337
|
+
#==============================================
|
338
|
+
# CONNECTION MANAGEMENT
|
339
|
+
#==============================================
|
340
|
+
|
341
|
+
# Tests the connection status
|
342
|
+
def active?
|
343
|
+
IBM_DB::active @connection
|
344
|
+
end
|
345
|
+
|
346
|
+
# Private method used by +reconnect!+.
|
347
|
+
# It connects to the database with the initially provided credentials
|
348
|
+
def connect
|
349
|
+
# If the type of connection is net based
|
350
|
+
if @host
|
351
|
+
# Connects and assigns the resulting IBM_DB::Connection to the +@connection+ instance variable
|
352
|
+
@connection = IBM_DB::connect "DRIVER={IBM DB2 ODBC DRIVER};DATABASE=#{@database};HOSTNAME=#{@host};\
|
353
|
+
PORT=#{@port};PROTOCOL=TCPIP;UID=#{@username};PWD=#{@password};", '', '', @conn_options
|
354
|
+
else
|
355
|
+
# Connects to the database using the local alias (@database)
|
356
|
+
# and assigns the connection object (IBM_DB::Connection) to @connection
|
357
|
+
@connection = IBM_DB::connect @database, @username, @password, @conn_options
|
358
|
+
end
|
359
|
+
# Sets the schema if different from default (username)
|
360
|
+
if @schema && @schema != @username
|
361
|
+
execute("SET SCHEMA #{@schema}")
|
362
|
+
end
|
363
|
+
end
|
364
|
+
private :connect
|
365
|
+
|
366
|
+
# Closes the current connection and opens a new one
|
367
|
+
def reconnect!
|
368
|
+
disconnect!
|
369
|
+
connect
|
370
|
+
end
|
371
|
+
|
372
|
+
# Closes the current connection
|
373
|
+
def disconnect!
|
374
|
+
# Attempts to close the connection. The methods will return:
|
375
|
+
# * true if succesfull
|
376
|
+
# * false if the connection is already closed
|
377
|
+
# * nil if an error is raised
|
378
|
+
IBM_DB::close(@connection) rescue nil
|
379
|
+
end
|
380
|
+
|
381
|
+
#==============================================
|
382
|
+
# DATABASE STATEMENTS
|
383
|
+
#==============================================
|
384
|
+
|
385
|
+
def create_table(name, options = {})
|
386
|
+
@servertype.setup_for_lob_table
|
387
|
+
super
|
388
|
+
@servertype.create_index_after_table(name, self)
|
389
|
+
end
|
390
|
+
|
391
|
+
# DB2 throws an error SQL0214N An expression in the ORDER BY
|
392
|
+
# clause in the following position, or starting with "UPPER..."
|
393
|
+
# in the "ORDER BY" clause is not valid. Reason code = "2".
|
394
|
+
# SQLSTATE=42822
|
395
|
+
# If the order_by columns do not match the select columns, we change
|
396
|
+
# the select columns.
|
397
|
+
def distinct(columns, order_by)
|
398
|
+
if !order_by.nil? && !order_by.empty?
|
399
|
+
order_by_columns = order_by.upcase.split(',').collect! { |e| (e.split).first }
|
400
|
+
distinct_sql = (columns.upcase.split(',') + order_by_columns).uniq
|
401
|
+
distinct_sql.collect! {|e| e + ", " }
|
402
|
+
return "DISTINCT #{distinct_sql.to_s.strip.chop}"
|
403
|
+
else
|
404
|
+
return "DISTINCT #{columns}"
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
# Returns an array of hashes with the column names as keys and
|
409
|
+
# column values as values. +sql+ is the select query, and +name+ is an optional description for logging
|
410
|
+
def select_all(sql, name = nil)
|
411
|
+
# Replaces {"= NULL" with "IS NULL"} OR {"IN (NULL)" with "IS NULL"}
|
412
|
+
sql.gsub!( /(=\s*NULL|IN\s*\(NULL\))/i, "IS NULL" )
|
413
|
+
|
414
|
+
results = []
|
415
|
+
# Invokes the method +execute+ in order to log and execute the sql instructions
|
416
|
+
# and get a IBM_DB::Statement from which is possible to fetch the results
|
417
|
+
if stmt = execute(sql, name)
|
418
|
+
begin
|
419
|
+
# Fetches all the results available. IBM_DB::fetch_assoc(stmt) returns
|
420
|
+
# an hash for each single record.
|
421
|
+
# The loop stops when there aren't any more valid records to fetch
|
422
|
+
while single_hash = IBM_DB::fetch_assoc(stmt)
|
423
|
+
# Add the record to the +results+ array
|
424
|
+
results << single_hash
|
425
|
+
end
|
426
|
+
ensure
|
427
|
+
# Ensures to free the resources associated with the statement
|
428
|
+
IBM_DB::free_result stmt
|
429
|
+
end
|
430
|
+
end
|
431
|
+
# The array of record hashes is returned
|
432
|
+
results
|
433
|
+
end
|
434
|
+
|
435
|
+
# Returns a record hash with the column names as keys and column values
|
436
|
+
# as values.
|
437
|
+
def select_one(sql, name = nil)
|
438
|
+
# Gets the first hash from the array of hashes returned by
|
439
|
+
# select_all
|
440
|
+
select_all(sql,name).first
|
441
|
+
end
|
442
|
+
|
443
|
+
# Perform an insert and returns the last ID generated.
|
444
|
+
# This can be the ID passed to the method or the one auto-generated by the database,
|
445
|
+
# and retrieved by the +last_generated_id+ method.
|
446
|
+
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
447
|
+
if stmt = execute(sql, name)
|
448
|
+
begin
|
449
|
+
@sql = sql
|
450
|
+
return id_value || @servertype.last_generated_id
|
451
|
+
# Ensures to free the resources associated with the statement
|
452
|
+
ensure
|
453
|
+
IBM_DB::free_result(stmt)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
# Executes and logs a +sql+ commands and
|
459
|
+
# returns a +IBM_DB::Statement+ object.
|
460
|
+
def execute(sql, name = nil)
|
461
|
+
# Logs and execute the sql instructions.
|
462
|
+
# The +log+ method is defined in the parent class +AbstractAdapter+
|
463
|
+
log(sql, name) do
|
464
|
+
if stmt = IBM_DB::exec(@connection, sql)
|
465
|
+
stmt # Return the statement object
|
466
|
+
else
|
467
|
+
raise StatementInvalid, IBM_DB::stmt_errormsg
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
def update(sql, name = nil)
|
473
|
+
# Make sure the WHERE clause handles NULL's correctly
|
474
|
+
sqlarray = sql.split(/\s*WHERE\s*/)
|
475
|
+
if !sqlarray[1].nil?
|
476
|
+
sqlarray[1].gsub!( /(=\s*NULL|IN\s*\(NULL\))/i, "IS NULL" )
|
477
|
+
sql = sqlarray[0] + " WHERE " + sqlarray[1]
|
478
|
+
end
|
479
|
+
|
480
|
+
# Logs and execute the given sql query.
|
481
|
+
if stmt = execute(sql, name)
|
482
|
+
begin
|
483
|
+
@sql = sql
|
484
|
+
# Retrieves the number of affected rows
|
485
|
+
IBM_DB::num_rows(stmt)
|
486
|
+
# Ensures to free the resources associated with the statement
|
487
|
+
ensure
|
488
|
+
IBM_DB::free_result(stmt)
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
# The delete method executes the delete
|
494
|
+
# statement and returns the number of affected rows.
|
495
|
+
# The method is an alias for +update+
|
496
|
+
alias_method :delete, :update
|
497
|
+
|
498
|
+
# Begins the transaction (and turns off auto-committing)
|
499
|
+
def begin_db_transaction
|
500
|
+
# Turns off the auto-commit
|
501
|
+
IBM_DB::autocommit(@connection, IBM_DB::SQL_AUTOCOMMIT_OFF)
|
502
|
+
end
|
503
|
+
|
504
|
+
# Commits the transaction (and turns on auto-committing)
|
505
|
+
def commit_db_transaction
|
506
|
+
# Commits the transaction
|
507
|
+
IBM_DB::commit @connection rescue nil
|
508
|
+
# Turns on auto-committing
|
509
|
+
IBM_DB::autocommit @connection, IBM_DB::SQL_AUTOCOMMIT_ON
|
510
|
+
end
|
511
|
+
|
512
|
+
# Rolls back the transaction (and turns on auto-committing). Must be
|
513
|
+
# done if the transaction block raises an exception or returns false
|
514
|
+
def rollback_db_transaction
|
515
|
+
# ROLLBACK the transaction
|
516
|
+
IBM_DB::rollback(@connection) rescue nil
|
517
|
+
# Turns on auto-committing
|
518
|
+
IBM_DB::autocommit @connection, IBM_DB::SQL_AUTOCOMMIT_ON
|
519
|
+
end
|
520
|
+
|
521
|
+
|
522
|
+
# Modifies a sql statement in order to implement a LIMIT and an OFFSET.
|
523
|
+
# A LIMIT defines the number of rows that should be fetched, while
|
524
|
+
# an OFFSET defines from what row the records must be fetched.
|
525
|
+
# IBM DB SQL implements a LIMIT with the instructions:
|
526
|
+
# FETCH FIRST n ROWS ONLY, where n is the number of rows required.
|
527
|
+
# The implementation of OFFSET is more elaborate, and require the usage of
|
528
|
+
# subqueries and the ROW_NUMBER() command in order to add row numbering
|
529
|
+
# as an additional column to a copy of the existing table.
|
530
|
+
# ==== Examples
|
531
|
+
# add_limit_offset!('SELECT * FROM staff', {:limit => 10})
|
532
|
+
# generates: "SELECT * FROM staff FETCH FIRST 10 ROWS ONLY"
|
533
|
+
#
|
534
|
+
# add_limit_offset!('SELECT * FROM staff', {:limit => 10, :offset => 30})
|
535
|
+
# generates "SELECT O.* FROM (SELECT I.*, ROW_NUMBER() OVER () sys_rownum
|
536
|
+
# FROM (SELECT * FROM staff) AS I) AS O WHERE sys_row_num BETWEEN 31 AND 40"
|
537
|
+
def add_limit_offset!(sql, options)
|
538
|
+
# If there is a limit
|
539
|
+
if limit = options[:limit]
|
540
|
+
# if the limit is zero
|
541
|
+
if limit == 0
|
542
|
+
# Returns a query that will always generate zero records
|
543
|
+
# (e.g. WHERE sys_row_num BETWEEN 1 and 0)
|
544
|
+
sql = query_offset_limit(sql, 0, limit)
|
545
|
+
# If there is a non-zero limit
|
546
|
+
else
|
547
|
+
offset = options[:offset]
|
548
|
+
# If an offset is specified builds the query with offset and limit, oterwise retrieves only the first +limit+ rows
|
549
|
+
offset ? sql = query_offset_limit(sql, offset, limit) : sql << " FETCH FIRST #{limit} ROWS ONLY"
|
550
|
+
end
|
551
|
+
end
|
552
|
+
# Returns the sql query in any case
|
553
|
+
sql
|
554
|
+
end # method add_limit_offset!
|
555
|
+
|
556
|
+
# Private method used by +add_limit_offset!+ to create a
|
557
|
+
# sql query given an offset and a limit
|
558
|
+
def query_offset_limit(sql, offset, limit)
|
559
|
+
# Defines what will be the last record
|
560
|
+
last_record = offset + limit
|
561
|
+
# Transforms the SELECT query in order to retrieve/fetch only
|
562
|
+
# a number of records after the specified offset.
|
563
|
+
# 'select' or 'SELECT' is replaced with the partial query below that adds the sys_row_num column
|
564
|
+
# to select with the condition of this column being between offset+1 and the offset+limit
|
565
|
+
sql.gsub!(/SELECT/i,"SELECT O.* FROM (SELECT I.*, ROW_NUMBER() OVER () sys_row_num FROM (SELECT")
|
566
|
+
# The final part of the query is appended to include a WHERE...BETWEEN...AND condition,
|
567
|
+
# and retrieve only a LIMIT number of records starting from the OFFSET+1
|
568
|
+
sql << ") AS I) AS O WHERE sys_row_num BETWEEN #{offset+1} AND #{last_record}"
|
569
|
+
end
|
570
|
+
private :query_offset_limit
|
571
|
+
|
572
|
+
def default_sequence_name(table, column) # :nodoc:
|
573
|
+
"#{table}_#{column}_seq"
|
574
|
+
end
|
575
|
+
|
576
|
+
|
577
|
+
#==============================================
|
578
|
+
# QUOTING
|
579
|
+
#==============================================
|
580
|
+
|
581
|
+
# Properly quotes the various data types.
|
582
|
+
# +value+ contains the data, +column+ is optional and contains info on the field
|
583
|
+
def quote(value, column = nil)
|
584
|
+
case value
|
585
|
+
# If it's a numeric value and the column type is not a string, it shouldn't be quoted
|
586
|
+
# (IBM DB doesn't accept quotes on numeric types)
|
587
|
+
when Numeric
|
588
|
+
# If the column type is text or string, return the quote value
|
589
|
+
if column && column.type == :text || column && column.type == :string
|
590
|
+
"'#{value}'"
|
591
|
+
else
|
592
|
+
# value is Numeric, column.type is not a string,
|
593
|
+
# therefore it converts the number to string without quoting it
|
594
|
+
value.to_s
|
595
|
+
end
|
596
|
+
when String, ActiveSupport::Multibyte::Chars
|
597
|
+
if column && column.type == :binary
|
598
|
+
# If quoting is required for the insert/update of a BLOB
|
599
|
+
unless caller[0] =~ /add_column_options/i
|
600
|
+
# Invokes a convertion from string to binary
|
601
|
+
"BLOB('?')"
|
602
|
+
else
|
603
|
+
# Quoting required for the default value of a column
|
604
|
+
"BLOB('#{value}')"
|
605
|
+
end
|
606
|
+
elsif column && column.type == :text
|
607
|
+
unless caller[0] =~ /add_column_options/i
|
608
|
+
"'@@@IBMTEXT@@@'"
|
609
|
+
else
|
610
|
+
"#{value}"
|
611
|
+
end
|
612
|
+
elsif column && column.type == :xml
|
613
|
+
unless caller[0] =~ /add_column_options/i
|
614
|
+
"'<ibm>@@@IBMXML@@@</ibm>'"
|
615
|
+
else
|
616
|
+
"#{value}"
|
617
|
+
end
|
618
|
+
else
|
619
|
+
"'#{quote_string(value)}'"
|
620
|
+
end
|
621
|
+
when TrueClass then quoted_true # return '1' for true
|
622
|
+
when FalseClass then quoted_false # return '0' for false
|
623
|
+
when NilClass
|
624
|
+
if column && column.instance_of?(IBM_DBColumn) && !column.primary && !column.null
|
625
|
+
"''" # allow empty inserts if not nullable or identity
|
626
|
+
else # in order to support default ActiveRecord constructors
|
627
|
+
"NULL"
|
628
|
+
end
|
629
|
+
else super # rely on superclass handling
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
# Quotes a given string, escaping single quote (') characters.
|
634
|
+
def quote_string(string)
|
635
|
+
string.gsub(/'/, "''")
|
636
|
+
end
|
637
|
+
|
638
|
+
# *true* is represented by a smallint 1, *false*
|
639
|
+
# by 0, as no native boolean type exists in DB2.
|
640
|
+
# Numerics are not quoted in DB2.
|
641
|
+
def quoted_true
|
642
|
+
"1"
|
643
|
+
end
|
644
|
+
|
645
|
+
def quoted_false
|
646
|
+
"0"
|
647
|
+
end
|
648
|
+
|
649
|
+
def quote_column_name(name)
|
650
|
+
@servertype.check_reserved_words(name)
|
651
|
+
end
|
652
|
+
|
653
|
+
#==============================================
|
654
|
+
# SCHEMA STATEMENTS
|
655
|
+
#==============================================
|
656
|
+
|
657
|
+
# Returns a Hash of mappings from the abstract data types to the native
|
658
|
+
# database types
|
659
|
+
def native_database_types
|
660
|
+
{
|
661
|
+
:primary_key => @servertype.primary_key,
|
662
|
+
:string => { :name => "varchar", :limit => 255 },
|
663
|
+
:text => { :name => "clob" },
|
664
|
+
:integer => { :name => "integer" },
|
665
|
+
:float => { :name => "float" },
|
666
|
+
:datetime => { :name => "timestamp" },
|
667
|
+
:timestamp => { :name => "timestamp" },
|
668
|
+
:time => { :name => "time" },
|
669
|
+
:date => { :name => "date" },
|
670
|
+
:binary => { :name => "blob" },
|
671
|
+
|
672
|
+
# IBM DB doesn't have a native boolean type.
|
673
|
+
# A boolean can be represented by a smallint,
|
674
|
+
# adopting the convention that False is 0 and True is 1
|
675
|
+
:boolean => { :name => "smallint"},
|
676
|
+
:xml => { :name => "xml"},
|
677
|
+
:decimal => { :name => "decimal" }
|
678
|
+
}
|
679
|
+
end
|
680
|
+
|
681
|
+
# DB2 does not support limits on the integer and double data types
|
682
|
+
# unlike MySQL. It does support limits on float and decimal/numeric
|
683
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
684
|
+
if type == :integer && (!limit.nil? && limit > 0)
|
685
|
+
return 'integer'
|
686
|
+
elsif type == :double && (!limit.nil? && limit > 0)
|
687
|
+
return 'double'
|
688
|
+
else
|
689
|
+
return super
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
# Returns the maximum length a table alias identifier can be.
|
694
|
+
# IBM DB (cross-platform) table limit is 128 characters
|
695
|
+
def table_alias_length
|
696
|
+
128
|
697
|
+
end
|
698
|
+
|
699
|
+
# Returns an array of tables within the given shema
|
700
|
+
def tables(name = nil)
|
701
|
+
# Initializes the tables array
|
702
|
+
tables = []
|
703
|
+
# Returns a IBM_DB::Statement used to retrieve the tables
|
704
|
+
if stmt = IBM_DB::tables(@connection, nil, @schema.upcase)
|
705
|
+
# Fetches all the records available
|
706
|
+
while tab = IBM_DB::fetch_assoc(stmt)
|
707
|
+
# Adds the lowercase table name to the array
|
708
|
+
tables << tab["table_name"].downcase
|
709
|
+
end
|
710
|
+
end
|
711
|
+
# Returns the tables array
|
712
|
+
tables
|
713
|
+
end
|
714
|
+
|
715
|
+
# Returns an array of non-primary key indexes for the given table
|
716
|
+
def indexes(table_name, name = nil)
|
717
|
+
# to_s required because +table_name+ may be a symbol.
|
718
|
+
table_name = table_name.to_s
|
719
|
+
# Checks if a blank table name has been given.
|
720
|
+
# If so it returns an empty array of columns.
|
721
|
+
return [] if table_name.strip.empty?
|
722
|
+
|
723
|
+
# +indexes+ will contain the resulting array
|
724
|
+
indexes = []
|
725
|
+
# Query table statistics for all indexes on the table
|
726
|
+
# "TABLE_NAME: #{index_stats[2]}"
|
727
|
+
# "NON_UNIQUE: #{index_stats[3]}"
|
728
|
+
# "INDEX_NAME: #{index_stats[5]}"
|
729
|
+
# "COLUMN_NAME: #{index_stats[8]}"
|
730
|
+
stmt = IBM_DB::statistics( @connection, nil, @schema.upcase, table_name.upcase, 1)
|
731
|
+
while ( index_stats = IBM_DB::fetch_array(stmt) )
|
732
|
+
if index_stats[5] # INDEX_NAME
|
733
|
+
index_name = index_stats[5].downcase
|
734
|
+
# Non-unique index type (not the primary key)
|
735
|
+
unless index_stats[3] == 0 # NON_UNIQUE
|
736
|
+
index_unique = (index_stats[3] == 0)
|
737
|
+
index_columns = index_stats[8].map{|c| c.downcase} # COLUMN_NAME
|
738
|
+
# Create an IndexDefinition object and add to the indexes array
|
739
|
+
indexes << IndexDefinition.new(table_name, index_name, index_unique, index_columns)
|
740
|
+
end
|
741
|
+
end
|
742
|
+
end
|
743
|
+
# Returns the indexes array
|
744
|
+
return indexes
|
745
|
+
# Ensures to free the resources associated with the statement
|
746
|
+
ensure
|
747
|
+
IBM_DB::free_result(stmt) if stmt
|
748
|
+
end
|
749
|
+
|
750
|
+
# Returns an array of Column objects for the table specified by +table_name+
|
751
|
+
def columns(table_name, name = nil)
|
752
|
+
# to_s required because it may be a symbol.
|
753
|
+
table_name = table_name.to_s.upcase
|
754
|
+
# Checks if a blank table name has been given.
|
755
|
+
# If so it returns an empty array
|
756
|
+
return [] if table_name.strip.empty?
|
757
|
+
# +columns+ will contain the resulting array
|
758
|
+
columns = []
|
759
|
+
# Statement required to access all the columns information
|
760
|
+
if stmt = IBM_DB::columns(@connection, nil, @schema.upcase, table_name.upcase)
|
761
|
+
begin
|
762
|
+
# Fetches all the columns and assigns them to col.
|
763
|
+
# +col+ is an hash with keys/value pairs for a column
|
764
|
+
while col = IBM_DB::fetch_assoc(stmt)
|
765
|
+
column_name = col["column_name"].downcase
|
766
|
+
# Assigns the column default value.
|
767
|
+
column_default_value = col["column_def"]
|
768
|
+
# If there is no default value, it assigns NIL
|
769
|
+
column_default_value = nil if (column_default_value && column_default_value.upcase == 'NULL')
|
770
|
+
# Removes single quotes from the default value
|
771
|
+
column_default_value.gsub!(/^'(.*)'$/, '\1') unless column_default_value.nil?
|
772
|
+
# Assigns the column type
|
773
|
+
column_type = col["type_name"].downcase
|
774
|
+
# Assigns the field length (size) for the column
|
775
|
+
column_length = col["column_size"]
|
776
|
+
column_scale = col["decimal_digits"]
|
777
|
+
# The initializer of the class Column, requires the +column_length+ to be declared between brackets after
|
778
|
+
# the datatype(e.g VARCHAR(50)) for :string and :text types. If it's a "for bit data" field it does a subsitution in place, if not
|
779
|
+
# it appends the (column_length) string on the supported data types
|
780
|
+
unless column_length.nil? || column_length == '' || column_type.sub!(/ \(\) for bit data/i,"(#{column_length}) FOR BIT DATA") || !column_type =~ /char|lob|graphic/i
|
781
|
+
if column_type =~ /decimal/i
|
782
|
+
column_type << "(#{column_length},#{column_scale})"
|
783
|
+
else
|
784
|
+
column_type << "(#{column_length})"
|
785
|
+
end
|
786
|
+
end
|
787
|
+
# col["NULLABLE"] is 1 if the field is nullable, 0 if not.
|
788
|
+
column_nullable = (col["nullable"] == 1) ? true : false
|
789
|
+
# Make sure the hidden column (db2_generated_rowid_for_lobs) in DB2 z/OS isn't added to the list
|
790
|
+
if !(column_name =~ /db2_generated_rowid_for_lobs/i)
|
791
|
+
# Pushes into the array the *IBM_DBColumn* object, created by passing to the initializer
|
792
|
+
# +column_name+, +default_value+, +column_type+ and +column_nullable+.
|
793
|
+
columns << IBM_DBColumn.new(column_name, column_default_value, column_type, column_nullable)
|
794
|
+
end
|
795
|
+
end
|
796
|
+
rescue
|
797
|
+
raise "Failed to retrieve column metadata due to error: #{IBM_DB::conn_errormsg} ... #{IBM_DB::stmt_errormsg}"
|
798
|
+
end
|
799
|
+
end
|
800
|
+
# Returns the columns array
|
801
|
+
columns
|
802
|
+
end
|
803
|
+
|
804
|
+
# Renames a table.
|
805
|
+
# ==== Example
|
806
|
+
# rename_table('octopuses', 'octopi')
|
807
|
+
# Overriden to satisfy IBM DB syntax
|
808
|
+
def rename_table(name, new_name)
|
809
|
+
# SQL rename table statement
|
810
|
+
rename_table_sql = "RENAME TABLE #{name} TO #{new_name}"
|
811
|
+
stmt = execute(rename_table_sql)
|
812
|
+
# Ensures to free the resources associated with the statement
|
813
|
+
ensure
|
814
|
+
IBM_DB::free_result stmt if stmt
|
815
|
+
end
|
816
|
+
|
817
|
+
# Renames a column.
|
818
|
+
# ===== Example
|
819
|
+
# rename_column(:suppliers, :description, :name)
|
820
|
+
def rename_column(table_name, column_name, new_column_name)
|
821
|
+
@servertype.rename_column(table_name, column_name, new_column_name)
|
822
|
+
end
|
823
|
+
|
824
|
+
# Removes the column from the table definition.
|
825
|
+
# ===== Examples
|
826
|
+
# remove_column(:suppliers, :qualification)
|
827
|
+
def remove_column(table_name, column_name)
|
828
|
+
@servertype.remove_column(table_name, column_name)
|
829
|
+
end
|
830
|
+
|
831
|
+
# Changes the column's definition according to the new options.
|
832
|
+
# See TableDefinition#column for details of the options you can use.
|
833
|
+
# ===== Examples
|
834
|
+
# change_column(:suppliers, :name, :string, :limit => 80)
|
835
|
+
# change_column(:accounts, :description, :text)
|
836
|
+
def change_column(table_name, column_name, type, options = {})
|
837
|
+
@servertype.change_column(table_name, column_name, type, options)
|
838
|
+
end
|
839
|
+
|
840
|
+
# Sets a new default value for a column. You can't set the default
|
841
|
+
# value to +NULL+, In that case, you need to
|
842
|
+
# DatabaseStatements#execute the apppropriate SQL statement.
|
843
|
+
# ==== Examples
|
844
|
+
# change_column_default(:suppliers, :qualification, 'new')
|
845
|
+
# change_column_default(:accounts, :authorized, 1)
|
846
|
+
# Method overriden to satisfy IBM DB syntax.
|
847
|
+
def change_column_default(table_name, column_name, default)
|
848
|
+
# SQL statement which alters column's default value
|
849
|
+
change_column_sql = "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET WITH DEFAULT #{quote(default)}"
|
850
|
+
stmt = execute(change_column_sql)
|
851
|
+
# Ensures to free the resources associated with the statement
|
852
|
+
ensure
|
853
|
+
IBM_DB::free_result stmt if stmt
|
854
|
+
end
|
855
|
+
|
856
|
+
# Adds a new index to the table. +column_name+ can be a single Symbol, or
|
857
|
+
# an Array of Symbols.
|
858
|
+
#
|
859
|
+
# The index will be named after the table and the first column names,
|
860
|
+
# unless you pass +:name+ as an option.
|
861
|
+
#
|
862
|
+
# When creating an index on multiple columns, the first column is used as a name
|
863
|
+
# for the index. For example, when you specify an index on two columns
|
864
|
+
# [+:first+, +:last+], the DBMS creates an index for both columns as well as an
|
865
|
+
# index for the first colum +:first+. Using just the first name for this index
|
866
|
+
# makes sense, because you will never have to create a singular index with this
|
867
|
+
# name.
|
868
|
+
#
|
869
|
+
# ===== Examples
|
870
|
+
# ====== Creating a simple index
|
871
|
+
# add_index(:suppliers, :name)
|
872
|
+
# generates
|
873
|
+
# CREATE INDEX suppliers_name_index ON suppliers(name)
|
874
|
+
# ====== Creating a unique index
|
875
|
+
# add_index(:accounts, [:branch_id, :party_id], :unique => true)
|
876
|
+
# generates
|
877
|
+
# CREATE UNIQUE INDEX accounts_branch_id_index ON accounts(branch_id, party_id)
|
878
|
+
# ====== Creating a named index
|
879
|
+
# add_index(:accounts, [:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
|
880
|
+
# generates
|
881
|
+
# CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id)
|
882
|
+
# Overidden to comply to the 18 characters cross-platform limit on index identifiers.
|
883
|
+
def add_index(table_name, column_name, options = {})
|
884
|
+
index_name = index_name(table_name, options)
|
885
|
+
if Hash === options # legacy support, since this param was a string
|
886
|
+
index_type = options[:unique] ? "UNIQUE" : ""
|
887
|
+
else
|
888
|
+
index_type = options
|
889
|
+
end
|
890
|
+
execute("CREATE #{index_type} INDEX #{index_name} ON #{table_name} (#{Array(column_name).join(", ")})")
|
891
|
+
end
|
892
|
+
|
893
|
+
# Remove the given index from the table.
|
894
|
+
#
|
895
|
+
# Remove the suppliers_name_index in the suppliers table (legacy support, use the second or third forms).
|
896
|
+
# remove_index :suppliers, :name
|
897
|
+
# Remove the index named accounts_branch_id in the accounts table.
|
898
|
+
# remove_index :accounts, :column => :branch_id
|
899
|
+
# Remove the index named by_branch_party in the accounts table.
|
900
|
+
# remove_index :accounts, :name => :by_branch_party
|
901
|
+
#
|
902
|
+
# You can remove an index on multiple columns by specifying the first column.
|
903
|
+
# add_index :accounts, [:username, :password]
|
904
|
+
# remove_index :accounts, :username
|
905
|
+
# Overriden to use the IBM DB SQL syntax.
|
906
|
+
def remove_index(table_name, options = {})
|
907
|
+
execute("DROP INDEX #{index_name(table_name, options)}")
|
908
|
+
end
|
909
|
+
|
910
|
+
# Builds an index name from a table_name and column. If an index name has been passed,
|
911
|
+
# the method returns it. Overrides the default method to respect the IBM DB (cross-platform)
|
912
|
+
# limit on indexes (max of 18 characters)
|
913
|
+
def index_name(table_name, options)
|
914
|
+
if Hash === options and options[:name]
|
915
|
+
# legacy support
|
916
|
+
# If a name has been specified, this is returned
|
917
|
+
options[:name]
|
918
|
+
else
|
919
|
+
# We reverse the table name to reduce the chance of hitting duplicate
|
920
|
+
# index name errors. For e.g. indexes on table names like accounts,
|
921
|
+
# accounts_favorites
|
922
|
+
"ror_#{table_name.to_s.reverse[0,10]}_idx"
|
923
|
+
end
|
924
|
+
end
|
925
|
+
private :index_name
|
926
|
+
|
927
|
+
end # class IBM_DBAdapter
|
928
|
+
|
929
|
+
# This class contains common code across DB's (DB2 LUW, zOS and i5)
|
930
|
+
class IBM_DataServer
|
931
|
+
def initialize(connection, caller)
|
932
|
+
@connection = connection
|
933
|
+
@caller = caller
|
934
|
+
end
|
935
|
+
|
936
|
+
# Implemented by concrete DataServer if applicable
|
937
|
+
def last_generated_id
|
938
|
+
end
|
939
|
+
|
940
|
+
# Implemented by concrete DataServer if applicable
|
941
|
+
def create_index_after_table (table_name, caller)
|
942
|
+
end
|
943
|
+
|
944
|
+
# Implemented by concrete DataServer if applicable
|
945
|
+
def setup_for_lob_table ()
|
946
|
+
end
|
947
|
+
|
948
|
+
# Implemented by concrete DataServer if applicable
|
949
|
+
def reorg_table(table_name)
|
950
|
+
end
|
951
|
+
|
952
|
+
# Implemented by concrete DataServer if applicable
|
953
|
+
def check_reserved_words(col_name)
|
954
|
+
col_name
|
955
|
+
end
|
956
|
+
|
957
|
+
# This is supported by LUW and i5
|
958
|
+
def remove_column(table_name, column_name)
|
959
|
+
@caller.execute "ALTER TABLE #{table_name} DROP #{column_name}"
|
960
|
+
reorg_table(table_name)
|
961
|
+
end
|
962
|
+
|
963
|
+
end # class IBM_DataServer
|
964
|
+
|
965
|
+
class IBM_DB2 < IBM_DataServer
|
966
|
+
def rename_column(table_name, column_name, new_column_name)
|
967
|
+
raise NotImplementedError, "rename_column is not implemented yet in the IBM DB Adapter"
|
968
|
+
end
|
969
|
+
|
970
|
+
def primary_key
|
971
|
+
return "INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 100) PRIMARY KEY"
|
972
|
+
end
|
973
|
+
|
974
|
+
# Method that returns the last automatically generated ID
|
975
|
+
# on the given +@connection+. This method is required by the +insert+
|
976
|
+
# method
|
977
|
+
def last_generated_id
|
978
|
+
# Queries the db to obtain the last ID that was automatically generated
|
979
|
+
sql = "SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1"
|
980
|
+
stmt = IBM_DB::exec(@connection, sql)
|
981
|
+
# Fetches the only record available (containing the last id)
|
982
|
+
IBM_DB::fetch_row(stmt)
|
983
|
+
# Retrieves and returns the result of the query with the last id.
|
984
|
+
IBM_DB::result(stmt,0)
|
985
|
+
end
|
986
|
+
|
987
|
+
def change_column(table_name, column_name, type, options)
|
988
|
+
@caller.execute "ALTER TABLE #{table_name} ALTER #{column_name} SET DATA TYPE #{@caller.type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
989
|
+
reorg_table(table_name)
|
990
|
+
if !options[:default].nil?
|
991
|
+
@caller.change_column_default(table_name, column_name, options[:default])
|
992
|
+
end
|
993
|
+
end
|
994
|
+
end # class IBM_DB2
|
995
|
+
|
996
|
+
class IBM_DB2_LUW < IBM_DB2
|
997
|
+
# Reorganizes the table for column changes
|
998
|
+
def reorg_table(table_name)
|
999
|
+
@caller.execute("CALL ADMIN_CMD('REORG TABLE #{table_name}')")
|
1000
|
+
end
|
1001
|
+
end # class IBM_DB2_LUW
|
1002
|
+
|
1003
|
+
module HostedDataServer
|
1004
|
+
require 'pathname'
|
1005
|
+
#find DB2-i5-zOS rezerved words file relative path
|
1006
|
+
rfile = Pathname.new(File.dirname(__FILE__)).parent + 'vendor' + 'db2-i5-zOS.yaml'
|
1007
|
+
if rfile
|
1008
|
+
RESERVED_WORDS = open(rfile.to_s) {|f| YAML.load(f) }
|
1009
|
+
def check_reserved_words(col_name)
|
1010
|
+
if RESERVED_WORDS[col_name]
|
1011
|
+
'"' + RESERVED_WORDS[col_name] + '"'
|
1012
|
+
else
|
1013
|
+
col_name
|
1014
|
+
end
|
1015
|
+
end
|
1016
|
+
else
|
1017
|
+
raise "Failed to locate IBM_DB Adapter dependency: #{rfile}"
|
1018
|
+
end
|
1019
|
+
end # module HostedDataServer
|
1020
|
+
|
1021
|
+
class IBM_DB2_ZOS < IBM_DB2
|
1022
|
+
# since v9 doesn't need, suggest putting it in HostedDataServer?
|
1023
|
+
def create_index_after_table(table_name, caller)
|
1024
|
+
caller.add_index(table_name, "id", :unique => true)
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
# This call is needed on DB2 z/OS v8 and earlier for the creation of tables
|
1028
|
+
# with LOBs. When issued, this call does the following:
|
1029
|
+
# DB2 creates LOB table spaces, auxiliary tables, and indexes on auxiliary
|
1030
|
+
# tables for LOB columns.
|
1031
|
+
def setup_for_lob_table()
|
1032
|
+
@caller.execute "SET CURRENT RULES = 'STD'"
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
def remove_column(table_name, column_name)
|
1036
|
+
raise NotImplementedError, "remove_column is not supported for DB2/zOS server"
|
1037
|
+
end
|
1038
|
+
end # class IBM_DB2_ZOS
|
1039
|
+
|
1040
|
+
class IBM_DB2_ZOS_8 < IBM_DB2_ZOS
|
1041
|
+
include HostedDataServer
|
1042
|
+
end # class IBM_DB2_ZOS_8
|
1043
|
+
|
1044
|
+
class IBM_DB2_ZOS_7 < IBM_DB2_ZOS
|
1045
|
+
include HostedDataServer
|
1046
|
+
end # class IBM_DB2_ZOS_7
|
1047
|
+
|
1048
|
+
class IBM_DB2_I5 < IBM_DB2
|
1049
|
+
include HostedDataServer
|
1050
|
+
end # class IBM_DB2_I5
|
1051
|
+
|
1052
|
+
end # module ConnectionAdapters
|
1053
|
+
end # module ActiveRecord
|