activerecord-jdbc-adapter 60.0.rc1-java → 60.1-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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.travis.yml +42 -30
  4. data/README.md +36 -18
  5. data/Rakefile +30 -4
  6. data/Rakefile.jdbc +8 -1
  7. data/activerecord-jdbc-adapter.gemspec +6 -9
  8. data/lib/arjdbc/abstract/connection_management.rb +5 -0
  9. data/lib/arjdbc/abstract/core.rb +1 -1
  10. data/lib/arjdbc/abstract/database_statements.rb +8 -21
  11. data/lib/arjdbc/db2/adapter.rb +11 -0
  12. data/lib/arjdbc/db2/column.rb +0 -39
  13. data/lib/arjdbc/derby/adapter.rb +1 -20
  14. data/lib/arjdbc/firebird/adapter.rb +0 -21
  15. data/lib/arjdbc/h2/adapter.rb +0 -15
  16. data/lib/arjdbc/hsqldb/adapter.rb +0 -14
  17. data/lib/arjdbc/informix/adapter.rb +0 -23
  18. data/lib/arjdbc/jdbc.rb +0 -4
  19. data/lib/arjdbc/jdbc/adapter.rb +4 -0
  20. data/lib/arjdbc/jdbc/column.rb +1 -5
  21. data/lib/arjdbc/mysql/adapter.rb +12 -1
  22. data/lib/arjdbc/mysql/connection_methods.rb +13 -7
  23. data/lib/arjdbc/postgresql/adapter.rb +10 -19
  24. data/lib/arjdbc/postgresql/column.rb +6 -3
  25. data/lib/arjdbc/postgresql/connection_methods.rb +3 -1
  26. data/lib/arjdbc/sqlite3/adapter.rb +14 -21
  27. data/lib/arjdbc/sqlite3/connection_methods.rb +1 -0
  28. data/lib/arjdbc/tasks/databases.rake +3 -1
  29. data/lib/arjdbc/version.rb +1 -1
  30. data/rakelib/02-test.rake +0 -3
  31. data/rakelib/rails.rake +1 -1
  32. data/src/java/arjdbc/jdbc/RubyJdbcConnection.java +103 -33
  33. data/src/java/arjdbc/mssql/MSSQLRubyJdbcConnection.java +259 -14
  34. data/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java +1 -13
  35. data/src/java/arjdbc/util/DateTimeUtils.java +34 -12
  36. metadata +8 -22
  37. data/lib/active_record/connection_adapters/mssql_adapter.rb +0 -1
  38. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +0 -1
  39. data/lib/arjdbc/mssql.rb +0 -7
  40. data/lib/arjdbc/mssql/adapter.rb +0 -804
  41. data/lib/arjdbc/mssql/column.rb +0 -200
  42. data/lib/arjdbc/mssql/connection_methods.rb +0 -79
  43. data/lib/arjdbc/mssql/explain_support.rb +0 -99
  44. data/lib/arjdbc/mssql/limit_helpers.rb +0 -231
  45. data/lib/arjdbc/mssql/lock_methods.rb +0 -77
  46. data/lib/arjdbc/mssql/types.rb +0 -343
  47. data/lib/arjdbc/mssql/utils.rb +0 -82
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  ArJdbc::ConnectionMethods.module_eval do
3
3
  def sqlite3_connection(config)
4
+ config = config.deep_dup
4
5
  config[:adapter_spec] ||= ::ArJdbc::SQLite3
5
6
  config[:adapter_class] = ActiveRecord::ConnectionAdapters::SQLite3Adapter unless config.key?(:adapter_class)
6
7
 
@@ -5,12 +5,14 @@ module ActiveRecord::Tasks
5
5
  DatabaseTasks.module_eval do
6
6
 
7
7
  # @override patched to adapt jdbc configuration
8
- def each_current_configuration(environment)
8
+ def each_current_configuration(environment, spec_name = nil)
9
9
  environments = [environment]
10
10
  environments << 'test' if environment == 'development'
11
11
 
12
12
  environments.each do |env|
