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 +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
|
[![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
|
-
|
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
|