activerecord-jdbc-adapter 50.3-java → 50.8-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -3913,6 +3961,12 @@ public class RubyJdbcConnection extends RubyObject {
3913
3961
  }
3914
3962
  }
3915
3963
 
3964
+ public static void debugMessage(final ThreadContext context, final IRubyObject obj) {
3965
+ if ( isDebug(context.runtime) ) {
3966
+ debugMessage(context.runtime, obj.callMethod(context, "inspect"));
3967
+ }
3968
+ }
3969
+
3916
3970
  public static void debugMessage(final Ruby runtime, final String msg, final Object e) {
3917
3971
  if ( isDebug(runtime) ) {
3918
3972
  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,187 @@ 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
+ /**
204
+ * Executes an INSERT SQL statement
205
+ * @param context
206
+ * @param sql
207
+ * @param pk Rails PK
208
+ * @return ActiveRecord::Result
209
+ * @throws SQLException
210
+ */
211
+ @Override
212
+ @JRubyMethod(name = "execute_insert_pk", required = 2)
213
+ public IRubyObject execute_insert_pk(final ThreadContext context, final IRubyObject sql, final IRubyObject pk) {
214
+
215
+ // MSSQL does not like composite primary keys here so chop it if there is more than one column
216
+ IRubyObject modifiedPk = pk;
217
+
218
+ if (pk instanceof RubyArray) {
219
+ RubyArray ary = (RubyArray) pk;
220
+ if (ary.size() > 0) {
221
+ modifiedPk = ary.eltInternal(0);
222
+ }
223
+ }
224
+
225
+ return super.execute_insert_pk(context, sql, modifiedPk);
226
+ }
227
+
228
+ /**
229
+ * Executes an INSERT SQL statement using a prepared statement
230
+ * @param context
231
+ * @param sql
232
+ * @param binds RubyArray of values to be bound to the query
233
+ * @param pk Rails PK
234
+ * @return ActiveRecord::Result
235
+ * @throws SQLException
236
+ */
237
+ @Override
238
+ @JRubyMethod(name = "execute_insert_pk", required = 3)
239
+ public IRubyObject execute_insert_pk(final ThreadContext context, final IRubyObject sql, final IRubyObject binds,
240
+ final IRubyObject pk) {
241
+ // MSSQL does not like composite primary keys here so chop it if there is more than one column
242
+ IRubyObject modifiedPk = pk;
243
+
244
+ if (pk instanceof RubyArray) {
245
+ RubyArray ary = (RubyArray) pk;
246
+ if (ary.size() > 0) {
247
+ modifiedPk = ary.eltInternal(0);
248
+ }
249
+ }
250
+
251
+ return super.execute_insert_pk(context, sql, binds, modifiedPk);
252
+ }
253
+
254
+ @Override
255
+ protected Integer jdbcTypeFor(final String type) {
256
+
257
+ Integer typeValue = MSSQL_JDBC_TYPE_FOR.get(type);
258
+
259
+ if ( typeValue != null ) {
260
+ return typeValue;
261
+ }
262
+
263
+ return super.jdbcTypeFor(type);
264
+ }
265
+
266
+ // Datetimeoffset values also make it into here
267
+ @Override
268
+ protected void setStringParameter(final ThreadContext context, final Connection connection,
269
+ final PreparedStatement statement, final int index, final IRubyObject value,
270
+ final IRubyObject attribute, final int type) throws SQLException {
271
+
272
+ // datetimeoffset values also make it in here
273
+ if (type == DATETIMEOFFSET_TYPE) {
274
+
275
+ Object dto = convertToDateTimeOffset(context, value);
276
+
277
+ try {
278
+
279
+ Object[] setStatementArgs = { index, dto };
280
+ PreparedStatementSetDateTimeOffsetMethod.invoke(statement, setStatementArgs);
281
+
282
+ } catch (IllegalAccessException e) {
283
+ debugMessage(context.runtime, e.getMessage());
284
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
285
+ } catch (InvocationTargetException 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
+ }
289
+
290
+ return;
291
+ }
292
+ super.setStringParameter(context, connection, statement, index, value, attribute, type);
293
+ }
294
+
295
+ // We need higher precision than the default for Time objects (which is milliseconds) so we use a DateTimeOffset object
296
+ @Override
297
+ protected void setTimeParameter(final ThreadContext context,
298
+ final Connection connection, final PreparedStatement statement,
299
+ final int index, IRubyObject value,
300
+ final IRubyObject attribute, final int type) throws SQLException {
301
+
302
+ statement.setObject(index, convertToDateTimeOffset(context, value), Types.TIME);
303
+
304
+ }
305
+
306
+ private Object convertToDateTimeOffset(final ThreadContext context, final IRubyObject value) {
307
+
308
+ RubyTime time = (RubyTime) value;
309
+ DateTime dt = time.getDateTime();
310
+ Timestamp timestamp = new Timestamp(dt.getMillis());
311
+ timestamp.setNanos(timestamp.getNanos() + (int) time.getNSec());
312
+ int offsetMinutes = dt.getZone().getOffset(dt.getMillis()) / 60000;
313
+
314
+ try {
315
+
316
+ Object[] dtoArgs = { timestamp, offsetMinutes };
317
+ return DateTimeOffsetValueOfMethod.invoke(null, dtoArgs);
318
+
319
+ } catch (IllegalAccessException e) {
320
+ debugMessage(context.runtime, e.getMessage());
321
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
322
+ } catch (InvocationTargetException e) {
323
+ debugMessage(context.runtime, e.getMessage());
324
+ throw new RuntimeException("Please make sure you are using the latest version of the Microsoft JDBC driver");
325
+ }
326
+ }
327
+
328
+
86
329
  @Override
87
330
  protected RubyArray mapTables(final ThreadContext context, final Connection connection,
88
331
  final String catalog, final String schemaPattern, final String tablePattern,
@@ -124,16 +367,79 @@ public class MSSQLRubyJdbcConnection extends RubyJdbcConnection {
124
367
 
125
368
  /**
126
369
  * Treat LONGVARCHAR as CLOB on MSSQL for purposes of converting a JDBC value to Ruby.
370
+ * Also handle datetimeoffset values here
127
371
  */
128
372
  @Override
129
373
  protected IRubyObject jdbcToRuby(
130
374
  final ThreadContext context, final Ruby runtime,
131
375
  final int column, int type, final ResultSet resultSet)
132
376
  throws SQLException {
133
- if ( type == Types.LONGVARCHAR || type == Types.LONGNVARCHAR ) type = Types.CLOB;
377
+
378
+ if (type == DATETIMEOFFSET_TYPE) {
379
+
380
+ Object dto = resultSet.getObject(column); // Returns a microsoft.sql.DateTimeOffset
381
+
382
+ if (dto == null) return context.nil;
383
+
384
+ try {
385
+
386
+ int minutes = (int) DateTimeOffsetGetMinutesOffsetMethod.invoke(dto);
387
+ DateTimeZone zone = DateTimeZone.forOffsetHoursMinutes(minutes / 60, minutes % 60);
388
+ Timestamp ts = (Timestamp) DateTimeOffsetGetTimestampMethod.invoke(dto);
389
+
390
+ int nanos = ts.getNanos(); // max 999-999-999
391
+ nanos = nanos % 1000000;
392
+
393
+ // We have to do this differently than the newTime helper because the Timestamp loses its zone information when passed around
394
+ DateTime dateTime = new DateTime(ts.getTime(), zone);
395
+ return RubyTime.newTime(context.runtime, dateTime, nanos);
396
+
397
+ } catch (IllegalAccessException e) {
398
+ debugMessage(runtime, e.getMessage());
399
+ return context.nil;
400
+ } catch (InvocationTargetException e) {
401
+ debugMessage(runtime, e.getMessage());
402
+ return context.nil;
403
+ }
404
+ }
405
+
406
+ if (type == Types.LONGVARCHAR || type == Types.LONGNVARCHAR) type = Types.CLOB;
134
407
  return super.jdbcToRuby(context, runtime, column, type, resultSet);
135
408
  }
136
409
 
410
+ /**
411
+ * Converts a JDBC date object to a Ruby date by referencing Date#civil
412
+ * @param context current thread context
413
+ * @param resultSet the jdbc result set to pull the value from
414
+ * @param index the index of the column to convert
415
+ * @return RubyNil if NULL or RubyDate if there is a value
416
+ * @throws SQLException if it fails to retrieve the value from the result set
417
+ */
418
+ @Override
419
+ protected IRubyObject dateToRuby(ThreadContext context, Ruby runtime, ResultSet resultSet, int index) throws SQLException {
420
+
421
+ final Date value = resultSet.getDate(index);
422
+
423
+ if (value == null) return context.nil;
424
+
425
+ return DateTimeUtils.newDate(context, value);
426
+ }
427
+
428
+ /**
429
+ * Converts a JDBC time to a Ruby time. We use timestamp because java.sql.Time doesn't support sub-millisecond values
430
+ * @param context current thread context
431
+ * @param resultSet the jdbc result set to pull the value from
432
+ * @param index the index of the column to convert
433
+ * @return RubyNil if NULL or RubyTime if there is a value
434
+ * @throws SQLException if it fails to retrieve the value from the result set
435
+ */
436
+ @Override
437
+ protected IRubyObject timeToRuby(final ThreadContext context,final Ruby runtime,
438
+ final ResultSet resultSet, final int column) throws SQLException {
439
+
440
+ return timestampToRuby(context, runtime, resultSet, column);
441
+ }
442
+
137
443
  @Override
138
444
  protected ColumnData[] extractColumns(final ThreadContext context,
139
445
  final Connection connection, final ResultSet resultSet,
@@ -163,19 +469,9 @@ public class MSSQLRubyJdbcConnection extends RubyJdbcConnection {
163
469
  return columns;
164
470
  }
165
471
 
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
- });
472
+ @Override
473
+ protected void releaseSavepoint(final Connection connection, final Savepoint savepoint) throws SQLException {
474
+ // MSSQL doesn't support releasing savepoints
179
475
  }
180
476
 
181
477
  }