13
13
  ActiveRecord::Base.configurations.configs_for(env_name: env).each do |db_config|
14
+ next if spec_name && spec_name != db_config.spec_name
15
+
14
16
  yield adapt_jdbc_config(db_config.config), db_config.spec_name, env unless db_config.config['database'].blank?
15
17
  end
16
18
  end
@@ -1,3 +1,3 @@
1
1
  module ArJdbc
2
- VERSION = '60.0.rc1'
2
+ VERSION = '60.1'
3
3
  end
data/rakelib/02-test.rake CHANGED
@@ -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
 
data/rakelib/rails.rake CHANGED
@@ -51,7 +51,7 @@ namespace :rails do
51
51
  ruby_opts_string += " -C \"#{ar_path}\""
52
52
  ruby_opts_string += " -rbundler/setup"
53
53
  ruby_opts_string += " -rminitest -rminitest/excludes" unless ENV['NO_EXCLUDES'].eql?('true')
54
- file_list = ENV["TEST"] ? FileList[ ENV["TEST"] ] : test_files_finder.call
54
+ file_list = ENV["TEST"] ? FileList[ ENV["TEST"].split(',') ] : test_files_finder.call
55
55
  file_list_string = file_list.map { |fn| "\"#{fn}\"" }.join(' ')
56
56
  # test_loader_code = "-e \"ARGV.each{|f| require f}\"" # :direct
57
57
  option_list = ( ENV["TESTOPTS"] || ENV["TESTOPT"] || ENV["TEST_OPTS"] || '' )
@@ -85,6 +85,7 @@ import org.jruby.RubyTime;
85
85
  import org.jruby.anno.JRubyMethod;
86
86
  import org.jruby.exceptions.RaiseException;
87
87
  import org.jruby.ext.bigdecimal.RubyBigDecimal;
88
+ import org.jruby.ext.date.RubyDate;
88
89
  import org.jruby.javasupport.JavaEmbedUtils;
89
90
  import org.jruby.javasupport.JavaUtil;
90
91
  import org.jruby.runtime.Block;
@@ -465,7 +466,7 @@ public class RubyJdbcConnection extends RubyObject {
465
466
  }
466
467
 
467
468
  final Connection connection = getConnectionInternal(true);
468
- connection.releaseSavepoint((Savepoint) savepoint);
469
+ releaseSavepoint(connection, (Savepoint) savepoint);
469
470
  return context.nil;
470
471
  }
471
472
  catch (SQLException e) {
@@ -473,6 +474,11 @@ public class RubyJdbcConnection extends RubyObject {
473
474
  }
474
475
  }
475
476
 
