Modified:
MYSQLPlus/MYSQLPlusLib/MDescriptor.cpp
MYSQLPlus/MYSQLPlusLib/MDiagnostic.cpp
MYSQLPlus/MYSQLPlusLib/MResult.cpp
MYSQLPlus/MYSQLPlusLib/MResult.h
MYSQLPlus/MYSQLPlusLib/MResultPlus.cpp
MYSQLPlus/MYSQLPlusLib/MResultRes.cpp
MYSQLPlus/MYSQLPlusLib/MResultStmt.cpp
MYSQLPlus/MYSQLPlusLib/MStatement.cpp
Log:
Modified: MYSQLPlus/MYSQLPlusLib/MDescriptor.cpp
===================================================================
--- MYSQLPlus/MYSQLPlusLib/MDescriptor.cpp 2006-05-26 18:25:45 UTC (rev 273)
+++ MYSQLPlus/MYSQLPlusLib/MDescriptor.cpp 2006-05-26 23:21:48 UTC (rev 274)
@@ -25,7 +25,13 @@
pConnection->getDiagnostic()->doClear();
- /* we only want MDescriptorRecord* children so create MDiagnostic without it being a
child of this */
+ /*!
+ \internal
+ \note
+
+ We only want MDescriptorRecord* children so create MDiagnostic without it being a
child of this. We
+ do this to help with performance.
+ */
pDiagnostic = new MDiagnostic();
nAllocType = SQL_DESC_ALLOC_USER;
doInit();
Modified: MYSQLPlus/MYSQLPlusLib/MDiagnostic.cpp
===================================================================
--- MYSQLPlus/MYSQLPlusLib/MDiagnostic.cpp 2006-05-26 18:25:45 UTC (rev 273)
+++ MYSQLPlus/MYSQLPlusLib/MDiagnostic.cpp 2006-05-26 23:21:48 UTC (rev 274)
@@ -294,6 +294,17 @@
switch ( nDiagIdentifier )
{
case SQL_DIAG_CURSOR_ROW_COUNT:
+ /*!
+ \internal ODBC RULE
+
+ An application can call SQLGetDiagField to return any diagnostic field at
any time, with the exception of SQL_DIAG_CURSOR_ROW_COUNT or
+ SQL_DIAG_ROW_COUNT, which will return SQL_ERROR if Handle is not a
statement handle. If any other diagnostic field is undefined, the
+ call to SQLGetDiagField will return SQL_SUCCESS (provided no other
diagnostic is encountered) and an undefined value is returned for
+ the field.
+ */
+ if ( parent() && parent()->className() != "MStatement" )
+ MYODBCDbgReturn( SQL_ERROR );
+
*(SQLINTEGER*)pDiagInfoPtr = getCursorRowCount();
break;
@@ -322,8 +333,20 @@
break;
case SQL_DIAG_ROW_COUNT:
+ /*!
+ \internal ODBC RULE
+
+ An application can call SQLGetDiagField to return any diagnostic field at
any time, with the exception of SQL_DIAG_CURSOR_ROW_COUNT or
+ SQL_DIAG_ROW_COUNT, which will return SQL_ERROR if Handle is not a
statement handle. If any other diagnostic field is undefined, the
+ call to SQLGetDiagField will return SQL_SUCCESS (provided no other
diagnostic is encountered) and an undefined value is returned for
+ the field.
+ */
+ if ( parent() && parent()->className() != "MStatement" )
+ MYODBCDbgReturn( SQL_ERROR );
+
*(SQLINTEGER*)pDiagInfoPtr = getRowCount();
break;
+
default:
if ( nRecNumber < 1 )
MYODBCDbgReturn( SQL_ERROR );
@@ -687,7 +710,7 @@
because the SQL_DIAG_ROW_COUNT field is reset to 0 by any function
call.
- see SQLRowCount
+ \sa SQLRowCount
*/
nRowCount = 0;
Modified: MYSQLPlus/MYSQLPlusLib/MResult.cpp
===================================================================
--- MYSQLPlus/MYSQLPlusLib/MResult.cpp 2006-05-26 18:25:45 UTC (rev 273)
+++ MYSQLPlus/MYSQLPlusLib/MResult.cpp 2006-05-26 23:21:48 UTC (rev 274)
@@ -94,8 +94,10 @@
Q_ASSERT( !pStatement );
- nState = STATE_UNINITIALIZED;
- bBuffered = true;
+ nState = STATE_UNINITIALIZED;
+ bBuffered = true;
+ nRowsAffected = 0;
+ nStatementType = STATEMENT_TYPE_NULL;
pStatement->getImpParamDesc()->doClear();
pStatement->getImpRowDesc()->doClear();
@@ -324,6 +326,13 @@
MYODBCDbgReturn( getDiagnostic()->doAppend( MDiagnostic::DIA_HY000 ) );
}
+qulonglong MResult::getRowsAffected()
+{
+ MYODBCDbgEnter();
+
+ MYODBCDbgReturn3( "%d", nRowsAffected );
+}
+
BOOLEAN MResult::isBuffered()
{
MYODBCDbgEnter();
@@ -427,6 +436,13 @@
MYODBCDbgReturn3( "%p", getStatement()->getImpRowDesc() );
}
+STATEMENT_TYPE MResult::getStatementType()
+{
+ MYODBCDbgEnter();
+
+ MYODBCDbgReturn3( "%d", nStatementType );
+}
+
/*!
\brief Gets data based upon the type information in the data source.
@@ -628,6 +644,48 @@
MYODBCDbgReturn( SQL_SUCCESS );
}
+SQLRETURN MResult::setRowsAffected( qulonglong nRowsAffected )
+{
+ MYODBCDbgEnter();
+
+ this->nRowsAffected = nRowsAffected;
+
+ MYODBCDbgReturn( SQL_SUCCESS );
+}
+
+SQLRETURN MResult::setStatementType( STATEMENT_TYPE nStatementType )
+{
+ MYODBCDbgEnter();
+
+ this->nStatementType = nStatementType;
+
+ MYODBCDbgReturn( SQL_SUCCESS );
+}
+
+SQLRETURN MResult::setStatementType( const QString &stringStatement )
+{
+ MYODBCDbgEnter();
+
+ /*!
+ \internal
+ \todo
+
+ Make this smarter.
+ */
+ if ( stringStatement.startsWith( "SELECT ", Qt::CaseInsensitive ) )
+ MYODBCDbgReturn( setStatementType( STATEMENT_TYPE_SELECT ) );
+ else if ( stringStatement.startsWith( "SHOW ", Qt::CaseInsensitive ) )
+ MYODBCDbgReturn( setStatementType( STATEMENT_TYPE_SHOW ) );
+ else if ( stringStatement.startsWith( "UPDATE ", Qt::CaseInsensitive ) )
+ MYODBCDbgReturn( setStatementType( STATEMENT_TYPE_UPDATE ) );
+ else if ( stringStatement.startsWith( "DELETE ", Qt::CaseInsensitive ) )
+ MYODBCDbgReturn( setStatementType( STATEMENT_TYPE_DELETE ) );
+ else if ( stringStatement.startsWith( "INSERT ", Qt::CaseInsensitive ) )
+ MYODBCDbgReturn( setStatementType( STATEMENT_TYPE_INSERT ) );
+
+ MYODBCDbgReturn( setStatementType( STATEMENT_TYPE_OTHER ) );
+}
+
/*!
\brief Get character column data from current row.
Modified: MYSQLPlus/MYSQLPlusLib/MResult.h
===================================================================
--- MYSQLPlus/MYSQLPlusLib/MResult.h 2006-05-26 18:25:45 UTC (rev 273)
+++ MYSQLPlus/MYSQLPlusLib/MResult.h 2006-05-26 23:21:48 UTC (rev 274)
@@ -70,6 +70,26 @@
STATE_EXECUTED
};
+ /*!
+ \internal
+ \brief The basic statement type.
+
+ This information is gleaned from the first bit of the statement during a
doPrepare(). It
+ is useful to keep such information - look for usage in the source to see why.
+ */
+ enum STATMENT_TYPE
+ {
+ STATEMENT_TYPE_NULL, /*!< we do not have a statement
*/
+ STATEMENT_TYPE_MULTI, /*!< multiple statements, possibly a mix of types,
has been prepared */
+ STATEMENT_TYPE_SELECT, /*!< a single SELECT statement has been prepared
*/
+ STATEMENT_TYPE_SHOW, /*!< a single SHOW statement has been prepared
*/
+ STATEMENT_TYPE_UPDATE, /*!< a single UPDATE statement has been prepared
*/
+ STATEMENT_TYPE_DELETE, /*!< a single DELETE statement has been prepared
*/
+ STATEMENT_TYPE_INSERT, /*!< a single INSERT statement has been prepared
*/
+ STATEMENT_TYPE_PLUS, /*!< resultset produced for a catalog function or
other MResultPlus func*/
+ STATEMENT_TYPE_OTHER /*!< unhandled statement type (treat like
STATEMENT_TYPE_MULTI) */
+ };
+
MResult( MStatement *pStatement );
virtual ~MResult();
@@ -80,11 +100,13 @@
/* getters */
STATE getState();
- virtual SQLRETURN getColumns( uint *pnColumns ) = 0;
- virtual SQLRETURN getData( SQLUSMALLINT nColumn, QVariant &variantData ) = 0;
- virtual SQLRETURN getData( SQLUSMALLINT nColumn, SQLSMALLINT nTargetType, SQLPOINTER
pTargetValue, SQLINTEGER nBufferLength, SQLINTEGER *pnLength, SQLINTEGER *pnIndicator );
- virtual SQLRETURN getRow( qulonglong *pnRow ) = 0;
- virtual SQLRETURN getRows( qulonglong *pnRows ) = 0;
+ virtual SQLRETURN getColumns( uint *pnColumns ) = 0;
+ virtual SQLRETURN getData( SQLUSMALLINT nColumn, QVariant &variantData ) = 0;
+ virtual SQLRETURN getData( SQLUSMALLINT nColumn, SQLSMALLINT nTargetType,
SQLPOINTER pTargetValue, SQLINTEGER nBufferLength, SQLINTEGER *pnLength, SQLINTEGER
*pnIndicator );
+ virtual SQLRETURN getRow( qulonglong *pnRow ) = 0;
+ virtual SQLRETURN getRows( qulonglong *pnRows ) = 0;
+ qulonglong getRowsAffected();
+ STATEMENT_TYPE getStatementType();
/* doers */
virtual SQLRETURN doAppend() = 0;
@@ -127,6 +149,9 @@
/* support for getData: this is called when target C type is SQL_C_DEFAULT (derive
type from IRD) */
SQLRETURN setGetDataDefault();
+ SQLRETURN setRowsAffected( qulonglong nRowsAffected );
+ SQLRETURN setStatementType( STATEMENT_TYPE nStatementType );
+ SQLRETURN setStatementType( const QString &stringStatement );
/* support for getData: these enforce ODBC rules generalized based upon SQL type (ie
see "SQL to C: Character" in odbc spec) */
SQLRETURN fromCharacterSQL();
@@ -163,8 +188,10 @@
virtual SQLRETURN doStateRollBack( STATE nState ) = 0;
private:
- STATE nState; /*!< our state
*/
- BOOLEAN bBuffered; /*!< true causes entire resultset to get pulled to client
at execute (enabling other features) (default=true) */
+ STATE nState; /*!< our state
*/
+ BOOLEAN bBuffered; /*!< true causes entire resultset to get pulled to
client at execute (enabling other features) (default=true) */
+ qulonglong nRowsAffected; /*!< number of rows affected by a non-SELECT
statement (catalog and SHOW statements count as SELECT in this case) */
+ STATMENT_TYPE nStatementType;
};
#endif
Modified: MYSQLPlus/MYSQLPlusLib/MResultPlus.cpp
===================================================================
--- MYSQLPlus/MYSQLPlusLib/MResultPlus.cpp 2006-05-26 18:25:45 UTC (rev 273)
+++ MYSQLPlus/MYSQLPlusLib/MResultPlus.cpp 2006-05-26 23:21:48 UTC (rev 274)
@@ -5,7 +5,8 @@
{
MYODBCDbgEnter();
- nRow = 0;
+ nRow = 0;
+ nStatementType = STATEMENT_TYPE_PLUS;
setState( STATE_INITIALIZED );
MYODBCDbgReturn2();
@@ -479,6 +480,27 @@
MYODBCDbgReturn( getDiagnostic()->doAppend( MDiagnostic::DIA_HY004, 0,
NULL ) );
}
+ /*!
+ \internal ODBC RULE
+
+ When SQLExecute, SQLExecDirect, SQLBulkOperations, SQLSetPos, or SQLMoreResults
is called, the SQL_DIAG_ROW_COUNT
+ field of the diagnostic data structure is set to the row count, and the row count
is cached in an implementation-dependent
+ way. SQLRowCount returns the cached row count value. The cached row count value
is valid until the statement handle is set
+ back to the prepared or allocated state, the statement is reexecuted, or
SQLCloseCursor is called. Note that if a function
+ has been called since the SQL_DIAG_ROW_COUNT field was set, the value returned by
SQLRowCount might be different from the
+ value in the SQL_DIAG_ROW_COUNT field because the SQL_DIAG_ROW_COUNT field is
reset to 0 by any function call.
+
+ \internal MYODBC RULE
+
+ Wet set SQL_DIAG_ROW_COUNT when any result set is requested - for example we also
set SQL_DIAG_ROW_COUNT for catalog
+ functions.
+ */
+ qulonglong nRows = -1;
+ if ( !SQL_SUCCEEDED( getRows( &nRows ) ) )
+ getDiagnostic()->setRowCount( -1 );
+ else
+ getDiagnostic()->setRowCount( nRows );
+
/* we should be at last record so next will make us eof and a subsequent next will be
first record */
setState( STATE_EXECUTED );
doNext();
@@ -538,6 +560,7 @@
case STATE_EXECUTED:
listResults.clear();
nRow = 0;
+ nRowsAffected = 0;
getImpParamDesc()->doClear();
getImpRowDesc()->doClear();
resultGetData.doClear();
Modified: MYSQLPlus/MYSQLPlusLib/MResultRes.cpp
===================================================================
--- MYSQLPlus/MYSQLPlusLib/MResultRes.cpp 2006-05-26 18:25:45 UTC (rev 273)
+++ MYSQLPlus/MYSQLPlusLib/MResultRes.cpp 2006-05-26 23:21:48 UTC (rev 274)
@@ -439,6 +439,30 @@
MYODBCDbgReturn( nReturn );
}
+ /*!
+ \internal ODBC RULE
+
+ When SQLExecute, SQLExecDirect, SQLBulkOperations, SQLSetPos, or SQLMoreResults
is called, the SQL_DIAG_ROW_COUNT
+ field of the diagnostic data structure is set to the row count, and the row count
is cached in an implementation-dependent
+ way. SQLRowCount returns the cached row count value. The cached row count value
is valid until the statement handle is set
+ back to the prepared or allocated state, the statement is reexecuted, or
SQLCloseCursor is called. Note that if a function
+ has been called since the SQL_DIAG_ROW_COUNT field was set, the value returned by
SQLRowCount might be different from the
+ value in the SQL_DIAG_ROW_COUNT field because the SQL_DIAG_ROW_COUNT field is
reset to 0 by any function call.
+
+ \internal MYODBC RULE
+
+ Wet set SQL_DIAG_ROW_COUNT when any result set is requested - for example we also
set SQL_DIAG_ROW_COUNT for catalog
+ functions.
+ */
+ my_ulonglong nAffectedRow = mysql_affected_rows( getMySQL() );
+
+
+ qulonglong nRows = -1;
+ if ( !SQL_SUCCEEDED( getRows( &nRows ) ) )
+ getDiagnostic()->setRowCount( -1 );
+ else
+ getDiagnostic()->setRowCount( nRows );
+
setState( STATE_EXECUTED );
MYODBCDbgReturn( SQL_SUCCESS );
@@ -638,6 +662,7 @@
stringStatement = QString::fromUtf16( psStatement );
bytearrayStatementTemplate = stringStatement.toUtf8().data();
+ setStatementType( stringStatement );
setState( STATE_PREPARED );
MYODBCDbgReturn( SQL_SUCCESS );
@@ -878,6 +903,7 @@
getImpRowDesc()->doClear();
stringStatement = QString::null;
bytearrayStatementTemplate.clear();
+ nStatementType = STATEMENT_TYPE_NULL;
setState(STATE_INITIALIZED );
break;
@@ -885,6 +911,7 @@
mysql_free_result( pRes );
pRes = NULL;
nRow = 0;
+ nRowsAffected = 0;
resultGetData.doClear();
setState( STATE_PREPARED );
break;
Modified: MYSQLPlus/MYSQLPlusLib/MResultStmt.cpp
===================================================================
--- MYSQLPlus/MYSQLPlusLib/MResultStmt.cpp 2006-05-26 18:25:45 UTC (rev 273)
+++ MYSQLPlus/MYSQLPlusLib/MResultStmt.cpp 2006-05-26 23:21:48 UTC (rev 274)
@@ -502,6 +502,28 @@
nRow = 0; /* eof/bof */
+ /*!
+ \internal ODBC RULE
+
+ When SQLExecute, SQLExecDirect, SQLBulkOperations, SQLSetPos, or SQLMoreResults
is called, the SQL_DIAG_ROW_COUNT
+ field of the diagnostic data structure is set to the row count, and the row count
is cached in an implementation-dependent
+ way. SQLRowCount returns the cached row count value. The cached row count value
is valid until the statement handle is set
+ back to the prepared or allocated state, the statement is reexecuted, or
SQLCloseCursor is called. Note that if a function
+ has been called since the SQL_DIAG_ROW_COUNT field was set, the value returned by
SQLRowCount might be different from the
+ value in the SQL_DIAG_ROW_COUNT field because the SQL_DIAG_ROW_COUNT field is
reset to 0 by any function call.
+
+ \internal MYODBC RULE
+
+ Wet set SQL_DIAG_ROW_COUNT when any result set is requested - for example we also
set SQL_DIAG_ROW_COUNT for catalog
+ functions.
+ */
+ mysql_affected_rows(MYSQL *mysql)
+ qulonglong nRows = -1;
+ if ( !SQL_SUCCEEDED( getRows( &nRows ) ) )
+ getDiagnostic()->setRowCount( -1 );
+ else
+ getDiagnostic()->setRowCount( nRows );
+
setState( STATE_EXECUTED );
MYODBCDbgReturn( SQL_SUCCESS );
@@ -726,6 +748,7 @@
if ( mysql_stmt_prepare( pstm, stringStatement.toUtf8().data(),
stringStatement.length() ) )
MYODBCDbgReturn( getDiagnostic()->doAppend( MDiagnostic::DIA_HY000,
mysql_stmt_errno( pstm ), mysql_stmt_error( pstm ) ) );
+ setStatementType( stringStatement );
setState( STATE_PREPARED );
/* get result-set meta-data */
@@ -1005,13 +1028,15 @@
getImpParamDesc()->doClear();
getImpRowDesc()->doClear();
}
- setState(STATE_INITIALIZED );
+ nStatementType = STATEMENT_TYPE_NULL;
+ setState( STATE_INITIALIZED );
break;
case STATE_EXECUTED:
if ( !mysql_stmt_reset( pstm ) )
MYODBCDbgReturn( getDiagnostic()->doAppend( MDiagnostic::DIA_HY000,
mysql_stmt_errno( pstm ), mysql_stmt_error( pstm ) ) );
nRow = 0;
+ nRowsAffected = 0;
resultGetData.doClear();
setState( STATE_PREPARED );
break;
Modified: MYSQLPlus/MYSQLPlusLib/MStatement.cpp
===================================================================
--- MYSQLPlus/MYSQLPlusLib/MStatement.cpp 2006-05-26 18:25:45 UTC (rev 273)
+++ MYSQLPlus/MYSQLPlusLib/MStatement.cpp 2006-05-26 23:21:48 UTC (rev 274)
@@ -1951,7 +1951,23 @@
pResult = NULL;
}
- setState( STATE_S3 );
+ switch ( pResult->getStatementType() )
+ {
+ case STATEMENT_TYPE_UPDATE:
+ case STATEMENT_TYPE_DELETE:
+ case STATEMENT_TYPE_INSERT:
+ setState( STATE_S2 );
+ break;
+ case STATEMENT_TYPE_NULL:
+ pDiagnostic->doAppend( MDiagnostic::DIA_01000, 0, tr("STATEMENT_TYPE_NULL
should not happen here") );
+ case STATEMENT_TYPE_MULTI:
+ case STATEMENT_TYPE_SELECT:
+ case STATEMENT_TYPE_SHOW:
+ case STATEMENT_TYPE_PLUS:
+ case STATEMENT_TYPE_OTHER:
+ setState( STATE_S3 );
+ break;
+ }
MYODBCDbgReturn( nReturn );
}
@@ -2012,7 +2028,7 @@
MYODBCDbgReturn( SQL_ERROR );
}
-SQLRETURN MStatement::doRowCount( SQLINTEGER *pnRowCountPtr )
+SQLRETURN MStatement::doRowCount( SQLINTEGER *pnRowCount )
{
MYODBCDbgEnter();
@@ -2020,10 +2036,53 @@
\internal ODBC RULE
We clear diagnostic each time an ODBC API call is made (with exceptions).
+
+ \note
+
+ We can do this with this func because we are not going to get value from
+ diagnostic header but rather from our cached, internal, value.
*/
pDiagnostic->doClear();
- MYODBCDbgReturn( SQL_ERROR );
+ if ( !pnRowCount )
+ MYODBCDbgReturn( getDiagnotic()->doAppend( MDiagnostic::DIA_HY000, 0,
tr("pnRowCount must not be null") ) );
+
+ /*!
+ \internal ODBC RULE
+
+ If the last SQL statement executed on the statement handle was not an UPDATE,
INSERT,
+ or DELETE statement or if the Operation argument in the previous call to
SQLBulkOperations
+ was not SQL_ADD, SQL_UPDATE_BY_BOOKMARK, or SQL_DELETE_BY_BOOKMARK, or if the
Operation
+ argument in the previous call to SQLSetPos was not SQL_UPDATE or SQL_DELETE, the
value of
+ *RowCountPtr is driver-defined.
+
+ \internal MYODBC RULE
+
+ We return the number of resultset rows for SELECT type statements - but this will
be -1
+ when we can not do this at this time (unbuffered results for example).
+ */
+ switch ( pResult->getStatementType() )
+ {
+ case STATEMENT_TYPE_UPDATE:
+ case STATEMENT_TYPE_DELETE:
+ case STATEMENT_TYPE_INSERT:
+ *pnRowCount = getRowsAffected();
+ MYODBCDbgReturn( SQL_SUCCESS );
+ case STATEMENT_TYPE_NULL:
+ pDiagnostic->doAppend( MDiagnostic::DIA_01000, 0, tr("STATEMENT_TYPE_NULL
should not happen here") );
+ case STATEMENT_TYPE_MULTI:
+ case STATEMENT_TYPE_SELECT:
+ case STATEMENT_TYPE_SHOW:
+ case STATEMENT_TYPE_PLUS:
+ case STATEMENT_TYPE_OTHER:
+ if ( pResult->getRows( pnRowCount ) == SQL_SUCCESS )
+ MYODBCDbgReturn( SQL_SUCCESS );
+ break;
+ }
+
+ *pnRowCount = -1;
+
+ MYODBCDbgReturn( SQL_SUCCESS_WITH_INFO );
}
SQLRETURN MStatement::doSpecialColumns( SQLSMALLINT nIdentifierType, SQLWCHAR
*psCatalogName, SQLSMALLINT nNameLength1, SQLWCHAR *psSchemaName, SQLSMALLINT
nNameLength2, SQLWCHAR *psTableName, SQLSMALLINT nNameLength3, SQLSMALLINT nScope,
SQLSMALLINT nNullable )
@@ -2999,7 +3058,23 @@
case STATE_S11:
case STATE_S12:
pResult->doStateRollBack( MResult::STATE_PREPARED );
- setState( STATE_S2 );
+ switch ( pResult->getStatementType() )
+ {
+ case STATEMENT_TYPE_UPDATE:
+ case STATEMENT_TYPE_DELETE:
+ case STATEMENT_TYPE_INSERT:
+ setState( STATE_S2 );
+ break;
+ case STATEMENT_TYPE_NULL:
+ pDiagnostic->doAppend( MDiagnostic::DIA_01000, 0,
tr("STATEMENT_TYPE_NULL should not happen here") );
+ case STATEMENT_TYPE_MULTI:
+ case STATEMENT_TYPE_SELECT:
+ case STATEMENT_TYPE_SHOW:
+ case STATEMENT_TYPE_PLUS:
+ case STATEMENT_TYPE_OTHER:
+ setState( STATE_S3 );
+ break;
+ }
break;
default:
| Thread |
|---|
| • Connector/ODBC 5 commit: r274 - MYSQLPlus/MYSQLPlusLib | pharvey | 27 May |