activerecord-jdbc-adapter 52.3-java → 52.4-java

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e777dea191806a6cde6a0503077077c57698b5d524e1acc31c98a6fe2dd8cfd
4
- data.tar.gz: 2f681bfdab105c7e598533d465476e3b265b9ad845d5d3eab494b763af71711c
3
+ metadata.gz: 7ef0f50586bac5757fd73503a4bbef0cca8dffe6611e490ce9e96f8762bee222
4
+ data.tar.gz: 65f0feb78bd45194736ba1855584553b4a70dee3e0484b48dc926beadbe57797
5
5
  SHA512:
6
- metadata.gz: 7931428c670ff5ed6ff3db57dbc00eca7462602caef72b6ae92003b344e6df201ed8b82e4cc5e16e16304506ae1501beaf2c7bf0e4bf312ecab08218060ccd8d
7
- data.tar.gz: f55aee0913cb8ffb5103a9123bff3188cbdef23744d05dde034948dadd2ffb82190c31653563c2ed00ed061849f65906f59a7f6a7da99ee3c69eaab0795e74f3
6
+ metadata.gz: ab6db0ce815b95b89b0b522c0691b6af891463ffbe69f479d09135dee85ae9801993a9aed1440504644909589436593761c1358205aba34df725e377c737ec94
7
+ data.tar.gz: f557a9c315b0dbd425598589db0bf2ea90e27d73bebd265344a51cbdf3e6ce1fa291e8a6593ffe82b409f9eb64aff6499dc5ef37619c637e4babfa6dc4d1b31c
data/README.md CHANGED
@@ -5,15 +5,16 @@
5
5
  ActiveRecord-JDBC-Adapter (AR-JDBC) is the main database adapter for Rails'
6
6
  *ActiveRecord* component that can be used with [JRuby][0].
7
7
  ActiveRecord-JDBC-Adapter provides full or nearly full support for:
8
- **MySQL**, **PostgreSQL**, **SQLite3**. In the near future there are plans to
9
- add support **MSSQL**. Unless we get more contributions we will not be going
10
- beyond these four adapters. Note that the amount of work needed to get
11
- another adapter is not huge but the amount of testing required to make sure
12
- that adapter continues to work is not something we can do with the resources
13
- we currently have.
8
+ **MySQL**, **PostgreSQL**, **SQLite3** and **MSSQL*** (SQLServer).
14
9
 
15
- For Oracle database users you are encouraged to use
16
- https://github.com/rsim/oracle-enhanced.
10
+ Unless we get more contributions we will not be supporting more adapters.
11
+ Note that the amount of work needed to get another adapter is not huge but
12
+ the amount of testing required to make sure that adapter continues to work
13
+ is not something we can do with the resources we currently have.
14
+
15
+ - for **Oracle** database users you are encouraged to use
16
+ https://github.com/rsim/oracle-enhanced
17
+ - **MSSQL** adapter's gem parts reside in a [separate repository][8]
17
18
 
18
19
  Versions are targeted at certain versions of Rails and live on their own branches.
19
20
 
@@ -39,6 +40,7 @@ adapters are available:
39
40
  - MySQL (`activerecord-jdbcmysql-adapter`)
40
41
  - PostgreSQL (`activerecord-jdbcpostgresql-adapter`)
41
42
  - SQLite3 (`activerecord-jdbcsqlite3-adapter`)
43
+ - MSSQL (`activerecord-jdbcsqlserver-adapter`)
42
44
 
43
45
  2. If you're generating a new Rails application, use the following command:
44
46
 
@@ -173,4 +175,4 @@ license the database's drivers are licensed. See each driver gem's LICENSE.txt.
173
175
  [5]: https://github.com/jruby/activerecord-jdbc-adapter/wiki
174
176
  [6]: https://webchat.freenode.net/?channels=#jruby
175
177
  [7]: http://badge.fury.io/rb/activerecord-jdbc-adapter