477
+ // MSSQL doesn't support releasing savepoints so we make it possible to override the actual release action
478
+ protected void releaseSavepoint(final Connection connection, final Savepoint savepoint) throws SQLException {
479
+ connection.releaseSavepoint(savepoint);
480
+ }
481
+
476
482
  protected static RuntimeException newSavepointNotSetError(final ThreadContext context, final IRubyObject name, final String op) {
477
483
  RubyClass StatementInvalid = ActiveRecord(context).getClass("StatementInvalid");
478
484
  return context.runtime.newRaiseException(StatementInvalid, "could not " + op + " savepoint: '" + name + "' (not set)");
@@ -643,6 +649,13 @@ public class RubyJdbcConnection extends RubyObject {
643
649
  return context.runtime.newBoolean( isConnectionValid(context, connection) );
644
650
  }
645
651
 
652
+ @JRubyMethod(name = "really_valid?")
653
+ public RubyBoolean really_valid_p(final ThreadContext context) {
654
+ final Connection connection = getConnection(true);
655
+ if (connection == null) return context.fals;
656
+ return context.runtime.newBoolean(isConnectionValid(context, connection));
657
+ }
658
+
646
659
  @JRubyMethod(name = "disconnect!")
647
660
  public synchronized IRubyObject disconnect(final ThreadContext context) {
648
661
  setConnection(null); connected = false;
@@ -663,7 +676,10 @@ public class RubyJdbcConnection extends RubyObject {
663
676
 
664
677
  private void connectImpl(final boolean forceConnection) throws SQLException {
665
678
  setConnection( forceConnection ? newConnection() : null );
666
- if ( forceConnection ) configureConnection();
679
+ if (forceConnection) {
680
+ if (getConnectionImpl() == null) throw new SQLException("Didn't get a connection. Wrong URL?");
681
+ configureConnection();
682
+ }
667
683
  }
668
684
 
669
685
  @JRubyMethod(name = "read_only?")
@@ -819,62 +835,104 @@ public class RubyJdbcConnection extends RubyObject {
819
835
  return mapQueryResult(context, connection, resultSet);
820
836
  }
821
837
 
838
+ private static String[] createStatementPk(IRubyObject pk) {
839
+ String[] statementPk;
840
+ if (pk instanceof RubyArray) {
841
+ RubyArray ary = (RubyArray) pk;
842
+ int size = ary.size();
843
+ statementPk = new String[size];
844
+ for (int i = 0; i < size; i++) {
845
+ statementPk[i] = sqlString(ary.eltInternal(i));
846
+ }
847
+ } else {
848
+ statementPk = new String[] { sqlString(pk) };
849
+ }
850
+ return statementPk;
851
+ }
852
+
822
853
  /**
823
854
  * Executes an INSERT SQL statement
824
855
  * @param context
825
856
  * @param sql
857
+ * @param pk Rails PK
826
858
  * @return ActiveRecord::Result
827
859
  * @throws SQLException
828
860
  */
829
- @JRubyMethod(name = "execute_insert", required = 1)
830
- public IRubyObject execute_insert(final ThreadContext context, final IRubyObject sql) {
831
- return withConnection(context, connection -> {
832
- Statement statement = null;
833
- final String query = sqlString(sql);
834
- try {
861
+ @JRubyMethod(name = "execute_insert_pk", required = 2)
862
+ public IRubyObject execute_insert_pk(final ThreadContext context, final IRubyObject sql, final IRubyObject pk) {
863
+ return withConnection(context, new Callable<IRubyObject>() {
864
+ public IRubyObject call(final Connection connection) throws SQLException {
865
+ Statement statement = null;
866
+ final String query = sqlString(sql);
867
+ try {
835
868
 
836
- statement = createStatement(context, connection);
837
- statement.executeUpdate(query, Statement.RETURN_GENERATED_KEYS);
838
- return mapGeneratedKeys(context, connection, statement);
869
+ statement = createStatement(context, connection);
839
870
 
840
- } catch (final SQLException e) {
841
- debugErrorSQL(context, query);
842
- throw e;
843
- } finally {
844
- close(statement);
871
+ if (pk == context.nil || pk == context.fals || !supportsGeneratedKeys(connection)) {
872
+ statement.executeUpdate(query, Statement.RETURN_GENERATED_KEYS);
873
+ } else {
874
+ statement.executeUpdate(query, createStatementPk(pk));
875
+ }
876
+
877
+ return mapGeneratedKeys(context, connection, statement);
878
+ } catch (final SQLException e) {
879
+ debugErrorSQL(context, query);
880
+ throw e;
881
+ } finally {
882
+ close(statement);
883
+ }
845
884
  }
846
885
  });
847
886
  }
848
887
 
888
+ @Deprecated
889
+ @JRubyMethod(name = "execute_insert", required = 1)
890
+ public IRubyObject execute_insert(final ThreadContext context, final IRubyObject sql) {
891
+ return execute_insert_pk(context, sql, context.nil);
892
+ }
893
+
849
894
  /**
850
895
  * Executes an INSERT SQL statement using a prepared statement
851
896
  * @param context
852
897
  * @param sql
853
898
  * @param binds RubyArray of values to be bound to the query
899
+ * @param pk Rails PK
854
900
  * @return ActiveRecord::Result
855
901
  * @throws SQLException
856
902
  */
857
- @JRubyMethod(name = "execute_insert", required = 2)
858
- public IRubyObject execute_insert(final ThreadContext context, final IRubyObject sql, final IRubyObject binds) {
859
- return withConnection(context, connection -> {
860
- PreparedStatement statement = null;
861
- final String query = sqlString(sql);
862
- try {
863
-
864
- statement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);
865
- setStatementParameters(context, connection, statement, (RubyArray) binds);
866
- statement.executeUpdate();
867
- return mapGeneratedKeys(context, connection, statement);
903
+ @JRubyMethod(name = "execute_insert_pk", required = 3)
904
+ public IRubyObject execute_insert_pk(final ThreadContext context, final IRubyObject sql, final IRubyObject binds,
905
+ final IRubyObject pk) {
906
+ return withConnection(context, new Callable<IRubyObject>() {
907
+ public IRubyObject call(final Connection connection) throws SQLException {
908
+ PreparedStatement statement = null;
909
+ final String query = sqlString(sql);
910
+ try {
911
+ if (pk == context.nil || pk == context.fals || !supportsGeneratedKeys(connection)) {
912
+ statement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);
913
+ } else {
914
+ statement = connection.prepareStatement(query, createStatementPk(pk));
915
+ }
868
916
 
869
- } catch (final SQLException e) {
870
- debugErrorSQL(context, query);
871
- throw e;
872
- } finally {
873
- close(statement);
917
+ setStatementParameters(context, connection, statement, (RubyArray) binds);
918
+ statement.executeUpdate();
919
+ return mapGeneratedKeys(context, connection, statement);
920
+ } catch (final SQLException e) {
921
+ debugErrorSQL(context, query);
922
+ throw e;
923
+ } finally {
924
+ close(statement);
925
+ }
874
926
  }
875
927
  });
876
928
  }
877
929
 
930
+ @Deprecated
931
+ @JRubyMethod(name = "execute_insert", required = 2)
932
+ public IRubyObject execute_insert(final ThreadContext context, final IRubyObject binds, final IRubyObject sql) {
933
+ return execute_insert_pk(context, sql, binds, context.nil);
934
+ }
935
+
878
936
  /**
879
937
  * Executes an UPDATE (DELETE) SQL statement
880
938
  * @param context
@@ -2679,6 +2737,12 @@ public class RubyJdbcConnection extends RubyObject {
2679
2737
  value = value.callMethod(context, "to_date");
2680
2738
  }
2681
2739
 
2740
+ if (value instanceof RubyDate) {
2741
+ RubyDate rubyDate = (RubyDate) value;
2742
+ statement.setDate(index, rubyDate.toJava(Date.class));
2743
+ return;
2744
+ }
2745
+
2682
2746
  // NOTE: assuming Date#to_s does right ...
2683
2747
  statement.setDate(index, Date.valueOf(value.toString()));
2684
2748
  }
@@ -3158,8 +3222,8 @@ public class RubyJdbcConnection extends RubyObject {
3158
3222
  final ColumnData[] columns = extractColumns(context, connection, resultSet, false);
3159
3223
 
3160
3224
  final Ruby runtime = context.runtime;
3161
- final IRubyObject[] blockArgs = new IRubyObject[columns.length];
3162
3225
  while ( resultSet.next() ) {
3226
+ final IRubyObject[] blockArgs = new IRubyObject[columns.length];
3163
3227
  for ( int i = 0; i < columns.length; i++ ) {
3164
3228
  final ColumnData column = columns[i];
3165
3229
  blockArgs[i] = jdbcToRuby(context, runtime, column.index, column.type, resultSet);
@@ -3680,6 +3744,12 @@ public class RubyJdbcConnection extends RubyObject {
3680
3744
  }
3681
3745
  }
3682
3746
 
3747
+ public static void debugMessage(final ThreadContext context, final IRubyObject obj) {
3748
+ if ( isDebug(context.runtime) ) {
3749
+ debugMessage(context.runtime, obj.callMethod(context, "inspect"));
3750
+ }
3751
+ }
3752
+
3683
3753
  public static void debugMessage(final Ruby runtime, final String msg, final Object e) {
3684
3754
  if ( isDebug(runtime) ) {
3685
3755
  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
  }