odbc_adapter 5.0.0 → 5.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 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