Created
November 9, 2014 07:10
-
-
Save prathamesh-sonpatki/a63bfe00c51efeb2ca19 to your computer and use it in GitHub Desktop.
git diff
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/src/java/arjdbc/mssql/MssqlRubyJdbcConnection.java b/src/java/arjdbc/mssql/MssqlRubyJdbcConnection.java | |
index 6f1a441..b099b77 100644 | |
--- a/src/java/arjdbc/mssql/MssqlRubyJdbcConnection.java | |
+++ b/src/java/arjdbc/mssql/MssqlRubyJdbcConnection.java | |
@@ -1,40 +1,184 @@ | |
-/* | |
- * The MIT License | |
+/***** 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> | |
* | |
- * Copyright 2013 Karol Bucek. | |
+ * 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: | |
* | |
- * 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 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. | |
- */ | |
+ * 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.mssql; | |
+import arjdbc.jdbc.Callable; | |
+import arjdbc.jdbc.RubyJdbcConnection; | |
+ | |
+import java.sql.Connection; | |
+import java.sql.DatabaseMetaData; | |
+import java.sql.ResultSet; | |
+import java.sql.SQLException; | |
+import java.sql.Types; | |
+ | |
+import org.jcodings.specific.UTF8Encoding; | |
+ | |
import org.jruby.Ruby; | |
+import org.jruby.RubyArray; | |
import org.jruby.RubyClass; | |
+import org.jruby.RubyString; | |
+import org.jruby.anno.JRubyMethod; | |
+import org.jruby.runtime.ObjectAllocator; | |
+import org.jruby.runtime.ThreadContext; | |
+import org.jruby.runtime.builtin.IRubyObject; | |
+import org.jruby.util.ByteList; | |
/** | |
- * @see MSSQLRubyJdbcConnection | |
- * @deprecated only for Java API backwards-compatibility | |
+ * | |
+ * @author nicksieger | |
*/ | |
-@Deprecated | |
-public class MssqlRubyJdbcConnection extends MSSQLRubyJdbcConnection { | |
- | |
- protected MssqlRubyJdbcConnection(Ruby runtime, RubyClass metaClass) { | |
+public class MSSQLRubyJdbcConnection extends RubyJdbcConnection { | |
+ private static final long serialVersionUID = -745716565005219263L; | |
+ | |
+ protected MSSQLRubyJdbcConnection(Ruby runtime, RubyClass metaClass) { | |
super(runtime, metaClass); | |
} | |
- | |
+ | |
+ public static RubyClass createMSSQLJdbcConnectionClass(Ruby runtime, RubyClass jdbcConnection) { | |
+ final RubyClass clazz = getConnectionAdapters(runtime). // ActiveRecord::ConnectionAdapters | |
+ defineClassUnder("MSSQLJdbcConnection", jdbcConnection, MSSQL_JDBCCONNECTION_ALLOCATOR); | |
+ clazz.defineAnnotatedMethods(MSSQLRubyJdbcConnection.class); | |
+ getConnectionAdapters(runtime).setConstant("MssqlJdbcConnection", clazz); // backwards-compat | |
+ return clazz; | |
+ } | |
+ | |
+ private static ObjectAllocator MSSQL_JDBCCONNECTION_ALLOCATOR = new ObjectAllocator() { | |
+ public IRubyObject allocate(Ruby runtime, RubyClass klass) { | |
+ return new MSSQLRubyJdbcConnection(runtime, klass); | |
+ } | |
+ }; | |
+ | |
+ private static final byte[] EXEC = new byte[] { 'e', 'x', 'e', 'c' }; | |
+ | |
+ @JRubyMethod(name = "exec?", required = 1, meta = true, frame = false) | |
+ public static IRubyObject exec_p(ThreadContext context, IRubyObject self, IRubyObject sql) { | |
+ final ByteList sqlBytes = sql.convertToString().getByteList(); | |
+ return context.getRuntime().newBoolean( startsWithIgnoreCase(sqlBytes, EXEC) ); | |
+ } | |
+ | |
+ @Override | |
+ protected RubyArray mapTables(final Ruby runtime, final DatabaseMetaData metaData, | |
+ final String catalog, final String schemaPattern, final String tablePattern, | |
+ final ResultSet tablesSet) throws SQLException, IllegalStateException { | |
+ | |
+ final RubyArray tables = runtime.newArray(); | |
+ | |
+ while ( tablesSet.next() ) { | |
+ String schema = tablesSet.getString(TABLES_TABLE_SCHEM); | |
+ if ( schema != null ) schema = schema.toLowerCase(); | |
+ // Under MS-SQL, don't return system tables/views unless explicitly asked for : | |
+ if ( schemaPattern == null && | |
+ ( "sys".equals(schema) || "information_schema".equals(schema) ) ) { | |
+ continue; | |
+ } | |
+ String name = tablesSet.getString(TABLES_TABLE_NAME); | |
+ if ( name == null ) { | |
+ // NOTE: seems there's a jTDS but when doing getColumns while | |
+ // EXPLAIN is on (e.g. `SET SHOWPLAN_TEXT ON`) not returning | |
+ // correct result set with table info (null NAME, invalid CAT) | |
+ throw new IllegalStateException("got null name while matching table(s): [" + | |
+ catalog + "." + schemaPattern + "." + tablePattern + "] check " + | |
+ "if this happened during EXPLAIN (SET SHOWPLAN_TEXT ON) if so please try " + | |
+ "turning it off using the system property 'arjdbc.mssql.explain_support.disabled=true' " + | |
+ "or programatically by changing: `ArJdbc::MSSQL::ExplainSupport::DISABLED`"); | |
+ } | |
+ name = caseConvertIdentifierForRails(metaData, name); | |
+ tables.add(RubyString.newUnicodeString(runtime, name)); | |
+ } | |
+ return tables; | |
+ } | |
+ | |
+ /** | |
+ * Microsoft SQL 2000+ support schemas | |
+ */ | |
+ @Override | |
+ protected boolean databaseSupportsSchemas() { | |
+ return true; | |
+ } | |
+ | |
+ /** | |
+ * Treat LONGVARCHAR as CLOB on MSSQL for purposes of converting a JDBC value to Ruby. | |
+ */ | |
+ @Override | |
+ protected IRubyObject jdbcToRuby( | |
+ final ThreadContext context, final Ruby runtime, | |
+ final int column, int type, final ResultSet resultSet) | |
+ throws SQLException { | |
+ if ( type == Types.LONGVARCHAR || type == Types.LONGNVARCHAR ) type = Types.CLOB; | |
+ return super.jdbcToRuby(context, runtime, column, type, resultSet); | |
+ } | |
+ | |
+ @Override | |
+ protected ColumnData[] extractColumns(final Ruby runtime, | |
+ final Connection connection, final ResultSet resultSet, | |
+ final boolean downCase) throws SQLException { | |
+ return filterRowNumFromColumns( super.extractColumns(runtime, connection, resultSet, downCase) ); | |
+ } | |
+ | |
+ private static final ByteList _row_num; // "_row_num" | |
+ static { | |
+ _row_num = new ByteList(new byte[] { '_','r','o','w','_','n','u','m' }, false); | |
+ _row_num.setEncoding(UTF8Encoding.INSTANCE); | |
+ } | |
+ | |
+ /** | |
+ * Filter out the <tt>_row_num</tt> column from results. | |
+ */ | |
+ private static ColumnData[] filterRowNumFromColumns(final ColumnData[] columns) { | |
+ for ( int i = 0; i < columns.length; i++ ) { | |
+ if ( _row_num.equal( columns[i].name.getByteList() ) ) { | |
+ final ColumnData[] filtered = new ColumnData[columns.length - 1]; | |
+ | |
+ if ( i > 0 ) { | |
+ System.arraycopy(columns, 0, filtered, 0, i); | |
+ } | |
+ | |
+ if ( i + 1 < columns.length ) { | |
+ System.arraycopy(columns, i + 1, filtered, i, columns.length - (i + 1)); | |
+ } | |
+ | |
+ return filtered; | |
+ } | |
+ } | |
+ return columns; | |
+ } | |
+ | |
+ // internal helper not meant as a "public" API - used in one place thus every | |
+ @JRubyMethod(name = "jtds_driver?") | |
+ public IRubyObject jtds_driver_p(final ThreadContext context) throws SQLException { | |
+ // "jTDS Type 4 JDBC Driver for MS SQL Server and Sybase" | |
+ // SQLJDBC: "Microsoft JDBC Driver 4.0 for SQL Server" | |
+ return withConnection(context, new Callable<IRubyObject>() { | |
+ // NOTE: only used in one place for now (on release_savepoint) ... | |
+ // might get optimized to only happen once since driver won't change | |
+ public IRubyObject call(final Connection connection) throws SQLException { | |
+ final String driver = connection.getMetaData().getDriverName(); | |
+ return context.getRuntime().newBoolean( driver.indexOf("jTDS") >= 0 ); | |
+ } | |
+ }); | |
+ } | |
+ | |
} | |
diff --git a/src/java/arjdbc/postgresql/PostgresqlRubyJdbcConnection.java b/src/java/arjdbc/postgresql/PostgresqlRubyJdbcConnection.java | |
index 5e4829d..1fd166d 100644 | |
--- a/src/java/arjdbc/postgresql/PostgresqlRubyJdbcConnection.java | |
+++ b/src/java/arjdbc/postgresql/PostgresqlRubyJdbcConnection.java | |
@@ -1,40 +1,754 @@ | |
-/* | |
- * The MIT License | |
+/***** 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> | |
* | |
- * Copyright 2013 Karol Bucek. | |
+ * 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: | |
* | |
- * 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 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. | |
- */ | |
+ * 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.postgresql; | |
+import java.io.ByteArrayInputStream; | |
+import java.io.InputStream; | |
+import java.sql.Array; | |
+import java.sql.Connection; | |
+import java.sql.PreparedStatement; | |
+import java.sql.ResultSet; | |
+import java.sql.SQLException; | |
+import java.sql.Statement; | |
+import java.sql.Timestamp; | |
+import java.sql.Types; | |
+import java.util.Map; | |
+import java.util.UUID; | |
+ | |
import org.jruby.Ruby; | |
+import org.jruby.RubyArray; | |
+import org.jruby.RubyBoolean; | |
import org.jruby.RubyClass; | |
+import org.jruby.RubyFixnum; | |
+import org.jruby.RubyFloat; | |
+import org.jruby.RubyHash; | |
+import org.jruby.RubyIO; | |
+import org.jruby.RubyString; | |
+import org.jruby.anno.JRubyMethod; | |
+import org.jruby.javasupport.JavaUtil; | |
+import org.jruby.runtime.ObjectAllocator; | |
+import org.jruby.runtime.ThreadContext; | |
+import org.jruby.runtime.builtin.IRubyObject; | |
+import org.jruby.util.ByteList; | |
+ | |
+import org.postgresql.PGConnection; | |
+import org.postgresql.PGStatement; | |
+import org.postgresql.core.BaseConnection; | |
+import org.postgresql.jdbc4.Jdbc4Array; | |
+import org.postgresql.util.PGInterval; | |
+import org.postgresql.util.PGobject; | |
/** | |
- * @see PostgreSQLRubyJdbcConnection | |
- * @deprecated only for Java API backwards-compatibility | |
+ * | |
+ * @author enebo | |
*/ | |
-@Deprecated | |
-public class PostgresqlRubyJdbcConnection extends PostgreSQLRubyJdbcConnection { | |
- | |
- protected PostgresqlRubyJdbcConnection(Ruby runtime, RubyClass metaClass) { | |
+public class PostgreSQLRubyJdbcConnection extends arjdbc.jdbc.RubyJdbcConnection { | |
+ private static final long serialVersionUID = 7235537759545717760L; | |
+ | |
+ protected PostgreSQLRubyJdbcConnection(Ruby runtime, RubyClass metaClass) { | |
super(runtime, metaClass); | |
} | |
- | |
+ | |
+ public static RubyClass createPostgreSQLJdbcConnectionClass(Ruby runtime, RubyClass jdbcConnection) { | |
+ final RubyClass clazz = getConnectionAdapters(runtime). | |
+ defineClassUnder("PostgreSQLJdbcConnection", jdbcConnection, POSTGRESQL_JDBCCONNECTION_ALLOCATOR); | |
+ clazz.defineAnnotatedMethods(PostgreSQLRubyJdbcConnection.class); | |
+ getConnectionAdapters(runtime).setConstant("PostgresJdbcConnection", clazz); // backwards-compat | |
+ return clazz; | |
+ } | |
+ | |
+ private static ObjectAllocator POSTGRESQL_JDBCCONNECTION_ALLOCATOR = new ObjectAllocator() { | |
+ public IRubyObject allocate(Ruby runtime, RubyClass klass) { | |
+ return new PostgreSQLRubyJdbcConnection(runtime, klass); | |
+ } | |
+ }; | |
+ | |
+ // enables testing if the bug is fixed (please run our test-suite) | |
+ // using `rake test_postgresql JRUBY_OPTS="-J-Darjdbc.postgresql.generated_keys=true"` | |
+ protected static final boolean generatedKeys; | |
+ static { | |
+ String genKeys = System.getProperty("arjdbc.postgresql.generated_keys"); | |
+ if ( genKeys == null ) { // @deprecated system property name : | |
+ genKeys = System.getProperty("arjdbc.postgresql.generated.keys"); | |
+ } | |
+ generatedKeys = Boolean.parseBoolean(genKeys); | |
+ } | |
+ | |
+ @Override | |
+ protected IRubyObject mapGeneratedKeys( | |
+ final Ruby runtime, final Connection connection, | |
+ final Statement statement, final Boolean singleResult) | |
+ throws SQLException { | |
+ // NOTE: PostgreSQL driver supports generated keys but does not work | |
+ // correctly for all cases e.g. for tables whene no keys are generated | |
+ // during an INSERT getGeneratedKeys return all inserted rows instead | |
+ // of an empty result set ... thus disabled until issue is resolved ! | |
+ if ( ! generatedKeys ) return null; // not supported | |
+ // NOTE: generated-keys is implemented by the Postgre's JDBC driver by | |
+ // adding a "RETURNING" suffix after the executeUpdate passed query ... | |
+ return super.mapGeneratedKeys(runtime, connection, statement, singleResult); | |
+ } | |
+ | |
+ // storesMixedCaseIdentifiers() return false; | |
+ // storesLowerCaseIdentifiers() return true; | |
+ // storesUpperCaseIdentifiers() return false; | |
+ | |
+ @Override | |
+ protected String caseConvertIdentifierForRails(final Connection connection, final String value) | |
+ throws SQLException { | |
+ return value; | |
+ } | |
+ | |
+ @Override | |
+ protected String caseConvertIdentifierForJdbc(final Connection connection, final String value) | |
+ throws SQLException { | |
+ return value; | |
+ } | |
+ | |
+ @Override | |
+ protected Connection newConnection() throws SQLException { | |
+ final Connection connection = getConnectionFactory().newConnection(); | |
+ final PGConnection pgConnection; | |
+ if ( connection instanceof PGConnection ) { | |
+ pgConnection = (PGConnection) connection; | |
+ } | |
+ else { | |
+ pgConnection = connection.unwrap(PGConnection.class); | |
+ } | |
+ pgConnection.addDataType("daterange", DateRangeType.class); | |
+ pgConnection.addDataType("tsrange", TsRangeType.class); | |
+ pgConnection.addDataType("tstzrange", TstzRangeType.class); | |
+ pgConnection.addDataType("int4range", Int4RangeType.class); | |
+ pgConnection.addDataType("int8range", Int8RangeType.class); | |
+ pgConnection.addDataType("numrange", NumRangeType.class); | |
+ return connection; | |
+ } | |
+ | |
+ @Override // due statement.setNull(index, Types.BLOB) not working : | |
+ // org.postgresql.util.PSQLException: ERROR: column "sample_binary" is of type bytea but expression is of type oid | |
+ protected void setBlobParameter(final ThreadContext context, | |
+ final Connection connection, final PreparedStatement statement, | |
+ final int index, final Object value, | |
+ final IRubyObject column, final int type) throws SQLException { | |
+ if ( value instanceof IRubyObject ) { | |
+ setBlobParameter(context, connection, statement, index, (IRubyObject) value, column, type); | |
+ } | |
+ else { | |
+ if ( value == null ) statement.setNull(index, Types.BINARY); | |
+ else { | |
+ statement.setBinaryStream(index, (InputStream) value); | |
+ } | |
+ } | |
+ } | |
+ | |
+ @Override // due statement.setNull(index, Types.BLOB) not working : | |
+ // org.postgresql.util.PSQLException: ERROR: column "sample_binary" is of type bytea but expression is of type oid | |
+ protected void setBlobParameter(final ThreadContext context, | |
+ final Connection connection, final PreparedStatement statement, | |
+ final int index, final IRubyObject value, | |
+ final IRubyObject column, final int type) throws SQLException { | |
+ if ( value.isNil() ) { | |
+ statement.setNull(index, Types.BINARY); | |
+ } | |
+ else { | |
+ if ( value instanceof RubyIO ) { // IO/File | |
+ statement.setBinaryStream(index, ((RubyIO) value).getInStream()); | |
+ } | |
+ else { // should be a RubyString | |
+ final ByteList blob = value.asString().getByteList(); | |
+ statement.setBinaryStream(index, | |
+ new ByteArrayInputStream(blob.unsafeBytes(), blob.getBegin(), blob.getRealSize()), | |
+ blob.getRealSize() // length | |
+ ); | |
+ } | |
+ } | |
+ } | |
+ | |
+ @Override // to handle infinity timestamp values | |
+ protected void setTimestampParameter(final ThreadContext context, | |
+ final Connection connection, final PreparedStatement statement, | |
+ final int index, IRubyObject value, | |
+ final IRubyObject column, final int type) throws SQLException { | |
+ | |
+ if ( value instanceof RubyFloat ) { | |
+ final double _value = ( (RubyFloat) value ).getValue(); | |
+ if ( Double.isInfinite(_value) ) { | |
+ final Timestamp timestamp; | |
+ if ( _value < 0 ) { | |
+ timestamp = new Timestamp(PGStatement.DATE_NEGATIVE_INFINITY); | |
+ } | |
+ else { | |
+ timestamp = new Timestamp(PGStatement.DATE_POSITIVE_INFINITY); | |
+ } | |
+ statement.setTimestamp( index, timestamp ); | |
+ return; | |
+ } | |
+ } | |
+ | |
+ super.setTimestampParameter(context, connection, statement, index, value, column, type); | |
+ } | |
+ | |
+ private static final ByteList INTERVAL = | |
+ new ByteList( new byte[] { 'i','n','t','e','r','v','a','l' }, false ); | |
+ | |
+ private static final ByteList ARRAY_END = new ByteList( new byte[] { '[',']' }, false ); | |
+ | |
+ @Override | |
+ protected void setStringParameter(final ThreadContext context, | |
+ final Connection connection, final PreparedStatement statement, | |
+ final int index, final IRubyObject value, | |
+ final IRubyObject column, final int type) throws SQLException { | |
+ final RubyString sqlType; | |
+ if ( column != null && ! column.isNil() ) { | |
+ sqlType = (RubyString) column.callMethod(context, "sql_type"); | |
+ } | |
+ else { | |
+ sqlType = null; | |
+ } | |
+ | |
+ if ( value.isNil() ) { | |
+ if ( rawArrayType == Boolean.TRUE ) { // array's type is :string | |
+ if ( sqlType != null && sqlType.getByteList().endsWith( ARRAY_END ) ) { | |
+ statement.setNull(index, Types.ARRAY); return; | |
+ } | |
+ statement.setNull(index, type); return; | |
+ } | |
+ statement.setNull(index, Types.VARCHAR); | |
+ } | |
+ else { | |
+ final String valueStr = value.asString().toString(); | |
+ if ( sqlType != null ) { | |
+ if ( rawArrayType == Boolean.TRUE && sqlType.getByteList().endsWith( ARRAY_END ) ) { | |
+ final int oid = oid(context, column); | |
+ final Array valueArr = new Jdbc4Array(connection.unwrap(BaseConnection.class), oid, valueStr); | |
+ statement.setArray(index, valueArr); return; | |
+ } | |
+ if ( sqlType.getByteList().startsWith( INTERVAL ) ) { | |
+ statement.setObject( index, new PGInterval( valueStr ) ); return; | |
+ } | |
+ } | |
+ statement.setString( index, valueStr ); | |
+ } | |
+ } | |
+ | |
+ private static int oid(final ThreadContext context, final IRubyObject column) { | |
+ // our column convention : | |
+ IRubyObject oid = column.getInstanceVariables().getInstanceVariable("@oid"); | |
+ if ( oid == null || oid.isNil() ) { // only for user instantiated Column | |
+ throw new IllegalStateException("missing @oid for column: " + column.inspect()); | |
+ } | |
+ return RubyFixnum.fix2int(oid); | |
+ } | |
+ | |
+ @Override | |
+ protected void setObjectParameter(final ThreadContext context, | |
+ final Connection connection, final PreparedStatement statement, | |
+ final int index, Object value, | |
+ final IRubyObject column, final int type) throws SQLException { | |
+ | |
+ final String columnType = column.callMethod(context, "type").asJavaString(); | |
+ | |
+ if ( columnType == (Object) "uuid" ) { | |
+ setUUIDParameter(statement, index, value); | |
+ return; | |
+ } | |
+ | |
+ if ( columnType == (Object) "json" ) { | |
+ setJsonParameter(context, statement, index, value, column); | |
+ return; | |
+ } | |
+ | |
+ if ( columnType == (Object) "tsvector" ) { | |
+ setTsVectorParameter(statement, index, value); | |
+ return; | |
+ } | |
+ | |
+ if ( columnType == (Object) "cidr" || columnType == (Object) "inet" | |
+ || columnType == (Object) "macaddr" ) { | |
+ setAddressParameter(context, statement, index, value, column, columnType); | |
+ return; | |
+ } | |
+ | |
+ if ( columnType != null && columnType.endsWith("range") ) { | |
+ setRangeParameter(context, statement, index, value, column, columnType); | |
+ return; | |
+ } | |
+ | |
+ super.setObjectParameter(context, connection, statement, index, value, column, type); | |
+ } | |
+ | |
+ private void setUUIDParameter( | |
+ final PreparedStatement statement, final int index, | |
+ Object value) throws SQLException { | |
+ | |
+ if ( value instanceof IRubyObject ) { | |
+ final IRubyObject rubyValue = (IRubyObject) value; | |
+ if ( rubyValue.isNil() ) { | |
+ statement.setNull(index, Types.OTHER); return; | |
+ } | |
+ } | |
+ else if ( value == null ) { | |
+ statement.setNull(index, Types.OTHER); return; | |
+ } | |
+ | |
+ final Object uuid = UUID.fromString( value.toString() ); | |
+ statement.setObject(index, uuid); | |
+ } | |
+ | |
+ private void setJsonParameter(final ThreadContext context, | |
+ final PreparedStatement statement, final int index, | |
+ Object value, final IRubyObject column) throws SQLException { | |
+ | |
+ if ( value instanceof IRubyObject ) { | |
+ final IRubyObject rubyValue = (IRubyObject) value; | |
+ if ( rubyValue.isNil() ) { | |
+ statement.setNull(index, Types.OTHER); return; | |
+ } | |
+ value = column.getMetaClass().callMethod(context, "json_to_string", rubyValue); | |
+ } | |
+ else if ( value == null ) { | |
+ statement.setNull(index, Types.OTHER); return; | |
+ } | |
+ | |
+ final PGobject pgJson = new PGobject(); | |
+ pgJson.setType("json"); | |
+ pgJson.setValue(value.toString()); | |
+ statement.setObject(index, pgJson); | |
+ } | |
+ | |
+ private void setTsVectorParameter( | |
+ final PreparedStatement statement, final int index, | |
+ Object value) throws SQLException { | |
+ | |
+ if ( value instanceof IRubyObject ) { | |
+ final IRubyObject rubyValue = (IRubyObject) value; | |
+ if ( rubyValue.isNil() ) { | |
+ statement.setNull(index, Types.OTHER); return; | |
+ } | |
+ } | |
+ else if ( value == null ) { | |
+ statement.setNull(index, Types.OTHER); return; | |
+ } | |
+ | |
+ final PGobject pgTsVector = new PGobject(); | |
+ pgTsVector.setType("tsvector"); | |
+ pgTsVector.setValue(value.toString()); | |
+ statement.setObject(index, pgTsVector); | |
+ } | |
+ | |
+ private void setAddressParameter(final ThreadContext context, | |
+ final PreparedStatement statement, final int index, | |
+ Object value, final IRubyObject column, | |
+ final String columnType) throws SQLException { | |
+ | |
+ if ( value instanceof IRubyObject ) { | |
+ final IRubyObject rubyValue = (IRubyObject) value; | |
+ if ( rubyValue.isNil() ) { | |
+ statement.setNull(index, Types.OTHER); return; | |
+ } | |
+ value = column.getMetaClass().callMethod(context, "cidr_to_string", rubyValue); | |
+ } | |
+ else if ( value == null ) { | |
+ statement.setNull(index, Types.OTHER); return; | |
+ } | |
+ | |
+ final PGobject pgAddress = new PGobject(); | |
+ pgAddress.setType(columnType); | |
+ pgAddress.setValue(value.toString()); | |
+ statement.setObject(index, pgAddress); | |
+ } | |
+ | |
+ private void setRangeParameter(final ThreadContext context, | |
+ final PreparedStatement statement, final int index, | |
+ final Object value, final IRubyObject column, | |
+ final String columnType) throws SQLException { | |
+ | |
+ final String rangeValue; | |
+ | |
+ if ( value instanceof IRubyObject ) { | |
+ final IRubyObject rubyValue = (IRubyObject) value; | |
+ if ( rubyValue.isNil() ) { | |
+ statement.setNull(index, Types.OTHER); return; | |
+ } | |
+ rangeValue = column.getMetaClass().callMethod(context, "range_to_string", rubyValue).toString(); | |
+ } | |
+ else { | |
+ if ( value == null ) { | |
+ statement.setNull(index, Types.OTHER); return; | |
+ } | |
+ rangeValue = value.toString(); | |
+ } | |
+ | |
+ final Object pgRange; | |
+ if ( columnType == (Object) "daterange" ) { | |
+ pgRange = new DateRangeType(rangeValue); | |
+ } | |
+ else if ( columnType == (Object) "tsrange" ) { | |
+ pgRange = new TsRangeType(rangeValue); | |
+ } | |
+ else if ( columnType == (Object) "tstzrange" ) { | |
+ pgRange = new TstzRangeType(rangeValue); | |
+ } | |
+ else if ( columnType == (Object) "int4range" ) { | |
+ pgRange = new Int4RangeType(rangeValue); | |
+ } | |
+ else if ( columnType == (Object) "int8range" ) { | |
+ pgRange = new Int8RangeType(rangeValue); | |
+ } | |
+ else { // if ( columnType == (Object) "numrange" ) | |
+ pgRange = new NumRangeType(rangeValue); | |
+ } | |
+ statement.setObject(index, pgRange); | |
+ } | |
+ | |
+ @Override | |
+ protected String resolveArrayBaseTypeName(final ThreadContext context, | |
+ final Object value, final IRubyObject column, final int type) { | |
+ String sqlType = column.callMethod(context, "sql_type").toString(); | |
+ if ( sqlType.startsWith("character varying") ) return "text"; | |
+ final int index = sqlType.indexOf('('); // e.g. "character varying(255)" | |
+ if ( index > 0 ) sqlType = sqlType.substring(0, index); | |
+ return sqlType; | |
+ } | |
+ | |
+ private static final int HSTORE_TYPE = 100000 + 1111; | |
+ | |
+ @Override | |
+ protected int jdbcTypeFor(final ThreadContext context, final Ruby runtime, | |
+ final IRubyObject column, final Object value) throws SQLException { | |
+ // NOTE: likely wrong but native adapters handles this thus we should | |
+ // too - used from #table_exists? `binds << [ nil, schema ] if schema` | |
+ if ( column == null || column.isNil() ) return Types.VARCHAR; // assume type == :string | |
+ final int type = super.jdbcTypeFor(context, runtime, column, value); | |
+ /* | |
+ if ( type == Types.OTHER ) { | |
+ final IRubyObject columnType = column.callMethod(context, "type"); | |
+ if ( "hstore" == (Object) columnType.asJavaString() ) { | |
+ return HSTORE_TYPE; | |
+ } | |
+ } */ | |
+ return type; | |
+ } | |
+ | |
+ /** | |
+ * Override jdbcToRuby type conversions to handle infinite timestamps. | |
+ * Handing timestamp off to ruby as string so adapter can perform type | |
+ * conversion to timestamp | |
+ */ | |
+ @Override | |
+ protected IRubyObject jdbcToRuby( | |
+ final ThreadContext context, final Ruby runtime, | |
+ final int column, final int type, final ResultSet resultSet) | |
+ throws SQLException { | |
+ switch ( type ) { | |
+ case Types.BIT: | |
+ // we do get BIT for 't' 'f' as well as BIT strings e.g. "0110" : | |
+ final String bits = resultSet.getString(column); | |
+ if ( bits == null ) return runtime.getNil(); | |
+ if ( bits.length() > 1 ) { | |
+ return RubyString.newUnicodeString(runtime, bits); | |
+ } | |
+ return booleanToRuby(context, runtime, resultSet, column); | |
+ //case Types.JAVA_OBJECT: case Types.OTHER: | |
+ //return objectToRuby(runtime, resultSet, resultSet.getObject(column)); | |
+ } | |
+ return super.jdbcToRuby(context, runtime, column, type, resultSet); | |
+ } | |
+ | |
+ @Override | |
+ protected IRubyObject timestampToRuby(final ThreadContext context, | |
+ final Ruby runtime, final ResultSet resultSet, final int column) | |
+ throws SQLException { | |
+ // NOTE: using Timestamp we loose information such as BC : | |
+ // Timestamp: '0001-12-31 22:59:59.0' String: '0001-12-31 22:59:59 BC' | |
+ final String value = resultSet.getString(column); | |
+ if ( value == null ) { | |
+ if ( resultSet.wasNull() ) return runtime.getNil(); | |
+ return runtime.newString(); // "" | |
+ } | |
+ | |
+ final RubyString strValue = timestampToRubyString(runtime, value.toString()); | |
+ if ( rawDateTime != null && rawDateTime.booleanValue() ) return strValue; | |
+ | |
+ final IRubyObject adapter = callMethod(context, "adapter"); // self.adapter | |
+ if ( adapter.isNil() ) return strValue; // NOTE: we warn on init_connection | |
+ return adapter.callMethod(context, "_string_to_timestamp", strValue); | |
+ } | |
+ | |
+ @Override | |
+ protected IRubyObject arrayToRuby(final ThreadContext context, | |
+ final Ruby runtime, final ResultSet resultSet, final int column) | |
+ throws SQLException { | |
+ if ( rawArrayType == Boolean.TRUE ) { // pre AR 4.0 compatibility | |
+ return stringToRuby(context, runtime, resultSet, column); | |
+ } | |
+ // NOTE: avoid `finally { array.free(); }` on PostgreSQL due : | |
+ // java.sql.SQLFeatureNotSupportedException: | |
+ // Method org.postgresql.jdbc4.Jdbc4Array.free() is not yet implemented. | |
+ final Array value = resultSet.getArray(column); | |
+ | |
+ if ( value == null && resultSet.wasNull() ) return runtime.getNil(); | |
+ | |
+ final RubyArray array = runtime.newArray(); | |
+ | |
+ final ResultSet arrayResult = value.getResultSet(); // 1: index, 2: value | |
+ final int baseType = value.getBaseType(); | |
+ while ( arrayResult.next() ) { | |
+ array.append( jdbcToRuby(context, runtime, 2, baseType, arrayResult) ); | |
+ } | |
+ return array; | |
+ } | |
+ | |
+ @Override | |
+ protected IRubyObject objectToRuby(final ThreadContext context, | |
+ final Ruby runtime, final ResultSet resultSet, final int column) | |
+ throws SQLException { | |
+ | |
+ final Object object = resultSet.getObject(column); | |
+ | |
+ if ( object == null && resultSet.wasNull() ) return runtime.getNil(); | |
+ | |
+ final Class<?> objectClass = object.getClass(); | |
+ if ( objectClass == UUID.class ) { | |
+ return runtime.newString( object.toString() ); | |
+ } | |
+ | |
+ if ( objectClass == PGInterval.class ) { | |
+ return runtime.newString( formatInterval(object) ); | |
+ } | |
+ | |
+ if ( object instanceof PGobject ) { | |
+ // PG 9.2 JSON type will be returned here as well | |
+ return runtime.newString( object.toString() ); | |
+ } | |
+ | |
+ if ( object instanceof Map ) { // hstore | |
+ if ( rawHstoreType == Boolean.TRUE ) { | |
+ return runtime.newString( resultSet.getString(column) ); | |
+ } | |
+ // by default we avoid double parsing by driver and than column : | |
+ final RubyHash rubyObject = RubyHash.newHash(runtime); | |
+ rubyObject.putAll((Map) object); // converts keys/values to ruby | |
+ return rubyObject; | |
+ } | |
+ | |
+ return JavaUtil.convertJavaToRuby(runtime, object); | |
+ } | |
+ | |
+ @Override | |
+ protected TableName extractTableName( | |
+ final Connection connection, String catalog, String schema, | |
+ final String tableName) throws IllegalArgumentException, SQLException { | |
+ // The postgres JDBC driver will default to searching every schema if no | |
+ // schema search path is given. Default to the 'public' schema instead: | |
+ if ( schema == null ) schema = "public"; | |
+ return super.extractTableName(connection, catalog, schema, tableName); | |
+ } | |
+ | |
+ // NOTE: do not use PG classes in the API so that loading is delayed ! | |
+ private String formatInterval(final Object object) { | |
+ final PGInterval interval = (PGInterval) object; | |
+ if ( rawIntervalType ) return interval.getValue(); | |
+ | |
+ final StringBuilder str = new StringBuilder(32); | |
+ | |
+ final int years = interval.getYears(); | |
+ if ( years != 0 ) str.append(years).append(" years "); | |
+ final int months = interval.getMonths(); | |
+ if ( months != 0 ) str.append(months).append(" months "); | |
+ final int days = interval.getDays(); | |
+ if ( days != 0 ) str.append(days).append(" days "); | |
+ final int hours = interval.getHours(); | |
+ final int mins = interval.getMinutes(); | |
+ final int secs = (int) interval.getSeconds(); | |
+ if ( hours != 0 || mins != 0 || secs != 0 ) { // xx:yy:zz if not all 00 | |
+ if ( hours < 10 ) str.append('0'); | |
+ str.append(hours).append(':'); | |
+ if ( mins < 10 ) str.append('0'); | |
+ str.append(mins).append(':'); | |
+ if ( secs < 10 ) str.append('0'); | |
+ str.append(secs); | |
+ } | |
+ else { | |
+ if ( str.length() > 1 ) str.deleteCharAt( str.length() - 1 ); // " " at the end | |
+ } | |
+ | |
+ return str.toString(); | |
+ } | |
+ | |
+ protected static Boolean rawArrayType; | |
+ static { | |
+ final String arrayRaw = System.getProperty("arjdbc.postgresql.array.raw"); | |
+ if ( arrayRaw != null ) rawArrayType = Boolean.parseBoolean(arrayRaw); | |
+ } | |
+ | |
+ @JRubyMethod(name = "raw_array_type?", meta = true) | |
+ public static IRubyObject useRawArrayType(final ThreadContext context, final IRubyObject self) { | |
+ if ( rawArrayType == null ) return context.getRuntime().getNil(); | |
+ return context.getRuntime().newBoolean(rawArrayType); | |
+ } | |
+ | |
+ @JRubyMethod(name = "raw_array_type=", meta = true) | |
+ public static IRubyObject setRawArrayType(final IRubyObject self, final IRubyObject value) { | |
+ if ( value instanceof RubyBoolean ) { | |
+ rawArrayType = ((RubyBoolean) value).isTrue() ? Boolean.TRUE : Boolean.FALSE; | |
+ } | |
+ else { | |
+ rawArrayType = value.isNil() ? null : Boolean.TRUE; | |
+ } | |
+ return value; | |
+ } | |
+ | |
+ protected static Boolean rawHstoreType; | |
+ static { | |
+ final String hstoreRaw = System.getProperty("arjdbc.postgresql.hstore.raw"); | |
+ if ( hstoreRaw != null ) rawHstoreType = Boolean.parseBoolean(hstoreRaw); | |
+ } | |
+ | |
+ @JRubyMethod(name = "raw_hstore_type?", meta = true) | |
+ public static IRubyObject useRawHstoreType(final ThreadContext context, final IRubyObject self) { | |
+ if ( rawHstoreType == null ) return context.getRuntime().getNil(); | |
+ return context.getRuntime().newBoolean(rawHstoreType); | |
+ } | |
+ | |
+ @JRubyMethod(name = "raw_hstore_type=", meta = true) | |
+ public static IRubyObject setRawHstoreType(final IRubyObject self, final IRubyObject value) { | |
+ if ( value instanceof RubyBoolean ) { | |
+ rawHstoreType = ((RubyBoolean) value).isTrue() ? Boolean.TRUE : Boolean.FALSE; | |
+ } | |
+ else { | |
+ rawHstoreType = value.isNil() ? null : Boolean.TRUE; | |
+ } | |
+ return value; | |
+ } | |
+ | |
+ // whether to use "raw" interval values off by default - due native adapter compatibilty : | |
+ // RAW values : | |
+ // - 2 years 0 mons 0 days 0 hours 3 mins 0.00 secs | |
+ // - -1 years 0 mons -2 days 0 hours 0 mins 0.00 secs | |
+ // Rails style : | |
+ // - 2 years 00:03:00 | |
+ // - -1 years -2 days | |
+ protected static boolean rawIntervalType = Boolean.getBoolean("arjdbc.postgresql.iterval.raw"); | |
+ | |
+ @JRubyMethod(name = "raw_interval_type?", meta = true) | |
+ public static IRubyObject useRawIntervalType(final ThreadContext context, final IRubyObject self) { | |
+ return context.getRuntime().newBoolean(rawIntervalType); | |
+ } | |
+ | |
+ @JRubyMethod(name = "raw_interval_type=", meta = true) | |
+ public static IRubyObject setRawIntervalType(final IRubyObject self, final IRubyObject value) { | |
+ if ( value instanceof RubyBoolean ) { | |
+ rawIntervalType = ((RubyBoolean) value).isTrue(); | |
+ } | |
+ else { | |
+ rawIntervalType = ! value.isNil(); | |
+ } | |
+ return value; | |
+ } | |
+ | |
+ // NOTE: without these custom registered Postgre (driver) types | |
+ // ... we can not set range parameters in prepared statements ! | |
+ | |
+ public static class DateRangeType extends PGobject { | |
+ | |
+ public DateRangeType() { | |
+ setType("daterange"); | |
+ } | |
+ | |
+ public DateRangeType(final String value) throws SQLException { | |
+ this(); | |
+ setValue(value); | |
+ } | |
+ | |
+ } | |
+ | |
+ public static class TsRangeType extends PGobject { | |
+ | |
+ public TsRangeType() { | |
+ setType("tsrange"); | |
+ } | |
+ | |
+ public TsRangeType(final String value) throws SQLException { | |
+ this(); | |
+ setValue(value); | |
+ } | |
+ | |
+ } | |
+ | |
+ public static class TstzRangeType extends PGobject { | |
+ | |
+ public TstzRangeType() { | |
+ setType("tstzrange"); | |
+ } | |
+ | |
+ public TstzRangeType(final String value) throws SQLException { | |
+ this(); | |
+ setValue(value); | |
+ } | |
+ | |
+ } | |
+ | |
+ public static class Int4RangeType extends PGobject { | |
+ | |
+ public Int4RangeType() { | |
+ setType("int4range"); | |
+ } | |
+ | |
+ public Int4RangeType(final String value) throws SQLException { | |
+ this(); | |
+ setValue(value); | |
+ } | |
+ | |
+ } | |
+ | |
+ public static class Int8RangeType extends PGobject { | |
+ | |
+ public Int8RangeType() { | |
+ setType("int8range"); | |
+ } | |
+ | |
+ public Int8RangeType(final String value) throws SQLException { | |
+ this(); | |
+ setValue(value); | |
+ } | |
+ | |
+ } | |
+ | |
+ public static class NumRangeType extends PGobject { | |
+ | |
+ public NumRangeType() { | |
+ setType("numrange"); | |
+ } | |
+ | |
+ public NumRangeType(final String value) throws SQLException { | |
+ this(); | |
+ setValue(value); | |
+ } | |
+ | |
+ } | |
+ | |
} | |
diff --git a/src/java/arjdbc/sqlite3/Sqlite3RubyJdbcConnection.java b/src/java/arjdbc/sqlite3/Sqlite3RubyJdbcConnection.java | |
index 8558590..04c4aed 100644 | |
--- a/src/java/arjdbc/sqlite3/Sqlite3RubyJdbcConnection.java | |
+++ b/src/java/arjdbc/sqlite3/Sqlite3RubyJdbcConnection.java | |
@@ -1,40 +1,273 @@ | |
-/* | |
- * The MIT License | |
+/***** 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> | |
* | |
- * Copyright 2013 Karol Bucek. | |
+ * 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: | |
* | |
- * 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 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. | |
- */ | |
+ * 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 arjdbc.jdbc.Callable; | |
+import arjdbc.jdbc.RubyJdbcConnection; | |
+ | |
+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.Savepoint; | |
+import java.util.ArrayList; | |
+import java.util.List; | |
+ | |
import org.jruby.Ruby; | |
+import org.jruby.RubyArray; | |
import org.jruby.RubyClass; | |
+import org.jruby.RubyString; | |
+import org.jruby.anno.JRubyMethod; | |
+import org.jruby.runtime.ObjectAllocator; | |
+import org.jruby.runtime.ThreadContext; | |
+import org.jruby.runtime.builtin.IRubyObject; | |
+import org.jruby.util.ByteList; | |
/** | |
- * @see SQLite3RubyJdbcConnection | |
- * @deprecated only for Java API backwards-compatibility | |
+ * | |
+ * @author enebo | |
*/ | |
-@Deprecated | |
-public class Sqlite3RubyJdbcConnection extends SQLite3RubyJdbcConnection { | |
- | |
- protected Sqlite3RubyJdbcConnection(Ruby runtime, RubyClass metaClass) { | |
+public class SQLite3RubyJdbcConnection extends RubyJdbcConnection { | |
+ private static final long serialVersionUID = -5783855018818472773L; | |
+ | |
+ protected SQLite3RubyJdbcConnection(Ruby runtime, RubyClass metaClass) { | |
super(runtime, metaClass); | |
} | |
- | |
+ | |
+ public static RubyClass createSQLite3JdbcConnectionClass(Ruby runtime, RubyClass jdbcConnection) { | |
+ final RubyClass clazz = getConnectionAdapters(runtime). // ActiveRecord::ConnectionAdapters | |
+ defineClassUnder("SQLite3JdbcConnection", jdbcConnection, SQLITE3_JDBCCONNECTION_ALLOCATOR); | |
+ clazz.defineAnnotatedMethods( SQLite3RubyJdbcConnection.class ); | |
+ getConnectionAdapters(runtime).setConstant("Sqlite3JdbcConnection", clazz); // backwards-compat | |
+ return clazz; | |
+ } | |
+ | |
+ private static ObjectAllocator SQLITE3_JDBCCONNECTION_ALLOCATOR = new ObjectAllocator() { | |
+ public IRubyObject allocate(Ruby runtime, RubyClass klass) { | |
+ return new SQLite3RubyJdbcConnection(runtime, klass); | |
+ } | |
+ }; | |
+ | |
+ @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, new Callable<IRubyObject>() { | |
+ public IRubyObject call(final Connection connection) throws SQLException { | |
+ 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.getRuntime(), genKeys, true); | |
+ } | |
+ catch (final SQLException e) { | |
+ debugMessage(context, "failed to get generated keys: " + e.getMessage()); | |
+ 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 statementEscapeProcessing = getConfigValue(context, "statement_escape_processing"); | |
+ if ( ! statementEscapeProcessing.isNil() ) { | |
+ statement.setEscapeProcessing(statementEscapeProcessing.isTrue()); | |
+ } | |
+ // else leave as is by default | |
+ return statement; | |
+ } | |
+ | |
+ @Override | |
+ protected IRubyObject indexes(final ThreadContext context, String tableName, final String name, String schemaName) { | |
+ if ( tableName != null ) { | |
+ final int i = tableName.indexOf('.'); | |
+ if ( i > 0 && schemaName == null ) { | |
+ schemaName = tableName.substring(0, i); | |
+ tableName = tableName.substring(i + 1); | |
+ } | |
+ } | |
+ return super.indexes(context, tableName, name, schemaName); | |
+ } | |
+ | |
+ @Override | |
+ protected TableName extractTableName( | |
+ final Connection connection, String catalog, String schema, | |
+ final String tableName) throws IllegalArgumentException, SQLException { | |
+ | |
+ final String[] nameParts = tableName.split("\\."); | |
+ if ( nameParts.length > 3 ) { | |
+ throw new IllegalArgumentException("table name: " + tableName + " should not contain more than 2 '.'"); | |
+ } | |
+ | |
+ String name = tableName; | |
+ | |
+ if ( nameParts.length == 2 ) { | |
+ schema = nameParts[0]; | |
+ name = nameParts[1]; | |
+ } | |
+ else if ( nameParts.length == 3 ) { | |
+ catalog = nameParts[0]; | |
+ schema = nameParts[1]; | |
+ name = nameParts[2]; | |
+ } | |
+ | |
+ name = caseConvertIdentifierForJdbc(connection, name); | |
+ | |
+ if ( schema != null ) { | |
+ schema = caseConvertIdentifierForJdbc(connection, schema); | |
+ // 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); | |
+ } | |
+ return super.jdbcToRuby(context, runtime, column, type, resultSet); | |
+ } | |
+ | |
+ @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 ( resultSet.wasNull() ) return runtime.getNil(); | |
+ return runtime.newString( new ByteList(bytes, false) ); | |
+ } | |
+ | |
+ @Override | |
+ protected RubyArray mapTables(final Ruby runtime, final DatabaseMetaData metaData, | |
+ final String catalog, final String schemaPattern, final String tablePattern, | |
+ final ResultSet tablesSet) throws SQLException { | |
+ final List<IRubyObject> tables = new ArrayList<IRubyObject>(32); | |
+ while ( tablesSet.next() ) { | |
+ String name = tablesSet.getString(TABLES_TABLE_NAME); | |
+ name = name.toLowerCase(); // simply lower-case for SQLite3 | |
+ tables.add( RubyString.newUnicodeString(runtime, name) ); | |
+ } | |
+ return runtime.newArray(tables); | |
+ } | |
+ | |
+ private static class SavepointStub implements Savepoint { | |
+ | |
+ @Override | |
+ public int getSavepointId() throws SQLException { | |
+ throw new UnsupportedOperationException(); | |
+ } | |
+ | |
+ @Override | |
+ public String getSavepointName() throws SQLException { | |
+ throw new UnsupportedOperationException(); | |
+ } | |
+ | |
+ } | |
+ | |
+ @Override | |
+ @JRubyMethod(name = "create_savepoint", optional = 1) | |
+ public IRubyObject create_savepoint(final ThreadContext context, final IRubyObject[] args) { | |
+ final IRubyObject name = args.length > 0 ? args[0] : null; | |
+ if ( name == null || name.isNil() ) { | |
+ throw new IllegalArgumentException("create_savepoint (without name) not implemented!"); | |
+ } | |
+ final Connection connection = getConnection(true); | |
+ try { | |
+ connection.setAutoCommit(false); | |
+ // NOTE: JDBC driver does not support setSavepoint(String) : | |
+ connection.createStatement().execute("SAVEPOINT " + name.toString()); | |
+ | |
+ getSavepoints(context).put(name, new SavepointStub()); | |
+ | |
+ return name; | |
+ } | |
+ catch (SQLException e) { | |
+ return handleException(context, e); | |
+ } | |
+ } | |
+ | |
+ @Override | |
+ @JRubyMethod(name = "rollback_savepoint", required = 1) | |
+ public IRubyObject rollback_savepoint(final ThreadContext context, final IRubyObject name) { | |
+ final Connection connection = getConnection(true); | |
+ try { | |
+ if ( getSavepoints(context).get(name) == null ) { | |
+ throw context.getRuntime().newRuntimeError("could not rollback savepoint: '" + name + "' (not set)"); | |
+ } | |
+ // NOTE: JDBC driver does not implement rollback(Savepoint) : | |
+ connection.createStatement().execute("ROLLBACK TO SAVEPOINT " + name.toString()); | |
+ | |
+ return context.getRuntime().getNil(); | |
+ } | |
+ catch (SQLException e) { | |
+ return handleException(context, e); | |
+ } | |
+ } | |
+ | |
+ @Override | |
+ @JRubyMethod(name = "release_savepoint", required = 1) | |
+ public IRubyObject release_savepoint(final ThreadContext context, final IRubyObject name) { | |
+ final Connection connection = getConnection(true); | |
+ try { | |
+ if ( getSavepoints(context).get(name) == null ) { | |
+ throw context.getRuntime().newRuntimeError("could not release savepoint: '" + name + "' (not set)"); | |
+ } | |
+ // NOTE: JDBC driver does not implement release(Savepoint) : | |
+ connection.createStatement().execute("RELEASE SAVEPOINT " + name.toString()); | |
+ | |
+ return context.getRuntime().getNil(); | |
+ } | |
+ catch (SQLException e) { | |
+ return handleException(context, e); | |
+ } | |
+ } | |
+ | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment