activerecord-jdbc-adapter 0.9.7-java → 1.0.0.beta1-java
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.
- data/History.txt +11 -0
- data/Manifest.txt +71 -38
- data/lib/active_record/connection_adapters/cachedb_adapter.rb +1 -1
- data/lib/active_record/connection_adapters/derby_adapter.rb +1 -13
- data/lib/active_record/connection_adapters/h2_adapter.rb +1 -13
- data/lib/active_record/connection_adapters/hsqldb_adapter.rb +1 -13
- data/lib/active_record/connection_adapters/informix_adapter.rb +1 -1
- data/lib/active_record/connection_adapters/jdbc_adapter.rb +1 -661
- data/lib/active_record/connection_adapters/jndi_adapter.rb +1 -1
- data/lib/active_record/connection_adapters/mssql_adapter.rb +1 -13
- data/lib/active_record/connection_adapters/mysql_adapter.rb +1 -13
- data/lib/active_record/connection_adapters/oracle_adapter.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +1 -13
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +1 -13
- data/lib/activerecord-jdbc-adapter.rb +2 -2
- data/lib/arjdbc.rb +29 -0
- data/lib/arjdbc/cachedb.rb +3 -0
- data/lib/arjdbc/cachedb/adapter.rb +20 -0
- data/lib/arjdbc/cachedb/connection_methods.rb +10 -0
- data/lib/arjdbc/db2.rb +2 -0
- data/lib/{jdbc_adapter/jdbc_db2.rb → arjdbc/db2/adapter.rb} +1 -17
- data/lib/arjdbc/derby.rb +7 -0
- data/lib/{jdbc_adapter/jdbc_derby.rb → arjdbc/derby/adapter.rb} +8 -26
- data/lib/arjdbc/derby/connection_methods.rb +18 -0
- data/lib/arjdbc/discover.rb +99 -0
- data/lib/arjdbc/firebird.rb +2 -0
- data/lib/{jdbc_adapter/jdbc_firebird.rb → arjdbc/firebird/adapter.rb} +12 -16
- data/lib/arjdbc/h2.rb +4 -0
- data/lib/arjdbc/h2/adapter.rb +15 -0
- data/lib/arjdbc/h2/connection_methods.rb +12 -0
- data/lib/arjdbc/hsqldb.rb +4 -0
- data/lib/{jdbc_adapter/jdbc_hsqldb.rb → arjdbc/hsqldb/adapter.rb} +6 -58
- data/lib/arjdbc/hsqldb/connection_methods.rb +14 -0
- data/lib/arjdbc/informix.rb +3 -0
- data/lib/{jdbc_adapter/jdbc_informix.rb → arjdbc/informix/adapter.rb} +6 -19
- data/lib/arjdbc/informix/connection_methods.rb +10 -0
- data/lib/arjdbc/jdbc.rb +2 -0
- data/lib/arjdbc/jdbc/adapter.rb +235 -0
- data/lib/arjdbc/jdbc/adapter_java.jar +0 -0
- data/lib/arjdbc/jdbc/callbacks.rb +44 -0
- data/lib/arjdbc/jdbc/column.rb +38 -0
- data/lib/arjdbc/jdbc/compatibility.rb +51 -0
- data/lib/arjdbc/jdbc/connection.rb +97 -0
- data/lib/arjdbc/jdbc/connection_methods.rb +16 -0
- data/lib/arjdbc/jdbc/core_ext.rb +24 -0
- data/lib/arjdbc/jdbc/discover.rb +18 -0
- data/lib/arjdbc/jdbc/driver.rb +44 -0
- data/lib/arjdbc/jdbc/extension.rb +47 -0
- data/lib/arjdbc/jdbc/java.rb +14 -0
- data/lib/{jdbc_adapter → arjdbc/jdbc}/jdbc.rake +0 -0
- data/lib/{jdbc_adapter → arjdbc/jdbc}/missing_functionality_helper.rb +5 -5
- data/lib/arjdbc/jdbc/quoted_primary_key.rb +28 -0
- data/lib/{jdbc_adapter → arjdbc/jdbc}/railtie.rb +1 -1
- data/lib/{jdbc_adapter → arjdbc/jdbc}/rake_tasks.rb +0 -0
- data/lib/arjdbc/jdbc/require_driver.rb +16 -0
- data/lib/arjdbc/jdbc/type_converter.rb +119 -0
- data/lib/arjdbc/mimer.rb +2 -0
- data/lib/{jdbc_adapter/jdbc_mimer.rb → arjdbc/mimer/adapter.rb} +16 -19
- data/lib/arjdbc/mssql.rb +4 -0
- data/lib/{jdbc_adapter/jdbc_mssql.rb → arjdbc/mssql/adapter.rb} +19 -31
- data/lib/arjdbc/mssql/connection_methods.rb +13 -0
- data/lib/{jdbc_adapter → arjdbc/mssql}/tsql_helper.rb +1 -1
- data/lib/arjdbc/mysql.rb +4 -0
- data/lib/arjdbc/mysql/adapter.rb +388 -0
- data/lib/arjdbc/mysql/connection_methods.rb +26 -0
- data/lib/arjdbc/oracle.rb +3 -0
- data/lib/{jdbc_adapter/jdbc_oracle.rb → arjdbc/oracle/adapter.rb} +9 -17
- data/lib/arjdbc/oracle/connection_methods.rb +11 -0
- data/lib/arjdbc/postgresql.rb +4 -0
- data/lib/{jdbc_adapter/jdbc_postgre.rb → arjdbc/postgresql/adapter.rb} +7 -36
- data/lib/arjdbc/postgresql/connection_methods.rb +21 -0
- data/lib/arjdbc/sqlite3.rb +4 -0
- data/lib/{jdbc_adapter/jdbc_sqlite3.rb → arjdbc/sqlite3/adapter.rb} +106 -104
- data/lib/arjdbc/sqlite3/connection_methods.rb +33 -0
- data/lib/arjdbc/sybase.rb +2 -0
- data/lib/arjdbc/sybase/adapter.rb +46 -0
- data/lib/arjdbc/version.rb +8 -0
- data/lib/jdbc_adapter.rb +2 -27
- data/lib/jdbc_adapter/version.rb +3 -5
- data/rails_generators/templates/config/initializers/jdbc.rb +1 -1
- data/rakelib/compile.rake +3 -2
- data/rakelib/package.rake +3 -3
- data/src/java/{jdbc_adapter/JdbcDerbySpec.java → arjdbc/derby/DerbyModule.java} +32 -32
- data/src/java/{jdbc_adapter/JdbcAdapterInternalService.java → arjdbc/jdbc/AdapterJavaService.java} +13 -7
- data/src/java/{jdbc_adapter → arjdbc/jdbc}/JdbcConnectionFactory.java +6 -6
- data/src/java/{jdbc_adapter → arjdbc/jdbc}/RubyJdbcConnection.java +91 -16
- data/src/java/arjdbc/jdbc/SQLBlock.java +48 -0
- data/src/java/{jdbc_adapter → arjdbc/mssql}/MssqlRubyJdbcConnection.java +5 -2
- data/src/java/{jdbc_adapter/JdbcMySQLSpec.java → arjdbc/mysql/MySQLModule.java} +12 -12
- data/src/java/{jdbc_adapter/PostgresRubyJdbcConnection.java → arjdbc/postgresql/PostgresqlRubyJdbcConnection.java} +11 -9
- data/src/java/arjdbc/sqlite3/Sqlite3RubyJdbcConnection.java +64 -0
- data/test/abstract_db_create.rb +4 -1
- data/test/activerecord/connection_adapters/type_conversion_test.rb +1 -1
- data/test/db/cachedb.rb +0 -0
- data/test/db/derby.rb +12 -14
- data/test/db/hsqldb.rb +3 -2
- data/test/db/jndi_config.rb +4 -4
- data/test/db/sqlite3.rb +2 -6
- data/test/db2_simple_test.rb +23 -0
- data/test/derby_migration_test.rb +50 -3
- data/test/jdbc_common.rb +1 -1
- data/test/jndi_callbacks_test.rb +1 -0
- data/test/postgres_nonseq_pkey_test.rb +0 -2
- data/test/postgres_schema_search_path_test.rb +0 -2
- data/test/simple.rb +3 -3
- data/test/sybase_jtds_simple_test.rb +22 -0
- metadata +82 -46
- data/lib/active_record/connection_adapters/jdbc_adapter_spec.rb +0 -26
- data/lib/jdbc_adapter/jdbc_adapter_internal.jar +0 -0
- data/lib/jdbc_adapter/jdbc_cachedb.rb +0 -33
- data/lib/jdbc_adapter/jdbc_mysql.rb +0 -260
- data/lib/jdbc_adapter/jdbc_sybase.rb +0 -50
- data/src/java/jdbc_adapter/SQLBlock.java +0 -27
- data/src/java/jdbc_adapter/Sqlite3RubyJdbcConnection.java +0 -41
- data/test/jdbc_adapter/jdbc_db2_test.rb +0 -26
- data/test/jdbc_adapter/jdbc_sybase_test.rb +0 -33
- data/test/minirunit.rb +0 -109
- data/test/minirunit/testConnect.rb +0 -14
- data/test/minirunit/testH2.rb +0 -73
- data/test/minirunit/testHsqldb.rb +0 -73
- data/test/minirunit/testLoadActiveRecord.rb +0 -3
- data/test/minirunit/testMysql.rb +0 -83
- data/test/minirunit/testRawSelect.rb +0 -24
@@ -0,0 +1,28 @@
|
|
1
|
+
module ArJdbc
|
2
|
+
module QuotedPrimaryKeyExtension
|
3
|
+
def self.extended(base)
|
4
|
+
# Rails 3 method Rails 2 method
|
5
|
+
meth = [:arel_attributes_values, :attributes_with_quotes].detect do |m|
|
6
|
+
base.private_instance_methods.include?(m.to_s)
|
7
|
+
end
|
8
|
+
pk_hash_key = "self.class.primary_key"
|
9
|
+
pk_hash_value = '"?"'
|
10
|
+
if meth == :arel_attributes_values
|
11
|
+
pk_hash_key = "self.class.arel_table[#{pk_hash_key}]"
|
12
|
+
pk_hash_value = "Arel::SqlLiteral.new(#{pk_hash_value})"
|
13
|
+
end
|
14
|
+
if meth
|
15
|
+
base.module_eval <<-PK, __FILE__, __LINE__
|
16
|
+
alias :#{meth}_pre_pk :#{meth}
|
17
|
+
def #{meth}(include_primary_key = true, *args) #:nodoc:
|
18
|
+
aq = #{meth}_pre_pk(include_primary_key, *args)
|
19
|
+
if connection.is_a?(ArJdbc::Oracle) || connection.is_a?(ArJdbc::Mimer)
|
20
|
+
aq[#{pk_hash_key}] = #{pk_hash_value} if include_primary_key && aq[#{pk_hash_key}].nil?
|
21
|
+
end
|
22
|
+
aq
|
23
|
+
end
|
24
|
+
PK
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
File without changes
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Kernel
|
2
|
+
# load a JDBC driver library/gem, failing silently. If failed, trust
|
3
|
+
# that the driver jar is already present through some other means
|
4
|
+
def jdbc_require_driver(path, gem_name = nil)
|
5
|
+
gem_name ||= path.sub('/', '-')
|
6
|
+
2.times do
|
7
|
+
begin
|
8
|
+
require path
|
9
|
+
break
|
10
|
+
rescue LoadError
|
11
|
+
require 'rubygems'
|
12
|
+
begin; gem gem_name; rescue LoadError; end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
# I want to use JDBC's DatabaseMetaData#getTypeInfo to choose the best native types to
|
4
|
+
# use for ActiveRecord's Adapter#native_database_types in a database-independent way,
|
5
|
+
# but apparently a database driver can return multiple types for a given
|
6
|
+
# java.sql.Types constant. So this type converter uses some heuristics to try to pick
|
7
|
+
# the best (most common) type to use. It's not great, it would be better to just
|
8
|
+
# delegate to each database's existin AR adapter's native_database_types method, but I
|
9
|
+
# wanted to try to do this in a way that didn't pull in all the other adapters as
|
10
|
+
# dependencies. Suggestions appreciated.
|
11
|
+
class JdbcTypeConverter
|
12
|
+
# The basic ActiveRecord types, mapped to an array of procs that are used to #select
|
13
|
+
# the best type. The procs are used as selectors in order until there is only one
|
14
|
+
# type left. If all the selectors are applied and there is still more than one
|
15
|
+
# type, an exception will be raised.
|
16
|
+
AR_TO_JDBC_TYPES = {
|
17
|
+
:string => [ lambda {|r| Jdbc::Types::VARCHAR == r['data_type'].to_i},
|
18
|
+
lambda {|r| r['type_name'] =~ /^varchar/i},
|
19
|
+
lambda {|r| r['type_name'] =~ /^varchar$/i},
|
20
|
+
lambda {|r| r['type_name'] =~ /varying/i}],
|
21
|
+
:text => [ lambda {|r| [Jdbc::Types::LONGVARCHAR, Jdbc::Types::CLOB].include?(r['data_type'].to_i)},
|
22
|
+
lambda {|r| r['type_name'] =~ /^text$/i}, # For Informix
|
23
|
+
lambda {|r| r['type_name'] =~ /^(text|clob)$/i},
|
24
|
+
lambda {|r| r['type_name'] =~ /^character large object$/i},
|
25
|
+
lambda {|r| r['sql_data_type'] == 2005}],
|
26
|
+
:integer => [ lambda {|r| Jdbc::Types::INTEGER == r['data_type'].to_i},
|
27
|
+
lambda {|r| r['type_name'] =~ /^integer$/i},
|
28
|
+
lambda {|r| r['type_name'] =~ /^int4$/i},
|
29
|
+
lambda {|r| r['type_name'] =~ /^int$/i}],
|
30
|
+
:decimal => [ lambda {|r| Jdbc::Types::DECIMAL == r['data_type'].to_i},
|
31
|
+
lambda {|r| r['type_name'] =~ /^decimal$/i},
|
32
|
+
lambda {|r| r['type_name'] =~ /^numeric$/i},
|
33
|
+
lambda {|r| r['type_name'] =~ /^number$/i},
|
34
|
+
lambda {|r| r['type_name'] =~ /^real$/i},
|
35
|
+
lambda {|r| r['precision'] == '38'},
|
36
|
+
lambda {|r| r['data_type'] == '2'}],
|
37
|
+
:float => [ lambda {|r| [Jdbc::Types::FLOAT,Jdbc::Types::DOUBLE, Jdbc::Types::REAL].include?(r['data_type'].to_i)},
|
38
|
+
lambda {|r| r['data_type'].to_i == Jdbc::Types::REAL}, #Prefer REAL to DOUBLE for Postgresql
|
39
|
+
lambda {|r| r['type_name'] =~ /^float/i},
|
40
|
+
lambda {|r| r['type_name'] =~ /^double$/i},
|
41
|
+
lambda {|r| r['type_name'] =~ /^real$/i},
|
42
|
+
lambda {|r| r['precision'] == '15'}],
|
43
|
+
:datetime => [ lambda {|r| Jdbc::Types::TIMESTAMP == r['data_type'].to_i},
|
44
|
+
lambda {|r| r['type_name'] =~ /^datetime$/i},
|
45
|
+
lambda {|r| r['type_name'] =~ /^timestamp$/i},
|
46
|
+
lambda {|r| r['type_name'] =~ /^date/i},
|
47
|
+
lambda {|r| r['type_name'] =~ /^integer/i}], #Num of milliseconds for SQLite3 JDBC Driver
|
48
|
+
:timestamp => [ lambda {|r| Jdbc::Types::TIMESTAMP == r['data_type'].to_i},
|
49
|
+
lambda {|r| r['type_name'] =~ /^timestamp$/i},
|
50
|
+
lambda {|r| r['type_name'] =~ /^datetime/i},
|
51
|
+
lambda {|r| r['type_name'] =~ /^date/i},
|
52
|
+
lambda {|r| r['type_name'] =~ /^integer/i}], #Num of milliseconds for SQLite3 JDBC Driver
|
53
|
+
:time => [ lambda {|r| Jdbc::Types::TIME == r['data_type'].to_i},
|
54
|
+
lambda {|r| r['type_name'] =~ /^time$/i},
|
55
|
+
lambda {|r| r['type_name'] =~ /^datetime/i}, # For Informix
|
56
|
+
lambda {|r| r['type_name'] =~ /^date/i},
|
57
|
+
lambda {|r| r['type_name'] =~ /^integer/i}], #Num of milliseconds for SQLite3 JDBC Driver
|
58
|
+
:date => [ lambda {|r| Jdbc::Types::DATE == r['data_type'].to_i},
|
59
|
+
lambda {|r| r['type_name'] =~ /^date$/i},
|
60
|
+
lambda {|r| r['type_name'] =~ /^date/i},
|
61
|
+
lambda {|r| r['type_name'] =~ /^integer/i}], #Num of milliseconds for SQLite3 JDBC Driver3
|
62
|
+
:binary => [ lambda {|r| [Jdbc::Types::LONGVARBINARY,Jdbc::Types::BINARY,Jdbc::Types::BLOB].include?(r['data_type'].to_i)},
|
63
|
+
lambda {|r| r['type_name'] =~ /^blob/i},
|
64
|
+
lambda {|r| r['type_name'] =~ /sub_type 0$/i}, # For FireBird
|
65
|
+
lambda {|r| r['type_name'] =~ /^varbinary$/i}, # We want this sucker for Mimer
|
66
|
+
lambda {|r| r['type_name'] =~ /^binary$/i}, ],
|
67
|
+
:boolean => [ lambda {|r| [Jdbc::Types::TINYINT].include?(r['data_type'].to_i)},
|
68
|
+
lambda {|r| r['type_name'] =~ /^bool/i},
|
69
|
+
lambda {|r| r['data_type'] == '-7'},
|
70
|
+
lambda {|r| r['type_name'] =~ /^tinyint$/i},
|
71
|
+
lambda {|r| r['type_name'] =~ /^decimal$/i},
|
72
|
+
lambda {|r| r['type_name'] =~ /^integer$/i}]
|
73
|
+
}
|
74
|
+
|
75
|
+
def initialize(types)
|
76
|
+
@types = types
|
77
|
+
@types.each {|t| t['type_name'] ||= t['local_type_name']} # Sybase driver seems to want 'local_type_name'
|
78
|
+
end
|
79
|
+
|
80
|
+
def choose_best_types
|
81
|
+
type_map = {}
|
82
|
+
@types.each do |row|
|
83
|
+
name = row['type_name'].downcase
|
84
|
+
k = name.to_sym
|
85
|
+
type_map[k] = { :name => name }
|
86
|
+
type_map[k][:limit] = row['precision'].to_i if row['precision']
|
87
|
+
end
|
88
|
+
|
89
|
+
AR_TO_JDBC_TYPES.keys.each do |k|
|
90
|
+
typerow = choose_type(k)
|
91
|
+
type_map[k] = { :name => typerow['type_name'].downcase }
|
92
|
+
case k
|
93
|
+
when :integer, :string, :decimal
|
94
|
+
type_map[k][:limit] = typerow['precision'] && typerow['precision'].to_i
|
95
|
+
when :boolean
|
96
|
+
type_map[k][:limit] = 1
|
97
|
+
end
|
98
|
+
end
|
99
|
+
type_map
|
100
|
+
end
|
101
|
+
|
102
|
+
def choose_type(ar_type)
|
103
|
+
procs = AR_TO_JDBC_TYPES[ar_type]
|
104
|
+
types = @types
|
105
|
+
procs.each do |p|
|
106
|
+
new_types = types.reject {|r| r["data_type"].to_i == Jdbc::Types::OTHER}
|
107
|
+
new_types = new_types.select(&p)
|
108
|
+
new_types = new_types.inject([]) do |typs,t|
|
109
|
+
typs << t unless typs.detect {|el| el['type_name'] == t['type_name']}
|
110
|
+
typs
|
111
|
+
end
|
112
|
+
return new_types.first if new_types.length == 1
|
113
|
+
types = new_types if new_types.length > 0
|
114
|
+
end
|
115
|
+
raise "unable to choose type for #{ar_type} from:\n#{types.collect{|t| t['type_name']}.inspect}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/arjdbc/mimer.rb
ADDED
@@ -1,11 +1,8 @@
|
|
1
|
-
module
|
1
|
+
module ArJdbc
|
2
2
|
module Mimer
|
3
3
|
def self.extended(mod)
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def self.adapter_matcher(name, *)
|
8
|
-
name =~ /mimer/i ? self : false
|
4
|
+
require 'arjdbc/jdbc/quoted_primary_key'
|
5
|
+
ActiveRecord::Base.extend ArJdbc::QuotedPrimaryKeyExtension
|
9
6
|
end
|
10
7
|
|
11
8
|
def modify_types(tp)
|
@@ -20,7 +17,7 @@ module JdbcSpec
|
|
20
17
|
tp[:date] = { :name => "TIMESTAMP" }
|
21
18
|
tp
|
22
19
|
end
|
23
|
-
|
20
|
+
|
24
21
|
def default_sequence_name(table, column) #:nodoc:
|
25
22
|
"#{table}_seq"
|
26
23
|
end
|
@@ -54,13 +51,13 @@ module JdbcSpec
|
|
54
51
|
log(sql, name) { @connection.execute_insert sql,pk }
|
55
52
|
else # Assume the sql contains a bind-variable for the id
|
56
53
|
id_value = select_one("SELECT NEXT_VALUE OF #{sequence_name} AS val FROM MIMER.ONEROW")['val']
|
57
|
-
log(sql, name) {
|
54
|
+
log(sql, name) {
|
58
55
|
execute_prepared_insert(sql,id_value)
|
59
56
|
}
|
60
57
|
end
|
61
58
|
id_value
|
62
59
|
end
|
63
|
-
|
60
|
+
|
64
61
|
def execute_prepared_insert(sql, id)
|
65
62
|
@stmts ||= {}
|
66
63
|
@stmts[sql] ||= @connection.ps(sql)
|
@@ -72,24 +69,24 @@ module JdbcSpec
|
|
72
69
|
|
73
70
|
def quote(value, column = nil) #:nodoc:
|
74
71
|
return value.quoted_id if value.respond_to?(:quoted_id)
|
75
|
-
|
72
|
+
|
76
73
|
if String === value && column && column.type == :binary
|
77
74
|
return "X'#{quote_string(value.unpack("C*").collect {|v| v.to_s(16)}.join)}'"
|
78
75
|
end
|
79
76
|
case value
|
80
|
-
when String
|
77
|
+
when String
|
81
78
|
%Q{'#{quote_string(value)}'}
|
82
|
-
when NilClass
|
79
|
+
when NilClass
|
83
80
|
'NULL'
|
84
|
-
when TrueClass
|
81
|
+
when TrueClass
|
85
82
|
'1'
|
86
|
-
when FalseClass
|
83
|
+
when FalseClass
|
87
84
|
'0'
|
88
|
-
when Numeric
|
85
|
+
when Numeric
|
89
86
|
value.to_s
|
90
|
-
when Date, Time
|
87
|
+
when Date, Time
|
91
88
|
%Q{TIMESTAMP '#{value.strftime("%Y-%m-%d %H:%M:%S")}'}
|
92
|
-
else
|
89
|
+
else
|
93
90
|
%Q{'#{quote_string(value.to_yaml)}'}
|
94
91
|
end
|
95
92
|
end
|
@@ -106,7 +103,7 @@ module JdbcSpec
|
|
106
103
|
@limit = options[:limit]
|
107
104
|
@offset = options[:offset]
|
108
105
|
end
|
109
|
-
|
106
|
+
|
110
107
|
def select_all(sql, name = nil)
|
111
108
|
@offset ||= 0
|
112
109
|
if !@limit || @limit == -1
|
@@ -118,7 +115,7 @@ module JdbcSpec
|
|
118
115
|
ensure
|
119
116
|
@limit = @offset = nil
|
120
117
|
end
|
121
|
-
|
118
|
+
|
122
119
|
def select_one(sql, name = nil)
|
123
120
|
@offset ||= 0
|
124
121
|
select(sql, name)[@offset]
|
data/lib/arjdbc/mssql.rb
ADDED
@@ -1,22 +1,7 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
module ::JdbcSpec
|
4
|
-
|
5
|
-
module ActiveRecordExtensions
|
6
|
-
|
7
|
-
def mssql_connection(config)
|
8
|
-
require "active_record/connection_adapters/mssql_adapter"
|
9
|
-
config[:host] ||= "localhost"
|
10
|
-
config[:port] ||= 1433
|
11
|
-
config[:url] ||= "jdbc:jtds:sqlserver://#{config[:host]}:#{config[:port]}/#{config[:database]}"
|
12
|
-
config[:driver] ||= "net.sourceforge.jtds.jdbc.Driver"
|
13
|
-
embedded_driver(config)
|
14
|
-
end
|
15
|
-
|
16
|
-
end
|
1
|
+
require 'arjdbc/mssql/tsql_helper'
|
17
2
|
|
3
|
+
module ::ArJdbc
|
18
4
|
module MsSQL
|
19
|
-
|
20
5
|
include TSqlMethods
|
21
6
|
|
22
7
|
def self.extended(mod)
|
@@ -39,12 +24,8 @@ module ::JdbcSpec
|
|
39
24
|
mod.add_version_specific_add_limit_offset
|
40
25
|
end
|
41
26
|
|
42
|
-
def self.adapter_matcher(name, *)
|
43
|
-
name =~ /sqlserver|tds/i ? self : false
|
44
|
-
end
|
45
|
-
|
46
27
|
def self.column_selector
|
47
|
-
[/sqlserver|tds/i, lambda {|cfg,col| col.extend(::
|
28
|
+
[/sqlserver|tds/i, lambda {|cfg,col| col.extend(::ArJdbc::MsSQL::Column)}]
|
48
29
|
end
|
49
30
|
|
50
31
|
def self.jdbc_connection_class
|
@@ -102,7 +83,7 @@ module ::JdbcSpec
|
|
102
83
|
def type_cast(value)
|
103
84
|
return nil if value.nil? || value == "(null)" || value == "(NULL)"
|
104
85
|
case type
|
105
|
-
when :integer then unquote(value).to_i rescue value ? 1 : 0
|
86
|
+
when :integer then value.to_i rescue unquote(value).to_i rescue value ? 1 : 0
|
106
87
|
when :primary_key then value == true || value == false ? value == true ? 1 : 0 : value.to_i
|
107
88
|
when :decimal then self.class.value_to_decimal(unquote(value))
|
108
89
|
when :datetime then cast_to_datetime(value)
|
@@ -165,7 +146,7 @@ module ::JdbcSpec
|
|
165
146
|
when String, ActiveSupport::Multibyte::Chars
|
166
147
|
value = value.to_s
|
167
148
|
if column && column.type == :binary
|
168
|
-
"'#{quote_string(
|
149
|
+
"'#{quote_string(ArJdbc::MsSQL::Column.string_to_binary(value))}'" # ' (for ruby-mode)
|
169
150
|
elsif column && [:integer, :float].include?(column.type)
|
170
151
|
value = column.type == :integer ? value.to_i : value.to_f
|
171
152
|
value.to_s
|
@@ -209,7 +190,7 @@ module ::JdbcSpec
|
|
209
190
|
end_row = offset + limit.to_i
|
210
191
|
order = (options[:order] || determine_order_clause(sql))
|
211
192
|
sql.sub!(/ ORDER BY.*$/i, '')
|
212
|
-
find_select = /\b(SELECT(?:\s+DISTINCT)?)\b(.*)/
|
193
|
+
find_select = /\b(SELECT(?:\s+DISTINCT)?)\b(.*)/im
|
213
194
|
whole, select, rest_of_query = find_select.match(sql).to_a
|
214
195
|
if (start_row == 1) && (end_row ==1)
|
215
196
|
new_sql = "#{select} TOP 1 #{rest_of_query}"
|
@@ -240,7 +221,7 @@ module ::JdbcSpec
|
|
240
221
|
end_row = offset + limit.to_i
|
241
222
|
order = (options[:order] || determine_order_clause(sql))
|
242
223
|
sql.sub!(/ ORDER BY.*$/i, '')
|
243
|
-
find_select = /\b(SELECT(?:\s+DISTINCT)?)\b(.*)/
|
224
|
+
find_select = /\b(SELECT(?:\s+DISTINCT)?)\b(.*)/im
|
244
225
|
whole, select, rest_of_query = find_select.match(sql).to_a
|
245
226
|
new_sql = "#{select} t.* FROM (SELECT ROW_NUMBER() OVER(ORDER BY #{order}) AS row_num, #{rest_of_query}"
|
246
227
|
new_sql << ") AS t WHERE t.row_num BETWEEN #{start_row.to_s} AND #{end_row.to_s}"
|
@@ -371,6 +352,12 @@ module ::JdbcSpec
|
|
371
352
|
end
|
372
353
|
end
|
373
354
|
|
355
|
+
def select(sql, name = nil)
|
356
|
+
log(sql, name) do
|
357
|
+
@connection.execute_query(sql)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
374
361
|
#SELECT .. FOR UPDATE is not supported on Microsoft SQL Server
|
375
362
|
def add_lock!(sql, options)
|
376
363
|
sql
|
@@ -452,17 +439,18 @@ module ::JdbcSpec
|
|
452
439
|
|
453
440
|
def determine_order_clause(sql)
|
454
441
|
return $1 if sql =~ /ORDER BY (.*)$/
|
455
|
-
|
456
|
-
table_name = $1
|
442
|
+
table_name = get_table_name(sql)
|
457
443
|
"#{table_name}.#{determine_primary_key(table_name)}"
|
458
444
|
end
|
459
445
|
|
460
446
|
def determine_primary_key(table_name)
|
461
447
|
primary_key = columns(table_name).detect { |column| column.primary || column.identity }
|
462
|
-
|
448
|
+
return primary_key.name if primary_key
|
449
|
+
# Look for an id column. Return it, without changing case, to cover dbs with a case-sensitive collation.
|
450
|
+
columns(table_name).each { |column| return column.name if column.name =~ /^id$/i }
|
451
|
+
# Give up and provide something which is going to crash almost certainly
|
452
|
+
"id"
|
463
453
|
end
|
464
|
-
|
465
454
|
end
|
466
|
-
|
467
455
|
end
|
468
456
|
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class ActiveRecord::Base
|
2
|
+
class << self
|
3
|
+
def mssql_connection(config)
|
4
|
+
require "arjdbc/mssql"
|
5
|
+
config[:host] ||= "localhost"
|
6
|
+
config[:port] ||= 1433
|
7
|
+
config[:url] ||= "jdbc:jtds:sqlserver://#{config[:host]}:#{config[:port]}/#{config[:database]}"
|
8
|
+
config[:driver] ||= "net.sourceforge.jtds.jdbc.Driver"
|
9
|
+
embedded_driver(config)
|
10
|
+
end
|
11
|
+
alias_method :jdbcmssql_connection, :mssql_connection
|
12
|
+
end
|
13
|
+
end
|
@@ -12,7 +12,7 @@ module TSqlMethods
|
|
12
12
|
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
|
13
13
|
return 'uniqueidentifier' if (type.to_s == 'uniqueidentifier')
|
14
14
|
return super unless type.to_s == 'integer'
|
15
|
-
|
15
|
+
|
16
16
|
if limit.nil? || limit == 4
|
17
17
|
'int'
|
18
18
|
elsif limit == 2
|
data/lib/arjdbc/mysql.rb
ADDED
@@ -0,0 +1,388 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract/schema_definitions'
|
2
|
+
|
3
|
+
module ::ArJdbc
|
4
|
+
module MySQL
|
5
|
+
def self.column_selector
|
6
|
+
[/mysql/i, lambda {|cfg,col| col.extend(::ArJdbc::MySQL::Column)}]
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.extended(adapter)
|
10
|
+
adapter.configure_connection
|
11
|
+
end
|
12
|
+
|
13
|
+
def configure_connection
|
14
|
+
execute("SET SQL_AUTO_IS_NULL=0")
|
15
|
+
end
|
16
|
+
|
17
|
+
module Column
|
18
|
+
def extract_default(default)
|
19
|
+
if sql_type =~ /blob/i || type == :text
|
20
|
+
if default.blank?
|
21
|
+
return null ? nil : ''
|
22
|
+
else
|
23
|
+
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
|
24
|
+
end
|
25
|
+
elsif missing_default_forged_as_empty_string?(default)
|
26
|
+
nil
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_default?
|
33
|
+
return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
def simplified_type(field_type)
|
38
|
+
return :boolean if field_type =~ /tinyint\(1\)|bit/i
|
39
|
+
return :string if field_type =~ /enum/i
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
def extract_limit(sql_type)
|
44
|
+
case sql_type
|
45
|
+
when /blob|text/i
|
46
|
+
case sql_type
|
47
|
+
when /tiny/i
|
48
|
+
255
|
49
|
+
when /medium/i
|
50
|
+
16777215
|
51
|
+
when /long/i
|
52
|
+
2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
|
53
|
+
else
|
54
|
+
nil # we could return 65535 here, but we leave it undecorated by default
|
55
|
+
end
|
56
|
+
when /^bigint/i; 8
|
57
|
+
when /^int/i; 4
|
58
|
+
when /^mediumint/i; 3
|
59
|
+
when /^smallint/i; 2
|
60
|
+
when /^tinyint/i; 1
|
61
|
+
else
|
62
|
+
super
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# MySQL misreports NOT NULL column default when none is given.
|
67
|
+
# We can't detect this for columns which may have a legitimate ''
|
68
|
+
# default (string) but we can for others (integer, datetime, boolean,
|
69
|
+
# and the rest).
|
70
|
+
#
|
71
|
+
# Test whether the column has default '', is not null, and is not
|
72
|
+
# a type allowing default ''.
|
73
|
+
def missing_default_forged_as_empty_string?(default)
|
74
|
+
type != :string && !null && default == ''
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def modify_types(tp)
|
79
|
+
tp[:primary_key] = "int(11) DEFAULT NULL auto_increment PRIMARY KEY"
|
80
|
+
tp[:integer] = { :name => 'int', :limit => 4 }
|
81
|
+
tp[:decimal] = { :name => "decimal" }
|
82
|
+
tp[:timestamp] = { :name => "datetime" }
|
83
|
+
tp[:datetime][:limit] = nil
|
84
|
+
tp
|
85
|
+
end
|
86
|
+
|
87
|
+
def adapter_name #:nodoc:
|
88
|
+
'MySQL'
|
89
|
+
end
|
90
|
+
|
91
|
+
def case_sensitive_equality_operator
|
92
|
+
"= BINARY"
|
93
|
+
end
|
94
|
+
|
95
|
+
# QUOTING ==================================================
|
96
|
+
|
97
|
+
def quote(value, column = nil)
|
98
|
+
return value.quoted_id if value.respond_to?(:quoted_id)
|
99
|
+
|
100
|
+
if column && column.type == :primary_key
|
101
|
+
value.to_s
|
102
|
+
elsif column && String === value && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
103
|
+
s = column.class.string_to_binary(value).unpack("H*")[0]
|
104
|
+
"x'#{s}'"
|
105
|
+
elsif BigDecimal === value
|
106
|
+
"'#{value.to_s("F")}'"
|
107
|
+
else
|
108
|
+
super
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def quoted_true
|
113
|
+
"1"
|
114
|
+
end
|
115
|
+
|
116
|
+
def quoted_false
|
117
|
+
"0"
|
118
|
+
end
|
119
|
+
|
120
|
+
def begin_db_transaction #:nodoc:
|
121
|
+
@connection.begin
|
122
|
+
rescue Exception
|
123
|
+
# Transactions aren't supported
|
124
|
+
end
|
125
|
+
|
126
|
+
def commit_db_transaction #:nodoc:
|
127
|
+
@connection.commit
|
128
|
+
rescue Exception
|
129
|
+
# Transactions aren't supported
|
130
|
+
end
|
131
|
+
|
132
|
+
def rollback_db_transaction #:nodoc:
|
133
|
+
@connection.rollback
|
134
|
+
rescue Exception
|
135
|
+
# Transactions aren't supported
|
136
|
+
end
|
137
|
+
|
138
|
+
def supports_savepoints? #:nodoc:
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
def create_savepoint
|
143
|
+
execute("SAVEPOINT #{current_savepoint_name}")
|
144
|
+
end
|
145
|
+
|
146
|
+
def rollback_to_savepoint
|
147
|
+
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
148
|
+
end
|
149
|
+
|
150
|
+
def release_savepoint
|
151
|
+
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
152
|
+
end
|
153
|
+
|
154
|
+
def disable_referential_integrity(&block) #:nodoc:
|
155
|
+
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
|
156
|
+
begin
|
157
|
+
update("SET FOREIGN_KEY_CHECKS = 0")
|
158
|
+
yield
|
159
|
+
ensure
|
160
|
+
update("SET FOREIGN_KEY_CHECKS = #{old}")
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# SCHEMA STATEMENTS ========================================
|
165
|
+
|
166
|
+
def structure_dump #:nodoc:
|
167
|
+
if supports_views?
|
168
|
+
sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
|
169
|
+
else
|
170
|
+
sql = "SHOW TABLES"
|
171
|
+
end
|
172
|
+
|
173
|
+
select_all(sql).inject("") do |structure, table|
|
174
|
+
table.delete('Table_type')
|
175
|
+
|
176
|
+
hash = show_create_table(table.to_a.first.last)
|
177
|
+
|
178
|
+
if(table = hash["Create Table"])
|
179
|
+
structure += table + ";\n\n"
|
180
|
+
elsif(view = hash["Create View"])
|
181
|
+
structure += view + ";\n\n"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def recreate_database(name, options = {}) #:nodoc:
|
187
|
+
drop_database(name)
|
188
|
+
create_database(name, options)
|
189
|
+
end
|
190
|
+
|
191
|
+
def character_set(options) #:nodoc:
|
192
|
+
str = "CHARACTER SET `#{options[:charset] || 'utf8'}`"
|
193
|
+
str += " COLLATE `#{options[:collation]}`" if options[:collation]
|
194
|
+
str
|
195
|
+
end
|
196
|
+
private :character_set
|
197
|
+
|
198
|
+
def create_database(name, options = {}) #:nodoc:
|
199
|
+
execute "CREATE DATABASE `#{name}` DEFAULT #{character_set(options)}"
|
200
|
+
end
|
201
|
+
|
202
|
+
def drop_database(name) #:nodoc:
|
203
|
+
execute "DROP DATABASE IF EXISTS `#{name}`"
|
204
|
+
end
|
205
|
+
|
206
|
+
def current_database
|
207
|
+
select_one("SELECT DATABASE() as db")["db"]
|
208
|
+
end
|
209
|
+
|
210
|
+
def create_table(name, options = {}) #:nodoc:
|
211
|
+
super(name, {:options => "ENGINE=InnoDB #{character_set(options)}"}.merge(options))
|
212
|
+
end
|
213
|
+
|
214
|
+
def rename_table(name, new_name)
|
215
|
+
execute "RENAME TABLE #{quote_table_name(name)} TO #{quote_table_name(new_name)}"
|
216
|
+
end
|
217
|
+
|
218
|
+
def add_column(table_name, column_name, type, options = {})
|
219
|
+
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
220
|
+
add_column_options!(add_column_sql, options)
|
221
|
+
add_column_position!(add_column_sql, options)
|
222
|
+
execute(add_column_sql)
|
223
|
+
end
|
224
|
+
|
225
|
+
def change_column_default(table_name, column_name, default) #:nodoc:
|
226
|
+
column = column_for(table_name, column_name)
|
227
|
+
change_column table_name, column_name, column.sql_type, :default => default
|
228
|
+
end
|
229
|
+
|
230
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
231
|
+
column = column_for(table_name, column_name)
|
232
|
+
|
233
|
+
unless null || default.nil?
|
234
|
+
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
235
|
+
end
|
236
|
+
|
237
|
+
change_column table_name, column_name, column.sql_type, :null => null
|
238
|
+
end
|
239
|
+
|
240
|
+
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
241
|
+
column = column_for(table_name, column_name)
|
242
|
+
|
243
|
+
unless options_include_default?(options)
|
244
|
+
options[:default] = column.default
|
245
|
+
end
|
246
|
+
|
247
|
+
unless options.has_key?(:null)
|
248
|
+
options[:null] = column.null
|
249
|
+
end
|
250
|
+
|
251
|
+
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
252
|
+
add_column_options!(change_column_sql, options)
|
253
|
+
add_column_position!(change_column_sql, options)
|
254
|
+
execute(change_column_sql)
|
255
|
+
end
|
256
|
+
|
257
|
+
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
258
|
+
options = {}
|
259
|
+
if column = columns(table_name).find { |c| c.name == column_name.to_s }
|
260
|
+
options[:default] = column.default
|
261
|
+
options[:null] = column.null
|
262
|
+
else
|
263
|
+
raise ActiveRecord::ActiveRecordError, "No such column: #{table_name}.#{column_name}"
|
264
|
+
end
|
265
|
+
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
|
266
|
+
rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
|
267
|
+
add_column_options!(rename_column_sql, options)
|
268
|
+
execute(rename_column_sql)
|
269
|
+
end
|
270
|
+
|
271
|
+
def add_limit_offset!(sql, options) #:nodoc:
|
272
|
+
limit, offset = options[:limit], options[:offset]
|
273
|
+
if limit && offset
|
274
|
+
sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}"
|
275
|
+
elsif limit
|
276
|
+
sql << " LIMIT #{sanitize_limit(limit)}"
|
277
|
+
elsif offset
|
278
|
+
sql << " OFFSET #{offset.to_i}"
|
279
|
+
end
|
280
|
+
sql
|
281
|
+
end
|
282
|
+
|
283
|
+
def show_variable(var)
|
284
|
+
res = execute("show variables like '#{var}'")
|
285
|
+
row = res.detect {|row| row["Variable_name"] == var }
|
286
|
+
row && row["Value"]
|
287
|
+
end
|
288
|
+
|
289
|
+
def charset
|
290
|
+
show_variable("character_set_database")
|
291
|
+
end
|
292
|
+
|
293
|
+
def collation
|
294
|
+
show_variable("collation_database")
|
295
|
+
end
|
296
|
+
|
297
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
298
|
+
return super unless type.to_s == 'integer'
|
299
|
+
|
300
|
+
case limit
|
301
|
+
when 1; 'tinyint'
|
302
|
+
when 2; 'smallint'
|
303
|
+
when 3; 'mediumint'
|
304
|
+
when nil, 4, 11; 'int(11)' # compatibility with MySQL default
|
305
|
+
when 5..8; 'bigint'
|
306
|
+
else raise(ActiveRecordError, "No integer type has byte size #{limit}")
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def add_column_position!(sql, options)
|
311
|
+
if options[:first]
|
312
|
+
sql << " FIRST"
|
313
|
+
elsif options[:after]
|
314
|
+
sql << " AFTER #{quote_column_name(options[:after])}"
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
protected
|
319
|
+
def translate_exception(exception, message)
|
320
|
+
return super unless exception.respond_to?(:errno)
|
321
|
+
|
322
|
+
case exception.errno
|
323
|
+
when 1062
|
324
|
+
::ActiveRecord::RecordNotUnique.new(message, exception)
|
325
|
+
when 1452
|
326
|
+
::ActiveRecord::InvalidForeignKey.new(message, exception)
|
327
|
+
else
|
328
|
+
super
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
private
|
333
|
+
def column_for(table_name, column_name)
|
334
|
+
unless column = columns(table_name).find { |c| c.name == column_name.to_s }
|
335
|
+
raise "No such column: #{table_name}.#{column_name}"
|
336
|
+
end
|
337
|
+
column
|
338
|
+
end
|
339
|
+
|
340
|
+
def show_create_table(table)
|
341
|
+
select_one("SHOW CREATE TABLE #{quote_table_name(table)}")
|
342
|
+
end
|
343
|
+
|
344
|
+
def supports_views?
|
345
|
+
false
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
module ActiveRecord::ConnectionAdapters
|
351
|
+
class MysqlColumn < JdbcColumn
|
352
|
+
include ArJdbc::MySQL::Column
|
353
|
+
|
354
|
+
def initialize(name, *args)
|
355
|
+
if Hash === name
|
356
|
+
super
|
357
|
+
else
|
358
|
+
super(nil, name, *args)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
def call_discovered_column_callbacks(*)
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
class MysqlAdapter < JdbcAdapter
|
367
|
+
include ArJdbc::MySQL
|
368
|
+
|
369
|
+
def initialize(*args)
|
370
|
+
super
|
371
|
+
configure_connection
|
372
|
+
end
|
373
|
+
|
374
|
+
def adapter_spec(config)
|
375
|
+
# return nil to avoid extending ArJdbc::MySQL, which we've already done
|
376
|
+
end
|
377
|
+
|
378
|
+
def jdbc_column_class
|
379
|
+
ActiveRecord::ConnectionAdapters::MysqlColumn
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
module Mysql # :nodoc:
|
385
|
+
def self.client_version
|
386
|
+
50400 # faked out for AR tests
|
387
|
+
end
|
388
|
+
end
|