-
Notifications
You must be signed in to change notification settings - Fork 388
/
SQLite3RubyJdbcConnection.java
547 lines (472 loc) · 22.7 KB
/
SQLite3RubyJdbcConnection.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
/***** BEGIN LICENSE BLOCK *****
* Copyright (c) 2012-2013 Karol Bucek <self@kares.org>
* Copyright (c) 2006-2010 Nick Sieger <nick@nicksieger.com>
* Copyright (c) 2006-2007 Ola Bini <ola.bini@gmail.com>
* Copyright (c) 2008-2009 Thomas E Enebo <enebo@acm.org>
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
***** END LICENSE BLOCK *****/
package arjdbc.sqlite3;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.Savepoint;
import java.sql.Types;
import java.util.List;
import java.util.Locale;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyInteger;
import org.jruby.RubyNumeric;
import org.jruby.RubyString;
import org.jruby.RubyTime;
import org.jruby.anno.JRubyMethod;
import org.jruby.ext.bigdecimal.RubyBigDecimal;
import org.jruby.runtime.Block;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.SafePropertyAccessor;
import arjdbc.jdbc.Callable;
import arjdbc.jdbc.RubyJdbcConnection;
import static arjdbc.util.StringHelper.newDefaultInternalString;
import static arjdbc.util.StringHelper.newString;
/**
*
* @author enebo
*/
public class SQLite3RubyJdbcConnection extends RubyJdbcConnection {
private static final long serialVersionUID = -5783855018818472773L;
private final RubyString TIMESTAMP_FORMAT;
private IRubyObject encoding;
public SQLite3RubyJdbcConnection(Ruby runtime, RubyClass metaClass) {
super(runtime, metaClass);
TIMESTAMP_FORMAT = runtime.newString("%F %T.%6N");
}
public static RubyClass createSQLite3JdbcConnectionClass(Ruby runtime, RubyClass jdbcConnection) {
final RubyClass clazz = getConnectionAdapters(runtime). // ActiveRecord::ConnectionAdapters
defineClassUnder("SQLite3JdbcConnection", jdbcConnection, ALLOCATOR);
clazz.defineAnnotatedMethods( SQLite3RubyJdbcConnection.class );
getConnectionAdapters(runtime).setConstant("Sqlite3JdbcConnection", clazz); // backwards-compat
return clazz;
}
public static RubyClass load(final Ruby runtime) {
RubyClass jdbcConnection = getJdbcConnection(runtime);
return createSQLite3JdbcConnectionClass(runtime, jdbcConnection);
}
protected static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new SQLite3RubyJdbcConnection(runtime, klass);
}
};
@JRubyMethod
public IRubyObject encoding(final ThreadContext context) throws SQLException {
if (encoding != null) return encoding;
// FIXME: How many single result queries do we have in Java?
return withConnection(context, connection -> {
String query = "PRAGMA encoding";
Statement statement = null;
ResultSet resultSet = null;
try {
statement = createStatement(context, connection);
if (statement.execute(query)) {
// Enebo: I do not think we need to worry about failure here?
resultSet = statement.getResultSet();
if (!resultSet.next()) return context.nil;
String encodingString = resultSet.getString(1);
encoding = cachedString(context, encodingString);
return encoding;
}
} catch (final SQLException e) {
debugErrorSQL(context, query);
throw e;
} finally {
close(resultSet);
close(statement);
}
return context.nil;
});
}
@JRubyMethod(name = {"last_insert_rowid", "last_insert_id"}, alias = "last_insert_row_id")
public IRubyObject last_insert_rowid(final ThreadContext context)
throws SQLException {
return withConnection(context, connection -> {
Statement statement = null; ResultSet genKeys = null;
try {
statement = connection.createStatement();
// NOTE: strangely this will work and has been used for quite some time :
//return mapGeneratedKeys(context.getRuntime(), connection, statement, true);
// but we should assume SQLite JDBC will prefer sane API usage eventually :
genKeys = statement.executeQuery("SELECT last_insert_rowid()");
return doMapGeneratedKeys(context.runtime, genKeys, true);
}
catch (final SQLException e) {
debugMessage(context.runtime, "failed to get generated keys: ", e);
throw e;
}
finally { close(genKeys); close(statement); }
});
}
// NOTE: interestingly it supports getGeneratedKeys but not executeUpdate
// + the driver does not report it supports it via the meta-data yet does
@Override
protected boolean supportsGeneratedKeys(final Connection connection) throws SQLException {
return true;
}
@Override
protected Statement createStatement(final ThreadContext context, final Connection connection)
throws SQLException {
final Statement statement = connection.createStatement();
IRubyObject escapeProcessing = getConfigValue(context, "statement_escape_processing");
if ( escapeProcessing != null && ! escapeProcessing.isNil() ) {
statement.setEscapeProcessing( escapeProcessing.isTrue() );
}
// else leave as is by default
return statement;
}
@Override
protected IRubyObject indexes(final ThreadContext context, String table, final String name, String schema) {
if ( table != null ) {
final int i = table.indexOf('.');
if ( i > 0 && schema == null ) {
schema = table.substring(0, i);
table = table.substring(i + 1);
}
}
final String tableName = table;
final String schemaName = schema;
// return super.indexes(context, tableName, name, schemaName);
return withConnection(context, (Callable<IRubyObject>) connection -> {
final Ruby runtime = context.runtime;
final RubyClass IndexDefinition = getIndexDefinition(runtime);
final TableName table1 = extractTableName(connection, null, schemaName, tableName);
final List<RubyString> primaryKeys = primaryKeys(context, connection, table1);
final DatabaseMetaData metaData = connection.getMetaData();
ResultSet indexInfoSet;
try {
indexInfoSet = metaData.getIndexInfo(table1.catalog, table1.schema, table1.name, false, true);
}
catch (SQLException e) {
final String msg = e.getMessage();
if ( msg != null && msg.startsWith("[SQLITE_ERROR] SQL error or missing database") ) {
return RubyArray.newEmptyArray(runtime); // on 3.8.7 getIndexInfo fails if table has no indexes
}
throw e;
}
final RubyArray indexes = RubyArray.newArray(runtime, 8);
try {
String currentIndex = null;
while ( indexInfoSet.next() ) {
String indexName = indexInfoSet.getString(INDEX_INFO_NAME);
if ( indexName == null ) continue;
RubyArray currentColumns = null;
final String columnName = indexInfoSet.getString(INDEX_INFO_COLUMN_NAME);
final RubyString rubyColumnName = cachedString(context, columnName);
if ( primaryKeys.contains(rubyColumnName) ) continue;
// We are working on a new index
if ( ! indexName.equals(currentIndex) ) {
currentIndex = indexName;
String indexTableName = indexInfoSet.getString(INDEX_INFO_TABLE_NAME);
final boolean nonUnique = indexInfoSet.getBoolean(INDEX_INFO_NON_UNIQUE);
IRubyObject[] args = new IRubyObject[] {
cachedString(context, indexTableName), // table_name
cachedString(context, indexName), // index_name
nonUnique ? context.fals : context.tru, // unique
currentColumns = RubyArray.newArray(runtime, 4) // [] column names
};
indexes.append( IndexDefinition.newInstance(context, args, Block.NULL_BLOCK) ); // IndexDefinition.new
}
// one or more columns can be associated with an index
if ( currentColumns != null ) currentColumns.append(rubyColumnName);
}
return indexes;
} finally { close(indexInfoSet); }
});
}
@Override
protected TableName extractTableName(
final Connection connection, String catalog, String schema,
final String tableName) throws IllegalArgumentException, SQLException {
final List<String> nameParts = split(tableName, '.');
final int len = nameParts.size();
if ( len > 3 ) {
throw new IllegalArgumentException("table name: " + tableName + " should not contain more than 2 '.'");
}
String name = tableName;
if ( len == 2 ) {
schema = nameParts.get(0);
name = nameParts.get(1);
}
else if ( len == 3 ) {
catalog = nameParts.get(0);
schema = nameParts.get(1);
name = nameParts.get(2);
}
if ( schema != null ) {
// NOTE: hack to work-around SQLite JDBC ignoring schema :
return new TableName(catalog, null, schema + '.' + name);
}
return new TableName(catalog, schema, name);
}
@Override
protected IRubyObject jdbcToRuby(final ThreadContext context,
final Ruby runtime, final int column, int type, final ResultSet resultSet)
throws SQLException {
// This is rather gross, and only needed because the resultset metadata for SQLite tries to be overly
// clever, and returns a type for the column of the "current" row, so an integer value stored in a
// decimal column is returned as Types.INTEGER. Therefore, if the first row of a resultset was an
// integer value, all rows of that result set would get truncated.
if ( resultSet instanceof ResultSetMetaData ) {
type = ((ResultSetMetaData) resultSet).getColumnType(column);
}
// since JDBC 3.8 there seems to be more cleverness built-in that
// causes (<= 3.8.7) to get things wrong ... reports DATE SQL type
// for "datetime" columns :
if ( type == Types.DATE ) {
// return timestampToRuby(context, runtime, resultSet, column);
return stringToRuby(context, runtime, resultSet, column);
}
return super.jdbcToRuby(context, runtime, column, type, resultSet);
}
@Override
protected IRubyObject stringToRuby(final ThreadContext context,
final Ruby runtime, final ResultSet resultSet, final int column) throws SQLException {
final byte[] value = resultSet.getBytes(column);
if ( value == null ) return context.nil; // resultSet.wasNull()
return newDefaultInternalString(runtime, value);
}
@Override
protected IRubyObject streamToRuby(final ThreadContext context,
final Ruby runtime, final ResultSet resultSet, final int column)
throws SQLException, IOException {
final byte[] bytes = resultSet.getBytes(column);
if ( bytes == null ) return context.nil; // resultSet.wasNull()
return newString(runtime, bytes);
}
@Override
protected RubyArray mapTables(final ThreadContext context, final Connection connection,
final String catalog, final String schemaPattern, final String tablePattern,
final ResultSet tablesSet) throws SQLException {
final RubyArray tables = context.runtime.newArray(24);
while ( tablesSet.next() ) {
String name = tablesSet.getString(TABLES_TABLE_NAME);
name = name.toLowerCase(Locale.ENGLISH); // simply lower-case for SQLite3
tables.append( RubyString.newUnicodeString(context.runtime, name) );
}
return tables;
}
@Override
protected String caseConvertIdentifierForRails(final Connection connection, final String value) {
return value;
}
@Override
protected String caseConvertIdentifierForJdbc(final Connection connection, final String value) {
return value;
}
private static class SavepointStub implements Savepoint {
static final SavepointStub INSTANCE = new SavepointStub();
@Override
public int getSavepointId() throws SQLException {
throw new UnsupportedOperationException();
}
@Override
public String getSavepointName() throws SQLException {
throw new UnsupportedOperationException();
}
}
private static Boolean useSavepointAPI;
static {
final String savepoints = SafePropertyAccessor.getProperty("arjdbc.sqlite.savepoints");
if ( savepoints != null ) useSavepointAPI = Boolean.parseBoolean(savepoints);
}
private static boolean useSavepointAPI(final ThreadContext context) {
final Boolean working = useSavepointAPI;
if ( working == null ) {
try { // available since JDBC-SQLite 3.8.9
context.runtime.getJavaSupport().loadJavaClass("org.sqlite.jdbc3.JDBC3Savepoint");
return useSavepointAPI = Boolean.TRUE;
}
catch (ClassNotFoundException ex) { /* < 3.8.9 */ }
// catch (RuntimeException ex) { }
return useSavepointAPI = Boolean.FALSE;
}
return working;
}
@Override
protected boolean resetSavepoints(final ThreadContext context, final Connection connection) throws SQLException {
connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
return super.resetSavepoints(context, connection);
}
@Override
@JRubyMethod(name = "create_savepoint", required = 1)
public IRubyObject create_savepoint(final ThreadContext context, final IRubyObject name) {
if ( useSavepointAPI(context) ) return super.create_savepoint(context, name);
if ( name == context.nil ) {
throw context.runtime.newRaiseException(context.runtime.getNotImplementedError(),
"create_savepoint (without name) not implemented!"
);
}
Statement statement = null;
try {
final Connection connection = getConnectionInternal(true);
connection.setAutoCommit(false);
// NOTE: JDBC driver does not support setSavepoint(String) :
( statement = connection.createStatement() ).execute("SAVEPOINT " + name.toString());
getSavepoints(context).put(name, SavepointStub.INSTANCE);
return name;
}
catch (SQLException e) {
return handleException(context, e);
}
finally { close(statement); }
}
@Override
@JRubyMethod(name = "rollback_savepoint", required = 1)
public IRubyObject rollback_savepoint(final ThreadContext context, final IRubyObject name) {
if ( useSavepointAPI(context) ) return super.rollback_savepoint(context, name);
Statement statement = null;
try {
if ( getSavepoints(context).get(name) == null ) {
throw newSavepointNotSetError(context, name, "rollback");
}
// NOTE: JDBC driver does not implement rollback(Savepoint) :
final Connection connection = getConnectionInternal(true);
( statement = connection.createStatement() ).execute("ROLLBACK TO SAVEPOINT " + name.toString());
return context.nil;
}
catch (SQLException e) {
return handleException(context, e);
}
finally { close(statement); }
}
// FIXME: Update our JDBC adapter to later version which basically performs this SQL in
// this method. Then we can use base RubyJdbcConnection version.
@Override
@JRubyMethod(name = "release_savepoint", required = 1)
public IRubyObject release_savepoint(final ThreadContext context, final IRubyObject name) {
if ( useSavepointAPI(context) ) return super.release_savepoint(context, name);
Statement statement = null;
try {
if ( getSavepoints(context).remove(name) == null ) {
throw newSavepointNotSetError(context, name, "release");
}
// NOTE: JDBC driver does not implement release(Savepoint) :
final Connection connection = getConnectionInternal(true);
( statement = connection.createStatement() ).execute("RELEASE SAVEPOINT " + name.toString());
return context.nil;
} catch (SQLException e) {
return handleException(context, e);
}
finally { close(statement); }
}
// Note: transaction_support.rb overrides sqlite3 adapters version which just returns true.
// Rather than re-define it in SQLite3Adapter I will just define it here so we appear to have
// a consistent JDBC layer.
@JRubyMethod(name = "supports_savepoints?")
public IRubyObject supports_savepoints_p(final ThreadContext context) throws SQLException {
return context.tru;
}
@JRubyMethod(name = "readonly?")
public IRubyObject readonly_p(final ThreadContext context) throws SQLException {
final Connection connection = getConnection(true);
return context.runtime.newBoolean(connection.isReadOnly());
}
// note: sqlite3 cext uses this same method but we do not combine all our statements
// into a single ; delimited string but leave it as an array of statements. This is
// because the JDBC way of handling batches is to use addBatch().
@JRubyMethod(name = "execute_batch2")
public IRubyObject execute_batch2(ThreadContext context, IRubyObject statementsArg) {
// Assume we will only call this with an array.
final RubyArray statements = (RubyArray) statementsArg;
return withConnection(context, connection -> {
Statement statement = null;
try {
statement = createStatement(context, connection);
int length = statements.getLength();
for (int i = 0; i < length; i++) {
statement.addBatch(sqlString(statements.eltOk(i)));
}
statement.executeBatch();
return context.nil;
} catch (final SQLException e) {
// Generate list semicolon list of statements which should match AR error formatting more.
debugErrorSQL(context, sqlString(statements.join(context, context.runtime.newString(";\n"))));
throw e;
} finally {
close(statement);
}
});
}
@Override
protected void setDecimalParameter(final ThreadContext context,
final Connection connection, final PreparedStatement statement,
final int index, final IRubyObject value,
final IRubyObject attribute, final int type) throws SQLException {
if (value instanceof RubyBigDecimal) {
statement.setString(index, ((RubyBigDecimal) value).getValue().toString());
}
else if ( value instanceof RubyFixnum) {
statement.setLong(index, ((RubyFixnum) value).getLongValue());
}
else if ( value instanceof RubyInteger) { // Bignum
statement.setString(index, ((RubyInteger) value).getBigIntegerValue().toString());
}
else if ( value instanceof RubyNumeric ) {
statement.setDouble(index, ((RubyNumeric) value).getDoubleValue());
}
else { // e.g. `BigDecimal '42.00000000000000000001'`
RubyBigDecimal val = RubyBigDecimal.newInstance(context, context.runtime.getModule("BigDecimal"), value);
statement.setString(index, val.getValue().toString());
}
}
// Treat dates as strings, this can potentially be removed if we update
// the driver to the latest and tell it to store dates/times as text
@Override
protected void setDateParameter(final ThreadContext context,
final Connection connection, final PreparedStatement statement,
final int index, IRubyObject value,
final IRubyObject attribute, final int type) throws SQLException {
setStringParameter(context, connection, statement, index, value, attribute, type);
}
// Treat times as strings
@Override
protected void setTimeParameter(final ThreadContext context,
final Connection connection, final PreparedStatement statement,
final int index, IRubyObject value,
final IRubyObject attribute, final int type) throws SQLException {
setTimestampParameter(context, connection, statement, index, timeInDefaultTimeZone(context, value), attribute, type);
}
// Treat timestamps as strings
@Override
protected void setTimestampParameter(final ThreadContext context,
final Connection connection, final PreparedStatement statement,
final int index, IRubyObject value,
final IRubyObject attribute, final int type) throws SQLException {
if (value instanceof RubyTime) value = ((RubyTime) value).strftime(context, TIMESTAMP_FORMAT);
setStringParameter(context, connection, statement, index, value, attribute, type);
}
}