odbc_adapter 5.0.0 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a82f638f836342da6b1ef70597ad205ad869e796
4
- data.tar.gz: e2f9e57124fa42ebd816ba4e3446d68c2fc06d74
3
+ metadata.gz: f5157fd72c8b51e81109ffbc557706d5164e52ea
4
+ data.tar.gz: 07045f2ee0d6f4a8c61f0566e9a2ed4af6a88565
5
5
  SHA512:
6
- metadata.gz: c1766ac63ef69f279469a25c47c18495e8e2aac3c9dce4a73c0b2ca73987446fef18fb91492231a8c60dd5b1ca30613744576c6f93ccd6dcb4dc41944bf04fe2
7
- data.tar.gz: dc416dc3d4d359f7a6e5e5bac66fec0f08b567ffa576bbf56446d51d20dfa85836236a58cfb05811114a1d56f6fcf63ae43528d4eac4f5f697308fffea881e4a
6
+ metadata.gz: 5ac83f350948348b7f0e36bc68881b2b401f47ebd449f615abee9f5650d1c1bda3fb6fb5de7519e3af5d5d4efbc5b2d61b67bacd96795eb96b3fcdbe77b0e1cb
7
+ data.tar.gz: 7263035cff902e1776abc8000b961d55779d1c6e9084cbaea24561d3ea2aef85bc9331bfbceee3885d85ad2dfa8909150edc8dee275652566cd25a51f645c129
data/README.md CHANGED
@@ -2,10 +2,16 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.com/localytics/odbc_adapter.svg?token=kQUiABmGkzyHdJdMnCnv&branch=master)](https://travis-ci.com/localytics/odbc_adapter)
4
4
 
5
- An ActiveRecord ODBC adapter.
5
+ An ActiveRecord ODBC adapter. Master branch is working off of edge Rails. Previous work has been done to make it compatible with Rails 3.2 and 4.2; for those versions use the 3.2.x or 4.2.x gem releases.
6
+
7
+ This adapter will work for basic queries for most DBMSs out of the box, without support for migrations. Full support is built-in for MySQL 5 and PostgreSQL 9 databases. You can register your own adapter to get more support for your DBMS using the `ODBCAdapter.register` function.
8
+
9
+ A lot of this work is based on [OpenLink's ActiveRecord adapter](http://odbc-rails.rubyforge.org/) which works for earlier versions of Rails.
6
10
 
7
11
  ## Installation
8
12
 
13
+ Ensure you have the ODBC driver installed on your machine. You will also need the driver for whichever database to which you want ODBC to connect.
14
+
9
15
  Add this line to your application's Gemfile:
10
16
 
11
17
  ```ruby
@@ -22,11 +28,27 @@ Or install it yourself as:
22
28
 
23
29
  ## Usage
24
30
 
25
- ## Development
31
+ Configure your `database.yml` by either using the `dsn` option to point to a DSN that corresponds to a valid entry in your `~/.odbc.ini` file:
32
+
33
+ ```
34
+ development:
35
+ adapter: odbc
36
+ dsn: MyDatabaseDSN
37
+ ```
38
+
39
+ or by using the `conn_str` option and specifying the entire connection string:
40
+
41
+ ```
42
+ development:
43
+ adapter: odbc
44
+ conn_str: "DRIVER={PostgreSQL ANSI};SERVER=localhost;PORT=5432;DATABASE=my_database;UID=postgres;"
45
+ ```
46
+
47
+ ActiveRecord models that use this connection will now be connecting to the configured database using the ODBC driver.
26
48
 
27
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
49
+ ## Testing
28
50
 
29
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
51
+ 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.
30
52
 
31
53
  ## Contributing
32
54
 
data/bin/console CHANGED
@@ -3,5 +3,10 @@
3
3
  require 'bundler/setup'
4
4
  require 'odbc_adapter'
5
5
 
6
+ options = { adapter: 'odbc' }
7
+ options[:dsn] = ENV['DSN'] if ENV['DSN']
8
+ options[:conn_str] = ENV['CONN_STR'] if ENV['CONN_STR']
9
+ ActiveRecord::Base.establish_connection(options) if options.any?
10
+
6
11
  require 'irb'
7
12
  IRB.start
@@ -2,22 +2,23 @@ require 'active_record'
2
2
  require 'arel/visitors/bind_visitor'
3
3
  require 'odbc'
4
4
 
5
- require 'odbc_adapter'
6
5
  require 'odbc_adapter/database_limits'
7
6
  require 'odbc_adapter/database_statements'
7
+ require 'odbc_adapter/error'
8
8
  require 'odbc_adapter/quoting'
9
9
  require 'odbc_adapter/schema_statements'
10
10
 
11
11
  require 'odbc_adapter/column'
12
12
  require 'odbc_adapter/column_metadata'
13
- require 'odbc_adapter/dbms'
13
+ require 'odbc_adapter/database_metadata'
14
+ require 'odbc_adapter/registry'
14
15
  require 'odbc_adapter/type_caster'
15
16
  require 'odbc_adapter/version'
16
17
 
17
18
  module ActiveRecord
18
19
  class Base
19
20
  class << self
20
- def odbc_connection(config) # :nodoc:
21
+ def odbc_connection(config)
21
22
  config = config.symbolize_keys
22
23
 
23
24
  connection, options =
@@ -29,8 +30,8 @@ module ActiveRecord
29
30
  raise ArgumentError, "No data source name (:dsn) or connection string (:conn_str) specified."
30
31
  end
31
32
 
32
- dbms = ::ODBCAdapter::DBMS.new(connection)
33
- dbms.adapter_class.new(connection, logger, dbms)
33
+ database_metadata = ::ODBCAdapter::DatabaseMetadata.new(connection)
34
+ database_metadata.adapter_class.new(connection, logger, database_metadata)
34
35
  end
35
36
 
36
37
  private
@@ -75,14 +76,16 @@ module ActiveRecord
75
76
 
76
77
  ADAPTER_NAME = 'ODBC'.freeze
77
78
  BOOLEAN_TYPE = 'BOOLEAN'.freeze
79
+
78
80
  ERR_DUPLICATE_KEY_VALUE = 23505
81
+ ERR_QUERY_TIMED_OUT = /Query has timed out/
79
82
 
80
- attr_reader :dbms
83
+ attr_reader :database_metadata
81
84
 
82
- def initialize(connection, logger, dbms)
85
+ def initialize(connection, logger, database_metadata)
83
86
  super(connection, logger)
84
- @connection = connection
85
- @dbms = dbms
87
+ @connection = connection
88
+ @database_metadata = database_metadata
86
89
  end
87
90
 
88
91
  # Returns the human-readable name of the adapter. Use mixed case - one
@@ -165,9 +168,11 @@ module ActiveRecord
165
168
  end
166
169
 
167
170
  def translate_exception(exception, message)
168
- case exception.message[/^\d+/].to_i
169
- when ERR_DUPLICATE_KEY_VALUE
171
+ case
172
+ when exception.message[/^\d+/].to_i == ERR_DUPLICATE_KEY_VALUE
170
173
  ActiveRecord::RecordNotUnique.new(message, exception)
174
+ when exception.message =~ ERR_QUERY_TIMED_OUT
175
+ ::ODBCAdapter::QueryTimeoutError.new(message, exception)
171
176
  else
172
177
  super
173
178
  end
data/lib/odbc_adapter.rb CHANGED
@@ -1,17 +1,2 @@
1
1
  # Requiring with this pattern to mirror ActiveRecord
2
2
  require 'active_record/connection_adapters/odbc_adapter'
3
-
4
- module ODBCAdapter
5
- class << self
6
- def dbms_registry
7
- @dbms_registry ||= {
8
- /my.*sql/i => :MySQL,
9
- /postgres/i => :PostgreSQL
10
- }
11
- end
12
-
13
- def register(pattern, superclass, &block)
14
- dbms_registry[pattern] = Class.new(superclass, &block)
15
- end
16
- end
17
- end
@@ -1,7 +1,7 @@
1
1
  module ODBCAdapter
2
2
  module Adapters
3
3
  # Overrides specific to MySQL. Mostly taken from
4
- # ActiveRecord::ConnectionAdapters::MySQLAdapter
4
+ # ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
5
5
  class MySQLODBCAdapter < ActiveRecord::ConnectionAdapters::ODBCAdapter
6
6
  PRIMARY_KEY = 'INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY'.freeze
7
7
 
@@ -27,27 +27,6 @@ module ODBCAdapter
27
27
  execute("TRUNCATE TABLE #{quote_table_name(table_name)}", name)
28
28
  end
29
29
 
30
- def limited_update_conditions(where_sql, _quoted_table_name, _quoted_primary_key)
31
- where_sql
32
- end
33
-
34
- # Taken from ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
35
- def join_to_update(update, select) #:nodoc:
36
- if select.limit || select.offset || select.orders.any?
37
- subsubselect = select.clone
38
- subsubselect.projections = [update.key]
39
-
40
- subselect = Arel::SelectManager.new(select.engine)
41
- subselect.project Arel.sql(update.key.name)
42
- subselect.from subsubselect.as('__active_record_temp')
43
-
44
- update.where update.key.in(subselect)
45
- else
46
- update.table select.source
47
- update.wheres = select.constraints
48
- end
49
- end
50
-
51
30
  # Quotes a string, escaping any ' (single quote) and \ (backslash)
52
31
  # characters.
53
32
  def quote_string(string)
@@ -70,7 +49,7 @@ module ODBCAdapter
70
49
  0
71
50
  end
72
51
 
73
- def disable_referential_integrity(&block) #:nodoc:
52
+ def disable_referential_integrity(&block)
74
53
  old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
75
54
 
76
55
  begin
@@ -81,13 +60,14 @@ module ODBCAdapter
81
60
  end
82
61
  end
83
62
 
84
- # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
85
- # Charset defaults to utf8.
63
+ # Create a new MySQL database with optional <tt>:charset</tt> and
64
+ # <tt>:collation</tt>. Charset defaults to utf8.
86
65
  #
87
66
  # Example:
88
- # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
89
- # create_database 'matt_development'
90
- # create_database 'matt_development', :charset => :big5
67
+ # create_database 'charset_test', charset: 'latin1',
68
+ # collation: 'latin1_bin'
69
+ # create_database 'rails_development'
70
+ # create_database 'rails_development', charset: :big5
91
71
  def create_database(name, options = {})
92
72
  if options[:collation]
93
73
  execute("CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`")
@@ -99,8 +79,8 @@ module ODBCAdapter
99
79
  # Drops a MySQL database.
100
80
  #
101
81
  # Example:
102
- # drop_database('sebastian_development')
103
- def drop_database(name) #:nodoc:
82
+ # drop_database('rails_development')
83
+ def drop_database(name)
104
84
  execute("DROP DATABASE IF EXISTS `#{name}`")
105
85
  end
106
86
 
@@ -114,9 +94,8 @@ module ODBCAdapter
114
94
  end
115
95
 
116
96
  def change_column(table_name, column_name, type, options = {})
117
- # column_name.to_s used in case column_name is a symbol
118
97
  unless options_include_default?(options)
119
- options[:default] = columns(table_name).find { |c| c.name == column_name.to_s }.default
98
+ options[:default] = column_for(table_name, column_name).default
120
99
  end
121
100
 
122
101
  change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
@@ -124,26 +103,36 @@ module ODBCAdapter
124
103
  execute(change_column_sql)
125
104
  end
126
105
 
127
- def change_column_default(table_name, column_name, default)
128
- col = columns(table_name).detect { |c| c.name == column_name.to_s }
129
- change_column(table_name, column_name, col.type,
130
- default: default, limit: col.limit, precision: col.precision, scale: col.scale)
106
+ def change_column_default(table_name, column_name, default_or_changes)
107
+ default = extract_new_default_value(default_or_changes)
108
+ column = column_for(table_name, column_name)
109
+ change_column(table_name, column_name, column.sql_type, default: default)
110
+ end
111
+
112
+ def change_column_null(table_name, column_name, null, default = nil)
113
+ column = column_for(table_name, column_name)
114
+
115
+ unless null || default.nil?
116
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
117
+ end
118
+ change_column(table_name, column_name, column.sql_type, null: null)
131
119
  end
132
120
 
133
121
  def rename_column(table_name, column_name, new_column_name)
134
- col = columns(table_name).detect { |c| c.name == column_name.to_s }
135
- current_type = col.native_type
136
- current_type << "(#{col.limit})" if col.limit
122
+ column = column_for(table_name, column_name)
123
+ current_type = column.native_type
124
+ current_type << "(#{column.limit})" if column.limit
137
125
  execute("ALTER TABLE #{table_name} CHANGE #{column_name} #{new_column_name} #{current_type}")
138
126
  end
139
127
 
128
+ # Skip primary key indexes
140
129
  def indexes(table_name, name = nil)
141
- # Skip primary key indexes
142
- super(table_name, name).delete_if { |i| i.unique && i.name =~ /^PRIMARY$/ }
130
+ super(table_name, name).reject { |i| i.unique && i.name =~ /^PRIMARY$/ }
143
131
  end
144
132
 
133
+ # MySQL 5.x doesn't allow DEFAULT NULL for first timestamp column in a
134
+ # table
145
135
  def options_include_default?(options)
146
- # MySQL 5.x doesn't allow DEFAULT NULL for first timestamp column in a table
147
136
  if options.include?(:default) && options[:default].nil?
148
137
  if options.include?(:column) && options[:column].native_type =~ /timestamp/i
149
138
  options.delete(:default)
@@ -152,14 +141,6 @@ module ODBCAdapter
152
141
  super(options)
153
142
  end
154
143
 
155
- def structure_dump
156
- select_all("SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'").map do |table|
157
- table.delete('Table_type')
158
- sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
159
- exec_query(sql).first['Create Table'] + ";\n\n"
160
- end.join
161
- end
162
-
163
144
  protected
164
145
 
165
146
  def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
@@ -0,0 +1,31 @@
1
+ module ODBCAdapter
2
+ module Adapters
3
+ # A default adapter used for databases that are no explicitly listed in the
4
+ # registry. This allows for minimal support for DBMSs for which we don't
5
+ # have an explicit adapter.
6
+ class NullODBCAdapter < ActiveRecord::ConnectionAdapters::ODBCAdapter
7
+ class BindSubstitution < Arel::Visitors::ToSql
8
+ include Arel::Visitors::BindVisitor
9
+ end
10
+
11
+ # Using a BindVisitor so that the SQL string gets substituted before it is
12
+ # sent to the DBMS (to attempt to get as much coverage as possible for
13
+ # DBMSs we don't support).
14
+ def arel_visitor
15
+ BindSubstitution.new(self)
16
+ end
17
+
18
+ # Explicitly turning off prepared_statements in the null adapter because
19
+ # there isn't really a standard on which substitution character to use.
20
+ def prepared_statements
21
+ false
22
+ end
23
+
24
+ # Turning off support for migrations because there is no information to
25
+ # go off of for what syntax the DBMS will expect.
26
+ def supports_migrations?
27
+ false
28
+ end
29
+ end
30
+ end
31
+ end
@@ -6,6 +6,8 @@ module ODBCAdapter
6
6
  BOOLEAN_TYPE = 'bool'.freeze
7
7
  PRIMARY_KEY = 'SERIAL PRIMARY KEY'.freeze
8
8
 
9
+ alias :create :insert
10
+
9
11
  # Override to handle booleans appropriately
10
12
  def native_database_types
11
13
  @native_database_types ||= super.merge(boolean: { name: 'bool' })
@@ -25,34 +27,14 @@ module ODBCAdapter
25
27
  exec_query("TRUNCATE TABLE #{quote_table_name(table_name)}", name)
26
28
  end
27
29
 
28
- # Returns the sequence name for a table's primary key or some other specified key.
29
- def default_sequence_name(table_name, pk = nil) #:nodoc:
30
+ # Returns the sequence name for a table's primary key or some other
31
+ # specified key.
32
+ def default_sequence_name(table_name, pk = nil)
30
33
  serial_sequence(table_name, pk || 'id').split('.').last
31
34
  rescue ActiveRecord::StatementInvalid
32
35
  "#{table_name}_#{pk || 'id'}_seq"
33
36
  end
34
37
 
35
- # Returns the current ID of a table's sequence.
36
- def last_insert_id(sequence_name)
37
- r = exec_query("SELECT currval('#{sequence_name}')", 'SQL')
38
- Integer(r.rows.first.first)
39
- end
40
-
41
- # Executes an INSERT query and returns the new record's ID
42
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
43
- unless pk
44
- table_ref = extract_table_ref_from_insert_sql(sql)
45
- pk = primary_key(table_ref) if table_ref
46
- end
47
-
48
- if pk
49
- select_value("#{sql} RETURNING #{quote_column_name(pk)}")
50
- else
51
- super
52
- end
53
- end
54
- alias :create :insert
55
-
56
38
  def sql_for_insert(sql, pk, id_value, sequence_name, binds)
57
39
  unless pk
58
40
  table_ref = extract_table_ref_from_insert_sql(sql)
@@ -81,20 +63,21 @@ module ODBCAdapter
81
63
  string.gsub(/\\/, '\&\&').gsub(/'/, "''")
82
64
  end
83
65
 
84
- def disable_referential_integrity #:nodoc:
66
+ def disable_referential_integrity
85
67
  execute(tables.map { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(';'))
86
68
  yield
87
69
  ensure
88
70
  execute(tables.map { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(';'))
89
71
  end
90
72
 
91
- # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
92
- # <tt>:encoding</tt>, <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL
93
- # uses <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
73
+ # Create a new PostgreSQL database. Options include <tt>:owner</tt>,
74
+ # <tt>:template</tt>, <tt>:encoding</tt>, <tt>:tablespace</tt>, and
75
+ # <tt>:connection_limit</tt> (note that MySQL uses <tt>:charset</tt>
76
+ # while PostgreSQL uses <tt>:encoding</tt>).
94
77
  #
95
78
  # Example:
96
79
  # create_database config[:database], config
97
- # create_database 'foo_development', :encoding => 'unicode'
80
+ # create_database 'foo_development', encoding: 'unicode'
98
81
  def create_database(name, options = {})
99
82
  options = options.reverse_merge(encoding: 'utf8')
100
83
 
@@ -121,8 +104,8 @@ module ODBCAdapter
121
104
  # Drops a PostgreSQL database.
122
105
  #
123
106
  # Example:
124
- # drop_database 'matt_development'
125
- def drop_database(name) #:nodoc:
107
+ # drop_database 'rails_development'
108
+ def drop_database(name)
126
109
  execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
127
110
  end
128
111
 
@@ -152,17 +135,19 @@ module ODBCAdapter
152
135
  execute("ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}")
153
136
  end
154
137
 
155
- # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
138
+ # Returns a SELECT DISTINCT clause for a given set of columns and a given
139
+ # ORDER BY clause.
156
140
  #
157
- # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
158
- # requires that the ORDER BY include the distinct column.
141
+ # PostgreSQL requires the ORDER BY columns in the select list for
142
+ # distinct queries, and requires that the ORDER BY include the distinct
143
+ # column.
159
144
  #
160
145
  # distinct("posts.id", "posts.created_at desc")
161
146
  def distinct(columns, orders)
162
147
  return "DISTINCT #{columns}" if orders.empty?
163
148
 
164
- # Construct a clean list of column names from the ORDER BY clause, removing
165
- # any ASC/DESC modifiers
149
+ # Construct a clean list of column names from the ORDER BY clause,
150
+ # removing any ASC/DESC modifiers
166
151
  order_columns = orders.map { |s| s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') }
167
152
  order_columns.reject! { |c| c.blank? }
168
153
  order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
@@ -170,6 +155,28 @@ module ODBCAdapter
170
155
  "DISTINCT #{columns}, #{order_columns * ', '}"
171
156
  end
172
157
 
158
+ protected
159
+
160
+ # Executes an INSERT query and returns the new record's ID
161
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
162
+ unless pk
163
+ table_ref = extract_table_ref_from_insert_sql(sql)
164
+ pk = primary_key(table_ref) if table_ref
165
+ end
166
+
167
+ if pk
168
+ select_value("#{sql} RETURNING #{quote_column_name(pk)}")
169
+ else
170
+ super
171
+ end
172
+ end
173
+
174
+ # Returns the current ID of a table's sequence.
175
+ def last_insert_id(sequence_name)
176
+ r = exec_query("SELECT currval('#{sequence_name}')", 'SQL')
177
+ Integer(r.rows.first.first)
178
+ end
179
+
173
180
  private
174
181
 
175
182
  def serial_sequence(table, column)
@@ -2,8 +2,8 @@ module ODBCAdapter
2
2
  module DatabaseLimits
3
3
  # Returns the maximum length of a table name.
4
4
  def table_alias_length
5
- max_identifier_length = dbms.field_for(ODBC::SQL_MAX_IDENTIFIER_LEN)
6
- max_table_name_length = dbms.field_for(ODBC::SQL_MAX_TABLE_NAME_LEN)
5
+ max_identifier_length = database_metadata.max_identifier_len
6
+ max_table_name_length = database_metadata.max_table_name_len
7
7
  [max_identifier_length, max_table_name_length].max
8
8
  end
9
9
  end
@@ -0,0 +1,44 @@
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
+ ]
15
+
16
+ attr_reader :values
17
+
18
+ def initialize(connection)
19
+ @values = Hash[FIELDS.map { |field| [field, connection.get_info(ODBC.const_get(field))] }]
20
+ end
21
+
22
+ def adapter_class
23
+ ODBCAdapter.adapter_for(dbms_name)
24
+ end
25
+
26
+ def upcase_identifiers?
27
+ @upcase_identifiers ||= (identifier_case == ODBC::SQL_IC_UPPER)
28
+ end
29
+
30
+ # A little bit of metaprogramming magic here to create accessors for each of
31
+ # the fields reported on by the DBMS.
32
+ FIELDS.each do |field|
33
+ define_method(field.to_s.downcase.gsub('sql_', '')) do
34
+ value_for(field)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def value_for(field)
41
+ values[field]
42
+ end
43
+ end
44
+ end
@@ -87,8 +87,7 @@ module ODBCAdapter
87
87
 
88
88
  # Assume received identifier is in DBMS's data dictionary case.
89
89
  def format_case(identifier)
90
- case dbms.field_for(ODBC::SQL_IDENTIFIER_CASE)
91
- when ODBC::SQL_IC_UPPER
90
+ if database_metadata.upcase_identifiers?
92
91
  identifier =~ /[a-z]/ ? identifier : identifier.downcase
93
92
  else
94
93
  identifier
@@ -114,8 +113,7 @@ module ODBCAdapter
114
113
  # Converts an identifier to the case conventions used by the DBMS.
115
114
  # Assume received identifier is in ActiveRecord case.
116
115
  def native_case(identifier)
117
- case dbms.field_for(ODBC::SQL_IDENTIFIER_CASE)
118
- when ODBC::SQL_IC_UPPER
116
+ if database_metadata.upcase_identifiers?
119
117
  identifier =~ /[A-Z]/ ? identifier : identifier.upcase
120
118
  else
121
119
  identifier
@@ -0,0 +1,4 @@
1
+ module ODBCAdapter
2
+ class QueryTimeoutError < ActiveRecord::StatementInvalid
3
+ end
4
+ end
@@ -8,7 +8,7 @@ module ODBCAdapter
8
8
  # Returns a quoted form of the column name.
9
9
  def quote_column_name(name)
10
10
  name = name.to_s
11
- quote_char = dbms.field_for(ODBC::SQL_IDENTIFIER_QUOTE_CHAR).to_s.strip
11
+ quote_char = database_metadata.identifier_quote_char.to_s.strip
12
12
 
13
13
  return name if quote_char.length.zero?
14
14
  quote_char = quote_char[0]
@@ -16,9 +16,8 @@ module ODBCAdapter
16
16
  # Avoid quoting any already quoted name
17
17
  return name if name[0] == quote_char && name[-1] == quote_char
18
18
 
19
- # If DBMS's SQL_IDENTIFIER_CASE = SQL_IC_UPPER, only quote mixed
20
- # case names.
21
- if dbms.field_for(ODBC::SQL_IDENTIFIER_CASE) == ODBC::SQL_IC_UPPER
19
+ # If upcase identifiers, only quote mixed case names.
20
+ if database_metadata.upcase_identifiers?
22
21
  return name unless (name =~ /([A-Z]+[a-z])|([a-z]+[A-Z])/)
23
22
  end
24
23
 
@@ -0,0 +1,50 @@
1
+ module ODBCAdapter
2
+ class Registry
3
+ attr_reader :dbs
4
+
5
+ def initialize
6
+ @dbs = {
7
+ /my.*sql/i => :MySQL,
8
+ /postgres/i => :PostgreSQL
9
+ }
10
+ end
11
+
12
+ def adapter_for(reported_name)
13
+ reported_name = reported_name.downcase.gsub(/\s/, '')
14
+ found =
15
+ dbs.detect do |pattern, adapter|
16
+ adapter if reported_name =~ pattern
17
+ end
18
+
19
+ normalize_adapter(found && found.last || :Null)
20
+ end
21
+
22
+ def register(pattern, superclass = Object, &block)
23
+ dbs[pattern] = Class.new(superclass, &block)
24
+ end
25
+
26
+ private
27
+
28
+ def normalize_adapter(adapter)
29
+ return adapter unless adapter.is_a?(Symbol)
30
+ require "odbc_adapter/adapters/#{adapter.downcase}_odbc_adapter"
31
+ Adapters.const_get(:"#{adapter}ODBCAdapter")
32
+ end
33
+ end
34
+
35
+ class << self
36
+ def adapter_for(reported_name)
37
+ registry.adapter_for(reported_name)
38
+ end
39
+
40
+ def register(pattern, superclass = Object, &block)
41
+ registry.register(pattern, superclass, &block)
42
+ end
43
+
44
+ private
45
+
46
+ def registry
47
+ @registry ||= Registry.new
48
+ end
49
+ end
50
+ end
@@ -51,7 +51,7 @@ module ODBCAdapter
51
51
  next_row = result[row_idx + 1]
52
52
 
53
53
  if (row_idx == result.length - 1) || (next_row[6] == 0 || next_row[7] == 1)
54
- indices << IndexDefinition.new(table_name, format_case(index_name), unique, index_cols)
54
+ indices << ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, format_case(index_name), unique, index_cols)
55
55
  end
56
56
  end
57
57
  end
@@ -95,15 +95,34 @@ module ODBCAdapter
95
95
  result[0] && result[0][3]
96
96
  end
97
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(fk_from_table, fk_to_table,
108
+ name: key[11], # FK_NAME
109
+ column: key[3], # PKCOLUMN_NAME
110
+ primary_key: key[7], # FKCOLUMN_NAME
111
+ on_delete: key[10], # DELETE_RULE
112
+ on_update: key[9] # UPDATE_RULE
113
+ )
114
+ end
115
+ end
116
+
98
117
  # Ensure it's shorter than the maximum identifier length for the current
99
118
  # dbms
100
119
  def index_name(table_name, options)
101
- maximum = dbms.field_for(ODBC::SQL_MAX_IDENTIFIER_LEN) || 255
120
+ maximum = database_metadata.max_identifier_len || 255
102
121
  super(table_name, options)[0...maximum]
103
122
  end
104
123
 
105
124
  def current_database
106
- dbms.field_for(ODBC::SQL_DATABASE_NAME).strip
125
+ database_metadata.database_name.strip
107
126
  end
108
127
  end
109
128
  end
@@ -1,3 +1,3 @@
1
1
  module ODBCAdapter
2
- VERSION = '5.0.0'
2
+ VERSION = '5.0.1'
3
3
  end
data/odbc_adapter.gemspec CHANGED
@@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
25
25
  spec.add_development_dependency 'bundler', '~> 1.13'
26
26
  spec.add_development_dependency 'rake', '~> 10.0'
27
27
  spec.add_development_dependency 'minitest', '~> 5.0'
28
+ spec.add_development_dependency 'simplecov', '~> 0.12'
28
29
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: odbc_adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 5.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Localytics
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-01-31 00:00:00.000000000 Z
11
+ date: 2017-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-odbc
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.12'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.12'
69
83
  description:
70
84
  email:
71
85
  - oss@localytics.com
@@ -85,13 +99,16 @@ files:
85
99
  - lib/active_record/connection_adapters/odbc_adapter.rb
86
100
  - lib/odbc_adapter.rb
87
101
  - lib/odbc_adapter/adapters/mysql_odbc_adapter.rb
102
+ - lib/odbc_adapter/adapters/null_odbc_adapter.rb
88
103
  - lib/odbc_adapter/adapters/postgresql_odbc_adapter.rb
89
104
  - lib/odbc_adapter/column.rb
90
105
  - lib/odbc_adapter/column_metadata.rb
91
106
  - lib/odbc_adapter/database_limits.rb
107
+ - lib/odbc_adapter/database_metadata.rb
92
108
  - lib/odbc_adapter/database_statements.rb
93
- - lib/odbc_adapter/dbms.rb
109
+ - lib/odbc_adapter/error.rb
94
110
  - lib/odbc_adapter/quoting.rb
111
+ - lib/odbc_adapter/registry.rb
95
112
  - lib/odbc_adapter/schema_statements.rb
96
113
  - lib/odbc_adapter/type_caster.rb
97
114
  - lib/odbc_adapter/version.rb
@@ -1,50 +0,0 @@
1
- module ODBCAdapter
2
- # Caches SQLGetInfo output
3
- class DBMS
4
- FIELDS = [
5
- ODBC::SQL_DBMS_NAME,
6
- ODBC::SQL_DBMS_VER,
7
- ODBC::SQL_IDENTIFIER_CASE,
8
- ODBC::SQL_QUOTED_IDENTIFIER_CASE,
9
- ODBC::SQL_IDENTIFIER_QUOTE_CHAR,
10
- ODBC::SQL_MAX_IDENTIFIER_LEN,
11
- ODBC::SQL_MAX_TABLE_NAME_LEN,
12
- ODBC::SQL_USER_NAME,
13
- ODBC::SQL_DATABASE_NAME
14
- ]
15
-
16
- attr_reader :fields
17
-
18
- def initialize(connection)
19
- @fields = Hash[FIELDS.map { |field| [field, connection.get_info(field)] }]
20
- end
21
-
22
- def adapter_class
23
- return adapter unless adapter.is_a?(Symbol)
24
- require "odbc_adapter/adapters/#{adapter.downcase}_odbc_adapter"
25
- Adapters.const_get(:"#{adapter}ODBCAdapter")
26
- end
27
-
28
- def field_for(field)
29
- fields[field]
30
- end
31
-
32
- private
33
-
34
- # Maps a DBMS name to a symbol
35
- # Different ODBC drivers might return different names for the same DBMS
36
- def adapter
37
- @adapter ||=
38
- begin
39
- reported = field_for(ODBC::SQL_DBMS_NAME).downcase.gsub(/\s/, '')
40
- found =
41
- ODBCAdapter.dbms_registry.detect do |pattern, adapter|
42
- adapter if reported =~ pattern
43
- end
44
-
45
- raise ArgumentError, "ODBCAdapter: Unsupported database (#{reported})" if found.nil?
46
- found.last
47
- end
48
- end
49
- end
50
- end