diff options
Diffstat (limited to 'tqtinterface/qt4/src/sql/drivers/mysql/tqsql_mysql.cpp')
-rw-r--r-- | tqtinterface/qt4/src/sql/drivers/mysql/tqsql_mysql.cpp | 775 |
1 files changed, 775 insertions, 0 deletions
diff --git a/tqtinterface/qt4/src/sql/drivers/mysql/tqsql_mysql.cpp b/tqtinterface/qt4/src/sql/drivers/mysql/tqsql_mysql.cpp new file mode 100644 index 0000000..f97cd2e --- /dev/null +++ b/tqtinterface/qt4/src/sql/drivers/mysql/tqsql_mysql.cpp @@ -0,0 +1,775 @@ +/**************************************************************************** +** +** Implementation of MYSQL driver classes +** +** Created : 001103 +** +** Copyright (C) 2010 Timothy Pearson and (C) 1992-2008 Trolltech ASA. +** +** This file is part of the sql module of the TQt GUI Toolkit. +** +** This file may be used under the terms of the GNU General +** Public License versions 2.0 or 3.0 as published by the Free +** Software Foundation and appearing in the files LICENSE.GPL2 +** and LICENSE.GPL3 included in the packaging of this file. +** Alternatively you may (at your option) use any later version +** of the GNU General Public License if such license has been +** publicly approved by Trolltech ASA (or its successors, if any) +** and the KDE Free TQt Foundation. +** +** Please review the following information to ensure GNU General +** Public Licensing requirements will be met: +** http://trolltech.com/products/qt/licenses/licensing/opensource/. +** If you are unsure which license is appropriate for your use, please +** review the following information: +** http://trolltech.com/products/qt/licenses/licensing/licensingoverview +** or contact the sales department at sales@trolltech.com. +** +** This file may be used under the terms of the Q Public License as +** defined by Trolltech ASA and appearing in the file LICENSE.TQPL +** included in the packaging of this file. Licensees holding valid TQt +** Commercial licenses may use this file in accordance with the TQt +** Commercial License Agreement provided with the Software. +** +** This file is provided "AS IS" with NO WARRANTY OF ANY KIND, +** INCLUDING THE WARRANTIES OF DESIGN, MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE. Trolltech reserves all rights not granted +** herein. +** +**********************************************************************/ + +#include "tqsql_mysql.h" +#include <private/tqsqlextension_p.h> + +#include <tqdatetime.h> +#include <tqvaluevector.h> +#include <tqsqlrecord.h> + +#define TQMYSTQL_DRIVER_NAME "TQMYSQL3" + +#ifdef TQ_OS_WIN32 +// comment the next line out if you want to use MySQL/embedded on Win32 systems. +// note that it will crash if you don't statically link to the mysql/e library! +# define TQ_NO_MYSTQL_EMBEDDED +#endif + +TQPtrDict<TQSqlOpenExtension> *qSqlOpenExtDict(); + +static int qMySqlConnectionCount = 0; +static bool qMySqlInitHandledByUser = FALSE; + +class TQMYSTQLOpenExtension : public TQSqlOpenExtension +{ +public: + TQMYSTQLOpenExtension( TQMYSTQLDriver *dri ) + : TQSqlOpenExtension(), driver(dri) {} + ~TQMYSTQLOpenExtension() {} + + bool open( const TQString& db, + const TQString& user, + const TQString& password, + const TQString& host, + int port, + const TQString& connOpts ); + +private: + TQMYSTQLDriver *driver; +}; + +bool TQMYSTQLOpenExtension::open( const TQString& db, + const TQString& user, + const TQString& password, + const TQString& host, + int port, + const TQString& connOpts ) +{ + return driver->open( db, user, password, host, port, connOpts ); +} + +class TQMYSTQLDriverPrivate +{ +public: + TQMYSTQLDriverPrivate() : mysql(0) {} + MYSQL* mysql; +}; + +class TQMYSTQLResultPrivate : public TQMYSTQLDriverPrivate +{ +public: + TQMYSTQLResultPrivate() : TQMYSTQLDriverPrivate(), result(0) {} + MYSTQL_RES* result; + MYSTQL_ROW row; + TQValueVector<TQVariant::Type> fieldTypes; +}; + +TQSqlError qMakeError( const TQString& err, int type, const TQMYSTQLDriverPrivate* p ) +{ + return TQSqlError(TQMYSTQL_DRIVER_NAME ": " + err, TQString(mysql_error( p->mysql )), type, mysql_errno( p->mysql )); +} + +TQVariant::Type qDecodeMYSTQLType( int mysqltype, uint flags ) +{ + TQVariant::Type type; + switch ( mysqltype ) { + case FIELD_TYPE_TINY : + case FIELD_TYPE_SHORT : + case FIELD_TYPE_LONG : + case FIELD_TYPE_INT24 : + type = (flags & UNSIGNED_FLAG) ? TQVariant::UInt : TQVariant::Int; + break; + case FIELD_TYPE_YEAR : + type = TQVariant::Int; + break; + case FIELD_TYPE_LONGLONG : + type = (flags & UNSIGNED_FLAG) ? TQVariant::ULongLong : TQVariant::LongLong; + break; + case FIELD_TYPE_DECIMAL : + case FIELD_TYPE_FLOAT : + case FIELD_TYPE_DOUBLE : + type = TQVariant::Double; + break; + case FIELD_TYPE_DATE : + type = TQVariant::Date; + break; + case FIELD_TYPE_TIME : + type = TQVariant::Time; + break; + case FIELD_TYPE_DATETIME : + case FIELD_TYPE_TIMESTAMP : + type = TQVariant::DateTime; + break; + case FIELD_TYPE_BLOB : + case FIELD_TYPE_TINY_BLOB : + case FIELD_TYPE_MEDIUM_BLOB : + case FIELD_TYPE_LONG_BLOB : + type = (flags & BINARY_FLAG) ? TQVariant::ByteArray : TQVariant::CString; + break; + default: + case FIELD_TYPE_ENUM : + case FIELD_TYPE_SET : + case FIELD_TYPE_STRING : + case FIELD_TYPE_VAR_STRING : + type = TQVariant::String; + break; + } + return type; +} + +TQMYSTQLResult::TQMYSTQLResult( const TQMYSTQLDriver* db ) +: TQSqlResult( db ) +{ + d = new TQMYSTQLResultPrivate(); + d->mysql = db->d->mysql; +} + +TQMYSTQLResult::~TQMYSTQLResult() +{ + cleanup(); + delete d; +} + +MYSTQL_RES* TQMYSTQLResult::result() +{ + return d->result; +} + +void TQMYSTQLResult::cleanup() +{ + if ( d->result ) { + mysql_free_result( d->result ); + } + d->result = NULL; + d->row = NULL; + setAt( -1 ); + setActive( FALSE ); +} + +bool TQMYSTQLResult::fetch( int i ) +{ + if ( isForwardOnly() ) { // fake a forward seek + if ( at() < i ) { + int x = i - at(); + while ( --x && fetchNext() ); + return fetchNext(); + } else { + return FALSE; + } + } + if ( at() == i ) + return TRUE; + mysql_data_seek( d->result, i ); + d->row = mysql_fetch_row( d->result ); + if ( !d->row ) + return FALSE; + setAt( i ); + return TRUE; +} + +bool TQMYSTQLResult::fetchNext() +{ + d->row = mysql_fetch_row( d->result ); + if ( !d->row ) + return FALSE; + setAt( at() + 1 ); + return TRUE; +} + +bool TQMYSTQLResult::fetchLast() +{ + if ( isForwardOnly() ) { // fake this since MySQL can't seek on forward only queries + bool success = fetchNext(); // did we move at all? + while ( fetchNext() ); + return success; + } + my_ulonglong numRows = mysql_num_rows( d->result ); + if ( !numRows ) + return FALSE; + return fetch( numRows - 1 ); +} + +bool TQMYSTQLResult::fetchFirst() +{ + if ( isForwardOnly() ) // again, fake it + return fetchNext(); + return fetch( 0 ); +} + +TQVariant TQMYSTQLResult::data( int field ) +{ + if ( !isSelect() || field >= (int) d->fieldTypes.count() ) { + qWarning( "TQMYSTQLResult::data: column %d out of range", field ); + return TQVariant(); + } + + TQString val( d->row[field] ); + switch ( d->fieldTypes.at( field ) ) { + case TQVariant::LongLong: + return TQVariant( val.toLongLong() ); + case TQVariant::ULongLong: + return TQVariant( val.toULongLong() ); + case TQVariant::Int: + return TQVariant( val.toInt() ); + case TQVariant::UInt: + return TQVariant( val.toUInt() ); + case TQVariant::Double: + return TQVariant( val.toDouble() ); + case TQVariant::Date: + if ( val.isEmpty() ) { + return TQVariant( TQDate() ); + } else { + return TQVariant( TQDate::fromString( val, TQt::ISODate ) ); + } + case TQVariant::Time: + if ( val.isEmpty() ) { + return TQVariant( TQTime() ); + } else { + return TQVariant( TQTime::fromString( val, TQt::ISODate ) ); + } + case TQVariant::DateTime: + if ( val.isEmpty() ) + return TQVariant( TQDateTime() ); + if ( val.length() == 14u ) + // TIMESTAMPS have the format yyyyMMddhhmmss + val.insert(4, "-").insert(7, "-").insert(10, 'T').insert(13, ':').insert(16, ':'); + return TQVariant( TQDateTime::fromString( val, TQt::ISODate ) ); + case TQVariant::ByteArray: { + unsigned long* fl = mysql_fetch_lengths( d->result ); + TQByteArray ba; + ba.duplicate( d->row[field], fl[field] ); + return TQVariant( ba ); + } + default: + case TQVariant::String: + case TQVariant::CString: + return TQVariant( val ); + } +#ifdef TQT_CHECK_RANGE + qWarning("TQMYSTQLResult::data: unknown data type"); +#endif + return TQVariant(); +} + +bool TQMYSTQLResult::isNull( int field ) +{ + if ( d->row[field] == NULL ) + return TRUE; + return FALSE; +} + +bool TQMYSTQLResult::reset ( const TQString& query ) +{ + if ( !driver() ) + return FALSE; + if ( !driver()-> isOpen() || driver()->isOpenError() ) + return FALSE; + cleanup(); + + const char *encQuery = query.ascii(); + if ( mysql_real_query( d->mysql, encQuery, tqstrlen(encQuery) ) ) { + setLastError( qMakeError("Unable to execute query", TQSqlError::Statement, d ) ); + return FALSE; + } + if ( isForwardOnly() ) { + if ( isActive() || isValid() ) // have to empty the results from previous query + fetchLast(); + d->result = mysql_use_result( d->mysql ); + } else { + d->result = mysql_store_result( d->mysql ); + } + if ( !d->result && mysql_field_count( d->mysql ) > 0 ) { + setLastError( qMakeError( "Unable to store result", TQSqlError::Statement, d ) ); + return FALSE; + } + int numFields = mysql_field_count( d->mysql ); + setSelect( !( numFields == 0) ); + d->fieldTypes.resize( numFields ); + if ( isSelect() ) { + for( int i = 0; i < numFields; i++) { + MYSTQL_FIELD* field = mysql_fetch_field_direct( d->result, i ); + if ( field->type == FIELD_TYPE_DECIMAL ) + d->fieldTypes[i] = TQVariant::String; + else + d->fieldTypes[i] = qDecodeMYSTQLType( field->type, field->flags ); + } + } + setActive( TRUE ); + return TRUE; +} + +int TQMYSTQLResult::size() +{ + return isSelect() ? (int)mysql_num_rows( d->result ) : -1; +} + +int TQMYSTQLResult::numRowsAffected() +{ + return (int)mysql_affected_rows( d->mysql ); +} + +///////////////////////////////////////////////////////// +static void qServerEnd() +{ +#ifndef TQ_NO_MYSTQL_EMBEDDED +# if MYSTQL_VERSION_ID >= 40000 + mysql_server_end(); +# endif // MYSTQL_VERSION_ID +#endif // TQ_NO_MYSTQL_EMBEDDED +} + +static void qServerInit() +{ +#ifndef TQ_NO_MYSTQL_EMBEDDED +# if MYSTQL_VERSION_ID >= 40000 + if ( qMySqlInitHandledByUser || qMySqlConnectionCount > 1 ) + return; + + // this should only be called once + // has no effect on client/server library + // but is vital for the embedded lib + if ( mysql_server_init( 0, 0, 0 ) ) { +# ifdef TQT_CHECK_RANGE + qWarning( "TQMYSTQLDriver::qServerInit: unable to start server." ); +# endif + } + +# endif // MYSTQL_VERSION_ID +#endif // TQ_NO_MYSTQL_EMBEDDED +} + +TQMYSTQLDriver::TQMYSTQLDriver( TQObject * tqparent, const char * name ) + : TQSqlDriver( tqparent, name ? name : TQMYSTQL_DRIVER_NAME ) +{ + init(); + qServerInit(); +} + +/*! + Create a driver instance with an already open connection handle. +*/ + +TQMYSTQLDriver::TQMYSTQLDriver( MYSQL * con, TQObject * tqparent, const char * name ) + : TQSqlDriver( tqparent, name ? name : TQMYSTQL_DRIVER_NAME ) +{ + init(); + if ( con ) { + d->mysql = (MYSQL *) con; + setOpen( TRUE ); + setOpenError( FALSE ); + if (qMySqlConnectionCount == 1) + qMySqlInitHandledByUser = TRUE; + } else { + qServerInit(); + } +} + +void TQMYSTQLDriver::init() +{ + qSqlOpenExtDict()->insert( this, new TQMYSTQLOpenExtension(this) ); + d = new TQMYSTQLDriverPrivate(); + d->mysql = 0; + qMySqlConnectionCount++; +} + +TQMYSTQLDriver::~TQMYSTQLDriver() +{ + qMySqlConnectionCount--; + if (qMySqlConnectionCount == 0 && !qMySqlInitHandledByUser) + qServerEnd(); + + delete d; + if ( !qSqlOpenExtDict()->isEmpty() ) { + TQSqlOpenExtension *ext = qSqlOpenExtDict()->take( this ); + delete ext; + } +} + +bool TQMYSTQLDriver::hasFeature( DriverFeature f ) const +{ + switch ( f ) { + case Transactions: +// CLIENT_TRANSACTION should be defined in all recent mysql client libs > 3.23.34 +#ifdef CLIENT_TRANSACTIONS + if ( d->mysql ) { + if ( ( d->mysql->server_capabilities & CLIENT_TRANSACTIONS ) == CLIENT_TRANSACTIONS ) + return TRUE; + } +#endif + return FALSE; + case QuerySize: + return TRUE; + case BLOB: + return TRUE; + case Unicode: + return FALSE; + default: + return FALSE; + } +} + +bool TQMYSTQLDriver::open( const TQString&, + const TQString&, + const TQString&, + const TQString&, + int ) +{ + qWarning("TQMYSTQLDriver::open(): This version of open() is no longer supported." ); + return FALSE; +} + +bool TQMYSTQLDriver::open( const TQString& db, + const TQString& user, + const TQString& password, + const TQString& host, + int port, + const TQString& connOpts ) +{ + if ( isOpen() ) + close(); + + unsigned int optionFlags = 0; + + TQStringList raw = TQStringList::split( ';', connOpts ); + TQStringList opts; + TQStringList::ConstIterator it; + + // extract the real options from the string + for ( it = raw.begin(); it != raw.end(); ++it ) { + TQString tmp( *it ); + int idx; + if ( (idx = tmp.tqfind( '=' )) != -1 ) { + TQString val( tmp.mid( idx + 1 ) ); + val.simplifyWhiteSpace(); + if ( val == "TRUE" || val == "1" ) + opts << tmp.left( idx ); + else + qWarning( "TQMYSTQLDriver::open: Illegal connect option value '%s'", tmp.latin1() ); + } else { + opts << tmp; + } + } + + for ( it = opts.begin(); it != opts.end(); ++it ) { + TQString opt( (*it).upper() ); + if ( opt == "CLIENT_COMPRESS" ) + optionFlags |= CLIENT_COMPRESS; + else if ( opt == "CLIENT_FOUND_ROWS" ) + optionFlags |= CLIENT_FOUND_ROWS; + else if ( opt == "CLIENT_IGNORE_SPACE" ) + optionFlags |= CLIENT_IGNORE_SPACE; + else if ( opt == "CLIENT_INTERACTIVE" ) + optionFlags |= CLIENT_INTERACTIVE; + else if ( opt == "CLIENT_NO_SCHEMA" ) + optionFlags |= CLIENT_NO_SCHEMA; + else if ( opt == "CLIENT_ODBC" ) + optionFlags |= CLIENT_ODBC; + else if ( opt == "CLIENT_SSL" ) + optionFlags |= CLIENT_SSL; + else + qWarning( "TQMYSTQLDriver::open: Unknown connect option '%s'", (*it).latin1() ); + } + + if ( (d->mysql = mysql_init((MYSQL*) 0)) && + mysql_real_connect( d->mysql, + host, + user, + password, + db.isNull() ? TQString("") : db, + (port > -1) ? port : 0, + NULL, + optionFlags ) ) + { + if ( !db.isEmpty() && mysql_select_db( d->mysql, db )) { + setLastError( qMakeError("Unable open database '" + db + "'", TQSqlError::Connection, d ) ); + mysql_close( d->mysql ); + setOpenError( TRUE ); + return FALSE; + } + } else { + setLastError( qMakeError( "Unable to connect", TQSqlError::Connection, d ) ); + mysql_close( d->mysql ); + setOpenError( TRUE ); + return FALSE; + } + setOpen( TRUE ); + setOpenError( FALSE ); + return TRUE; +} + +void TQMYSTQLDriver::close() +{ + if ( isOpen() ) { + mysql_close( d->mysql ); + setOpen( FALSE ); + setOpenError( FALSE ); + } +} + +TQSqlQuery TQMYSTQLDriver::createQuery() const +{ + return TQSqlQuery( new TQMYSTQLResult( this ) ); +} + +TQStringList TQMYSTQLDriver::tables( const TQString& typeName ) const +{ + TQStringList tl; + if ( !isOpen() ) + return tl; + if ( !typeName.isEmpty() && !(typeName.toInt() & (int)TQSql::Tables) ) + return tl; + + MYSTQL_RES* tableRes = mysql_list_tables( d->mysql, NULL ); + MYSTQL_ROW row; + int i = 0; + while ( tableRes && TRUE ) { + mysql_data_seek( tableRes, i ); + row = mysql_fetch_row( tableRes ); + if ( !row ) + break; + tl.append( TQString(row[0]) ); + i++; + } + mysql_free_result( tableRes ); + return tl; +} + +TQSqlIndex TQMYSTQLDriver::primaryIndex( const TQString& tablename ) const +{ + TQSqlIndex idx; + if ( !isOpen() ) + return idx; + TQSqlQuery i = createQuery(); + TQString stmt( "show index from %1;" ); + TQSqlRecord fil = record( tablename ); + i.exec( stmt.arg( tablename ) ); + while ( i.isActive() && i.next() ) { + if ( i.value(2).toString() == "PRIMARY" ) { + idx.append( *fil.field( i.value(4).toString() ) ); + idx.setCursorName( i.value(0).toString() ); + idx.setName( i.value(2).toString() ); + } + } + return idx; +} + +TQSqlRecord TQMYSTQLDriver::record( const TQString& tablename ) const +{ + TQSqlRecord fil; + if ( !isOpen() ) + return fil; + MYSTQL_RES* r = mysql_list_fields( d->mysql, tablename.local8Bit().data(), 0); + if ( !r ) { + return fil; + } + MYSTQL_FIELD* field; + while ( (field = mysql_fetch_field( r ))) { + TQSqlField f ( TQString( field->name ) , qDecodeMYSTQLType( (int)field->type, field->flags ) ); + fil.append ( f ); + } + mysql_free_result( r ); + return fil; +} + +TQSqlRecord TQMYSTQLDriver::record( const TQSqlQuery& query ) const +{ + TQSqlRecord fil; + if ( !isOpen() ) + return fil; + if ( query.isActive() && query.isSelect() && query.driver() == this ) { + TQMYSTQLResult* result = (TQMYSTQLResult*)query.result(); + TQMYSTQLResultPrivate* p = result->d; + if ( !mysql_errno( p->mysql ) ) { + for ( ;; ) { + MYSTQL_FIELD* f = mysql_fetch_field( p->result ); + if ( f ) { + TQSqlField fi( TQString((const char*)f->name), qDecodeMYSTQLType( f->type, f->flags ) ); + fil.append( fi ); + } else + break; + } + } + mysql_field_seek( p->result, 0 ); + } + return fil; +} + +TQSqlRecordInfo TQMYSTQLDriver::recordInfo( const TQString& tablename ) const +{ + TQSqlRecordInfo info; + if ( !isOpen() ) + return info; + MYSTQL_RES* r = mysql_list_fields( d->mysql, tablename.local8Bit().data(), 0); + if ( !r ) { + return info; + } + MYSTQL_FIELD* field; + while ( (field = mysql_fetch_field( r ))) { + info.append ( TQSqlFieldInfo( TQString( field->name ), + qDecodeMYSTQLType( (int)field->type, field->flags ), + IS_NOT_NULL( field->flags ), + (int)field->length, + (int)field->decimals, + TQString( field->def ), + (int)field->type ) ); + } + mysql_free_result( r ); + return info; +} + +TQSqlRecordInfo TQMYSTQLDriver::recordInfo( const TQSqlQuery& query ) const +{ + TQSqlRecordInfo info; + if ( !isOpen() ) + return info; + if ( query.isActive() && query.isSelect() && query.driver() == this ) { + TQMYSTQLResult* result = (TQMYSTQLResult*)query.result(); + TQMYSTQLResultPrivate* p = result->d; + if ( !mysql_errno( p->mysql ) ) { + for ( ;; ) { + MYSTQL_FIELD* field = mysql_fetch_field( p->result ); + if ( field ) { + info.append ( TQSqlFieldInfo( TQString( field->name ), + qDecodeMYSTQLType( (int)field->type, field->flags ), + IS_NOT_NULL( field->flags ), + (int)field->length, + (int)field->decimals, + TQVariant(), + (int)field->type ) ); + + } else + break; + } + } + mysql_field_seek( p->result, 0 ); + } + return info; +} + +MYSQL* TQMYSTQLDriver::mysql() +{ + return d->mysql; +} + +bool TQMYSTQLDriver::beginTransaction() +{ +#ifndef CLIENT_TRANSACTIONS + return FALSE; +#endif + if ( !isOpen() ) { +#ifdef TQT_CHECK_RANGE + qWarning( "TQMYSTQLDriver::beginTransaction: Database not open" ); +#endif + return FALSE; + } + if ( mysql_query( d->mysql, "BEGIN WORK" ) ) { + setLastError( qMakeError("Unable to begin transaction", TQSqlError::Statement, d ) ); + return FALSE; + } + return TRUE; +} + +bool TQMYSTQLDriver::commitTransaction() +{ +#ifndef CLIENT_TRANSACTIONS + return FALSE; +#endif + if ( !isOpen() ) { +#ifdef TQT_CHECK_RANGE + qWarning( "TQMYSTQLDriver::commitTransaction: Database not open" ); +#endif + return FALSE; + } + if ( mysql_query( d->mysql, "COMMIT" ) ) { + setLastError( qMakeError("Unable to commit transaction", TQSqlError::Statement, d ) ); + return FALSE; + } + return TRUE; +} + +bool TQMYSTQLDriver::rollbackTransaction() +{ +#ifndef CLIENT_TRANSACTIONS + return FALSE; +#endif + if ( !isOpen() ) { +#ifdef TQT_CHECK_RANGE + qWarning( "TQMYSTQLDriver::rollbackTransaction: Database not open" ); +#endif + return FALSE; + } + if ( mysql_query( d->mysql, "ROLLBACK" ) ) { + setLastError( qMakeError("Unable to rollback transaction", TQSqlError::Statement, d ) ); + return FALSE; + } + return TRUE; +} + +TQString TQMYSTQLDriver::formatValue( const TQSqlField* field, bool trimStrings ) const +{ + TQString r; + if ( field->isNull() ) { + r = nullText(); + } else { + switch( field->type() ) { + case TQVariant::ByteArray: { + + const TQByteArray ba = field->value().toByteArray(); + // buffer has to be at least length*2+1 bytes + char* buffer = new char[ ba.size() * 2 + 1 ]; + /*uint escapedSize =*/ mysql_escape_string( buffer, ba.data(), ba.size() ); + r.append("'").append(buffer).append("'"); + delete[] buffer; + } + break; + case TQVariant::String: + case TQVariant::CString: { + // Escape '\' characters + r = TQSqlDriver::formatValue( field ); + r.tqreplace( "\\", "\\\\" ); + break; + } + default: + r = TQSqlDriver::formatValue( field, trimStrings ); + } + } + return r; +} |