176
- [8]: https://github.com/jruby/activerecord-jdbc-adapter/wiki/Migrating-from-1.2.x-to-1.3.0
178
+ [8]: https://github.com/jruby/activerecord-jdbcsqlserver-adapter
data/Rakefile CHANGED
@@ -1,3 +1,29 @@
1
+ # Common usage
2
+ #
3
+ # rake build:adapters - to build all specific adapter gems and the base gem
4
+ # rake release:do - build:adapters + git tag + push gems
5
+ #
6
+ # Environment variables used by this Rakefile:
7
+ #
8
+ # INCLUDE_JAR_IN_GEM [default task - false, other taks - true]:
9
+ # Note: This is something you should not normally have to set.
10
+ # For local development we always will end up including the jar file
11
+ # in any task which generates our main gem. The wrinkle to this
12
+ # is when we do a custom github link in bundler:
13
+ #
14
+ # gem 'ar-jdbc', github: '...'
15
+ #
16
+ # Because we stopped committing a .jar file for every change and so when
17
+ # we include a gem like this it clones the repository and does a default
18
+ # build in rake. This in turn will end up forcing a compile to generate
19
+ # that jar (similar to how c-extensions compile at the time of install).
20
+ # For shipped gems we do include the jar so that people do not require
21
+ # this compile step.
22
+ #
23
+ # NOOP [release:do - false]
24
+ #
25
+ # No commands or gem pushing during a release.
26
+
1
27
  require 'rake/clean'
2
28
 
3
29
  CLEAN.include 'derby*', 'test.db.*', '*test.sqlite3', 'test/reports'
@@ -38,7 +64,8 @@ rake = lambda { |task| ruby "-S", "rake", task }
38
64
 
39
65
  desc "Build #{gem_name} gem into the pkg directory."
40
66
  task :build => :jar do
41
- sh("RELEASE=#{ENV['RELEASE']} gem build -V '#{gemspec_path}'") do
67
+ include_jar = ENV['INCLUDE_JAR_IN_GEM'] || 'true'
68
+ sh("INCLUDE_JAR_IN_GEM=#{include_jar} gem build -V '#{gemspec_path}'") do
42
69
  gem_path = built_gem_path.call
43
70
  file_name = File.basename(gem_path)
44
71
  FileUtils.mkdir_p(File.join(base_dir, 'pkg'))
@@ -58,7 +85,6 @@ end
58
85
 
59
86
  desc "Releasing AR-JDBC gems (use NOOP=true to disable gem pushing)"
60
87
  task 'release:do' do
61
- ENV['RELEASE'] = 'true' # so that .gemspec is built with adapter_java.jar
62
88
  Rake::Task['build'].invoke
63
89
  Rake::Task['build:adapters'].invoke
64
90
 
@@ -297,7 +323,7 @@ if defined? JRUBY_VERSION
297
323
  # class_files.gsub!('$', '\$') unless windows?
298
324
  # args = class_files.map { |path| [ "-C #{classes_dir}", path ] }.flatten
299
325
 
300
- if ENV['RELEASE'] == 'true'; require 'tempfile'
326
+ if ENV['INCLUDE_JAR_IN_GEM'] == 'true'; require 'tempfile'
301
327
  manifest = "Built-Time: #{Time.now.utc.strftime('%Y-%m-%d %H:%M:%S')}\n"
302
328
  manifest += "Built-JRuby: #{JRUBY_VERSION}\n"
303
329
  manifest += "Specification-Title: ActiveRecord-JDBC\n"
@@ -27,7 +27,7 @@ Gem::Specification.new do |gem|
27
27
  reject { |f| f =~ /^(gemfiles)/ } # no tests - no Gemfile_s appraised ...
28
28
  gem.files += ['lib/arjdbc/jdbc/adapter_java.jar'] #if ENV['RELEASE'].eql?('true')
29
29
 
30
- if ENV['RELEASE'] != 'true' # @see Rakefile
30
+ if ENV['INCLUDE_JAR_IN_GEM'] != 'true' # @see Rakefile
31
31
  gem.extensions << 'Rakefile' # to support auto-building .jar with :git paths
32
32
 
33
33
  #gem.add_runtime_dependency 'jar-dependencies', '~> 0.1' # development not enough!
