activerecord-jdbc-alt-adapter 52.2.1-java → 52.6.0-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -127,7 +127,7 @@ module ActiveRecord
127
127
  end
128
128
 
129
129
  def identity_column_name(table_name)
130
- for column in columns(table_name)
130
+ for column in schema_cache.columns(table_name)
131
131
  return column.name if column.identity?
132
132
  end
133
133
  nil
@@ -46,7 +46,7 @@ module ActiveRecord
46
46
  # NOTE: This is ready, all implemented in the java part of adapter,
47
47
  # it uses MSSQLColumn, SqlTypeMetadata, etc.
48
48
  def columns(table_name)
49
- @connection.columns(table_name)
49
+ log('JDBC: GETCOLUMNS', 'SCHEMA') { @connection.columns(table_name) }
50
50
  rescue => e
51
51
  # raise translate_exception_class(e, nil)
52
52
  # FIXME: this breaks one arjdbc test but fixes activerecord tests
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  ArJdbc::ConnectionMethods.module_eval do
3
3
  def mysql_connection(config)
4
+ config = config.deep_dup
4
5
  # NOTE: this isn't "really" necessary but Rails (in tests) assumes being able to :
5
6
  # ActiveRecord::Base.mysql2_connection ActiveRecord::Base.configurations['arunit'].merge(database: ...)
6
7
  config = symbolize_keys_if_necessary(config)
@@ -10,11 +11,9 @@ ArJdbc::ConnectionMethods.module_eval do
10
11
 
11
12
  return jndi_connection(config) if jndi_config?(config)
12
13
 
13
- driver = config[:driver] ||=
14
- defined?(::Jdbc::MySQL.driver_name) ? ::Jdbc::MySQL.driver_name : 'com.mysql.jdbc.Driver'
15
-
16
- mysql_driver = driver.start_with?('com.mysql.')
17
- mariadb_driver = ! mysql_driver && driver.start_with?('org.mariadb.')
14
+ driver = config[:driver]
15
+ mysql_driver = driver.nil? || driver.to_s.start_with?('com.mysql.')
16
+ mariadb_driver = ! mysql_driver && driver.to_s.start_with?('org.mariadb.')
18
17
 
19
18
  begin
20
19
  require 'jdbc/mysql'
@@ -22,6 +21,11 @@ ArJdbc::ConnectionMethods.module_eval do
22
21
  rescue LoadError # assuming driver.jar is on the class-path
23
22
  end if mysql_driver
24
23
 
24
+ if driver.nil?
25
+ config[:driver] ||=
26
+ defined?(::Jdbc::MySQL.driver_name) ? ::Jdbc::MySQL.driver_name : 'com.mysql.jdbc.Driver'
27
+ end
28
+
25
29
  config[:username] = 'root' unless config.key?(:username)
26
30
  # jdbc:mysql://[host][,failoverhost...][:port]/[database]
27
31
  # - if the host name is not specified, it defaults to 127.0.0.1
@@ -36,7 +40,8 @@ ArJdbc::ConnectionMethods.module_eval do
36
40
 
37
41
  properties = ( config[:properties] ||= {} )
38
42
  if mysql_driver
39
- properties['zeroDateTimeBehavior'] ||= 'convertToNull'
43
+ properties['zeroDateTimeBehavior'] ||=
44
+ config[:driver].to_s.start_with?('com.mysql.cj.') ? 'CONVERT_TO_NULL' : 'convertToNull'
40
45
  properties['jdbcCompliantTruncation'] ||= false
41
46
  # NOTE: this is "better" than passing what users are used to set on MRI
42
47
  # e.g. 'utf8mb4' will fail cause the driver will check for a Java charset
@@ -108,7 +113,8 @@ ArJdbc::ConnectionMethods.module_eval do
108
113
  rescue LoadError # assuming driver.jar is on the class-path
109
114
  end
110
115
 
111
- config[:driver] ||= 'org.mariadb.jdbc.Driver'
116
+ config[:driver] ||=
117
+ defined?(::Jdbc::MariaDB.driver_name) ? ::Jdbc::MariaDB.driver_name : 'org.mariadb.jdbc.Driver'
112
118
 
