package arjdbc.jdbc; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import org.jruby.Ruby; import org.jruby.RubyArray; import org.jruby.RubyClass; import org.jruby.RubyHash; import org.jruby.RubyObject; import org.jruby.RubyString; import org.jruby.runtime.Block; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; /** * This is a base Result class to be returned as the "raw" result. * It should be overridden for specific adapters to manage type maps * and provide any additional methods needed. */ public class JdbcResult extends RubyObject { // Should these be private with accessors? protected final RubyArray values; protected RubyHash[] tuples; protected final int[] columnTypes; protected RubyString[] columnNames; protected final RubyJdbcConnection connection; protected JdbcResult(ThreadContext context, RubyClass clazz, RubyJdbcConnection connection, ResultSet resultSet) throws SQLException { super(context.runtime, clazz); values = context.runtime.newArray(); this.connection = connection; final ResultSetMetaData resultMetaData = resultSet.getMetaData(); final int columnCount = resultMetaData.getColumnCount(); // FIXME: if we support MSSQL we may need to change how we deal with omitting elements columnNames = new RubyString[columnCount]; columnTypes = new int[columnCount]; extractColumnInfo(context, resultMetaData); processResultSet(context, resultSet); } /** * Builds a type map for creating the AR::Result, most adapters don't need it * @param context which thread this is running on. * @return RubyNil * @throws SQLException postgres result can throw an exception */ protected IRubyObject columnTypeMap(ThreadContext context) throws SQLException { return context.nil; } /** * Build an array of column types * @param resultMetaData metadata from a ResultSet to determine column information from * @throws SQLException throws error! */ private void extractColumnInfo(ThreadContext context, ResultSetMetaData resultMetaData) throws SQLException { final int columnCount = resultMetaData.getColumnCount(); for (int i = 1; i <= columnCount; i++) { // metadata is one-based // This appears to not be used by Postgres, MySQL, or SQLite so leaving it off for now //name = caseConvertIdentifierForRails(connection, name); columnNames[i - 1] = RubyJdbcConnection.STRING_CACHE.get(context, resultMetaData.getColumnLabel(i)); columnTypes[i - 1] = resultMetaData.getColumnType(i); } } /** * @return an array with the column names as Ruby strings */ protected RubyString[] getColumnNames() { return columnNames; } /** * Builds an array of hashes with column names to column values * @param context current thread context */ protected void populateTuples(final ThreadContext context) { int columnCount = columnNames.length; tuples = new RubyHash[values.size()]; for (int i = 0; i < tuples.length; i++) { RubyArray currentRow = (RubyArray) values.eltInternal(i); RubyHash hash = RubyHash.newHash(context.runtime); for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { hash.fastASet(columnNames[columnIndex], currentRow.eltInternal(columnIndex)); } tuples[i] = hash; } } /** * Does the heavy lifting of turning the JDBC objects into Ruby objects * @param context current thread context * @param resultSet the set of results we are converting * @throws SQLException throws! */ private void processResultSet(final ThreadContext context, final ResultSet resultSet) throws SQLException { Ruby runtime = context.runtime; int columnCount = columnNames.length; while (resultSet.next()) { final IRubyObject[] row = new IRubyObject[columnCount]; for (int i = 0; i < columnCount; i++) { row[i] = connection.jdbcToRuby(context, runtime, i + 1, columnTypes[i], resultSet); // Result Set is 1 based } values.append(RubyArray.newArrayNoCopy(context.runtime, row)); } } /** * Creates an ActiveRecord::Result with the data from this result * @param context current thread context * @return ActiveRecord::Result object with the data from this result set * @throws SQLException can be caused by postgres generating its type map */ public IRubyObject toARResult(final ThreadContext context) throws SQLException { final RubyClass Result = RubyJdbcConnection.getResult(context.runtime); // FIXME: Is this broken? no copy of an array AR::Result can modify? or should it be frozen? final RubyArray rubyColumnNames = RubyArray.newArrayNoCopy(context.runtime, getColumnNames()); return Result.newInstance(context, rubyColumnNames, values, columnTypeMap(context), Block.NULL_BLOCK); } }