activerecord-jdbc-adapter 51.4-java → 51.5-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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63cf6264259b636000fa1b17e13d555ca97d91aa9e237026bb81bac717b3130f
4
- data.tar.gz: ca5867009951cda63cccf434b87ec38fc0b541cd8a206899ed9ba23841ceb0c4
3
+ metadata.gz: 5ae6545b5caaa121f681eea19ce6111cd1fff119af8a378c88217f6ae2e31f36
4
+ data.tar.gz: 115b33124dfa6c2c0d502e0c2d3883e513e995d22849dd49a33822c2dc01d998
5
5
  SHA512:
6
- metadata.gz: a762d4f3877d7c834da321fa67784cee8ca8999c22ac3ec9d9e3f5f65df0d52ed2261dde7a91287c4b9cd0d5641f6786e26e0ab470d1b4c4c6527624163bbaf5
7
- data.tar.gz: 5fb6ee5eafa8a10a7389f7c4a471061553e5ca97e9347b2f78f69d94ec2fca28277f109eeeebe636de053324a346dcab25eab379351e46afd475855296bd3ae4
6
+ metadata.gz: f8539261190260793902f87665626c09775da9d2b8004b268ca72e0ebce0daacd5f37537d797f6d7e54690d67be43b9e0410717d2e6bcb052c6da82addbe8e7b
7
+ data.tar.gz: fb1150aa6c36615549a7627b938b9a583306b73b06edcd2e859b61cb5255c6d107ca33de967ae9dcda14a0a8b14f1f75c816ddcf48cd6f18ec27741ef73a7f2f
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
  Version **50.x** supports Rails version 5.0.x and it lives on branch 50-stable.
19
20
  Version **51.x** supports Rails version 5.1.x and is currently on master until
@@ -32,6 +33,7 @@ adapters are available:
32
33
  - MySQL (`activerecord-jdbcmysql-adapter`)
33
34
  - PostgreSQL (`activerecord-jdbcpostgresql-adapter`)
34
35
  - SQLite3 (`activerecord-jdbcsqlite3-adapter`)
36
+ - MSSQL (`activerecord-jdbcsqlserver-adapter`)
35
37
 
36
38
  2. If you're generating a new Rails application, use the following command:
37
39
 
@@ -166,4 +168,4 @@ license the database's drivers are licensed. See each driver gem's LICENSE.txt.
166
168
  [5]: https://github.com/jruby/activerecord-jdbc-adapter/wiki
167
169
  [6]: https://webchat.freenode.net/?channels=#jruby
168
170
  [7]: http://badge.fury.io/rb/activerecord-jdbc-adapter
169
- [8]: https://github.com/jruby/activerecord-jdbc-adapter/wiki/Migrating-from-1.2.x-to-1.3.0
171
+ [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!
@@ -22,7 +22,7 @@ module ArJdbc
22
22
 
23
23
  connection.configure_connection # will call us (maybe)
24
24
  end
25
-
25
+
26
26
  # Retrieve the raw `java.sql.Connection` object.
27
27
  # The unwrap parameter is useful if an attempt to unwrap a pooled (JNDI)
28
28
  # connection should be made - to really return the 'native' JDBC object.
@@ -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
@@ -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
  class << self
13
9
 
@@ -26,23 +26,17 @@ module ActiveRecord
26
26
  end
27
27
  end
28
28
 
29
- if ArJdbc::AR50
30
- default = args[0].cast(default)
29
+ default = args[0].cast(default)
31
30
 
32
- sql_type = args.delete_at(1)
33
- type = args.delete_at(0)
31
+ sql_type = args.delete_at(1)
32
+ type = args.delete_at(0)
34
33
 
35
- args.unshift(SqlTypeMetadata.new(:sql_type => sql_type, :type => type))
36
- elsif ArJdbc::AR42
37
- default = args[0].type_cast_from_database(default)
38
- else
39
- default = default_value(default)
40
- end
34
+ args.unshift(SqlTypeMetadata.new(:sql_type => sql_type, :type => type))
41
35
 
42
36
  # super <= 4.1: (name, default, sql_type = nil, null = true)
43
37
  # super >= 4.2: (name, default, cast_type, sql_type = nil, null = true)
44
38
  # super >= 5.0: (name, default, sql_type_metadata = nil, null = true)
45
-
39
+
46
40
  super(name, default, *args)
47
41
  init_column(name, default, *args)
48
42
  end
@@ -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 = '51.4'
2
+ VERSION = '51.5'
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
  }