@@ -56,7 +56,7 @@ module ArJdbc
56
56
  case e
57
57
  when SystemExit, SignalException, NoMemoryError then e
58
58
  when ActiveModel::RangeError, TypeError, RuntimeError then e
59
- else ActiveRecord::StatementInvalid.new(message)
59
+ else super
60
60
  end
61
61
  end
62
62
 
@@ -10,6 +10,8 @@ module ArJdbc
10
10
  NO_BINDS = [].freeze
11
11
 
12
12
  def exec_insert(sql, name = nil, binds = NO_BINDS, pk = nil, sequence_name = nil)
13
+ binds = convert_legacy_binds_to_attributes(binds) if binds.first.is_a?(Array)
14
+
13
15
  if without_prepared_statement?(binds)
14
16
  log(sql, name) { @connection.execute_insert(sql) }
15
17
  else
@@ -22,6 +24,8 @@ module ArJdbc
22
24
  # It appears that at this point (AR 5.0) "prepare" should only ever be true
23
25
  # if prepared statements are enabled
24
26
  def exec_query(sql, name = nil, binds = NO_BINDS, prepare: false)
27
+ binds = convert_legacy_binds_to_attributes(binds) if binds.first.is_a?(Array)
28
+
25
29
  if without_prepared_statement?(binds)
26
30
  log(sql, name) { @connection.execute_query(sql) }
27
31
  else
@@ -34,6 +38,8 @@ module ArJdbc
34
38
  end
35
39
 
36
40
  def exec_update(sql, name = nil, binds = NO_BINDS)
41
+ binds = convert_legacy_binds_to_attributes(binds) if binds.first.is_a?(Array)
42
+
37
43
  if without_prepared_statement?(binds)
38
44
  log(sql, name) { @connection.execute_update(sql) }
39
45
  else
@@ -42,25 +48,6 @@ module ArJdbc
42
48
  end
43
49
  alias :exec_delete :exec_update
44
50
 
45
- # overridden to support legacy binds
46
- def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
47
- binds = convert_legacy_binds_to_attributes(binds) if binds.first.is_a?(Array)
48
- super
49
- end
50
- alias create insert
51
-
52
- # overridden to support legacy binds
53
- def update(arel, name = nil, binds = [])
54
- binds = convert_legacy_binds_to_attributes(binds) if binds.first.is_a?(Array)
55
- super
56
- end
57
-
58
- # overridden to support legacy binds
59
- def delete(arel, name = nil, binds = [])
60
- binds = convert_legacy_binds_to_attributes(binds) if binds.first.is_a?(Array)
61
- super
62
- end
63
-
64
51
  def execute(sql, name = nil)
65
52
  log(sql, name) { @connection.execute(sql) }
66
53
  end
@@ -2,12 +2,8 @@ require 'active_support/deprecation'
2
2
 
3
3
  module ArJdbc
4
4
 
5
- # @private
6
- AR40 = ::ActiveRecord::VERSION::MAJOR > 3
7
5
  # @private
8
6
  AR42 = ::ActiveRecord::VERSION::STRING >= '4.2'
9
- # @private
10
- AR50 = ::ActiveRecord::VERSION::MAJOR > 4
11
7
 
12
8
  # @private
13
9
  AR52 = ::ActiveRecord::VERSION::STRING >= '5.2'
@@ -28,17 +28,13 @@ module ActiveRecord
28
28
 
29
29
  if ArJdbc::AR52
30
30
  # undefined method `cast' for #<ActiveRecord::ConnectionAdapters::SqlTypeMetadata> on AR52
31
- elsif ArJdbc::AR50
31
+ else
32
32
  default = args[0].cast(default)
33
33
 
34
34
  sql_type = args.delete_at(1)
35
35
  type = args.delete_at(0)
36
36
 
37
37
  args.unshift(SqlTypeMetadata.new(:sql_type => sql_type, :type => type))
38
- elsif ArJdbc::AR42
39
- default = args[0].type_cast_from_database(default)
40
- else
41
- default = default_value(default)
42
38
  end
43
39
 
44
40
  # super <= 4.1: (name, default, sql_type = nil, null = true)
