/** * @param runtime * @param driver * @return */ public static RubyClass createResultClass(final Ruby runtime, DriverDefinition driver) { RubyModule doModule = runtime.getModule(DATA_OBJECTS_MODULE_NAME); RubyClass superClass = doModule.getClass(RUBY_CLASS_NAME); RubyModule driverModule = (RubyModule) doModule.getConstant(driver.getModuleName()); RubyClass resultClass = driverModule.defineClassUnder(RUBY_CLASS_NAME, superClass, RESULT_ALLOCATOR); resultClass.defineAnnotatedMethods(Result.class); return resultClass; }
/** * @param runtime * @param factory * @return */ public static RubyClass createCommandClass(final Ruby runtime, DriverDefinition factory) { RubyModule doModule = runtime.getModule(DATA_OBJECTS_MODULE_NAME); RubyClass superClass = doModule.getClass(RUBY_CLASS_NAME); RubyModule driverModule = (RubyModule) doModule.getConstant(factory.getModuleName()); RubyClass commandClass = runtime.defineClassUnder("Command", superClass, COMMAND_ALLOCATOR, driverModule); commandClass.setInstanceVariable("@__factory", JavaEmbedUtils.javaToRuby(runtime, factory)); commandClass.defineAnnotatedMethods(Command.class); setDriverDefinition(commandClass, runtime, factory); return commandClass; }
/** * @param ps the PreparedStatement for which the parameter should be set * @param recv * @param arg a parameter value * @param idx the index of the parameter * @throws java.sql.SQLException */ private static void setPreparedStatementParam( PreparedStatement ps, IRubyObject recv, IRubyObject arg, int idx) throws SQLException { Integer jdbcTypeId = null; try { jdbcTypeId = ps.getMetaData().getColumnType(idx); } catch (Exception ex) { } String rubyTypeName = arg.getType().getName(); if ("Fixnum".equals(rubyTypeName)) { ps.setInt(idx, Integer.parseInt(arg.toString())); } else if ("Bignum".equals(rubyTypeName)) { ps.setLong(idx, ((RubyBignum) arg).getLongValue()); } else if ("Float".equals(rubyTypeName)) { ps.setDouble(idx, RubyNumeric.num2dbl(arg)); } else if ("BigDecimal".equals(rubyTypeName)) { ps.setBigDecimal(idx, ((RubyBigDecimal) arg).getValue()); } else if ("NilClass".equals(rubyTypeName)) { // XXX In fact this should looks like ps.setNull(idx, Types.YYY); // where YYY is a JDBC type of column i.e. Types.VARCHAR // but this code works for MySQL :) ps.setNull(idx, Types.NULL); } else if ("TrueClass".equals(rubyTypeName) || "FalseClass".equals(rubyTypeName)) { ps.setBoolean(idx, arg.toString().equals("true")); } else if ("Class".equals(rubyTypeName)) { ps.setString(idx, arg.toString()); } else if ("Extlib::ByteArray".equals(rubyTypeName)) { ps.setBytes(idx, ((RubyString) arg).getBytes()); // TODO: add support for ps.setBlob(); } else if ("Date".equals(rubyTypeName)) { ps.setDate(idx, java.sql.Date.valueOf(arg.toString())); } else if ("Time".equals(rubyTypeName)) { RubyTime rubyTime = (RubyTime) arg; java.util.Date date = rubyTime.getJavaDate(); GregorianCalendar cal = new GregorianCalendar(); cal.setTime(date); cal.setTimeZone( TimeZone.getTimeZone("UTC")); // XXX works only if driver suports Calendars in PS java.sql.Timestamp ts; if (driver.supportsCalendarsInJDBCPreparedStatement() == true) { ts = new java.sql.Timestamp(cal.getTime().getTime()); ts.setNanos(cal.get(GregorianCalendar.MILLISECOND) * 100000); } else { // XXX ugly workaround for MySQL and Hsqldb ts = new Timestamp( cal.get(GregorianCalendar.YEAR) - 1900, cal.get(GregorianCalendar.MONTH), cal.get(GregorianCalendar.DAY_OF_MONTH), cal.get(GregorianCalendar.HOUR_OF_DAY), cal.get(GregorianCalendar.MINUTE), cal.get(GregorianCalendar.SECOND), cal.get(GregorianCalendar.MILLISECOND) * 100000); } ps.setTimestamp(idx, ts, cal); } else if ("DateTime".equals(rubyTypeName)) { ps.setTimestamp( idx, java.sql.Timestamp.valueOf( arg.toString().replace('T', ' ').replaceFirst("[-+]..:..$", ""))); } else if (arg.toString().indexOf("-") != -1 && arg.toString().indexOf(":") != -1) { // TODO: improve the above string pattern checking // Handle date patterns in strings java.util.Date parsedDate; try { parsedDate = FORMAT.parse(arg.asJavaString().replace('T', ' ')); java.sql.Timestamp timestamp = new java.sql.Timestamp(parsedDate.getTime()); ps.setTimestamp(idx, timestamp); } catch (ParseException ex) { ps.setString(idx, api.convertToRubyString(arg).getUnicodeValue()); } } else if (arg.toString().indexOf(":") != -1 && arg.toString().length() == 8) { // Handle time patterns in strings ps.setTime(idx, java.sql.Time.valueOf(arg.asJavaString())); } else { if (jdbcTypeId == null) { ps.setString(idx, api.convertToRubyString(arg).getUnicodeValue()); } else { // TODO: Here comes conversions like '.execute_reader("2")' // It definitly needs to be refactored... try { if (jdbcTypeId == Types.VARCHAR) { ps.setString(idx, api.convertToRubyString(arg).getUnicodeValue()); } else if (jdbcTypeId == Types.INTEGER) { ps.setObject(idx, Integer.valueOf(arg.toString()), jdbcTypeId); } else { // I'm not sure is it correct in 100% ps.setString(idx, api.convertToRubyString(arg).getUnicodeValue()); } } catch (NumberFormatException ex) { // i.e Integer.valueOf ps.setString(idx, api.convertToRubyString(arg).getUnicodeValue()); } } } }
@JRubyMethod(optional = 1, rest = true) public static IRubyObject execute_non_query(IRubyObject recv, IRubyObject[] args) { Ruby runtime = recv.getRuntime(); IRubyObject connection_instance = api.getInstanceVariable(recv, "@connection"); IRubyObject wrapped_jdbc_connection = api.getInstanceVariable(connection_instance, "@connection"); if (wrapped_jdbc_connection.isNil()) { throw DataObjectsUtils.newDriverError( runtime, errorName, "This connection has already been closed."); } java.sql.Connection conn = getConnection(wrapped_jdbc_connection); IRubyObject insert_key = runtime.getNil(); RubyClass resultClass = Result.createResultClass(runtime, moduleName, errorName, driver); // affectedCount == 1 means 1 updated row // or 1 row in result set that represents returned key (insert...returning), // other values represents numer of updated rows int affectedCount = 0; PreparedStatement sqlStatement = null; java.sql.ResultSet keys = null; // String sqlText = prepareSqlTextForPs(api.getInstanceVariable(recv, "@text").asJavaString(), // recv, args); String doSqlText = api.convertToRubyString(api.getInstanceVariable(recv, "@text")).getUnicodeValue(); String sqlText = prepareSqlTextForPs(doSqlText, recv, args); try { if (driver.supportsConnectionPrepareStatementMethodWithGKFlag()) { sqlStatement = conn.prepareStatement( sqlText, driver.supportsJdbcGeneratedKeys() ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS); } else { // If java.sql.PreparedStatement#getGeneratedKeys() is not supported, // then it is important to call java.sql.Connection#prepareStatement(String) // -- with just a single parameter -- rather java.sql.Connection# // prepareStatement(String, int) (and passing in Statement.NO_GENERATED_KEYS). // Some less-than-complete JDBC drivers do not implement all of // the overloaded prepareStatement methods: the main culprit // being SQLiteJDBC which currently throws an ugly (and cryptic) // "NYI" SQLException if Connection#prepareStatement(String, int) // is called. sqlStatement = conn.prepareStatement(sqlText); } prepareStatementFromArgs(sqlStatement, recv, args); // javaConn.setAutoCommit(true); // hangs with autocommit set to false // sqlStatement.setMaxRows(); long startTime = System.currentTimeMillis(); try { if (sqlText.contains("RETURNING")) { keys = sqlStatement.executeQuery(); } else { affectedCount = sqlStatement.executeUpdate(); } } catch (SQLException sqle) { // This is to handle the edge case of SELECT sleep(1): // an executeUpdate() will throw a SQLException if a SELECT // is passed, so we try the same query again with execute() affectedCount = 0; sqlStatement.execute(); } long endTime = System.currentTimeMillis(); debug(recv.getRuntime(), driver.toString(sqlStatement), Long.valueOf(endTime - startTime)); if (keys == null) { if (driver.supportsJdbcGeneratedKeys()) { // Derby, H2, and MySQL all support getGeneratedKeys(), but only // to varying extents. // // However, javaConn.getMetaData().supportsGetGeneratedKeys() // currently returns FALSE for the Derby driver, as its support // is limited. As such, we use supportsJdbcGeneratedKeys() from // our own driver definition. // // See http://issues.apache.org/jira/browse/DERBY-242 // See http://issues.apache.org/jira/browse/DERBY-2631 // (Derby only supplies getGeneratedKeys() for auto-incremented // columns) // // apparently the prepared statements always provide the // generated keys keys = sqlStatement.getGeneratedKeys(); } else { // If there is no support, then a custom method can be defined // to return a ResultSet with keys keys = driver.getGeneratedKeys(conn); } } if (keys != null) { insert_key = unmarshal_id_result(runtime, keys); affectedCount = (affectedCount > 0) ? affectedCount : 1; } // not needed as it will be closed in the finally clause // sqlStatement.close(); // sqlStatement = null; } catch (SQLException sqle) { // TODO: log // sqle.printStackTrace(); throw newQueryError(runtime, sqle, sqlStatement); } finally { if (sqlStatement != null) { try { sqlStatement.close(); } catch (SQLException sqle2) { } } } // return nil if no updates are made if (affectedCount <= 0) { return runtime.getNil(); } IRubyObject affected_rows = runtime.newFixnum(affectedCount); IRubyObject result = api.callMethod(resultClass, "new", new IRubyObject[] {recv, affected_rows, insert_key}); return result; }
@JRubyMethod(optional = 1, rest = true) public static IRubyObject execute_reader(IRubyObject recv, IRubyObject[] args) { Ruby runtime = recv.getRuntime(); IRubyObject connection_instance = api.getInstanceVariable(recv, "@connection"); IRubyObject wrapped_jdbc_connection = api.getInstanceVariable(connection_instance, "@connection"); if (wrapped_jdbc_connection.isNil()) { throw DataObjectsUtils.newDriverError( runtime, errorName, "This connection has already been closed."); } java.sql.Connection conn = getConnection(wrapped_jdbc_connection); RubyClass readerClass = Reader.createReaderClass(runtime, moduleName, errorName, driver); boolean inferTypes = false; int columnCount = 0; PreparedStatement sqlStatement = null; ResultSet resultSet = null; ResultSetMetaData metaData = null; // instantiate a new reader IRubyObject reader = readerClass.newInstance( runtime.getCurrentContext(), new IRubyObject[] {}, Block.NULL_BLOCK); // execute the query try { String sqlText = prepareSqlTextForPs(api.getInstanceVariable(recv, "@text").asJavaString(), recv, args); sqlStatement = conn.prepareStatement( sqlText, driver.supportsJdbcScrollableResultSets() ? ResultSet.TYPE_SCROLL_INSENSITIVE : ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); // sqlStatement.setMaxRows(); prepareStatementFromArgs(sqlStatement, recv, args); long startTime = System.currentTimeMillis(); resultSet = sqlStatement.executeQuery(); long endTime = System.currentTimeMillis(); debug(recv.getRuntime(), driver.toString(sqlStatement), Long.valueOf(endTime - startTime)); metaData = resultSet.getMetaData(); columnCount = metaData.getColumnCount(); // pass the response to the reader IRubyObject wrappedResultSet = Java.java_to_ruby(recv, JavaObject.wrap(recv.getRuntime(), resultSet), Block.NULL_BLOCK); reader.getInstanceVariables().setInstanceVariable("@reader", wrappedResultSet); wrappedResultSet.dataWrapStruct(resultSet); // handle each result // mark the reader as opened api.setInstanceVariable(reader, "@opened", runtime.newBoolean(true)); // TODO: if no response return nil api.setInstanceVariable(reader, "@position", runtime.newFixnum(0)); // save the field_count in reader api.setInstanceVariable(reader, "@field_count", runtime.newFixnum(columnCount)); // get the field types RubyArray field_names = runtime.newArray(); IRubyObject field_types = api.getInstanceVariable(recv, "@field_types"); // If no types are passed in, infer them if (field_types == null) { field_types = runtime.newArray(); inferTypes = true; } else { int fieldTypesCount = field_types.convertToArray().getLength(); if (field_types.isNil() || fieldTypesCount == 0) { field_types = runtime.newArray(); inferTypes = true; } else if (fieldTypesCount != columnCount) { // Wrong number of fields passed to set_types. Close the reader // and raise an error. api.callMethod(reader, "close"); throw runtime.newArgumentError( String.format( "Field-count mismatch. Expected %1$d fields, but the query yielded %2$d", fieldTypesCount, columnCount)); } } // for each field for (int i = 0; i < columnCount; i++) { RubyString field_name = runtime.newString(metaData.getColumnName(i + 1)); // infer the type if no types passed field_names.push_m(new IRubyObject[] {field_name}); if (inferTypes) { // TODO: do something } } // set the reader @field_names and @types (guessed or otherwise) api.setInstanceVariable(reader, "@fields", field_names); api.setInstanceVariable(reader, "@field_types", field_types); // keep the statement open // TODO why keep it open ??? // sqlStatement.close(); // sqlStatement = null; } catch (SQLException sqle) { // TODO: log sqle.printStackTrace(); throw newQueryError(runtime, sqle, sqlStatement); } finally { // if (sqlStatement != null) { // try { // sqlStatement.close(); // } catch (SQLException stsqlex) { // } // } } // return the reader return reader; }