db2_odbc_adapter 0.0.1
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.
- 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: []
|