113
119
  mysql_connection(config)
114
120
  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,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  ArJdbc::ConnectionMethods.module_eval do
3
3
  def postgresql_connection(config)
4
+ config = config.deep_dup
4
5
  # NOTE: this isn't "really" necessary but Rails (in tests) assumes being able to :
5
6
  # ActiveRecord::Base.postgresql_connection ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false)
6
7
  # ... while using symbols by default but than configurations returning string keys ;(
@@ -16,7 +17,8 @@ ArJdbc::ConnectionMethods.module_eval do
16
17
  ::Jdbc::Postgres.load_driver(:require) if defined?(::Jdbc::Postgres.load_driver)
17
18
  rescue LoadError # assuming driver.jar is on the class-path
18
19
  end
19
- driver = config[:driver] ||= 'org.postgresql.Driver'
20
+ driver = (config[:driver] ||=
21
+ defined?(::Jdbc::Postgres.driver_name) ? ::Jdbc::Postgres.driver_name : 'org.postgresql.Driver')
20
22
 
21
23
  host = config[:host] ||= ( config[:hostaddr] || ENV['PGHOST'] || 'localhost' )
22
24
  port = config[:port] ||= ( ENV['PGPORT'] || 5432 )
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module ArJdbc
2
- VERSION = '52.2.1'
2
+ VERSION = '52.6.0'
3
3
  end
@@ -1,6 +1,6 @@
1
1
  namespace :'tomcat-jndi' do # contains a FS JNDI impl (for tests)
2
2
 
3
- TOMCAT_MAVEN_REPO = 'http://repo2.maven.org/maven2/org/apache/tomcat'
3
+ TOMCAT_MAVEN_REPO = 'https://repo1.maven.org/maven2/org/apache/tomcat'
4
4
  TOMCAT_VERSION = '7.0.54'
5
5
 
6
6
  DOWNLOAD_DIR = File.expand_path('../test/jars', File.dirname(__FILE__))
@@ -48,4 +48,4 @@ namespace :'tomcat-jndi' do # contains a FS JNDI impl (for tests)
48
48
  rm jar_path if File.exist?(jar_path)
49
49
  end
50
50
 
51
- end
51
+ end
@@ -76,8 +76,6 @@ end
76
76
  test_task_for adapter, :desc => "Run tests against #{adapter} (ensure driver is on class-path)"
77
77
  end
78
78
 
79
- #test_task_for :MSSQL, :name => 'test_sqlserver', :driver => nil, :database_name => 'MS-SQL using SQLJDBC'
80
-
81
79
  test_task_for :AS400, :desc => "Run tests against AS400 (DB2) (ensure driver is on class-path)",
82
80
  :files => FileList["test/db2*_test.rb"] + FileList["test/db/db2/*_test.rb"]
83
81
 
@@ -57,7 +57,7 @@ namespace :rails do
57
57
  ruby_opts_string += " -C \"#{ar_path}\""
58
58
  ruby_opts_string += " -rbundler/setup"
59
59
  ruby_opts_string += " -rminitest -rminitest/excludes" unless ENV['NO_EXCLUDES'].eql?('true')
60
- file_list = ENV["TEST"] ? FileList[ ENV["TEST"] ] : test_files_finder.call
60
+ file_list = ENV["TEST"] ? FileList[ ENV["TEST"].split(',') ] : test_files_finder.call
61
61
  file_list_string = file_list.map { |fn| "\"#{fn}\"" }.join(' ')
62
62
  # test_loader_code = "-e \"ARGV.each{|f| require f}\"" # :direct
63
63
  option_list = ( ENV["TESTOPTS"] || ENV["TESTOPT"] || ENV["TEST_OPTS"] || '' )
