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 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: []