@@ -15,7 +15,7 @@ module ArJdbc
15
15
  end
16
16
 
17
17
  # Extracts the value from a PostgreSQL column default definition.
18
- def extract_value_from_default(default) # :nodoc:
18
+ def extract_value_from_default(default)
19
19
  case default
20
20
  # Quoted types
21
21
  when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
@@ -41,10 +41,13 @@ module ArJdbc
41
41
  end
42
42
  end
43
43
 
44
- def extract_default_function(default_value, default) # :nodoc:
45
- default if ! default_value && ( %r{\w+\(.*\)|\(.*\)::\w+} === default )
44
+ def extract_default_function(default_value, default)
45
+ default if has_default_function?(default_value, default)
46
46
  end
47
47
 
48
+ def has_default_function?(default_value, default)
49
+ !default_value && %r{\w+\(.*\)|\(.*\)::\w+|CURRENT_DATE|CURRENT_TIMESTAMP} === default
50
+ end
48
51
  end
49
52
 
50
53
  end
@@ -1,3 +1,3 @@
1
1
  module ArJdbc
2
- VERSION = '52.3'
2
+ VERSION = '52.4'
3
3
  end
@@ -58,7 +58,6 @@ end
58
58
  test_task_for :Derby, :desc => 'Run tests against (embedded) DerbyDB'
59
59
  test_task_for :H2, :desc => 'Run tests against H2 database engine'
60
60
  test_task_for :HSQLDB, :desc => 'Run tests against HyperSQL (Java) database'
61
- test_task_for :MSSQL, :driver => :jtds, :database_name => 'MS-SQL (SQLServer)'
62
61
  test_task_for :MySQL #, :prereqs => 'db:mysql'
63
62
  task :test_mysql2 => :test_mysql
64
63
  test_task_for :PostgreSQL, :driver => ENV['JDBC_POSTGRES_VERSION'] || 'postgres' #, :prereqs => 'db:postgresql'
@@ -76,8 +75,6 @@ end
76
75
  test_task_for adapter, :desc => "Run tests against #{adapter} (ensure driver is on class-path)"
77
76
  end
78
77
 
79
- #test_task_for :MSSQL, :name => 'test_sqlserver', :driver => nil, :database_name => 'MS-SQL using SQLJDBC'
80
-
81
78
  test_task_for :AS400, :desc => "Run tests against AS400 (DB2) (ensure driver is on class-path)",
82
79
  :files => FileList["test/db2*_test.rb"] + FileList["test/db/db2/*_test.rb"]
83
80
 
@@ -487,7 +487,7 @@ public class RubyJdbcConnection extends RubyObject {
487
487
  savepoint = ((IRubyObject) savepoint).toJava(Savepoint.class);
488
488
  }
489
489
 
490
- connection.releaseSavepoint((Savepoint) savepoint);
490
+ releaseSavepoint(connection, (Savepoint) savepoint);
491
491
  return context.nil;
492
492
  }
493
493
  catch (SQLException e) {
@@ -495,6 +495,11 @@ public class RubyJdbcConnection extends RubyObject {
495
495
  }
496
496
  }
497
497
 
498
+ // MSSQL doesn't support releasing savepoints so we make it possible to override the actual release action
499
+ protected void releaseSavepoint(final Connection connection, final Savepoint savepoint) throws SQLException {
500
+ connection.releaseSavepoint(savepoint);
501
+ }
502
+
498
503
  protected static RuntimeException newSavepointNotSetError(final ThreadContext context, final IRubyObject name, final String op) {
499
504
  RubyClass StatementInvalid = ActiveRecord(context).getClass("StatementInvalid");
500
505
  return context.runtime.newRaiseException(StatementInvalid, "could not " + op + " savepoint: '" + name + "' (not set)");
@@ -3917,6 +3922,12 @@ public class RubyJdbcConnection extends RubyObject {
3917
3922
  }
3918
3923
  }
3919
3924
 
