/*
* Microsoft JDBC Driver for SQL Server
*
* Copyright(c) Microsoft Corporation All rights reserved.
*
* This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information.
*/
package com.microsoft.sqlserver.testframework;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.sql.JDBCType;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.Calendar;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.microsoft.sqlserver.testframework.Utils.DBBinaryStream;
import com.microsoft.sqlserver.testframework.Utils.DBCharacterStream;
/**
* wrapper class for ResultSet
*
* @author Microsoft
*
*/
public class DBResultSet extends AbstractParentWrapper {
// TODO: add cursors
// TODO: add resultSet level holdability
// TODO: add concurrency control
public static final Logger log = Logger.getLogger("DBResultSet");
public static final int TYPE_DYNAMIC = ResultSet.TYPE_SCROLL_SENSITIVE + 1;
public static final int CONCUR_OPTIMISTIC = ResultSet.CONCUR_UPDATABLE + 2;
public static final int TYPE_CURSOR_FORWARDONLY = ResultSet.TYPE_FORWARD_ONLY + 1001;
public static final int TYPE_FORWARD_ONLY = ResultSet.TYPE_FORWARD_ONLY;
public static final int CONCUR_READ_ONLY = ResultSet.CONCUR_READ_ONLY;
public static final int TYPE_SCROLL_INSENSITIVE = ResultSet.TYPE_SCROLL_INSENSITIVE;
public static final int TYPE_SCROLL_SENSITIVE = ResultSet.TYPE_SCROLL_SENSITIVE;
public static final int CONCUR_UPDATABLE = ResultSet.CONCUR_UPDATABLE;
public static final int TYPE_DIRECT_FORWARDONLY = ResultSet.TYPE_FORWARD_ONLY + 1000;
protected DBTable currentTable;
public int _currentrow = -1; // The row this rowset is currently pointing to
ResultSet resultSet = null;
DBResultSetMetaData metaData;
DBResultSet(DBStatement dbstatement,
ResultSet internal) {
super(dbstatement, internal, "resultSet");
resultSet = internal;
}
DBResultSet(DBPreparedStatement dbpstmt,
ResultSet internal) {
super(dbpstmt, internal, "resultSet");
resultSet = internal;
}
/**
* Close the ResultSet object
*
* @throws SQLException
*/
public void close() throws SQLException {
if (null != resultSet) {
resultSet.close();
}
}
/**
*
* @return true new row is valid
* @throws SQLException
*/
public boolean next() throws SQLException {
_currentrow++;
return resultSet.next();
}
/**
*
* @param index
* @return Object with the column value
* @throws SQLException
*/
public Object getObject(int index) throws SQLException {
// call individual getters based on type
return resultSet.getObject(index);
}
/**
*
* @param x
* @return
* @throws SQLException
*/
public InputStream getBinaryStream(int x) throws SQLException {
return resultSet.getBinaryStream(x);
}
/**
*
* @param x
* @return
* @throws SQLException
*/
public InputStream getBinaryStream(String x) throws SQLException {
return resultSet.getBinaryStream(x);
}
/**
*
* @param x
* @return
* @throws SQLException
*/
public Reader getCharacterStream(int x) throws SQLException {
return resultSet.getCharacterStream(x);
}
/**
*
* @param x
* @return
* @throws SQLException
*/
public Reader getCharacterStream(String x) throws SQLException {
return resultSet.getCharacterStream(x);
}
/**
*
* @param index
* @return
* @throws SQLException
*/
public String getString(int index) throws SQLException {
// call individual getters based on type
return resultSet.getString(index);
}
/**
*
* @param index
* @return
*/
public void updateObject(int index) throws SQLException {
// TODO: update object based on cursor type
}
/**
*
* @throws SQLException
*/
public void verify(DBTable table) throws SQLException {
currentTable = table;
metaData = this.getMetaData();
metaData.verify();
while (this.next())
this.verifyCurrentRow(table);
}
/**
* @throws SQLException
*
*/
public void verifyCurrentRow(DBTable table) throws SQLException {
currentTable = table;
int totalColumns = ((ResultSet) product()).getMetaData().getColumnCount();
Class _class = Object.class;
for (int i = 0; i < totalColumns; i++)
verifydata(i, _class);
}
/**
*
* @param ordinal
* @param coercion
* @throws SQLException
* @throws Exception
*/
public void verifydata(int ordinal,
Class coercion) throws SQLException {
Object expectedData = currentTable.columns.get(ordinal).getRowValue(_currentrow);
// getXXX - default mapping
Object retrieved = this.getXXX(ordinal + 1, coercion);
// Verify
// TODO: Check the intermittent verification error
// verifydata(ordinal, coercion, expectedData, retrieved);
}
/**
* verifies data
*
* @param ordinal
* @param coercion
* @param expectedData
* @param retrieved
* @throws SQLException
*/
public void verifydata(int ordinal,
Class coercion,
Object expectedData,
Object retrieved) throws SQLException {
metaData = this.getMetaData();
switch (metaData.getColumnType(ordinal + 1)) {
case java.sql.Types.BIGINT:
assertTrue((((Long) expectedData).longValue() == ((Long) retrieved).longValue()),
"Unexpected bigint value, expected: " + ((Long) expectedData).longValue() + " .Retrieved: " + ((Long) retrieved).longValue());
break;
case java.sql.Types.INTEGER:
assertTrue((((Integer) expectedData).intValue() == ((Integer) retrieved).intValue()), "Unexpected int value, expected : "
+ ((Integer) expectedData).intValue() + " ,received: " + ((Integer) retrieved).intValue());
break;
case java.sql.Types.SMALLINT:
case java.sql.Types.TINYINT:
assertTrue((((Short) expectedData).shortValue() == ((Short) retrieved).shortValue()), "Unexpected smallint/tinyint value, expected: "
+ " " + ((Short) expectedData).shortValue() + " received: " + ((Short) retrieved).shortValue());
break;
case java.sql.Types.BIT:
if (expectedData.equals(1))
expectedData = true;
else
expectedData = false;
assertTrue((((Boolean) expectedData).booleanValue() == ((Boolean) retrieved).booleanValue()), "Unexpected bit value, expected: "
+ ((Boolean) expectedData).booleanValue() + " ,received: " + ((Boolean) retrieved).booleanValue());
break;
case java.sql.Types.DECIMAL:
case java.sql.Types.NUMERIC:
assertTrue(0 == (((BigDecimal) expectedData).compareTo((BigDecimal) retrieved)), "Unexpected decimal/numeric/money/smallmoney value "
+ ",expected: " + (BigDecimal) expectedData + " received: " + (BigDecimal) retrieved);
break;
case java.sql.Types.DOUBLE:
assertTrue((((Double) expectedData).doubleValue() == ((Double) retrieved).doubleValue()), "Unexpected float value, expected: "
+ ((Double) expectedData).doubleValue() + " received: " + ((Double) retrieved).doubleValue());
break;
case java.sql.Types.REAL:
assertTrue((((Float) expectedData).floatValue() == ((Float) retrieved).floatValue()),
"Unexpected real value, expected: " + ((Float) expectedData).floatValue() + " received: " + ((Float) retrieved).floatValue());
break;
case java.sql.Types.VARCHAR:
case java.sql.Types.NVARCHAR:
assertTrue((((String) expectedData).trim().equalsIgnoreCase(((String) retrieved).trim())), "Unexpected varchar/nvarchar value, "
+ "expected: " + ((String) expectedData).trim() + " ,received: " + ((String) retrieved).trim());
break;
case java.sql.Types.CHAR:
case java.sql.Types.NCHAR:
assertTrue((((String) expectedData).trim().equalsIgnoreCase(((String) retrieved).trim())), "Unexpected char/nchar value, "
+ "expected: " + ((String) expectedData).trim() + " ,received: " + ((String) retrieved).trim());
break;
case java.sql.Types.TIMESTAMP:
if (metaData.getColumnTypeName(ordinal + 1).equalsIgnoreCase("datetime")) {
assertTrue((((Timestamp) roundDatetimeValue(expectedData)).getTime() == (((Timestamp) retrieved).getTime())),
"Unexpected datetime value, expected: " + ((Timestamp) roundDatetimeValue(expectedData)).getTime() + " , received: "
+ (((Timestamp) retrieved).getTime()));
break;
}
else if (metaData.getColumnTypeName(ordinal + 1).equalsIgnoreCase("smalldatetime")) {
assertTrue((((Timestamp) roundSmallDateTimeValue(expectedData)).getTime() == (((Timestamp) retrieved).getTime())),
"Unexpected smalldatetime value, expected: " + ((Timestamp) roundSmallDateTimeValue(expectedData)).getTime()
+ " ,received: " + (((Timestamp) retrieved).getTime()));
break;
}
else
assertTrue(("" + Timestamp.valueOf((LocalDateTime) expectedData)).equalsIgnoreCase("" + retrieved), "Unexpected datetime2 value, "
+ "expected: " + Timestamp.valueOf((LocalDateTime) expectedData) + " ,received: " + retrieved);
break;
case java.sql.Types.DATE:
assertTrue((("" + expectedData).equalsIgnoreCase("" + retrieved)),
"Unexpected date value, expected: " + expectedData + " ,received: " + retrieved);
break;
case java.sql.Types.TIME:
assertTrue(("" + Time.valueOf((LocalTime) expectedData)).equalsIgnoreCase("" + retrieved),
"Unexpected time value, exptected: " + Time.valueOf((LocalTime) expectedData) + " ,received: " + retrieved);
break;
case microsoft.sql.Types.DATETIMEOFFSET:
assertTrue(("" + expectedData).equals("" + retrieved),
" unexpected DATETIMEOFFSET value, expected: " + expectedData + " ,received: " + retrieved);
break;
case java.sql.Types.BINARY:
assertTrue(Utils.parseByte((byte[]) expectedData, (byte[]) retrieved),
" unexpected BINARY value, expected: " + expectedData + " ,received: " + retrieved);
break;
case java.sql.Types.VARBINARY:
assertTrue(Arrays.equals((byte[]) expectedData, (byte[]) retrieved),
" unexpected BINARY value, expected: " + expectedData + " ,received: " + retrieved);
break;
default:
fail("Unhandled JDBCType " + JDBCType.valueOf(metaData.getColumnType(ordinal + 1)));
break;
}
}
/**
*
* @param idx
* @param coercion
* @return
* @throws SQLException
*/
public Object getXXX(Object idx,
Class coercion) throws SQLException {
int intOrdinal = 0;
String strOrdinal = "";
boolean isInteger = false;
if (idx == null) {
strOrdinal = null;
}
else if (idx instanceof Integer) {
isInteger = true;
intOrdinal = ((Integer) idx).intValue();
}
else {
// Otherwise
throw new SQLException("Unhandled ordinal type: " + idx.getClass());
}
if (coercion == Object.class) {
return this.getObject(intOrdinal);
}
else if (coercion == DBBinaryStream.class) {
return isInteger ? this.getBinaryStream(intOrdinal) : this.getBinaryStream(strOrdinal);
}
else if (coercion == DBCharacterStream.class) {
return isInteger ? this.getCharacterStream(intOrdinal) : this.getCharacterStream(strOrdinal);
}
else {
if (log.isLoggable(Level.FINE)) {
log.fine("coercion not supported! ");
}
else {
log.fine("coercion + " + coercion.toString() + " is not supported!");
}
}
return null;
}
/**
*
* @return
* @throws SQLException
*/
public DBResultSetMetaData getMetaData() throws SQLException {
DBResultSetMetaData metaData = new DBResultSetMetaData(this);
return metaData.getMetaData();
}
/**
*
* @return
* @throws SQLException
*/
public int getRow() throws SQLException {
int product = ((ResultSet) product()).getRow();
return product;
}
/**
*
* @return
* @throws SQLException
*/
public boolean previous() throws SQLException {
boolean validrow = ((ResultSet) product()).previous();
if (_currentrow > 0) {
_currentrow--;
}
return (validrow);
}
/**
*
* @throws SQLException
*/
public void afterLast() throws SQLException {
((ResultSet) product()).afterLast();
_currentrow = DBTable.getTotalRows();
}
/**
*
* @param x
* @return
* @throws SQLException
*/
public boolean absolute(int x) throws SQLException {
boolean validrow = ((ResultSet) product()).absolute(x);
_currentrow = x - 1;
return validrow;
}
private static Object roundSmallDateTimeValue(Object value) {
if (value == null) {
return null;
}
Calendar cal;
java.sql.Timestamp ts = null;
int nanos = -1;
if (value instanceof Calendar) {
cal = (Calendar) value;
}
else {
ts = (java.sql.Timestamp) value;
cal = Calendar.getInstance();
cal.setTimeInMillis(ts.getTime());
nanos = ts.getNanos();
}
// round to the nearest minute
double seconds = cal.get(Calendar.SECOND) + (nanos == -1 ? ((double) cal.get(Calendar.MILLISECOND) / 1000) : ((double) nanos / 1000000000));
if (seconds > 29.998) {
cal.set(Calendar.MINUTE, cal.get(Calendar.MINUTE) + 1);
}
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
nanos = 0;
// required to force computation
cal.getTimeInMillis();
// return appropriate value
if (value instanceof Calendar) {
return cal;
}
else {
ts.setTime(cal.getTimeInMillis());
ts.setNanos(nanos);
return ts;
}
}
private static Object roundDatetimeValue(Object value) {
if (value == null)
return null;
Timestamp ts = value instanceof Timestamp ? (Timestamp) value : new Timestamp(((Calendar) value).getTimeInMillis());
int millis = ts.getNanos() / 1000000;
int lastDigit = (int) (millis % 10);
switch (lastDigit) {
// 0, 1 -> 0
case 1:
ts.setNanos((millis - 1) * 1000000);
break;
// 2, 3, 4 -> 3
case 2:
ts.setNanos((millis + 1) * 1000000);
break;
case 4:
ts.setNanos((millis - 1) * 1000000);
break;
// 5, 6, 7, 8 -> 7
case 5:
ts.setNanos((millis + 2) * 1000000);
break;
case 6:
ts.setNanos((millis + 1) * 1000000);
break;
case 8:
ts.setNanos((millis - 1) * 1000000);
break;
// 9 -> 0 with overflow
case 9:
ts.setNanos(0);
ts.setTime(ts.getTime() + millis + 1);
break;
// default, i.e. 0, 3, 7 -> 0, 3, 7
// don't change the millis but make sure that any
// sub-millisecond digits are zeroed out
default:
ts.setNanos((millis) * 1000000);
}
if (value instanceof Calendar) {
((Calendar) value).setTimeInMillis(ts.getTime());
((Calendar) value).getTimeInMillis();
return value;
}
return ts;
}
/**
* @param i
* @return
* @throws SQLException
*/
public int getInt(int index) throws SQLException {
return resultSet.getInt(index);
}
/**
*
* @return
*/
public DBStatement statement() {
if (parent instanceof DBStatement) {
return ((DBStatement) parent);
}
return (null);
}
}