@@ -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)");
@@ -722,7 +727,10 @@ public class RubyJdbcConnection extends RubyObject {
722
727
 
723
728
  private void connectImpl(final boolean forceConnection) throws SQLException {
724
729
  setConnection( forceConnection ? newConnection() : null );
725
- if ( forceConnection ) configureConnection();
730
+ if (forceConnection) {
731
+ if (getConnectionImpl() == null) throw new SQLException("Didn't get a connection. Wrong URL?");
732
+ configureConnection();
733
+ }
726
734
  }
727
735
 
728
736
  @JRubyMethod(name = "read_only?")
@@ -880,15 +888,31 @@ public class RubyJdbcConnection extends RubyObject {
880
888
  return mapQueryResult(context, connection, resultSet);
881
889
  }
882
890
 
891
+ private static String[] createStatementPk(IRubyObject pk) {
892
+ String[] statementPk;
893
+ if (pk instanceof RubyArray) {
894
+ RubyArray ary = (RubyArray) pk;
895
+ int size = ary.size();
896
+ statementPk = new String[size];
897
+ for (int i = 0; i < size; i++) {
898
+ statementPk[i] = sqlString(ary.eltInternal(i));
899
+ }
900
+ } else {
901
+ statementPk = new String[] { sqlString(pk) };
902
+ }
903
+ return statementPk;
904
+ }
905
+
883
906
  /**
884
907
  * Executes an INSERT SQL statement
885
908
  * @param context
886
909
  * @param sql
910
+ * @param pk Rails PK
887
911
  * @return ActiveRecord::Result
888
912
  * @throws SQLException
889
913
  */
890
- @JRubyMethod(name = "execute_insert", required = 1)
891
- public IRubyObject execute_insert(final ThreadContext context, final IRubyObject sql) {
914
+ @JRubyMethod(name = "execute_insert_pk", required = 2)
915
+ public IRubyObject execute_insert_pk(final ThreadContext context, final IRubyObject sql, final IRubyObject pk) {
892
916
  return withConnection(context, new Callable<IRubyObject>() {
893
917
  public IRubyObject call(final Connection connection) throws SQLException {
894
918
  Statement statement = null;
@@ -896,7 +920,13 @@ public class RubyJdbcConnection extends RubyObject {
896
920
  try {
897
921
 
898
922
  statement = createStatement(context, connection);
899
- statement.executeUpdate(query, Statement.RETURN_GENERATED_KEYS);
923
+
924
+ if (pk == context.nil || pk == context.fals || !supportsGeneratedKeys(connection)) {
925
+ statement.executeUpdate(query, Statement.RETURN_GENERATED_KEYS);
926
+ } else {
927
+ statement.executeUpdate(query, createStatementPk(pk));
928
+ }
929
+
900
930
  return mapGeneratedKeys(context, connection, statement);
901
931
 
902
932
  } catch (final SQLException e) {
@@ -909,23 +939,35 @@ public class RubyJdbcConnection extends RubyObject {
909
939
  });
910
940
  }
911
941
 
942
+ @Deprecated
943
+ @JRubyMethod(name = "execute_insert", required = 1)
944
+ public IRubyObject execute_insert(final ThreadContext context, final IRubyObject sql) {
945
+ return execute_insert_pk(context, sql, context.nil);
946
+ }
947
+
912
948
  /**
913
949
  * Executes an INSERT SQL statement using a prepared statement
914
950
  * @param context
915
951
  * @param sql
916
952
  * @param binds RubyArray of values to be bound to the query
953
+ * @param pk Rails PK
917
954
  * @return ActiveRecord::Result
918
955
  * @throws SQLException
919
956
  */
920
- @JRubyMethod(name = "execute_insert", required = 2)
921
- public IRubyObject execute_insert(final ThreadContext context, final IRubyObject sql, final IRubyObject binds) {
957
+ @JRubyMethod(name = "execute_insert_pk", required = 3)
958
+ public IRubyObject execute_insert_pk(final ThreadContext context, final IRubyObject sql, final IRubyObject binds,
959
+ final IRubyObject pk) {
922
960
  return withConnection(context, new Callable<IRubyObject>() {
923
961
  public IRubyObject call(final Connection connection) throws SQLException {
924
962
  PreparedStatement statement = null;
925
963
  final String query = sqlString(sql);
926
964
  try {
965
+ if (pk == context.nil || pk == context.fals || !supportsGeneratedKeys(connection)) {
966
+ statement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);
967
+ } else {
968
+ statement = connection.prepareStatement(query, createStatementPk(pk));
969
+ }
927
970
 
928
- statement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);
929
971
  setStatementParameters(context, connection, statement, (RubyArray) binds);
930
972
  statement.executeUpdate();
931
973
  return mapGeneratedKeys(context, connection, statement);
@@ -940,6 +982,12 @@ public class RubyJdbcConnection extends RubyObject {
940
982
  });
941
983
  }
942
984
 
985
+ @Deprecated
986
+ @JRubyMethod(name = "execute_insert", required = 2)
987
+ public IRubyObject execute_insert(final ThreadContext context, final IRubyObject binds, final IRubyObject sql) {
988
+ return execute_insert_pk(context, sql, binds, context.nil);
989
+ }
990
+
943
991
  /**
944
992
  * Executes an UPDATE (DELETE) SQL statement
945
993
  * @param context
@@ -2300,7 +2348,7 @@ public class RubyJdbcConnection extends RubyObject {
2300
2348
  return RubyString.newString(runtime, DateTimeUtils.dateToString(value));
2301
2349
  }
2302
2350
 
2303
- return DateTimeUtils.newDateAsTime(context, value, null).callMethod(context, "to_date");
2351
+ return DateTimeUtils.newDateAsTime(context, value, DateTimeZone.UTC).callMethod(context, "to_date");
2304
2352
  }
2305
2353
 
2306
2354
  protected IRubyObject timeToRuby(final ThreadContext context,
@@ -3928,6 +3976,12 @@ public class RubyJdbcConnection extends RubyObject {
3928
3976
  }
3929
3977
  }
3930
3978
 
3979
+ public static void debugMessage(final ThreadContext context, final IRubyObject obj) {
3980
+ if ( isDebug(context.runtime) ) {
3981
+ debugMessage(context.runtime, obj.callMethod(context, "inspect"));
3982
+ }
3983
+ }
3984
+
3931
3985
  public static void debugMessage(final Ruby runtime, final String msg, final Object e) {
3932
3986
  if ( isDebug(runtime) ) {
3933
3987
  final PrintStream out = runtime != null ? runtime.getOut() : System.out;
@@ -29,22 +29,33 @@ import arjdbc.jdbc.Callable;
29
29
  import arjdbc.jdbc.RubyJdbcConnection;
30
30
  import arjdbc.util.DateTimeUtils;
31
31
 
32
+ import java.lang.reflect.InvocationTargetException;
33
+ import java.lang.reflect.Method;
32
34
  import java.sql.Connection;
33
35
  import java.sql.DatabaseMetaData;
36
+ import java.sql.Date;
34
37
  import java.sql.PreparedStatement;
35
38
  import java.sql.ResultSet;
36
39
  import java.sql.Savepoint;
40
+ import java.sql.Statement;
37
41
  import java.sql.SQLException;
38
- import java.sql.Types;
39
42
  import java.sql.Timestamp;
43
+ import java.sql.Types;
40
44
  import java.util.Locale;
45
+ import java.util.ArrayList;
46
+ import java.util.HashMap;
47
+ import java.util.List;
48
+ import java.util.Map;
41
49
 
50
+ import org.joda.time.DateTime;
51
+ import org.joda.time.DateTimeZone;
42
52
  import org.jruby.Ruby;
43
53
  import org.jruby.RubyArray;
44
54
  import org.jruby.RubyBoolean;
45
55
  import org.jruby.RubyClass;
46
56
  import org.jruby.RubyString;
47
57
  import org.jruby.RubySymbol;
58
+ import org.jruby.RubyTime;
48
59
  import org.jruby.anno.JRubyMethod;
49
60
  import org.jruby.runtime.ObjectAllocator;
50
61
  import org.jruby.runtime.ThreadContext;
@@ -58,6 +69,54 @@ import org.jruby.util.ByteList;
58
69
  public class MSSQLRubyJdbcConnection extends RubyJdbcConnection {
59
70
  private static final long serialVersionUID = -745716565005219263L;
60
71
 
72
+ private static final int DATETIMEOFFSET_TYPE;
73
+ private static final Method DateTimeOffsetGetMinutesOffsetMethod;
74
+ private static final Method DateTimeOffsetGetTimestampMethod;
75
+ private static final Method DateTimeOffsetValueOfMethod;
76
+ private static final Method PreparedStatementSetDateTimeOffsetMethod;
77
+
78
+ private static final Map<String, Integer> MSSQL_JDBC_TYPE_FOR = new HashMap<String, Integer>(32, 1);
79
+ static {
80
+
81
+ Class<?> DateTimeOffset;
82
+ Class<?> MssqlPreparedStatement;
83
+ Class<?> MssqlTypes;
84
+ try {
85
+ DateTimeOffset = Class.forName("microsoft.sql.DateTimeOffset");
86
+ MssqlPreparedStatement = Class.forName("com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement");
87
+ MssqlTypes = Class.forName("microsoft.sql.Types");
88
+ } catch (ClassNotFoundException e) {
89
+ System.err.println("You must require the Microsoft JDBC driver to use this gem"); // The exception doesn't bubble when ruby is initializing
90
+ throw new RuntimeException("You must require the Microsoft JDBC driver to use this gem");
91
+ }
92
+
93
+ try {
94
+ DATETIMEOFFSET_TYPE = MssqlTypes.getField("DATETIMEOFFSET").getInt(null);
95
+ DateTimeOffsetGetMinutesOffsetMethod = DateTimeOffset.getDeclaredMethod("getMinutesOffset");
96
+ DateTimeOffsetGetTimestampMethod = DateTimeOffset.getDeclaredMethod("getTimestamp");
97
+
98
+ Class<?>[] valueOfArgTypes = { Timestamp.class, int.class };
99
+ DateTimeOffsetValueOfMethod = DateTimeOffset.getDeclaredMethod("valueOf", valueOfArgTypes);
100
+
101
+ Class<?>[] setOffsetArgTypes = { int.class, DateTimeOffset };
102
+ PreparedStatementSetDateTimeOffsetMethod = MssqlPreparedStatement.getDeclaredMethod("setDateTimeOffset", setOffsetArgTypes);
103
+ } catch (Exception e) {
104
+ System.err.println("You must require the Microsoft JDBC driver to use this gem"); // The exception doesn't bubble when ruby is initializing
105
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
106
+ }
107
+
108
+ MSSQL_JDBC_TYPE_FOR.put("binary_basic", Types.BINARY);
109
+ MSSQL_JDBC_TYPE_FOR.put("image", Types.BINARY);
110
+ MSSQL_JDBC_TYPE_FOR.put("datetimeoffset", DATETIMEOFFSET_TYPE);
111
+ MSSQL_JDBC_TYPE_FOR.put("money", Types.DECIMAL);
112
+ MSSQL_JDBC_TYPE_FOR.put("smalldatetime", Types.TIMESTAMP);
113
+ MSSQL_JDBC_TYPE_FOR.put("smallmoney", Types.DECIMAL);
114
+ MSSQL_JDBC_TYPE_FOR.put("ss_timestamp", Types.BINARY);
115
+ MSSQL_JDBC_TYPE_FOR.put("text_basic", Types.LONGVARCHAR);
116
+ MSSQL_JDBC_TYPE_FOR.put("uuid", Types.CHAR);
117
+ MSSQL_JDBC_TYPE_FOR.put("varchar_max", Types.VARCHAR);
118
+ }
119
+
61
120
  public MSSQLRubyJdbcConnection(Ruby runtime, RubyClass metaClass) {
62
121
  super(runtime, metaClass);
63
122
  }
@@ -89,6 +148,176 @@ public class MSSQLRubyJdbcConnection extends RubyJdbcConnection {
89
148
  return context.runtime.newBoolean( startsWithIgnoreCase(sqlBytes, EXEC) );
90
149
  }
91
150
 
151
+ // Support multiple result sets for mssql
152
+ @Override
153
+ @JRubyMethod(name = "execute", required = 1)
154
+ public IRubyObject execute(final ThreadContext context, final IRubyObject sql) {
155
+ final String query = sqlString(sql);
156
+ return withConnection(context, new Callable<IRubyObject>() {
157
+ public IRubyObject call(final Connection connection) throws SQLException {
158
+ Statement statement = null;
159
+ try {
160
+ statement = createStatement(context, connection);
161
+
162
+ // For DBs that do support multiple statements, lets return the last result set
163
+ // to be consistent with AR
164
+ boolean hasResultSet = doExecute(statement, query);
165
+ int updateCount = statement.getUpdateCount();
166
+
167
+ final List<IRubyObject> results = new ArrayList<IRubyObject>();
168
+ ResultSet resultSet;
169
+
170
+ while (hasResultSet || updateCount != -1) {
171
+
172
+ if (hasResultSet) {
173
+ resultSet = statement.getResultSet();
174
+
175
+ // Unfortunately the result set gets closed when getMoreResults()
176
+ // is called, so we have to process the result sets as we get them
177
+ // this shouldn't be an issue in most cases since we're only getting 1 result set anyways
178
+ results.add(mapExecuteResult(context, connection, resultSet));
179
+ } else {
180
+ results.add(context.runtime.newFixnum(updateCount));
181
+ }
182
+
183
+ // Check to see if there is another result set
184
+ hasResultSet = statement.getMoreResults();
185
+ updateCount = statement.getUpdateCount();
186
+ }
187
+
188
+ if (results.size() == 0) {
189
+ return context.nil; // If no results, return nil
190
+ } else if (results.size() == 1) {
191
+ return results.get(0);
192
+ } else {
193
+ return context.runtime.newArray(results);
194
+ }
195
+
196
+ } catch (final SQLException e) {
197
+ debugErrorSQL(context, query);
198
+ throw e;
199
+ } finally {
200
+ close(statement);
201
+ }
202
+ }
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Executes an INSERT SQL statement
208
+ * @param context
209
+ * @param sql
210
+ * @param pk Rails PK
211
+ * @return ActiveRecord::Result
212
+ * @throws SQLException
213
+ */
214
+ @Override
215
+ @JRubyMethod(name = "execute_insert_pk", required = 2)
216
+ public IRubyObject execute_insert_pk(final ThreadContext context, final IRubyObject sql, final IRubyObject pk) {
217
+
218
+ // MSSQL does not like composite primary keys here so chop it if there is more than one column
219
+ IRubyObject modifiedPk = pk;
220
+
221
+ if (pk instanceof RubyArray) {
222
+ RubyArray ary = (RubyArray) pk;
223
+ if (ary.size() > 0) {
224
+ modifiedPk = ary.eltInternal(0);
225
+ }
226
+ }
227
+
228
+ return super.execute_insert_pk(context, sql, modifiedPk);
229
+ }
230
+
231
+ /**
232
+ * Executes an INSERT SQL statement using a prepared statement
233
+ * @param context
234
+ * @param sql
235
+ * @param binds RubyArray of values to be bound to the query
236
+ * @param pk Rails PK
237
+ * @return ActiveRecord::Result
238
+ * @throws SQLException
239
+ */
240
+ @Override
241
+ @JRubyMethod(name = "execute_insert_pk", required = 3)
242
+ public IRubyObject execute_insert_pk(final ThreadContext context, final IRubyObject sql, final IRubyObject binds,
243
+ final IRubyObject pk) {
244
+ // MSSQL does not like composite primary keys here so chop it if there is more than one column
245
+ IRubyObject modifiedPk = pk;
246
+
247
+ if (pk instanceof RubyArray) {
248
+ RubyArray ary = (RubyArray) pk;
249
+ if (ary.size() > 0) {
250
+ modifiedPk = ary.eltInternal(0);
251
+ }
252
+ }
253
+
254
+ return super.execute_insert_pk(context, sql, binds, modifiedPk);
255
+ }
256
+
257
+ @Override
258
+ protected Integer jdbcTypeFor(final String type) {
259
+
260
+ Integer typeValue = MSSQL_JDBC_TYPE_FOR.get(type);
261
+
262
+ if ( typeValue != null ) {
263
+ return typeValue;
264
+ }
265
+
266
+ return super.jdbcTypeFor(type);
267
+ }
268
+
269
+ // Datetimeoffset values also make it into here
270
+ @Override
271
+ protected void setStringParameter(final ThreadContext context, final Connection connection,
272
+ final PreparedStatement statement, final int index, final IRubyObject value,
273
+ final IRubyObject attribute, final int type) throws SQLException {
274
+
275
+ // datetimeoffset values also make it in here
276
+ if (type == DATETIMEOFFSET_TYPE) {
277
+
278
+ Object dto = convertToDateTimeOffset(context, value);
279
+
280
+ try {
281
+
282
+ Object[] setStatementArgs = { index, dto };
283
+ PreparedStatementSetDateTimeOffsetMethod.invoke(statement, setStatementArgs);
284
+
285
+ } catch (IllegalAccessException e) {
286
+ debugMessage(context.runtime, e.getMessage());
287
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
288
+ } catch (InvocationTargetException e) {
289
+ debugMessage(context.runtime, e.getMessage());
290
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
291
+ }
292
+
293
+ return;
294
+ }
295
+ super.setStringParameter(context, connection, statement, index, value, attribute, type);
296
+ }
297
+
298
+ private Object convertToDateTimeOffset(final ThreadContext context, final IRubyObject value) {
299
+
300
+ RubyTime time = (RubyTime) value;
301
+ DateTime dt = time.getDateTime();
302
+ Timestamp timestamp = new Timestamp(dt.getMillis());
303
+ timestamp.setNanos(timestamp.getNanos() + (int) time.getNSec());
304
+ int offsetMinutes = dt.getZone().getOffset(dt.getMillis()) / 60000;
305
+
306
+ try {
307
+
308
+ Object[] dtoArgs = { timestamp, offsetMinutes };
309
+ return DateTimeOffsetValueOfMethod.invoke(null, dtoArgs);
310
+
311
+ } catch (IllegalAccessException e) {
312
+ debugMessage(context.runtime, e.getMessage());
313
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
314
+ } catch (InvocationTargetException e) {
315
+ debugMessage(context.runtime, e.getMessage());
316
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
317
+ }
318
+ }
319
+
320
+
92
321
  @Override
93
322
  protected RubyArray mapTables(final ThreadContext context, final Connection connection,
94
323
  final String catalog, final String schemaPattern, final String tablePattern,
@@ -305,29 +534,6 @@ public class MSSQLRubyJdbcConnection extends RubyJdbcConnection {
305
534
  statement.setObject(index, timeStr, Types.NVARCHAR);
306
535
  }
307
536
 
308
- // Overrides the method in parent, we only remove the savepoint
309
- // from the getSavepoints Map
310
- @JRubyMethod(name = "release_savepoint", required = 1)
311
- public IRubyObject release_savepoint(final ThreadContext context, final IRubyObject name) {
312
- if (name == context.nil) throw context.runtime.newArgumentError("nil savepoint name given");
313
-
314
- final Connection connection = getConnection(true);
315
-
316
- Object savepoint = getSavepoints(context).remove(name);
317
-
318
- if (savepoint == null) throw newSavepointNotSetError(context, name, "release");
319
-
320
- // NOTE: RubyHash.remove does not convert to Java as get does :
321
- if (!(savepoint instanceof Savepoint)) {
322
- savepoint = ((IRubyObject) savepoint).toJava(Savepoint.class);
323
- }
324
-
325
- // The 'releaseSavepoint' method is not currently supported
326
- // by the Microsoft SQL Server JDBC Driver
327
- // connection.releaseSavepoint((Savepoint) savepoint);
328
- return context.nil;
329
- }
330
-
331
537
  //----------------------------------------------------------------
332
538
  // read_uncommitted: "READ UNCOMMITTED",
333
539
  // read_committed: "READ COMMITTED",
@@ -453,16 +659,64 @@ public class MSSQLRubyJdbcConnection extends RubyJdbcConnection {
453
659
 
454
660
  /**
455
661
  * Treat LONGVARCHAR as CLOB on MSSQL for purposes of converting a JDBC value to Ruby.
662
+ * Also handle datetimeoffset values here
456
663
  */
457
664
  @Override
458
665
  protected IRubyObject jdbcToRuby(
459
666
  final ThreadContext context, final Ruby runtime,
460
667
  final int column, int type, final ResultSet resultSet)
461
668
  throws SQLException {
462
- if ( type == Types.LONGVARCHAR || type == Types.LONGNVARCHAR ) type = Types.CLOB;
669
+
670
+ if (type == DATETIMEOFFSET_TYPE) {
671
+
672
+ Object dto = resultSet.getObject(column); // Returns a microsoft.sql.DateTimeOffset
673
+
674
+ if (dto == null) return context.nil;
675
+
676
+ try {
677
+
678
+ int minutes = (int) DateTimeOffsetGetMinutesOffsetMethod.invoke(dto);
679
+ DateTimeZone zone = DateTimeZone.forOffsetHoursMinutes(minutes / 60, minutes % 60);
680
+ Timestamp ts = (Timestamp) DateTimeOffsetGetTimestampMethod.invoke(dto);
681
+
682
+ int nanos = ts.getNanos(); // max 999-999-999
683
+ nanos = nanos % 1000000;
684
+
685
+ // We have to do this differently than the newTime helper because the Timestamp loses its zone information when passed around
686
+ DateTime dateTime = new DateTime(ts.getTime(), zone);
687
+ return RubyTime.newTime(context.runtime, dateTime, nanos);
688
+
689
+ } catch (IllegalAccessException e) {
690
+ debugMessage(runtime, e.getMessage());
691
+ return context.nil;
692
+ } catch (InvocationTargetException e) {
693
+ debugMessage(runtime, e.getMessage());
694
+ return context.nil;
695
+ }
696
+ }
697
+
698
+ if (type == Types.LONGVARCHAR || type == Types.LONGNVARCHAR) type = Types.CLOB;
463
699
  return super.jdbcToRuby(context, runtime, column, type, resultSet);
464
700
  }
465
701
 
702
+ /**
703
+ * Converts a JDBC date object to a Ruby date by referencing Date#civil
704
+ * @param context current thread context
705
+ * @param resultSet the jdbc result set to pull the value from
706
+ * @param index the index of the column to convert
707
+ * @return RubyNil if NULL or RubyDate if there is a value
708
+ * @throws SQLException if it fails to retrieve the value from the result set
709
+ */
710
+ @Override
711
+ protected IRubyObject dateToRuby(ThreadContext context, Ruby runtime, ResultSet resultSet, int index) throws SQLException {
712
+
713
+ final Date value = resultSet.getDate(index);
714
+
715
+ if (value == null) return context.nil;
716
+
717
+ return DateTimeUtils.newDate(context, value);
718
+ }
719
+
466
720
  @Override
467
721
  protected ColumnData[] extractColumns(final ThreadContext context,
468
722
  final Connection connection, final ResultSet resultSet,
@@ -492,19 +746,9 @@ public class MSSQLRubyJdbcConnection extends RubyJdbcConnection {
492
746
  return columns;
493
747
  }
494
748
 
495
- // internal helper not meant as a "public" API - used in one place thus every
496
- @JRubyMethod(name = "jtds_driver?")
497
- public RubyBoolean jtds_driver_p(final ThreadContext context) throws SQLException {
498
- // "jTDS Type 4 JDBC Driver for MS SQL Server and Sybase"
499
- // SQLJDBC: "Microsoft JDBC Driver 4.0 for SQL Server"
500
- return withConnection(context, new Callable<RubyBoolean>() {
501
- // NOTE: only used in one place for now (on release_savepoint) ...
502
- // might get optimized to only happen once since driver won't change
503
- public RubyBoolean call(final Connection connection) throws SQLException {
504
- final String driver = connection.getMetaData().getDriverName();
505
- return context.getRuntime().newBoolean( driver.indexOf("jTDS") >= 0 );
506
- }
507
- });
749
+ @Override
750
+ protected void releaseSavepoint(final Connection connection, final Savepoint savepoint) throws SQLException {
751
+ // MSSQL doesn't support releasing savepoints
508
752
  }
509
753
 
510
754
  }