String interposedStatement = null;
Result r, rOut;
int paramCount, lastSemi;
OdbcPreparedStatement odbcPs;
StatementPortal portal;
ResultMetaData pmd;
OdbcPacketInputStream inPacket = null;
Type[] colTypes;
PgType[] pgTypes;
try {
inPacket = OdbcPacketInputStream.newOdbcPacketInputStream(inC,
dataInput);
server.printWithThread("Got op (" + inPacket.packetType + ')');
server.printWithThread("Got packet length of "
+ inPacket.available()
+ " + type byte + 4 size header");
if (inPacket.available() >= 1000000000) {
throw new IOException("Insane packet length: "
+ inPacket.available()
+ " + type byte + 4 size header");
}
} catch (SocketException se) {
server.printWithThread("Ungraceful client exit: " + se);
throw cleanExit; // not "clean", but handled
} catch (IOException ioe) {
server.printWithThread("Fatal ODBC protocol failure: " + ioe);
try {
OdbcUtil.alertClient(OdbcUtil.ODBC_SEVERITY_FATAL,
ioe.getMessage(), "08P01", dataOutput);
// Code here means Protocol Violation
} catch (Exception e) {
// We just make an honest effort to notify the client
}
throw cleanExit; // not "clean", but handled
}
/**
* ODBC Service State Machine (the remainder of this method)
*/
switch (odbcCommMode) {
case OdbcUtil.ODBC_EXT_RECOVER_MODE :
if (inPacket.packetType != 'S') {
if (server.isTrace()) {
server.printWithThread("Ignoring a '"
+ inPacket.packetType + "'");
}
return;
}
odbcCommMode = OdbcUtil.ODBC_EXTENDED_MODE;
server.printWithThread(
"EXTENDED comm session being recovered");
// Now the main switch will handle the Sync packet carefully
// the same as if there were no recovery.
break;
case OdbcUtil.ODBC_SIMPLE_MODE :
switch (inPacket.packetType) {
case 'P' :
// This is the only way to make this switch, according
// to docs, but that does not allow for intermixing of
// static and prepared statement (selects or other).
// Therefore we allow all of the following, which works
// great.
case 'H' :
case 'S' :
case 'D' :
case 'B' :
case 'E' :
case 'C' :
odbcCommMode = OdbcUtil.ODBC_EXTENDED_MODE;
server.printWithThread(
"Switching mode from SIMPLE to EXTENDED");
// Do not detect unexpected ops here.
// In that case, leave the mode as it is, and the main
// switch below will handle appropriately.
}
break;
case OdbcUtil.ODBC_EXTENDED_MODE :
switch (inPacket.packetType) {
case 'Q' :
odbcCommMode = OdbcUtil.ODBC_SIMPLE_MODE;
server.printWithThread(
"Switching mode from EXTENDED to SIMPLE");
// Do not detect unexpected ops here.
// In that case, leave the mode as it is, and the main
// switch below will handle appropriately.
}
break;
default :
throw new RuntimeException("Unexpected ODBC comm mode value: "
+ odbcCommMode);
}
outPacket.reset();
try {
// Every switch case must either throw or break.
// For cases which break
// The packet will always be checked to make sure all bytes have
// been consumed.
// Set boolean sendReadyForQuery to send a Z/ReadyForQuery packet
// to client.
// DO NOT return early. If you need to abort, that is exceptional
// behavior and you should throw an Exception.
MAIN_ODBC_COMM_SWITCH:
switch (inPacket.packetType) {
case 'Q' : // Query packet
String sql = inPacket.readString();
// We don't ask for the null terminator
/* **********************************************
* These first few cases handle the driver's implicit handling
* of transactions. */
if (sql.startsWith("BEGIN;") || sql.equals("BEGIN")) {
/*
* We may get here because of Driver client trying to
* manage transactions implicitly; or because user/app.
* has really issued a "BEGIN" command.
* In the first case, we don't need to run the
* corresponding START TRANSACTION command, since the
* HyperSQL engine does this automatically, and can tell
* when it is needed far better than the client; however
* we do use this fact to update our Session autocommit
* state to match the client's notion.
* We ignore the latter case, because real HyperSQL
* user/apps will use "START TRANSACTION", not "BEGIN".
* Therefore, we just update autocommit state and run no
* other command against the engine.
*/
sql = sql.equals("BEGIN") ? null
: sql.substring(
"BEGIN;".length());
server.printWithThread(
"ODBC Trans started. Session AutoCommit -> F");
try {
session.setAutoCommit(false);
} catch (HsqlException he) {
throw new RecoverableOdbcFailure(
"Failed to change transaction state: "
+ he.getMessage(), he.getSQLState());
}
// Now just placate the driver
outPacket.write("BEGIN");
outPacket.xmit('C', dataOutput);
if (sql == null) {
sendReadyForQuery = true;
break;
}
}
if (sql.startsWith("SAVEPOINT ") && sql.indexOf(';') > 0) {
int firstSemi = sql.indexOf(';');
server.printWithThread(
"Interposing BEFORE primary statement: "
+ sql.substring(0, firstSemi));
odbcExecDirect(sql.substring(0, firstSemi));
sql = sql.substring(firstSemi + 1);
}
lastSemi = sql.lastIndexOf(';');
if (lastSemi > 0) {
String suffix = sql.substring(lastSemi + 1);
if (suffix.startsWith("RELEASE ")) {
interposedStatement = suffix;
sql = sql.substring(0, lastSemi);
}
}
/** ******************************************* */
String normalized = sql.trim().toLowerCase();
if (server.isTrace()) {
server.printWithThread("Received query (" + sql + ')');
}
/*
* BEWARE: We aren't supporting multiple result-sets from a
* compound statement. Plus, a general requirement is, the
* entire compound statement may return just one result set.
* I don't have time to check how it works elsewhere, but here,
* and for now, the Rowset-generating statement (SELECT, etc.)
* must be first in order for us to detect that we need to
* return a result set.
* If we do parse out the component statement here, the states
* set above apply to all executions, and only one Z packet
* should be sent at the very end.
*
* I find that the Driver can't handle compound statements
* which mix resultset + non-resultset statements (even in
* SIMPLE mode), so we are more capable than our client is.
*/
if (normalized.startsWith("select current_schema()")) {
server.printWithThread(
"Implement 'select current_schema() emulation!");
throw new RecoverableOdbcFailure(
"current_schema() not supported yet", "0A000");
}
if (normalized.startsWith("select n.nspname,")) {
// Executed by psqlodbc after every user-specified query.
server.printWithThread(
"Swallowing 'select n.nspname,...'");
outPacket.writeShort(1); // Num cols.
outPacket.write("oid");
outPacket.writeInt(201);
outPacket.writeShort(1);
outPacket.writeInt(23);
outPacket.writeShort(4);
outPacket.writeInt(-1);
outPacket.writeShort(0);
outPacket.xmit('T', dataOutput); // Xmit Row Definition
// This query returns no rows. typenam "lo"??
outPacket.write("SELECT");
outPacket.xmit('C', dataOutput);
sendReadyForQuery = true;
break;
}
if (normalized.startsWith(
"select oid, typbasetype from")) {
// Executed by psqlodbc immediately after connecting.
server.printWithThread(
"Simulating 'select oid, typbasetype...'");
/*
* This query is run as "a hack to get the oid of our
* large object oid type.
*/
outPacket.writeShort(2); // Num cols.
outPacket.write("oid"); // Col. name
outPacket.writeInt(101); // table ID
outPacket.writeShort(102); // column id
outPacket.writeInt(26); // Datatype ID [adtid]
outPacket.writeShort(4); // Datatype size [adtsize]
outPacket.writeInt(-1); // Var size [atttypmod]
outPacket.writeShort(0); // text "format code"
outPacket.write("typbasetype"); // Col. name
outPacket.writeInt(101); // table ID
outPacket.writeShort(103); // column id
outPacket.writeInt(26); // Datatype ID [adtid]
outPacket.writeShort(4); // Datatype size [adtsize]
outPacket.writeInt(-1); // Var size [atttypmod]
outPacket.writeShort(0); // text "format code"
outPacket.xmit('T', dataOutput); // sending a Tuple (row)
// This query returns no rows. typenam "lo"??
outPacket.write("SELECT");
outPacket.xmit('C', dataOutput);
sendReadyForQuery = true;
break;
}
if (normalized.startsWith("select ")) {
server.printWithThread(
"Performing a real non-prepared SELECT...");
r = Result.newExecuteDirectRequest();
r.setPrepareOrExecuteProperties(
sql, 0, 0, StatementTypes.RETURN_RESULT, 0,
ResultProperties.defaultPropsValue,
java.sql.Statement.NO_GENERATED_KEYS, null, null);
rOut = session.execute(r);
switch (rOut.getType()) {
case ResultConstants.DATA :
break;
case ResultConstants.ERROR :
throw new RecoverableOdbcFailure(rOut);
default :
throw new RecoverableOdbcFailure(
"Output Result from Query execution is of "
+ "unexpected type: " + rOut.getType());
}
// See Result.newDataHeadResult() for what we have here
// .metaData, .navigator
RowSetNavigator navigator = rOut.getNavigator();
ResultMetaData md = rOut.metaData;
if (md == null) {
throw new RecoverableOdbcFailure(
"Failed to get metadata for query results");
}
int columnCount = md.getColumnCount();
String[] colLabels = md.getGeneratedColumnNames();
colTypes = md.columnTypes;
pgTypes = new PgType[columnCount];
for (int i = 0; i < pgTypes.length; i++) {
pgTypes[i] = PgType.getPgType(colTypes[i],
md.isTableColumn(i));
}
// fredt : colLabels may not contain some column names
// colDefs is used when no label is present:
// SELECT TABLECOL AS COLLABLE has both name and label
// SELECT TABLECOL has name 'TABLECOL'
// SELECT 2 AS CONST has label 'CONST'
ColumnBase[] colDefs = md.columns;
// Num cols.
outPacket.writeShort(columnCount);
for (int i = 0; i < columnCount; i++) {
// col name
if (colLabels[i] != null) {
outPacket.write(colLabels[i]);
} else {
outPacket.write(colDefs[i].getNameString());
}
// table ID [relid]:
outPacket.writeInt(OdbcUtil.getTableOidForColumn(i,
md));
// column id [attid]
outPacket.writeShort(OdbcUtil.getIdForColumn(i,
md));
outPacket.writeInt(pgTypes[i].getOid());
// Datatype size [adtsize]
outPacket.writeShort(pgTypes[i].getTypeWidth());
outPacket.writeInt(pgTypes[i].getLPConstraint());
// Var size [atttypmod]
// This is the size constraint integer
// like VARCHAR(12) or DECIMAL(4).
// -1 if none specified for this column.
outPacket.writeShort(0);
// format code, 0 = text column, 1 = binary column,
// but entirely ignored by our driver.
// Would only be non-0 if a 'B' command requested it.
}
outPacket.xmit('T', dataOutput); // Xmit Row Definition
int rowNum = 0;
while (navigator.next()) {
rowNum++;
Object[] rowData = navigator.getCurrent();
// Row.getData(). Don't know why *Data.getCurrent()
// method returns Object instead of O[].
// TODO: Remove the assertion here:
if (rowData == null) {
throw new RecoverableOdbcFailure("Null row?");
}
if (rowData.length < columnCount) {
throw new RecoverableOdbcFailure(
"Data element mismatch. " + columnCount
+ " metadata cols, yet " + rowData.length
+ " data elements for row " + rowNum);
}
//server.printWithThread("Row " + rowNum + " has "
//+ rowData.length + " elements");
outPacket.writeShort(columnCount);
// This field is just swallowed by PG ODBC
// client, but OdbcUtil.validated by psql.
for (int i = 0; i < columnCount; i++) {
if (rowData[i] == null) {
/*
server.printWithThread("R" + rowNum + "C"
+ (i+1) + " => [null]");
*/
outPacket.writeInt(-1);
} else {
dataString =
pgTypes[i].valueString(rowData[i]);
outPacket.writeSized(dataString);
if (server.isTrace()) {
server.printWithThread(
"R" + rowNum + "C" + (i + 1)
+ " => ("
+ rowData[i].getClass().getName()
+ ") [" + dataString + ']');
}
}
}
outPacket.xmit('D', dataOutput);
}
outPacket.write("SELECT");
outPacket.xmit('C', dataOutput);
sendReadyForQuery = true;
break;
}
if (normalized.startsWith("deallocate \"")
&& normalized.charAt(normalized.length() - 1)
== '"') {
tmpStr = sql.trim().substring(
"deallocate \"".length()).trim();
// Must use "sql" directly since name is case-sensitive
handle = tmpStr.substring(0, tmpStr.length() - 1);
odbcPs = (OdbcPreparedStatement) sessionOdbcPsMap.get(
handle);
if (odbcPs != null) {
odbcPs.close();
}
portal =
(StatementPortal) sessionOdbcPortalMap.get(handle);
if (portal != null) {
portal.close();
}
if (odbcPs == null && portal == null) {
/*
throw new RecoverableOdbcFailure(null,
"No object present for handle: " + handle, "08P01");
Driver does not handle state change correctly, so
for now we just issue a warning:
OdbcUtil.alertClient(OdbcUtil.ODBC_SEVERITY_ERROR,
"No object present for handle: " + handle,
dataOutput);
TODO: Retest this. May have been side-effect of
other problems.
*/
server.printWithThread(
"Ignoring bad 'DEALLOCATE' cmd");
}
if (server.isTrace()) {
server.printWithThread("Deallocated PS/Portal '"
+ handle + "'");
}
outPacket.write("DEALLOCATE");
outPacket.xmit('C', dataOutput);
sendReadyForQuery = true;
break;
}
if (normalized.startsWith("set client_encoding to ")) {
server.printWithThread("Stubbing EXECDIR for: " + sql);
outPacket.write("SET");
outPacket.xmit('C', dataOutput);
sendReadyForQuery = true;
break;
}
// Case below is non-String-matched Qs:
server.printWithThread("Performing a real EXECDIRECT...");
odbcExecDirect(sql);
sendReadyForQuery = true;
break;
case 'X' : // Terminate packet
if (sessionOdbcPsMap.size()
> (sessionOdbcPsMap.containsKey("") ? 1
: 0)) {
server.printWithThread("Client left "
+ sessionOdbcPsMap.size()
+ " PS objects open");
}
if (sessionOdbcPortalMap.size()
> (sessionOdbcPortalMap.containsKey("") ? 1
: 0)) {
server.printWithThread("Client left "
+ sessionOdbcPortalMap.size()
+ " Portal objects open");
}
OdbcUtil.validateInputPacketSize(inPacket);
throw cleanExit;
case 'H' : // Flush packet
// No-op. It is impossible to cache while supporting multiple
// ps and portal objects, so there is nothing for a Flush to
// do. There isn't even a reply to a Flush packet.
break;
case 'S' : // Sync packet
// Special case for Sync packets.
// To facilitate recovery, we do not abort in case of problems.
if (session.isAutoCommit()) {
try {
// I don't see how this can be useful. If we ran DML, it
// will have autocommitted. If we have just switched to
// autoCommit mode, then according to spec we must have
// executed an implicit commit then.
server.printWithThread(
"Silly implicit commit by Sync");
session.commit(true);
// TODO: Find out if chain param should be T or F.
} catch (HsqlException he) {
server.printWithThread("Implicit commit failed: "
+ he);
OdbcUtil.alertClient(OdbcUtil.ODBC_SEVERITY_ERROR,
"Implicit commit failed",
he.getSQLState(), dataOutput);
}
}
sendReadyForQuery = true;
break;
case 'P' : // Parse packet
psHandle = inPacket.readString();
String query = OdbcUtil.revertMungledPreparedQuery(
inPacket.readString());
paramCount = inPacket.readUnsignedShort();
for (int i = 0; i < paramCount; i++) {
if (inPacket.readInt() != 0) {
throw new RecoverableOdbcFailure(
null,
"Parameter-type OID specifiers not supported yet",
"0A000");
}
}
if (server.isTrace()) {
server.printWithThread(
"Received Prepare request for query (" + query
+ ") with handle '" + psHandle + "'");
}
if (psHandle.length() > 0
&& sessionOdbcPsMap.containsKey(psHandle)) {
throw new RecoverableOdbcFailure(
null,
"PS handle '" + psHandle + "' already in use. "
+ "You must close it before recreating", "08P01");
}
new OdbcPreparedStatement(psHandle, query,
sessionOdbcPsMap, session);
outPacket.xmit('1', dataOutput);
break;
case 'D' : // Describe packet
c = inPacket.readByteChar();
handle = inPacket.readString();
odbcPs = null;
portal = null;
if (c == 'S') {
odbcPs = (OdbcPreparedStatement) sessionOdbcPsMap.get(
handle);
} else if (c == 'P') {
portal =
(StatementPortal) sessionOdbcPortalMap.get(handle);
} else {
throw new RecoverableOdbcFailure(
null,
"Description packet request type invalid: " + c,
"08P01");
}
if (server.isTrace()) {
server.printWithThread("Received Describe request for "
+ c + " of handle '" + handle
+ "'");
}
if (odbcPs == null && portal == null) {
throw new RecoverableOdbcFailure(
null,
"No object present for " + c + " handle: "
+ handle, "08P01");
}
Result ackResult = (odbcPs == null) ? portal.ackResult
: odbcPs.ackResult;
pmd = ackResult.parameterMetaData;
paramCount = pmd.getColumnCount();
Type[] paramTypes = pmd.getParameterTypes();
if (paramCount != paramTypes.length) {
throw new RecoverableOdbcFailure(
"Parameter count mismatch. Count of "
+ paramCount + " reported, but there are "
+ paramTypes.length + " param md objects");
}
if (c == 'S') {
outPacket.writeShort(paramCount);
for (int i = 0; i < paramTypes.length; i++) {
outPacket.writeInt(
PgType.getPgType(
paramTypes[i], true).getOid());
// TODO: Determine whether parameter typing works
// better for Strings when try to match table column
// or not. 2nd param to getPgType().
}
outPacket.xmit('t', dataOutput);
// ParameterDescription packet
}
ResultMetaData md = ackResult.metaData;
if (md.getColumnCount() < 1) {
if (server.isTrace()) {
server.printWithThread(
"Non-rowset query so returning NoData packet");
}
// Send NoData packet because no columnar output from
// this statement.
outPacket.xmit('n', dataOutput);
break;
}
// TODO:
// May need to pass the extra BIGINT pseudo-column for
// updatable-row or other purposes. In that case, it may
// make sense to use getExtendedColumnCount(), etc.
String[] colNames = md.getGeneratedColumnNames();
if (md.getColumnCount() != colNames.length) {
throw new RecoverableOdbcFailure(
"Couldn't get all column names: "
+ md.getColumnCount() + " cols. but only got "
+ colNames.length + " col. names");
}
colTypes = md.columnTypes;
pgTypes = new PgType[colNames.length];
ColumnBase[] colDefs = md.columns;
for (int i = 0; i < pgTypes.length; i++) {
pgTypes[i] = PgType.getPgType(colTypes[i],
md.isTableColumn(i));
}
if (colNames.length != colDefs.length) {
throw new RecoverableOdbcFailure(
"Col data mismatch. " + colDefs.length