db2_odbc_adapter 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +64 -0
- data/db2_odbc_adapter.gemspec +21 -0
- data/lib/active_record/connection_adapters/odbc_adapter.rb +218 -0
- data/lib/odbc_adapter.rb +2 -0
- data/lib/odbc_adapter/column.rb +13 -0
- data/lib/odbc_adapter/column_metadata.rb +76 -0
- data/lib/odbc_adapter/database_limits.rb +10 -0
- data/lib/odbc_adapter/database_metadata.rb +46 -0
- data/lib/odbc_adapter/database_statements.rb +134 -0
- data/lib/odbc_adapter/db2_adapter.rb +16 -0
- data/lib/odbc_adapter/error.rb +6 -0
- data/lib/odbc_adapter/quoting.rb +42 -0
- data/lib/odbc_adapter/schema_statements.rb +131 -0
- data/lib/odbc_adapter/version.rb +3 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f204595e9630c40fcf1fc1d520f29eae633a06739d1c9b40f6eca716ddff3d9c
|
4
|
+
data.tar.gz: b58bfc5ad2d8b1b910ad19d320f5926f615e6e9ed3ae309ac30482148997105b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f7fb09975d463f81d0c144278ad1397ad2665546b68e7c6ebb70a57839fa62f477375669deb79ef73cf80c00b9ec6f8aa1864d29efed6600f38bf1d064375638
|
7
|
+
data.tar.gz: e592c5c2f236534f37626b1dc4ba87a9747eb029aa5c763d2e96f33cc2f27e1b4c74582097f9b05c5bc8cab9e1f6f009c3a13966ee1f958489e423ed03ecf37d
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Localytics http://www.localytics.com
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# DB2 ODBCAdapter
|
2
|
+
|
3
|
+
An ActiveRecord DB2 ODBC adapter.
|
4
|
+
It is a fork of [ActiveRecord odbc_adapter](https://github.com/localytics/odbc_adapter) with a minor heck so that it can work only with DB2 Connection at Rails 6+.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Ensure you have the ODBC driver installed on your machine. And have a DB2 server to connect.
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'db2_odbc_adapter'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
```
|
18
|
+
$ bundle
|
19
|
+
```
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
```
|
23
|
+
$ gem install db2_odbc_adapter
|
24
|
+
```
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
Configure your `database.yml` by either using the `dsn` option to point to a DSN that corresponds to a valid entry in your `~/etc/odbc.ini` file:
|
29
|
+
|
30
|
+
```
|
31
|
+
db2_connection: // connection name (as you wish)
|
32
|
+
adapter: odbc // compulsori
|
33
|
+
dsn: YourDatabaseDSN // as in the odbc.ini file
|
34
|
+
```
|
35
|
+
|
36
|
+
and use it at your model as follow.
|
37
|
+
|
38
|
+
Single table connection
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class MyTable < ActiveRecord::Base
|
42
|
+
establish_connection :db2_connection
|
43
|
+
self.table_name = "TableName" #table name at DB2 server
|
44
|
+
self.primary_key = 'column_name' #colum that have unique content since db2 have RRN instead of id
|
45
|
+
end
|
46
|
+
|
47
|
+
```
|
48
|
+
|
49
|
+
Raw SQL connection
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
class MyCustomModel < ActiveRecord::Base
|
53
|
+
establish_connection :db2_connection
|
54
|
+
scope :method_name, -> arg {
|
55
|
+
connection.exec_query("SELECT * FROM .........WHERE ..'#{arg}...")
|
56
|
+
}
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
ActiveRecord models that use this connection will now be connecting to the configured database using the ODBC driver.
|
61
|
+
|
62
|
+
## License
|
63
|
+
|
64
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
@@ -0,0 +1,21 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'odbc_adapter/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'db2_odbc_adapter'
|
7
|
+
spec.version = ODBCAdapter::VERSION
|
8
|
+
spec.authors = ['Yohanes']
|
9
|
+
spec.email = ['yohanes.lumentut@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'An ActiveRecord DB2 ODBC adapter'
|
12
|
+
spec.homepage = 'https://github.com/yohaneslumentut/db2_odbc_adapter'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_dependency 'ruby-odbc', '~> 0.99999'
|
21
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'arel/visitors'
|
3
|
+
require 'odbc'
|
4
|
+
require 'odbc_utf8'
|
5
|
+
|
6
|
+
require 'odbc_adapter/database_limits'
|
7
|
+
require 'odbc_adapter/database_statements'
|
8
|
+
require 'odbc_adapter/error'
|
9
|
+
require 'odbc_adapter/quoting'
|
10
|
+
require 'odbc_adapter/schema_statements'
|
11
|
+
require 'odbc_adapter/column'
|
12
|
+
require 'odbc_adapter/column_metadata'
|
13
|
+
require 'odbc_adapter/database_metadata'
|
14
|
+
require 'odbc_adapter/version'
|
15
|
+
|
16
|
+
module ActiveRecord
|
17
|
+
class Base
|
18
|
+
class << self
|
19
|
+
# Build a new ODBC connection with the given configuration.
|
20
|
+
def odbc_connection(config)
|
21
|
+
config = config.symbolize_keys
|
22
|
+
|
23
|
+
connection, config =
|
24
|
+
if config.key?(:dsn)
|
25
|
+
odbc_dsn_connection(config)
|
26
|
+
elsif config.key?(:conn_str)
|
27
|
+
odbc_conn_str_connection(config)
|
28
|
+
else
|
29
|
+
raise ArgumentError, 'No data source name (:dsn) or connection string (:conn_str) specified.'
|
30
|
+
end
|
31
|
+
|
32
|
+
database_metadata = ::ODBCAdapter::DatabaseMetadata.new(connection, config[:encoding_bug])
|
33
|
+
::ODBCAdapter::Db2Adapter.new(connection, logger, config, database_metadata)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Connect using a predefined DSN.
|
39
|
+
def odbc_dsn_connection(config)
|
40
|
+
username = config[:username] ? config[:username].to_s : nil
|
41
|
+
password = config[:password] ? config[:password].to_s : nil
|
42
|
+
odbc_module = config[:encoding] == 'utf8' ? ODBC_UTF8 : ODBC
|
43
|
+
connection = odbc_module.connect(config[:dsn], username, password)
|
44
|
+
|
45
|
+
# encoding_bug indicates that the driver is using non ASCII and has the issue referenced here https://github.com/larskanis/ruby-odbc/issues/2
|
46
|
+
[connection, config.merge(username: username, password: password, encoding_bug: config[:encoding] == 'utf8')]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Connect using ODBC connection string
|
50
|
+
# Supports DSN-based or DSN-less connections
|
51
|
+
# e.g. "DSN=virt5;UID=rails;PWD=rails"
|
52
|
+
# "DRIVER={OpenLink Virtuoso};HOST=carlmbp;UID=rails;PWD=rails"
|
53
|
+
def odbc_conn_str_connection(config)
|
54
|
+
attrs = config[:conn_str].split(';').map { |option| option.split('=', 2) }.to_h
|
55
|
+
odbc_module = attrs['ENCODING'] == 'utf8' ? ODBC_UTF8 : ODBC
|
56
|
+
driver = odbc_module::Driver.new
|
57
|
+
driver.name = 'odbc'
|
58
|
+
driver.attrs = attrs
|
59
|
+
|
60
|
+
connection = odbc_module::Database.new.drvconnect(driver)
|
61
|
+
# encoding_bug indicates that the driver is using non ASCII and has the issue referenced here https://github.com/larskanis/ruby-odbc/issues/2
|
62
|
+
[connection, config.merge(driver: driver, encoding: attrs['ENCODING'], encoding_bug: attrs['ENCODING'] == 'utf8')]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module ConnectionAdapters
|
68
|
+
class ODBCAdapter < AbstractAdapter
|
69
|
+
include ::ODBCAdapter::DatabaseLimits
|
70
|
+
include ::ODBCAdapter::DatabaseStatements
|
71
|
+
include ::ODBCAdapter::Quoting
|
72
|
+
include ::ODBCAdapter::SchemaStatements
|
73
|
+
|
74
|
+
ADAPTER_NAME = 'ODBC'.freeze
|
75
|
+
BOOLEAN_TYPE = 'BOOLEAN'.freeze
|
76
|
+
|
77
|
+
ERR_DUPLICATE_KEY_VALUE = 23_505
|
78
|
+
ERR_QUERY_TIMED_OUT = 57_014
|
79
|
+
ERR_QUERY_TIMED_OUT_MESSAGE = /Query has timed out/
|
80
|
+
ERR_CONNECTION_FAILED_REGEX = '^08[0S]0[12347]'.freeze
|
81
|
+
ERR_CONNECTION_FAILED_MESSAGE = /Client connection failed/
|
82
|
+
|
83
|
+
# The object that stores the information that is fetched from the DBMS
|
84
|
+
# when a connection is first established.
|
85
|
+
attr_reader :database_metadata
|
86
|
+
|
87
|
+
def initialize(connection, logger, config, database_metadata)
|
88
|
+
configure_time_options(connection)
|
89
|
+
super(connection, logger, config)
|
90
|
+
@database_metadata = database_metadata
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns the human-readable name of the adapter.
|
94
|
+
def adapter_name
|
95
|
+
ADAPTER_NAME
|
96
|
+
end
|
97
|
+
|
98
|
+
# Does this adapter support migrations? Backend specific, as the abstract
|
99
|
+
# adapter always returns +false+.
|
100
|
+
def supports_migrations?
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
# CONNECTION MANAGEMENT ====================================
|
105
|
+
|
106
|
+
# Checks whether the connection to the database is still active. This
|
107
|
+
# includes checking whether the database is actually capable of
|
108
|
+
# responding, i.e. whether the connection isn't stale.
|
109
|
+
def active?
|
110
|
+
@connection.connected?
|
111
|
+
end
|
112
|
+
|
113
|
+
# Disconnects from the database if already connected, and establishes a
|
114
|
+
# new connection with the database.
|
115
|
+
def reconnect!
|
116
|
+
disconnect!
|
117
|
+
odbc_module = @config[:encoding] == 'utf8' ? ODBC_UTF8 : ODBC
|
118
|
+
@connection =
|
119
|
+
if @config.key?(:dsn)
|
120
|
+
odbc_module.connect(@config[:dsn], @config[:username], @config[:password])
|
121
|
+
else
|
122
|
+
odbc_module::Database.new.drvconnect(@config[:driver])
|
123
|
+
end
|
124
|
+
configure_time_options(@connection)
|
125
|
+
super
|
126
|
+
end
|
127
|
+
alias reset! reconnect!
|
128
|
+
|
129
|
+
# Disconnects from the database if already connected. Otherwise, this
|
130
|
+
# method does nothing.
|
131
|
+
def disconnect!
|
132
|
+
@connection.disconnect if @connection.connected?
|
133
|
+
end
|
134
|
+
|
135
|
+
# Build a new column object from the given options. Effectively the same
|
136
|
+
# as super except that it also passes in the native type.
|
137
|
+
# rubocop:disable Metrics/ParameterLists
|
138
|
+
def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil, native_type = nil)
|
139
|
+
::ODBCAdapter::Column.new(name, default, sql_type_metadata, null, table_name, default_function, collation, native_type)
|
140
|
+
end
|
141
|
+
|
142
|
+
protected
|
143
|
+
|
144
|
+
# Build the type map for ActiveRecord
|
145
|
+
# Here, ODBC and ODBC_UTF8 constants are interchangeable
|
146
|
+
def initialize_type_map(map)
|
147
|
+
map.register_type 'boolean', Type::Boolean.new
|
148
|
+
map.register_type ODBC::SQL_CHAR, Type::String.new
|
149
|
+
map.register_type ODBC::SQL_LONGVARCHAR, Type::Text.new
|
150
|
+
map.register_type ODBC::SQL_TINYINT, Type::Integer.new(limit: 4)
|
151
|
+
map.register_type ODBC::SQL_SMALLINT, Type::Integer.new(limit: 8)
|
152
|
+
map.register_type ODBC::SQL_INTEGER, Type::Integer.new(limit: 16)
|
153
|
+
map.register_type ODBC::SQL_BIGINT, Type::BigInteger.new(limit: 32)
|
154
|
+
map.register_type ODBC::SQL_REAL, Type::Float.new(limit: 24)
|
155
|
+
map.register_type ODBC::SQL_FLOAT, Type::Float.new
|
156
|
+
map.register_type ODBC::SQL_DOUBLE, Type::Float.new(limit: 53)
|
157
|
+
map.register_type ODBC::SQL_DECIMAL, Type::Float.new
|
158
|
+
map.register_type ODBC::SQL_NUMERIC, Type::Integer.new
|
159
|
+
map.register_type ODBC::SQL_BINARY, Type::Binary.new
|
160
|
+
map.register_type ODBC::SQL_DATE, Type::Date.new
|
161
|
+
map.register_type ODBC::SQL_DATETIME, Type::DateTime.new
|
162
|
+
map.register_type ODBC::SQL_TIME, Type::Time.new
|
163
|
+
map.register_type ODBC::SQL_TIMESTAMP, Type::DateTime.new
|
164
|
+
map.register_type ODBC::SQL_GUID, Type::String.new
|
165
|
+
|
166
|
+
alias_type map, ODBC::SQL_BIT, 'boolean'
|
167
|
+
alias_type map, ODBC::SQL_VARCHAR, ODBC::SQL_CHAR
|
168
|
+
alias_type map, ODBC::SQL_WCHAR, ODBC::SQL_CHAR
|
169
|
+
alias_type map, ODBC::SQL_WVARCHAR, ODBC::SQL_CHAR
|
170
|
+
alias_type map, ODBC::SQL_WLONGVARCHAR, ODBC::SQL_LONGVARCHAR
|
171
|
+
alias_type map, ODBC::SQL_VARBINARY, ODBC::SQL_BINARY
|
172
|
+
alias_type map, ODBC::SQL_LONGVARBINARY, ODBC::SQL_BINARY
|
173
|
+
alias_type map, ODBC::SQL_TYPE_DATE, ODBC::SQL_DATE
|
174
|
+
alias_type map, ODBC::SQL_TYPE_TIME, ODBC::SQL_TIME
|
175
|
+
alias_type map, ODBC::SQL_TYPE_TIMESTAMP, ODBC::SQL_TIMESTAMP
|
176
|
+
end
|
177
|
+
|
178
|
+
# Translate an exception from the native DBMS to something usable by
|
179
|
+
# ActiveRecord.
|
180
|
+
def translate_exception(exception, message)
|
181
|
+
error_number = exception.message[/^\d+/].to_i
|
182
|
+
|
183
|
+
if error_number == ERR_DUPLICATE_KEY_VALUE
|
184
|
+
ActiveRecord::RecordNotUnique.new(message, exception)
|
185
|
+
elsif error_number == ERR_QUERY_TIMED_OUT || exception.message =~ ERR_QUERY_TIMED_OUT_MESSAGE
|
186
|
+
::ODBCAdapter::QueryTimeoutError.new(message, exception)
|
187
|
+
elsif exception.message.match(ERR_CONNECTION_FAILED_REGEX) || exception.message =~ ERR_CONNECTION_FAILED_MESSAGE
|
188
|
+
begin
|
189
|
+
reconnect!
|
190
|
+
::ODBCAdapter::ConnectionFailedError.new(message, exception)
|
191
|
+
rescue => e
|
192
|
+
puts "unable to reconnect #{e}"
|
193
|
+
end
|
194
|
+
else
|
195
|
+
super
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
# Can't use the built-in ActiveRecord map#alias_type because it doesn't
|
202
|
+
# work with non-string keys, and in our case the keys are (almost) all
|
203
|
+
# numeric
|
204
|
+
def alias_type(map, new_type, old_type)
|
205
|
+
map.register_type(new_type) do |_, *args|
|
206
|
+
map.lookup(old_type, *args)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Ensure ODBC is mapping time-based fields to native ruby objects
|
211
|
+
def configure_time_options(connection)
|
212
|
+
connection.use_time = true
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
require "odbc_adapter/db2_adapter"
|
data/lib/odbc_adapter.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module ODBCAdapter
|
2
|
+
class Column < ActiveRecord::ConnectionAdapters::Column
|
3
|
+
attr_reader :native_type
|
4
|
+
|
5
|
+
# Add the native_type accessor to allow the native DBMS to report back what
|
6
|
+
# it uses to represent the column internally.
|
7
|
+
# rubocop:disable Metrics/ParameterLists
|
8
|
+
def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, native_type = nil, default_function = nil, collation = nil)
|
9
|
+
super(name, default, sql_type_metadata, null, table_name)
|
10
|
+
@native_type = native_type
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module ODBCAdapter
|
2
|
+
class ColumnMetadata
|
3
|
+
GENERICS = {
|
4
|
+
primary_key: [ODBC::SQL_INTEGER, ODBC::SQL_SMALLINT],
|
5
|
+
string: [ODBC::SQL_VARCHAR],
|
6
|
+
text: [ODBC::SQL_LONGVARCHAR, ODBC::SQL_VARCHAR],
|
7
|
+
integer: [ODBC::SQL_INTEGER, ODBC::SQL_SMALLINT],
|
8
|
+
decimal: [ODBC::SQL_NUMERIC, ODBC::SQL_DECIMAL],
|
9
|
+
float: [ODBC::SQL_DOUBLE, ODBC::SQL_REAL],
|
10
|
+
datetime: [ODBC::SQL_TYPE_TIMESTAMP, ODBC::SQL_TIMESTAMP],
|
11
|
+
timestamp: [ODBC::SQL_TYPE_TIMESTAMP, ODBC::SQL_TIMESTAMP],
|
12
|
+
time: [ODBC::SQL_TYPE_TIME, ODBC::SQL_TIME, ODBC::SQL_TYPE_TIMESTAMP, ODBC::SQL_TIMESTAMP],
|
13
|
+
date: [ODBC::SQL_TYPE_DATE, ODBC::SQL_DATE, ODBC::SQL_TYPE_TIMESTAMP, ODBC::SQL_TIMESTAMP],
|
14
|
+
binary: [ODBC::SQL_LONGVARBINARY, ODBC::SQL_VARBINARY],
|
15
|
+
boolean: [ODBC::SQL_BIT, ODBC::SQL_TINYINT, ODBC::SQL_SMALLINT, ODBC::SQL_INTEGER]
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
attr_reader :adapter
|
19
|
+
|
20
|
+
def initialize(adapter)
|
21
|
+
@adapter = adapter
|
22
|
+
end
|
23
|
+
|
24
|
+
def native_database_types
|
25
|
+
grouped = reported_types.group_by { |row| row[1] }
|
26
|
+
|
27
|
+
GENERICS.each_with_object({}) do |(abstract, candidates), mapped|
|
28
|
+
candidates.detect do |candidate|
|
29
|
+
next unless grouped[candidate]
|
30
|
+
mapped[abstract] = native_type_mapping(abstract, grouped[candidate])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Creates a Hash describing a mapping from an abstract type to a
|
38
|
+
# DBMS native type for use by #native_database_types
|
39
|
+
def native_type_mapping(abstract, rows)
|
40
|
+
# The appropriate SQL for :primary_key is hard to derive as
|
41
|
+
# ODBC doesn't provide any info on a DBMS's native syntax for
|
42
|
+
# autoincrement columns. So we use a lookup instead.
|
43
|
+
return adapter.class::PRIMARY_KEY if abstract == :primary_key
|
44
|
+
selected_row = rows[0]
|
45
|
+
|
46
|
+
# If more than one native type corresponds to the SQL type we're
|
47
|
+
# handling, the type in the first descriptor should be the
|
48
|
+
# best match, because the ODBC specification states that
|
49
|
+
# SQLGetTypeInfo returns the results ordered by SQL type and then by
|
50
|
+
# how closely the native type maps to that SQL type.
|
51
|
+
# But, for :text and :binary, select the native type with the
|
52
|
+
# largest capacity. (Compare SQLGetTypeInfo:COLUMN_SIZE values)
|
53
|
+
selected_row = rows.max_by { |row| row[2] } if %i[text binary].include?(abstract)
|
54
|
+
result = { name: selected_row[0] } # SQLGetTypeInfo: TYPE_NAME
|
55
|
+
|
56
|
+
create_params = selected_row[5]
|
57
|
+
# Depending on the column type, the CREATE_PARAMS keywords can
|
58
|
+
# include length, precision or scale.
|
59
|
+
if create_params && !create_params.strip.empty? && abstract != :decimal
|
60
|
+
result[:limit] = selected_row[2] # SQLGetTypeInfo: COL_SIZE
|
61
|
+
end
|
62
|
+
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
def reported_types
|
67
|
+
@reported_types ||=
|
68
|
+
begin
|
69
|
+
stmt = adapter.raw_connection.types
|
70
|
+
stmt.fetch_all
|
71
|
+
ensure
|
72
|
+
stmt.drop unless stmt.nil?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module ODBCAdapter
|
2
|
+
module DatabaseLimits
|
3
|
+
# Returns the maximum length of a table name.
|
4
|
+
def table_alias_length
|
5
|
+
max_identifier_length = database_metadata.max_identifier_len
|
6
|
+
max_table_name_length = database_metadata.max_table_name_len
|
7
|
+
[max_identifier_length, max_table_name_length].max
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ODBCAdapter
|
2
|
+
# Caches SQLGetInfo output
|
3
|
+
class DatabaseMetadata
|
4
|
+
FIELDS = %i[
|
5
|
+
SQL_DBMS_NAME
|
6
|
+
SQL_DBMS_VER
|
7
|
+
SQL_IDENTIFIER_CASE
|
8
|
+
SQL_QUOTED_IDENTIFIER_CASE
|
9
|
+
SQL_IDENTIFIER_QUOTE_CHAR
|
10
|
+
SQL_MAX_IDENTIFIER_LEN
|
11
|
+
SQL_MAX_TABLE_NAME_LEN
|
12
|
+
SQL_USER_NAME
|
13
|
+
SQL_DATABASE_NAME
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
attr_reader :values
|
17
|
+
|
18
|
+
# has_encoding_bug refers to https://github.com/larskanis/ruby-odbc/issues/2 where ruby-odbc in UTF8 mode
|
19
|
+
# returns incorrectly encoded responses to getInfo
|
20
|
+
def initialize(connection, has_encoding_bug = false)
|
21
|
+
@values = Hash[FIELDS.map do |field|
|
22
|
+
info = connection.get_info(ODBC.const_get(field))
|
23
|
+
info = info.encode(Encoding.default_external, 'UTF-16LE') if info.is_a?(String) && has_encoding_bug
|
24
|
+
[field, info]
|
25
|
+
end]
|
26
|
+
end
|
27
|
+
|
28
|
+
def upcase_identifiers?
|
29
|
+
@upcase_identifiers ||= (identifier_case == ODBC::SQL_IC_UPPER)
|
30
|
+
end
|
31
|
+
|
32
|
+
# A little bit of metaprogramming magic here to create accessors for each of
|
33
|
+
# the fields reported on by the DBMS.
|
34
|
+
FIELDS.each do |field|
|
35
|
+
define_method(field.to_s.downcase.gsub('sql_', '')) do
|
36
|
+
value_for(field)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def value_for(field)
|
43
|
+
values[field]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module ODBCAdapter
|
2
|
+
module DatabaseStatements
|
3
|
+
# ODBC constants missing from Christian Werner's Ruby ODBC driver
|
4
|
+
SQL_NO_NULLS = 0
|
5
|
+
SQL_NULLABLE = 1
|
6
|
+
SQL_NULLABLE_UNKNOWN = 2
|
7
|
+
|
8
|
+
# Executes the SQL statement in the context of this connection.
|
9
|
+
# Returns the number of rows affected.
|
10
|
+
def execute(sql, name = nil, binds = [])
|
11
|
+
log(sql, name) do
|
12
|
+
if prepared_statements
|
13
|
+
@connection.do(sql, *prepared_binds(binds))
|
14
|
+
else
|
15
|
+
@connection.do(sql)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Executes +sql+ statement in the context of this connection using
|
21
|
+
# +binds+ as the bind substitutes. +name+ is logged along with
|
22
|
+
# the executed +sql+ statement.
|
23
|
+
def exec_query(sql, name = 'SQL', binds = [], prepare: false) # rubocop:disable Lint/UnusedMethodArgument
|
24
|
+
log(sql, name) do
|
25
|
+
stmt =
|
26
|
+
if prepared_statements
|
27
|
+
@connection.run(sql, *prepared_binds(binds))
|
28
|
+
else
|
29
|
+
@connection.run(sql)
|
30
|
+
end
|
31
|
+
|
32
|
+
columns = stmt.columns
|
33
|
+
values = stmt.to_a
|
34
|
+
stmt.drop
|
35
|
+
|
36
|
+
values = dbms_type_cast(columns.values, values)
|
37
|
+
column_names = columns.keys.map { |key| format_case(key) }
|
38
|
+
ActiveRecord::Result.new(column_names, values)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Executes delete +sql+ statement in the context of this connection using
|
43
|
+
# +binds+ as the bind substitutes. +name+ is logged along with
|
44
|
+
# the executed +sql+ statement.
|
45
|
+
def exec_delete(sql, name, binds)
|
46
|
+
execute(sql, name, binds)
|
47
|
+
end
|
48
|
+
alias exec_update exec_delete
|
49
|
+
|
50
|
+
# Begins the transaction (and turns off auto-committing).
|
51
|
+
def begin_db_transaction
|
52
|
+
@connection.autocommit = false
|
53
|
+
end
|
54
|
+
|
55
|
+
# Commits the transaction (and turns on auto-committing).
|
56
|
+
def commit_db_transaction
|
57
|
+
@connection.commit
|
58
|
+
@connection.autocommit = true
|
59
|
+
end
|
60
|
+
|
61
|
+
# Rolls back the transaction (and turns on auto-committing). Must be
|
62
|
+
# done if the transaction block raises an exception or returns false.
|
63
|
+
def exec_rollback_db_transaction
|
64
|
+
@connection.rollback
|
65
|
+
@connection.autocommit = true
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the default sequence name for a table.
|
69
|
+
# Used for databases which don't support an autoincrementing column
|
70
|
+
# type, but do support sequences.
|
71
|
+
def default_sequence_name(table, _column)
|
72
|
+
"#{table}_seq"
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# A custom hook to allow end users to overwrite the type casting before it
|
78
|
+
# is returned to ActiveRecord. Useful before a full adapter has made its way
|
79
|
+
# back into this repository.
|
80
|
+
def dbms_type_cast(_columns, values)
|
81
|
+
values
|
82
|
+
end
|
83
|
+
|
84
|
+
# Assume received identifier is in DBMS's data dictionary case.
|
85
|
+
def format_case(identifier)
|
86
|
+
if database_metadata.upcase_identifiers?
|
87
|
+
identifier =~ /[a-z]/ ? identifier : identifier.downcase
|
88
|
+
else
|
89
|
+
identifier
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# In general, ActiveRecord uses lowercase attribute names. This may
|
94
|
+
# conflict with the database's data dictionary case.
|
95
|
+
#
|
96
|
+
# The ODBCAdapter uses the following conventions for databases
|
97
|
+
# which report SQL_IDENTIFIER_CASE = SQL_IC_UPPER:
|
98
|
+
# * if a name is returned from the DBMS in all uppercase, convert it
|
99
|
+
# to lowercase before returning it to ActiveRecord.
|
100
|
+
# * if a name is returned from the DBMS in lowercase or mixed case,
|
101
|
+
# assume the underlying schema object's name was quoted when
|
102
|
+
# the schema object was created. Leave the name untouched before
|
103
|
+
# returning it to ActiveRecord.
|
104
|
+
# * before making an ODBC catalog call, if a supplied identifier is all
|
105
|
+
# lowercase, convert it to uppercase. Leave mixed case or all
|
106
|
+
# uppercase identifiers unchanged.
|
107
|
+
# * columns created with quoted lowercase names are not supported.
|
108
|
+
#
|
109
|
+
# Converts an identifier to the case conventions used by the DBMS.
|
110
|
+
# Assume received identifier is in ActiveRecord case.
|
111
|
+
def native_case(identifier)
|
112
|
+
if database_metadata.upcase_identifiers?
|
113
|
+
identifier =~ /[A-Z]/ ? identifier : identifier.upcase
|
114
|
+
else
|
115
|
+
identifier
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Assume column is nullable if nullable == SQL_NULLABLE_UNKNOWN
|
120
|
+
def nullability(col_name, is_nullable, nullable)
|
121
|
+
not_nullable = (!is_nullable || !nullable.to_s.match('NO').nil?)
|
122
|
+
result = !(not_nullable || nullable == SQL_NO_NULLS)
|
123
|
+
|
124
|
+
# HACK!
|
125
|
+
# MySQL native ODBC driver doesn't report nullability accurately.
|
126
|
+
# So force nullability of 'id' columns
|
127
|
+
col_name == 'id' ? false : result
|
128
|
+
end
|
129
|
+
|
130
|
+
def prepared_binds(binds)
|
131
|
+
prepare_binds_for_database(binds).map { |bind| _type_cast(bind) }
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ODBCAdapter
|
2
|
+
class Db2Adapter < ActiveRecord::ConnectionAdapters::ODBCAdapter
|
3
|
+
|
4
|
+
def arel_visitor
|
5
|
+
Arel::Visitors::IBM_DB.new(self)
|
6
|
+
end
|
7
|
+
|
8
|
+
def prepared_statements
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
12
|
+
def supports_migrations?
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ODBCAdapter
|
2
|
+
module Quoting
|
3
|
+
# Quotes a string, escaping any ' (single quote) characters.
|
4
|
+
def quote_string(string)
|
5
|
+
string.gsub(/\'/, "''")
|
6
|
+
end
|
7
|
+
|
8
|
+
# Returns a quoted form of the column name.
|
9
|
+
def quote_column_name(name)
|
10
|
+
name = name.to_s
|
11
|
+
quote_char = database_metadata.identifier_quote_char.to_s.strip
|
12
|
+
|
13
|
+
return name if quote_char.length.zero?
|
14
|
+
quote_char = quote_char[0]
|
15
|
+
|
16
|
+
# Avoid quoting any already quoted name
|
17
|
+
return name if name[0] == quote_char && name[-1] == quote_char
|
18
|
+
|
19
|
+
# If upcase identifiers, only quote mixed case names.
|
20
|
+
if database_metadata.upcase_identifiers?
|
21
|
+
return name unless name =~ /([A-Z]+[a-z])|([a-z]+[A-Z])/
|
22
|
+
end
|
23
|
+
|
24
|
+
"#{quote_char.chr}#{name}#{quote_char.chr}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Ideally, we'd return an ODBC date or timestamp literal escape
|
28
|
+
# sequence, but not all ODBC drivers support them.
|
29
|
+
def quoted_date(value)
|
30
|
+
if value.acts_like?(:time)
|
31
|
+
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
|
32
|
+
|
33
|
+
if value.respond_to?(zone_conversion_method)
|
34
|
+
value = value.send(zone_conversion_method)
|
35
|
+
end
|
36
|
+
value.strftime('%Y-%m-%d %H:%M:%S') # Time, DateTime
|
37
|
+
else
|
38
|
+
value.strftime('%Y-%m-%d') # Date
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module ODBCAdapter
|
2
|
+
module SchemaStatements
|
3
|
+
# Returns a Hash of mappings from the abstract data types to the native
|
4
|
+
# database types. See TableDefinition#column for details on the recognized
|
5
|
+
# abstract data types.
|
6
|
+
def native_database_types
|
7
|
+
@native_database_types ||= ColumnMetadata.new(self).native_database_types
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns an array of table names, for database tables visible on the
|
11
|
+
# current connection.
|
12
|
+
def tables(_name = nil)
|
13
|
+
stmt = @connection.tables
|
14
|
+
result = stmt.fetch_all || []
|
15
|
+
stmt.drop
|
16
|
+
|
17
|
+
result.each_with_object([]) do |row, table_names|
|
18
|
+
schema_name, table_name, table_type = row[1..3]
|
19
|
+
next if respond_to?(:table_filtered?) && table_filtered?(schema_name, table_type)
|
20
|
+
table_names << format_case(table_name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns an array of view names defined in the database.
|
25
|
+
def views
|
26
|
+
[]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns an array of indexes for the given table.
|
30
|
+
def indexes(table_name, _name = nil)
|
31
|
+
stmt = @connection.indexes(native_case(table_name.to_s))
|
32
|
+
result = stmt.fetch_all || []
|
33
|
+
stmt.drop unless stmt.nil?
|
34
|
+
|
35
|
+
index_cols = []
|
36
|
+
index_name = nil
|
37
|
+
unique = nil
|
38
|
+
|
39
|
+
result.each_with_object([]).with_index do |(row, indices), row_idx|
|
40
|
+
# Skip table statistics
|
41
|
+
next if row[6].zero? # SQLStatistics: TYPE
|
42
|
+
|
43
|
+
if row[7] == 1 # SQLStatistics: ORDINAL_POSITION
|
44
|
+
# Start of column descriptor block for next index
|
45
|
+
index_cols = []
|
46
|
+
unique = row[3].zero? # SQLStatistics: NON_UNIQUE
|
47
|
+
index_name = String.new(row[5]) # SQLStatistics: INDEX_NAME
|
48
|
+
end
|
49
|
+
|
50
|
+
index_cols << format_case(row[8]) # SQLStatistics: COLUMN_NAME
|
51
|
+
next_row = result[row_idx + 1]
|
52
|
+
|
53
|
+
if (row_idx == result.length - 1) || (next_row[6].zero? || next_row[7] == 1)
|
54
|
+
indices << ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, format_case(index_name), unique, index_cols)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns an array of Column objects for the table specified by
|
60
|
+
# +table_name+.
|
61
|
+
def columns(table_name, _name = nil)
|
62
|
+
stmt = @connection.columns(native_case(table_name.to_s))
|
63
|
+
result = stmt.fetch_all || []
|
64
|
+
stmt.drop
|
65
|
+
|
66
|
+
result.each_with_object([]) do |col, cols|
|
67
|
+
col_name = col[3] # SQLColumns: COLUMN_NAME
|
68
|
+
col_default = col[12] # SQLColumns: COLUMN_DEF
|
69
|
+
col_sql_type = col[4] # SQLColumns: DATA_TYPE
|
70
|
+
col_native_type = col[5] # SQLColumns: TYPE_NAME
|
71
|
+
col_limit = col[6] # SQLColumns: COLUMN_SIZE
|
72
|
+
col_scale = col[8] # SQLColumns: DECIMAL_DIGITS
|
73
|
+
|
74
|
+
# SQLColumns: IS_NULLABLE, SQLColumns: NULLABLE
|
75
|
+
col_nullable = nullability(col_name, col[17], col[10])
|
76
|
+
|
77
|
+
args = { sql_type: col_sql_type, type: col_sql_type, limit: col_limit }
|
78
|
+
args[:sql_type] = 'boolean' if col_native_type == self.class::BOOLEAN_TYPE
|
79
|
+
|
80
|
+
if [ODBC::SQL_DECIMAL, ODBC::SQL_NUMERIC].include?(col_sql_type)
|
81
|
+
args[:scale] = col_scale || 0
|
82
|
+
args[:precision] = col_limit
|
83
|
+
end
|
84
|
+
sql_type_metadata = ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(**args)
|
85
|
+
|
86
|
+
cols << new_column(format_case(col_name), col_default, sql_type_metadata, col_nullable, table_name, col_native_type)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns just a table's primary key
|
91
|
+
def primary_key(table_name)
|
92
|
+
stmt = @connection.primary_keys(native_case(table_name.to_s))
|
93
|
+
result = stmt.fetch_all || []
|
94
|
+
stmt.drop unless stmt.nil?
|
95
|
+
result[0] && result[0][3]
|
96
|
+
end
|
97
|
+
|
98
|
+
def foreign_keys(table_name)
|
99
|
+
stmt = @connection.foreign_keys(native_case(table_name.to_s))
|
100
|
+
result = stmt.fetch_all || []
|
101
|
+
stmt.drop unless stmt.nil?
|
102
|
+
|
103
|
+
result.map do |key|
|
104
|
+
fk_from_table = key[2] # PKTABLE_NAME
|
105
|
+
fk_to_table = key[6] # FKTABLE_NAME
|
106
|
+
|
107
|
+
ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
|
108
|
+
fk_from_table,
|
109
|
+
fk_to_table,
|
110
|
+
name: key[11], # FK_NAME
|
111
|
+
column: key[3], # PKCOLUMN_NAME
|
112
|
+
primary_key: key[7], # FKCOLUMN_NAME
|
113
|
+
on_delete: key[10], # DELETE_RULE
|
114
|
+
on_update: key[9] # UPDATE_RULE
|
115
|
+
)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Ensure it's shorter than the maximum identifier length for the current
|
120
|
+
# dbms
|
121
|
+
def index_name(table_name, options)
|
122
|
+
maximum = database_metadata.max_identifier_len || 255
|
123
|
+
super(table_name, options)[0...maximum]
|
124
|
+
end
|
125
|
+
|
126
|
+
def current_database
|
127
|
+
byebug
|
128
|
+
database_metadata.database_name.strip
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: db2_odbc_adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yohanes
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-04-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ruby-odbc
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.99999'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.99999'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- yohanes.lumentut@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".gitignore"
|
35
|
+
- Gemfile
|
36
|
+
- LICENSE
|
37
|
+
- README.md
|
38
|
+
- db2_odbc_adapter.gemspec
|
39
|
+
- lib/active_record/connection_adapters/odbc_adapter.rb
|
40
|
+
- lib/odbc_adapter.rb
|
41
|
+
- lib/odbc_adapter/column.rb
|
42
|
+
- lib/odbc_adapter/column_metadata.rb
|
43
|
+
- lib/odbc_adapter/database_limits.rb
|
44
|
+
- lib/odbc_adapter/database_metadata.rb
|
45
|
+
- lib/odbc_adapter/database_statements.rb
|
46
|
+
- lib/odbc_adapter/db2_adapter.rb
|
47
|
+
- lib/odbc_adapter/error.rb
|
48
|
+
- lib/odbc_adapter/quoting.rb
|
49
|
+
- lib/odbc_adapter/schema_statements.rb
|
50
|
+
- lib/odbc_adapter/version.rb
|
51
|
+
homepage: https://github.com/yohaneslumentut/db2_odbc_adapter
|
52
|
+
licenses:
|
53
|
+
- MIT
|
54
|
+
metadata: {}
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
requirements: []
|
70
|
+
rubygems_version: 3.0.3
|
71
|
+
signing_key:
|
72
|
+
specification_version: 4
|
73
|
+
summary: An ActiveRecord DB2 ODBC adapter
|
74
|
+
test_files: []
|