3925
+ public static void debugMessage(final ThreadContext context, final IRubyObject obj) {
3926
+ if ( isDebug(context.runtime) ) {
3927
+ debugMessage(context.runtime, obj.callMethod(context, "inspect"));
3928
+ }
3929
+ }
3930
+
3920
3931
  public static void debugMessage(final Ruby runtime, final String msg, final Object e) {
3921
3932
  if ( isDebug(runtime) ) {
3922
3933
  final PrintStream out = runtime != null ? runtime.getOut() : System.out;
@@ -28,17 +28,32 @@ package arjdbc.mssql;
28
28
  import arjdbc.jdbc.Callable;
29
29
  import arjdbc.jdbc.RubyJdbcConnection;
30
30
 
31
+ import java.lang.reflect.InvocationTargetException;
32
+ import java.lang.reflect.Method;
31
33
  import java.sql.Connection;
32
34
  import java.sql.DatabaseMetaData;
35
+ import java.sql.Date;
36
+ import java.sql.PreparedStatement;
33
37
  import java.sql.ResultSet;
38
+ import java.sql.Savepoint;
39
+ import java.sql.Statement;
34
40
  import java.sql.SQLException;
41
+ import java.sql.Timestamp;
35
42
  import java.sql.Types;
43
+ import java.util.ArrayList;
44
+ import java.util.HashMap;
45
+ import java.util.List;
46
+ import java.util.Map;
36
47
 
48
+ import arjdbc.util.DateTimeUtils;
49
+ import org.joda.time.DateTime;
50
+ import org.joda.time.DateTimeZone;
37
51
  import org.jruby.Ruby;
38
52
  import org.jruby.RubyArray;
39
53
  import org.jruby.RubyBoolean;
40
54
  import org.jruby.RubyClass;
41
55
  import org.jruby.RubyString;
56
+ import org.jruby.RubyTime;
42
57
  import org.jruby.anno.JRubyMethod;
43
58
  import org.jruby.runtime.ObjectAllocator;
44
59
  import org.jruby.runtime.ThreadContext;
@@ -52,6 +67,53 @@ import org.jruby.util.ByteList;
52
67
  public class MSSQLRubyJdbcConnection extends RubyJdbcConnection {
53
68
  private static final long serialVersionUID = -745716565005219263L;
54
69
 
70
+ private static final int DATETIMEOFFSET_TYPE;
71
+ private static final Method DateTimeOffsetGetMinutesOffsetMethod;
72
+ private static final Method DateTimeOffsetGetTimestampMethod;
73
+ private static final Method DateTimeOffsetValueOfMethod;
74
+ private static final Method PreparedStatementSetDateTimeOffsetMethod;
75
+
76
+ private static final Map<String, Integer> MSSQL_JDBC_TYPE_FOR = new HashMap<String, Integer>(32, 1);
77
+ static {
78
+
79
+ Class<?> DateTimeOffset;
80
+ Class<?> MssqlPreparedStatement;
81
+ Class<?> MssqlTypes;
82
+ try {
83
+ DateTimeOffset = Class.forName("microsoft.sql.DateTimeOffset");
84
+ MssqlPreparedStatement = Class.forName("com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement");
85
+ MssqlTypes = Class.forName("microsoft.sql.Types");
86
+ } catch (ClassNotFoundException e) {
87
+ System.err.println("You must require the Microsoft JDBC driver to use this gem"); // The exception doesn't bubble when ruby is initializing
88
+ throw new RuntimeException("You must require the Microsoft JDBC driver to use this gem");
89
+ }
90
+
91
+ try {
92
+ DATETIMEOFFSET_TYPE = MssqlTypes.getField("DATETIMEOFFSET").getInt(null);
93
+ DateTimeOffsetGetMinutesOffsetMethod = DateTimeOffset.getDeclaredMethod("getMinutesOffset");
94
+ DateTimeOffsetGetTimestampMethod = DateTimeOffset.getDeclaredMethod("getTimestamp");
95
+
96
+ Class<?>[] valueOfArgTypes = { Timestamp.class, int.class };
97
+ DateTimeOffsetValueOfMethod = DateTimeOffset.getDeclaredMethod("valueOf", valueOfArgTypes);
98
+
99
+ Class<?>[] setOffsetArgTypes = { int.class, DateTimeOffset };
100
+ PreparedStatementSetDateTimeOffsetMethod = MssqlPreparedStatement.getDeclaredMethod("setDateTimeOffset", setOffsetArgTypes);
101
+ } catch (Exception e) {
102
+ System.err.println("You must require the Microsoft JDBC driver to use this gem"); // The exception doesn't bubble when ruby is initializing
103
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
104
+ }
105
+
106
+ MSSQL_JDBC_TYPE_FOR.put("binary_basic", Types.BINARY);
107
+ MSSQL_JDBC_TYPE_FOR.put("datetimeoffset", DATETIMEOFFSET_TYPE);
108
+ MSSQL_JDBC_TYPE_FOR.put("money", Types.DECIMAL);
109
+ MSSQL_JDBC_TYPE_FOR.put("smalldatetime", Types.TIMESTAMP);
110
+ MSSQL_JDBC_TYPE_FOR.put("smallmoney", Types.DECIMAL);
111
+ MSSQL_JDBC_TYPE_FOR.put("ss_timestamp", Types.BINARY);
112
+ MSSQL_JDBC_TYPE_FOR.put("text_basic", Types.LONGVARCHAR);
113
+ MSSQL_JDBC_TYPE_FOR.put("uuid", Types.CHAR);
114
+ MSSQL_JDBC_TYPE_FOR.put("varchar_max", Types.VARCHAR);
115
+ }
116
+
55
117
  public MSSQLRubyJdbcConnection(Ruby runtime, RubyClass metaClass) {
56
118
  super(runtime, metaClass);
57
119
  }
@@ -83,6 +145,136 @@ public class MSSQLRubyJdbcConnection extends RubyJdbcConnection {
83
145
  return context.runtime.newBoolean( startsWithIgnoreCase(sqlBytes, EXEC) );
84
146
  }
85
147
 
148
+ // Support multiple result sets for mssql
149
+ @Override
150
+ @JRubyMethod(name = "execute", required = 1)
151
+ public IRubyObject execute(final ThreadContext context, final IRubyObject sql) {
152
+ final String query = sqlString(sql);
153
+ return withConnection(context, new Callable<IRubyObject>() {
154
+ public IRubyObject call(final Connection connection) throws SQLException {
155
+ Statement statement = null;
156
+ try {
157
+ statement = createStatement(context, connection);
158
+
159
+ // For DBs that do support multiple statements, lets return the last result set
160
+ // to be consistent with AR
161
+ boolean hasResultSet = doExecute(statement, query);
162
+ int updateCount = statement.getUpdateCount();
163
+
164
+ final List<IRubyObject> results = new ArrayList<IRubyObject>();
165
+ ResultSet resultSet;
166
+
167
+ while (hasResultSet || updateCount != -1) {
168
+
169
+ if (hasResultSet) {
170
+ resultSet = statement.getResultSet();
171
+
172
+ // Unfortunately the result set gets closed when getMoreResults()
173
+ // is called, so we have to process the result sets as we get them
174
+ // this shouldn't be an issue in most cases since we're only getting 1 result set anyways
175
+ results.add(mapExecuteResult(context, connection, resultSet));
176
+ } else {
177
+ results.add(context.runtime.newFixnum(updateCount));
178
+ }
179
+
180
+ // Check to see if there is another result set
181
+ hasResultSet = statement.getMoreResults();
182
+ updateCount = statement.getUpdateCount();
183
+ }
184
+
185
+ if (results.size() == 0) {
186
+ return context.nil; // If no results, return nil
187
+ } else if (results.size() == 1) {
188
+ return results.get(0);
189
+ } else {
190
+ return context.runtime.newArray(results);
191
+ }
192
+
193
+ } catch (final SQLException e) {
194
+ debugErrorSQL(context, query);
195
+ throw e;
196
+ } finally {
197
+ close(statement);
198
+ }
199
+ }
200
+ });
201
+ }
202
+
203
+ @Override
204
+ protected Integer jdbcTypeFor(final String type) {
205
+
206
+ Integer typeValue = MSSQL_JDBC_TYPE_FOR.get(type);
207
+
208
+ if ( typeValue != null ) {
209
+ return typeValue;
210
+ }
211
+
212
+ return super.jdbcTypeFor(type);
213
+ }
214
+
215
+ // Datetimeoffset values also make it into here
216
+ @Override
217
+ protected void setStringParameter(final ThreadContext context, final Connection connection,
218
+ final PreparedStatement statement, final int index, final IRubyObject value,
219
+ final IRubyObject attribute, final int type) throws SQLException {
220
+
221
+ // datetimeoffset values also make it in here
222
+ if (type == DATETIMEOFFSET_TYPE) {
223
+
224
+ Object dto = convertToDateTimeOffset(context, value);
225
+
226
+ try {
227
+
228
+ Object[] setStatementArgs = { index, dto };
229
+ PreparedStatementSetDateTimeOffsetMethod.invoke(statement, setStatementArgs);
230
+
231
+ } catch (IllegalAccessException e) {
232
+ debugMessage(context.runtime, e.getMessage());
233
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
234
+ } catch (InvocationTargetException e) {
235
+ debugMessage(context.runtime, e.getMessage());
236
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
237
+ }
238
+
239
+ return;
240
+ }
241
+ super.setStringParameter(context, connection, statement, index, value, attribute, type);
242
+ }
243
+
244
+ // We need higher precision than the default for Time objects (which is milliseconds) so we use a DateTimeOffset object
245
+ @Override
246
+ protected void setTimeParameter(final ThreadContext context,
247
+ final Connection connection, final PreparedStatement statement,
248
+ final int index, IRubyObject value,
249
+ final IRubyObject attribute, final int type) throws SQLException {
250
+
251
+ statement.setObject(index, convertToDateTimeOffset(context, value), Types.TIME);
252
+
253
+ }
254
+
255
+ private Object convertToDateTimeOffset(final ThreadContext context, final IRubyObject value) {
256
+
257
+ RubyTime time = (RubyTime) value;
258
+ DateTime dt = time.getDateTime();
259
+ Timestamp timestamp = new Timestamp(dt.getMillis());
260
+ timestamp.setNanos(timestamp.getNanos() + (int) time.getNSec());
261
+ int offsetMinutes = dt.getZone().getOffset(dt.getMillis()) / 60000;
262
+
263
+ try {
264
+
265
+ Object[] dtoArgs = { timestamp, offsetMinutes };
266
+ return DateTimeOffsetValueOfMethod.invoke(null, dtoArgs);
267
+
268
+ } catch (IllegalAccessException e) {
269
+ debugMessage(context.runtime, e.getMessage());
270
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
271
+ } catch (InvocationTargetException e) {
272
+ debugMessage(context.runtime, e.getMessage());
273
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
274
+ }
275
+ }
276
+
277
+
86
278
  @Override
87
279
  protected RubyArray mapTables(final ThreadContext context, final Connection connection,
88
280
  final String catalog, final String schemaPattern, final String tablePattern,
@@ -124,16 +316,79 @@ public class MSSQLRubyJdbcConnection extends RubyJdbcConnection {
124
316
 
125
317
  /**
126
318
  * Treat LONGVARCHAR as CLOB on MSSQL for purposes of converting a JDBC value to Ruby.
319
+ * Also handle datetimeoffset values here
127
320
  */
128
321
  @Override
129
322
  protected IRubyObject jdbcToRuby(
130
323
  final ThreadContext context, final Ruby runtime,
131
324
  final int column, int type, final ResultSet resultSet)
132
325
  throws SQLException {
133
- if ( type == Types.LONGVARCHAR || type == Types.LONGNVARCHAR ) type = Types.CLOB;
326
+
327
+ if (type == DATETIMEOFFSET_TYPE) {
328
+
329
+ Object dto = resultSet.getObject(column); // Returns a microsoft.sql.DateTimeOffset
330
+
331
+ if (dto == null) return context.nil;
332
+
333
+ try {
334
+
335
+ int minutes = (int) DateTimeOffsetGetMinutesOffsetMethod.invoke(dto);
336
+ DateTimeZone zone = DateTimeZone.forOffsetHoursMinutes(minutes / 60, minutes % 60);
337
+ Timestamp ts = (Timestamp) DateTimeOffsetGetTimestampMethod.invoke(dto);
338
+
339
+ int nanos = ts.getNanos(); // max 999-999-999
340
+ nanos = nanos % 1000000;
341
+
342
+ // We have to do this differently than the newTime helper because the Timestamp loses its zone information when passed around
343
+ DateTime dateTime = new DateTime(ts.getTime(), zone);
344
+ return RubyTime.newTime(context.runtime, dateTime, nanos);
345
+
346
+ } catch (IllegalAccessException e) {
347
+ debugMessage(runtime, e.getMessage());
348
+ return context.nil;
349
+ } catch (InvocationTargetException e) {
350
+ debugMessage(runtime, e.getMessage());
351
+ return context.nil;
352
+ }
353
+ }
354
+
355
+ if (type == Types.LONGVARCHAR || type == Types.LONGNVARCHAR) type = Types.CLOB;
134
356
  return super.jdbcToRuby(context, runtime, column, type, resultSet);
135
357
  }
136
358
 
359
+ /**
360
+ * Converts a JDBC date object to a Ruby date by referencing Date#civil
361
+ * @param context current thread context
362
+ * @param resultSet the jdbc result set to pull the value from
363
+ * @param index the index of the column to convert
364
+ * @return RubyNil if NULL or RubyDate if there is a value
365
+ * @throws SQLException if it fails to retrieve the value from the result set
366
+ */
367
+ @Override
368
+ protected IRubyObject dateToRuby(ThreadContext context, Ruby runtime, ResultSet resultSet, int index) throws SQLException {
369
+
370
+ final Date value = resultSet.getDate(index);
371
+
372
+ if (value == null) return context.nil;
373
+
374
+ return DateTimeUtils.newDate(context, value);
375
+ }
376
+
377
+ /**
378
+ * Converts a JDBC time to a Ruby time. We use timestamp because java.sql.Time doesn't support sub-millisecond values
379
+ * @param context current thread context
380
+ * @param resultSet the jdbc result set to pull the value from
381
+ * @param index the index of the column to convert
382
+ * @return RubyNil if NULL or RubyTime if there is a value
383
+ * @throws SQLException if it fails to retrieve the value from the result set
384
+ */
385
+ @Override
386
+ protected IRubyObject timeToRuby(final ThreadContext context,final Ruby runtime,
387
+ final ResultSet resultSet, final int column) throws SQLException {
388
+
389
+ return timestampToRuby(context, runtime, resultSet, column);
390
+ }
391
+
137
392
  @Override
138
393
  protected ColumnData[] extractColumns(final ThreadContext context,
139
394
  final Connection connection, final ResultSet resultSet,
@@ -163,19 +418,9 @@ public class MSSQLRubyJdbcConnection extends RubyJdbcConnection {
163
418
  return columns;
164
419
  }
165
420
 
166
- // internal helper not meant as a "public" API - used in one place thus every
167
- @JRubyMethod(name = "jtds_driver?")
168
- public RubyBoolean jtds_driver_p(final ThreadContext context) throws SQLException {
169
- // "jTDS Type 4 JDBC Driver for MS SQL Server and Sybase"
170
- // SQLJDBC: "Microsoft JDBC Driver 4.0 for SQL Server"
171
- return withConnection(context, new Callable<RubyBoolean>() {
172
- // NOTE: only used in one place for now (on release_savepoint) ...
173
- // might get optimized to only happen once since driver won't change
174
- public RubyBoolean call(final Connection connection) throws SQLException {
175
- final String driver = connection.getMetaData().getDriverName();
176
- return context.getRuntime().newBoolean( driver.indexOf("jTDS") >= 0 );
177
- }
178
- });
421
+ @Override
422
+ protected void releaseSavepoint(final Connection connection, final Savepoint savepoint) throws SQLException {
423
+ // MSSQL doesn't support releasing savepoints
179
424
  }
180
425
 
181
426
  }