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 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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'activerecord', '6.0.2.2'
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"
@@ -0,0 +1,2 @@
1
+ # Requiring with this pattern to mirror ActiveRecord
2
+ require 'active_record/connection_adapters/odbc_adapter'
@@ -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,6 @@
1
+ module ODBCAdapter
2
+ class QueryTimeoutError < ActiveRecord::StatementInvalid
3
+ end
4
+ class ConnectionFailedError < ActiveRecord::StatementInvalid
5
+ end
6
+ 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
@@ -0,0 +1,3 @@
1
+ module ODBCAdapter
2
+ VERSION = '0.0.1'.freeze
3
+ 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: []