Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull lots of fixes and improvements from Springbuk's fork #1

Merged
merged 69 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
d36c64e
Tests are passing in Docker container
bwarminski Jan 15, 2019
da38fcb
Merge pull request #28 from localytics/docker-tests
bwarminski Jan 16, 2019
7e27df4
Reenable Rubocop, add encoding config parameter (#29)
bwarminski Jan 17, 2019
8e0b7b3
Handle connection failure by trying to reconnect and translating the …
mshearer76 Apr 5, 2019
96bbdfe
Bump version
mshearer76 Apr 5, 2019
b285f15
Merge pull request #32 from localytics/mshearer_expired_token
mshearer76 Apr 5, 2019
95c3965
Change gemspecs and gemfile
Shehbaz Sep 1, 2020
af47f3d
Upgrade to rails5.2.1
Shehbaz Sep 2, 2020
abdfd45
Upgrade to rails 6
Shehbaz Sep 29, 2020
b51a593
Backward compatibility
Shehbaz Sep 29, 2020
7a43f84
Update bind params
Shehbaz Sep 29, 2020
7c56bcc
Change params for Column class initializer
Shehbaz Oct 8, 2020
bf7a016
Support json type field
Shehbaz Oct 16, 2020
2696c7a
Support json and date datatype
Shehbaz Oct 29, 2020
fb7aa4a
Typo fix
Shehbaz Oct 29, 2020
bade9d9
Merge pull request #1 from patterninc/upgrade-to-rails6
dracco1993 Jun 14, 2021
6722dc7
Multi-database support
bfredenburg Aug 25, 2021
bf065a8
[APP-2051] Update Column#initialize to pass on **kwargs (rails 6.1 co…
Dec 2, 2021
4ffba40
Merge pull request #2 from springbuk/app_2051_updates_column_initiali…
ccflack Dec 2, 2021
7df4b1d
Update schema_statements.rb
bfredenburg Feb 1, 2022
7f0a521
Merge pull request #3 from springbuk/MOJ-58_limit_db_info_to_current_…
JaredAlford Feb 1, 2022
86c96b7
Corrections for handling names
bfredenburg Feb 10, 2022
758f81e
Merge pull request #4 from springbuk/MOJ-92_schema_statements_correct…
JaredAlford Feb 10, 2022
9b8b8de
Oops
bfredenburg Feb 11, 2022
489b079
Merge pull request #5 from springbuk/MOJ-93_correct_columns
JaredAlford Feb 11, 2022
ef14e72
Changes because arg list to ActiveRecord::ConnectionAdapters::Column.…
bfredenburg Feb 16, 2022
3d5481c
Change to return primary key name as lowercase
bfredenburg Feb 16, 2022
715f9e7
Changes for columns function to return better types
bfredenburg Feb 16, 2022
b382182
Merge pull request #6 from springbuk/MOJ-95_columns_corrections
dracco1993 Feb 18, 2022
b47df43
[APP-2295] Update signature of translate_exception for ruby 3
rcrenshaw Mar 3, 2022
bc94dda
Merge pull request #7 from springbuk/APP-2295-fix-for-ruby-3
rcrenshaw Mar 3, 2022
e317229
Correct column default values
bfredenburg Mar 4, 2022
8cea2c9
Merge pull request #8 from springbuk/MOJ-98_correct_column_default_va…
JaredAlford Mar 4, 2022
87f077c
Add better return types
dracco1993 Mar 11, 2022
041ae82
Clean up dev code
dracco1993 Mar 11, 2022
9b6fdb2
Add better handling of other types of null
dracco1993 Mar 14, 2022
88a6537
Merge pull request #9 from springbuk/moj_97_fix_odbc_adapter_returned…
bfredenburg Mar 16, 2022
dfeefcd
Claims page was broken, discovered the bug is in the ODBC Adapter.
ACerka-Springbuk Mar 17, 2022
7713fcb
Changes to eliminate garbage collection warning
bfredenburg Mar 18, 2022
cc8e3d5
Merge pull request #10 from springbuk/MOJ-121-BugFixesForODBCAdapter
bfredenburg Mar 18, 2022
a008f83
MOJ-137 Can't quote error is caused by active_record not knowing what…
ACerka-Springbuk Apr 11, 2022
75c607f
Merge pull request #12 from springbuk/MOJ-137-FixCantQuoteErrors
ACerka-Springbuk Apr 14, 2022
9beade6
MOJ-150 MOJ-151 Snowflake type mapping & array_of... types for
ACerka-Springbuk Apr 19, 2022
e6b6855
Merge pull request #13 from springbuk/MOJ-150-151-MissingSnowTypesAnd…
ACerka-Springbuk Apr 20, 2022
f906f7a
MOJ-154 This is all of the work to get objects and arrays quoted prop…
ACerka-Springbuk Apr 22, 2022
13d4989
MOJ-156 This is a working methodology for automatically generating IDs.
ACerka-Springbuk Apr 28, 2022
6ddc251
MOJ-156 Removing the AutoIdentified concern. No longer necessary or u…
ACerka-Springbuk Apr 29, 2022
acd567f
MOJ-156 updates from comments
ACerka-Springbuk Apr 29, 2022
2d728f8
Merge pull request #14 from springbuk/MOJ-154-156-InsertingStructured…
ACerka-Springbuk May 2, 2022
31aae0b
MOJ-178 Updates to issues identified when working on MOJ-178
ACerka-Springbuk May 12, 2022
0104f25
MOJ-178 More fixes discovered while working on MOJ-178
ACerka-Springbuk May 13, 2022
8529387
Merge pull request #15 from springbuk/MOJ-178-GetRefreshQuestionsWorking
ACerka-Springbuk May 23, 2022
7f938a2
MOJ-203 Discovered that snowflake returns slightly different types en…
ACerka-Springbuk Jul 22, 2022
3f94eab
changed Integer to bigInteger
markgoudie Aug 2, 2022
5ccedfe
Merge pull request #17 from springbuk/MOJ-224-UpdateToSupportBigInts
ACerka-Springbuk Aug 2, 2022
e82500c
Merge branch 'master' into MOJ-203-InsightWorkerPerRule
ACerka-Springbuk Aug 2, 2022
b047f33
Fix associated record saving
rcrenshaw Aug 2, 2022
dedd931
Merge pull request #16 from springbuk/MOJ-203-InsightWorkerPerRule
ACerka-Springbuk Aug 2, 2022
a49fe3b
Merge pull request #18 from springbuk/MOJ-225-fix-associated-records-…
markgoudie Aug 3, 2022
1c59af4
Moj 450 update with prefetch primary key (#19)
ACerka-Springbuk Dec 19, 2022
83586f4
Do not use bool alias (#21)
bfredenburg Jan 9, 2023
e3ec758
MOJ-366 Two updates for added functionality (#20)
ACerka-Springbuk Jan 9, 2023
dbe56e2
[COR-3018] Support Rails 7 (#22)
bfredenburg Feb 20, 2023
91d9c13
Moj 574 speedup hr uploads processing (#23)
ACerka-Springbuk May 11, 2023
9ef47f6
[MOJ-578] Use new springbuk odbc-ruby gem. (#24)
mbscoggins Jun 2, 2023
6be7bb6
[MOJ-586] Update odbc-ruby gem. (#26)
mbscoggins Jun 15, 2023
8ad3e62
Moj 519 plan years controller (#27)
ACerka-Springbuk Jul 18, 2023
ecd6964
APP-3029 Fixes prune duplicates. All records with a nil primary key g…
ACerka-Springbuk Jul 24, 2023
d3576fd
Revert to ruby-odbc for now
divoxx Aug 11, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
AllCops:
DisplayCopNames: true
DisplayStyleGuide: true
TargetRubyVersion: 2.1
TargetRubyVersion: 2.2
Exclude:
- 'vendor/**/*'

Expand Down
12 changes: 12 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM ruby:2.4.0
MAINTAINER [email protected]

ENV DEBIAN_FRONTEND noninteractive
RUN echo "deb http://deb.debian.org/debian/ jessie main" > /etc/apt/sources.list
RUN echo "deb-src http://deb.debian.org/debian/ jessie main" >> /etc/apt/sources.list
RUN echo "deb http://security.debian.org/ jessie/updates main" >> /etc/apt/sources.list
RUN echo "deb-src http://security.debian.org/ jessie/updates main" >> /etc/apt/sources.list
RUN apt-get update && apt-get -y install libnss3-tools unixodbc-dev libmyodbc mysql-client odbc-postgresql postgresql

WORKDIR /workspace
CMD docker/docker-entrypoint.sh
3 changes: 0 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
source 'https://rubygems.org'

gemspec

gem 'activerecord', '5.0.1'
gem 'pry', '~> 0.11.1'
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@ ActiveRecord models that use this connection will now be connecting to the confi

To run the tests, you'll need the ODBC driver as well as the connection adapter for each database against which you're trying to test. Then run `DSN=MyDatabaseDSN bundle exec rake test` and the test suite will be run by connecting to your database.

## Testing Using a Docker Container Because ODBC on Mac is Hard

Tested on Sierra.


Run from project root:

```
bundle package
docker build -f Dockerfile.dev -t odbc-dev .
# Local mount mysql directory to avoid some permissions problems
mkdir -p /tmp/mysql
docker run -it --rm -v $(pwd):/workspace -v /tmp/mysql:/var/lib/mysql odbc-dev:latest
# In container
docker/test.sh
```

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/localytics/odbc_adapter.
Expand Down
21 changes: 21 additions & 0 deletions docker/docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
set -e -x

# Installing mysql at startup due to file permissions: https://github.com/geerlingguy/drupal-vm/issues/1497
apt-get install -y mysql-server
bundle install --local
service mysql start

# Allows passwordless auth from command line and odbc
sed -i "s/local all postgres peer/local all postgres trust/" /etc/postgresql/9.4/main/pg_hba.conf
sed -i "s/host all all 127.0.0.1\/32 md5/host all all 127.0.0.1\/32 trust/" /etc/postgresql/9.4/main/pg_hba.conf
service postgresql start

odbcinst -i -d -f /usr/share/libmyodbc/odbcinst.ini
mysql -e "DROP DATABASE IF EXISTS odbc_test; CREATE DATABASE IF NOT EXISTS odbc_test;" -uroot
mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost';" -uroot

odbcinst -i -d -f /usr/share/psqlodbc/odbcinst.ini.template
psql -c "CREATE DATABASE odbc_test;" -U postgres

/bin/bash
5 changes: 5 additions & 0 deletions docker/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

echo "Testing mysql" && CONN_STR='DRIVER=MySQL;SERVER=localhost;DATABASE=odbc_test;USER=root;PASSWORD=;' bundle exec rake && \
echo "Testing postgres" && CONN_STR='DRIVER={PostgreSQL ANSI};SERVER=localhost;PORT=5432;DATABASE=odbc_test;UID=postgres;' bundle exec rake && \
echo "Testing postgres utf8" && CONN_STR='DRIVER={PostgreSQL UNICODE};SERVER=localhost;PORT=5432;DATABASE=odbc_test;UID=postgres;ENCODING=utf8' bundle exec rake
133 changes: 83 additions & 50 deletions lib/active_record/connection_adapters/odbc_adapter.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require 'active_record'
require 'arel/visitors/bind_visitor'
require 'odbc'
require 'odbc_utf8'

require 'odbc_adapter/database_limits'
require 'odbc_adapter/database_statements'
Expand All @@ -14,6 +14,9 @@
require 'odbc_adapter/registry'
require 'odbc_adapter/version'

require 'odbc_adapter/type/type'
require 'odbc_adapter/concerns/concern'

module ActiveRecord
class Base
class << self
Expand All @@ -30,7 +33,7 @@ def odbc_connection(config)
raise ArgumentError, 'No data source name (:dsn) or connection string (:conn_str) specified.'
end

database_metadata = ::ODBCAdapter::DatabaseMetadata.new(connection)
database_metadata = ::ODBCAdapter::DatabaseMetadata.new(connection, config[:encoding_bug])
database_metadata.adapter_class.new(connection, logger, config, database_metadata)
end

Expand All @@ -40,21 +43,27 @@ def odbc_connection(config)
def odbc_dsn_connection(config)
username = config[:username] ? config[:username].to_s : nil
password = config[:password] ? config[:password].to_s : nil
connection = ODBC.connect(config[:dsn], username, password)
[connection, config.merge(username: username, password: password)]
odbc_module = config[:encoding] == 'utf8' ? ODBC_UTF8 : ODBC
connection = odbc_module.connect(config[:dsn], username, password)

# encoding_bug indicates that the driver is using non ASCII and has the issue referenced here https://github.com/larskanis/ruby-odbc/issues/2
[connection, config.merge(username: username, password: password, encoding_bug: config[:encoding] == 'utf8')]
end

# Connect using ODBC connection string
# Supports DSN-based or DSN-less connections
# e.g. "DSN=virt5;UID=rails;PWD=rails"
# "DRIVER={OpenLink Virtuoso};HOST=carlmbp;UID=rails;PWD=rails"
def odbc_conn_str_connection(config)
driver = ODBC::Driver.new
attrs = config[:conn_str].split(';').map { |option| option.split('=', 2) }.to_h
odbc_module = attrs['ENCODING'] == 'utf8' ? ODBC_UTF8 : ODBC
driver = odbc_module::Driver.new
driver.name = 'odbc'
driver.attrs = config[:conn_str].split(';').map { |option| option.split('=', 2) }.to_h
driver.attrs = attrs

connection = ODBC::Database.new.drvconnect(driver)
[connection, config.merge(driver: driver)]
connection = odbc_module::Database.new.drvconnect(driver)
# encoding_bug indicates that the driver is using non ASCII and has the issue referenced here https://github.com/larskanis/ruby-odbc/issues/2
[connection, config.merge(driver: driver, encoding: attrs['ENCODING'], encoding_bug: attrs['ENCODING'] == 'utf8')]
end
end
end
Expand All @@ -68,10 +77,15 @@ class ODBCAdapter < AbstractAdapter

ADAPTER_NAME = 'ODBC'.freeze
BOOLEAN_TYPE = 'BOOLEAN'.freeze
VARIANT_TYPE = 'VARIANT'.freeze
DATE_TYPE = 'DATE'.freeze
JSON_TYPE = 'JSON'.freeze

ERR_DUPLICATE_KEY_VALUE = 23_505
ERR_QUERY_TIMED_OUT = 57_014
ERR_QUERY_TIMED_OUT_MESSAGE = /Query has timed out/
ERR_DUPLICATE_KEY_VALUE = 23_505
ERR_QUERY_TIMED_OUT = 57_014
ERR_QUERY_TIMED_OUT_MESSAGE = /Query has timed out/
ERR_CONNECTION_FAILED_REGEX = '^08[0S]0[12347]'.freeze
ERR_CONNECTION_FAILED_MESSAGE = /Client connection failed/

# The object that stores the information that is fetched from the DBMS
# when a connection is first established.
Expand Down Expand Up @@ -107,11 +121,12 @@ def active?
# new connection with the database.
def reconnect!
disconnect!
odbc_module = @config[:encoding] == 'utf8' ? ODBC_UTF8 : ODBC
@connection =
if @config.key?(:dsn)
ODBC.connect(@config[:dsn], @config[:username], @config[:password])
odbc_module.connect(@config[:dsn], @config[:username], @config[:password])
else
ODBC::Database.new.drvconnect(@config[:driver])
odbc_module::Database.new.drvconnect(@config[:driver])
end
configure_time_options(@connection)
super
Expand All @@ -127,54 +142,72 @@ def disconnect!
# Build a new column object from the given options. Effectively the same
# as super except that it also passes in the native type.
# rubocop:disable Metrics/ParameterLists
def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil, native_type = nil)
::ODBCAdapter::Column.new(name, default, sql_type_metadata, null, table_name, default_function, collation, native_type)
def new_column(name, default, sql_type_metadata, null, native_type = nil)
::ODBCAdapter::Column.new(name, default, sql_type_metadata, null, native_type)
end

#Snowflake doesn't have a mechanism to return the primary key on inserts, it needs prefetched
def prefetch_primary_key?(table_name = nil)
true
end

def next_sequence_value(table_name = nil)
exec_query("SELECT #{table_name}.NEXTVAL as new_id").first["new_id"]
end

def build_merge_sql(merge) # :nodoc:
<<~SQL
MERGE #{merge.into} AS TARGET USING (#{merge.values_list}) AS SOURCE ON #{merge.match}
#{merge.merge_delete}
#{merge.merge_update}
#{merge.merge_insert}
SQL
end

def exec_merge_all(sql, name) # :nodoc:
exec_query(sql, name)
end

protected

# Build the type map for ActiveRecord
#Snowflake ODBC Adapter specific
def initialize_type_map(map)
map.register_type 'boolean', Type::Boolean.new
map.register_type ODBC::SQL_CHAR, Type::String.new
map.register_type ODBC::SQL_LONGVARCHAR, Type::Text.new
map.register_type ODBC::SQL_TINYINT, Type::Integer.new(limit: 4)
map.register_type ODBC::SQL_SMALLINT, Type::Integer.new(limit: 8)
map.register_type ODBC::SQL_INTEGER, Type::Integer.new(limit: 16)
map.register_type ODBC::SQL_BIGINT, Type::BigInteger.new(limit: 32)
map.register_type ODBC::SQL_REAL, Type::Float.new(limit: 24)
map.register_type ODBC::SQL_FLOAT, Type::Float.new
map.register_type ODBC::SQL_DOUBLE, Type::Float.new(limit: 53)
map.register_type ODBC::SQL_DECIMAL, Type::Float.new
map.register_type ODBC::SQL_NUMERIC, Type::Integer.new
map.register_type ODBC::SQL_BINARY, Type::Binary.new
map.register_type ODBC::SQL_DATE, Type::Date.new
map.register_type ODBC::SQL_DATETIME, Type::DateTime.new
map.register_type ODBC::SQL_TIME, Type::Time.new
map.register_type ODBC::SQL_TIMESTAMP, Type::DateTime.new
map.register_type ODBC::SQL_GUID, Type::String.new

alias_type map, ODBC::SQL_BIT, 'boolean'
alias_type map, ODBC::SQL_VARCHAR, ODBC::SQL_CHAR
alias_type map, ODBC::SQL_WCHAR, ODBC::SQL_CHAR
alias_type map, ODBC::SQL_WVARCHAR, ODBC::SQL_CHAR
alias_type map, ODBC::SQL_WLONGVARCHAR, ODBC::SQL_LONGVARCHAR
alias_type map, ODBC::SQL_VARBINARY, ODBC::SQL_BINARY
alias_type map, ODBC::SQL_LONGVARBINARY, ODBC::SQL_BINARY
alias_type map, ODBC::SQL_TYPE_DATE, ODBC::SQL_DATE
alias_type map, ODBC::SQL_TYPE_TIME, ODBC::SQL_TIME
alias_type map, ODBC::SQL_TYPE_TIMESTAMP, ODBC::SQL_TIMESTAMP
map.register_type %r(boolean)i, Type::Boolean.new
map.register_type %r(date)i, Type::Date.new
map.register_type %r(varchar)i, Type::String.new
map.register_type %r(time)i, Type::Time.new
map.register_type %r(timestamp)i, Type::DateTime.new
map.register_type %r(binary)i, Type::Binary.new
map.register_type %r(double)i, Type::Float.new
map.register_type(%r(decimal)i) do |sql_type|
scale = extract_scale(sql_type)
if scale == 0
::ODBCAdapter::Type::SnowflakeInteger.new
else
Type::Decimal.new(precision: extract_precision(sql_type), scale: scale)
end
end
map.register_type %r(struct)i, ::ODBCAdapter::Type::SnowflakeObject.new
map.register_type %r(array)i, ::ODBCAdapter::Type::ArrayOfValues.new
map.register_type %r(variant)i, ::ODBCAdapter::Type::Variant.new
end

# Translate an exception from the native DBMS to something usable by
# ActiveRecord.
def translate_exception(exception, message)
def translate_exception(exception, message:, sql:, binds:)
error_number = exception.message[/^\d+/].to_i

if error_number == ERR_DUPLICATE_KEY_VALUE
ActiveRecord::RecordNotUnique.new(message, exception)
ActiveRecord::RecordNotUnique.new(message, sql: sql, binds: binds)
elsif error_number == ERR_QUERY_TIMED_OUT || exception.message =~ ERR_QUERY_TIMED_OUT_MESSAGE
::ODBCAdapter::QueryTimeoutError.new(message, exception)
::ODBCAdapter::QueryTimeoutError.new(message, sql: sql, binds: binds)
elsif exception.message.match(ERR_CONNECTION_FAILED_REGEX) || exception.message =~ ERR_CONNECTION_FAILED_MESSAGE
begin
reconnect!
::ODBCAdapter::ConnectionFailedError.new(message, sql: sql, binds: binds)
rescue => e
puts "unable to reconnect #{e}"
end
else
super
end
Expand All @@ -186,8 +219,8 @@ def translate_exception(exception, message)
# work with non-string keys, and in our case the keys are (almost) all
# numeric
def alias_type(map, new_type, old_type)
map.register_type(new_type) do |_, *args|
map.lookup(old_type, *args)
map.register_type(new_type) do |_|
map.lookup(old_type)
end
end

Expand Down
Loading