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 +4 -4
- data/README.md +26 -4
- data/bin/console +5 -0
- data/lib/active_record/connection_adapters/odbc_adapter.rb +16 -11
- data/lib/odbc_adapter.rb +0 -15
- data/lib/odbc_adapter/adapters/mysql_odbc_adapter.rb +31 -50
- data/lib/odbc_adapter/adapters/null_odbc_adapter.rb +31 -0
- data/lib/odbc_adapter/adapters/postgresql_odbc_adapter.rb +42 -35
- data/lib/odbc_adapter/database_limits.rb +2 -2
- data/lib/odbc_adapter/database_metadata.rb +44 -0
- data/lib/odbc_adapter/database_statements.rb +2 -4
- data/lib/odbc_adapter/error.rb +4 -0
- data/lib/odbc_adapter/quoting.rb +3 -4
- data/lib/odbc_adapter/registry.rb +50 -0
- data/lib/odbc_adapter/schema_statements.rb +22 -3
- data/lib/odbc_adapter/version.rb +1 -1
- data/odbc_adapter.gemspec +1 -0
- metadata +20 -3
- data/lib/odbc_adapter/dbms.rb +0 -50
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f5157fd72c8b51e81109ffbc557706d5164e52ea
|
4
|
+
data.tar.gz: 07045f2ee0d6f4a8c61f0566e9a2ed4af6a88565
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ac83f350948348b7f0e36bc68881b2b401f47ebd449f615abee9f5650d1c1bda3fb6fb5de7519e3af5d5d4efbc5b2d61b67bacd96795eb96b3fcdbe77b0e1cb
|
7
|
+
data.tar.gz: 7263035cff902e1776abc8000b961d55779d1c6e9084cbaea24561d3ea2aef85bc9331bfbceee3885d85ad2dfa8909150edc8dee275652566cd25a51f645c129
|
data/README.md
CHANGED
@@ -2,10 +2,16 @@
|
|
2
2
|
|
3
3
|
[](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
|
-
|
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
|
-
|
49
|
+
## Testing
|
28
50
|
|
29
|
-
To
|
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/
|
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)
|
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
|
-
|
33
|
-
|
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 :
|
83
|
+
attr_reader :database_metadata
|
81
84
|
|
82
|
-
def initialize(connection, logger,
|
85
|
+
def initialize(connection, logger, database_metadata)
|
83
86
|
super(connection, logger)
|
84
|
-
@connection
|
85
|
-
@
|
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
|
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::
|
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)
|
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
|
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', :
|
89
|
-
#
|
90
|
-
# create_database '
|
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('
|
103
|
-
def drop_database(name)
|
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] =
|
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,
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
135
|
-
current_type =
|
136
|
-
current_type << "(#{
|
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
|
-
|
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
|
29
|
-
|
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
|
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>,
|
92
|
-
# <tt>:
|
93
|
-
#
|
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', :
|
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 '
|
125
|
-
def drop_database(name)
|
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
|
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
|
158
|
-
# requires that the ORDER BY include the distinct
|
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,
|
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 =
|
6
|
-
max_table_name_length =
|
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
|
-
|
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
|
-
|
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
|
data/lib/odbc_adapter/quoting.rb
CHANGED
@@ -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 =
|
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
|
20
|
-
|
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 =
|
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
|
-
|
125
|
+
database_metadata.database_name.strip
|
107
126
|
end
|
108
127
|
end
|
109
128
|
end
|
data/lib/odbc_adapter/version.rb
CHANGED
data/odbc_adapter.gemspec
CHANGED
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.
|
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-
|
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/
|
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
|
data/lib/odbc_adapter/dbms.rb
DELETED
@@ -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
|