diff -r 000000000000 -r 1918ee327afb src/sql/drivers/psql/qsql_psql.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/sql/drivers/psql/qsql_psql.cpp Mon Jan 11 14:00:40 2010 +0000 @@ -0,0 +1,1320 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtSql module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qsql_psql.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include +#include + +#include +#include +// below code taken from an example at http://www.gnu.org/software/hello/manual/autoconf/Function-Portability.html +#ifndef isnan + # define isnan(x) \ + (sizeof (x) == sizeof (long double) ? isnan_ld (x) \ + : sizeof (x) == sizeof (double) ? isnan_d (x) \ + : isnan_f (x)) + static inline int isnan_f (float x) { return x != x; } + static inline int isnan_d (double x) { return x != x; } + static inline int isnan_ld (long double x) { return x != x; } +#endif + +#ifndef isinf + # define isinf(x) \ + (sizeof (x) == sizeof (long double) ? isinf_ld (x) \ + : sizeof (x) == sizeof (double) ? isinf_d (x) \ + : isinf_f (x)) + static inline int isinf_f (float x) { return isnan (x - x); } + static inline int isinf_d (double x) { return isnan (x - x); } + static inline int isinf_ld (long double x) { return isnan (x - x); } +#endif + + +// workaround for postgres defining their OIDs in a private header file +#define QBOOLOID 16 +#define QINT8OID 20 +#define QINT2OID 21 +#define QINT4OID 23 +#define QNUMERICOID 1700 +#define QFLOAT4OID 700 +#define QFLOAT8OID 701 +#define QABSTIMEOID 702 +#define QRELTIMEOID 703 +#define QDATEOID 1082 +#define QTIMEOID 1083 +#define QTIMETZOID 1266 +#define QTIMESTAMPOID 1114 +#define QTIMESTAMPTZOID 1184 +#define QOIDOID 2278 +#define QBYTEAOID 17 +#define QREGPROCOID 24 +#define QXIDOID 28 +#define QCIDOID 29 + +/* This is a compile time switch - if PQfreemem is declared, the compiler will use that one, + otherwise it'll run in this template */ +template +inline void PQfreemem(T *t, int = 0) { free(t); } + +Q_DECLARE_METATYPE(PGconn*) +Q_DECLARE_METATYPE(PGresult*) + +QT_BEGIN_NAMESPACE + +inline void qPQfreemem(void *buffer) +{ + PQfreemem(buffer); +} + +class QPSQLDriverPrivate +{ +public: + QPSQLDriverPrivate() : connection(0), isUtf8(false), pro(QPSQLDriver::Version6), sn(0) {} + PGconn *connection; + bool isUtf8; + QPSQLDriver::Protocol pro; + QSocketNotifier *sn; + QStringList seid; + + void appendTables(QStringList &tl, QSqlQuery &t, QChar type); +}; + +void QPSQLDriverPrivate::appendTables(QStringList &tl, QSqlQuery &t, QChar type) +{ + QString query; + if (pro >= QPSQLDriver::Version73) { + query = QString::fromLatin1("select pg_class.relname, pg_namespace.nspname from pg_class " + "left join pg_namespace on (pg_class.relnamespace = pg_namespace.oid) " + "where (pg_class.relkind = '%1') and (pg_class.relname !~ '^Inv') " + "and (pg_class.relname !~ '^pg_') " + "and (pg_namespace.nspname != 'information_schema') ").arg(type); + } else { + query = QString::fromLatin1("select relname, null from pg_class where (relkind = '%1') " + "and (relname !~ '^Inv') " + "and (relname !~ '^pg_') ").arg(type); + } + t.exec(query); + while (t.next()) { + QString schema = t.value(1).toString(); + if (schema.isEmpty() || schema == QLatin1String("public")) + tl.append(t.value(0).toString()); + else + tl.append(t.value(0).toString().prepend(QLatin1Char('.')).prepend(schema)); + } +} + +class QPSQLResultPrivate +{ +public: + QPSQLResultPrivate(QPSQLResult *qq): q(qq), driver(0), result(0), currentSize(-1), preparedQueriesEnabled(false) {} + + QPSQLResult *q; + const QPSQLDriverPrivate *driver; + PGresult *result; + int currentSize; + bool preparedQueriesEnabled; + QString preparedStmtId; + + bool processResults(); +}; + +static QSqlError qMakeError(const QString& err, QSqlError::ErrorType type, + const QPSQLDriverPrivate *p) +{ + const char *s = PQerrorMessage(p->connection); + QString msg = p->isUtf8 ? QString::fromUtf8(s) : QString::fromLocal8Bit(s); + return QSqlError(QLatin1String("QPSQL: ") + err, msg, type); +} + +bool QPSQLResultPrivate::processResults() +{ + if (!result) + return false; + + int status = PQresultStatus(result); + if (status == PGRES_TUPLES_OK) { + q->setSelect(true); + q->setActive(true); + currentSize = PQntuples(result); + return true; + } else if (status == PGRES_COMMAND_OK) { + q->setSelect(false); + q->setActive(true); + currentSize = -1; + return true; + } + q->setLastError(qMakeError(QCoreApplication::translate("QPSQLResult", + "Unable to create query"), QSqlError::StatementError, driver)); + return false; +} + +static QVariant::Type qDecodePSQLType(int t) +{ + QVariant::Type type = QVariant::Invalid; + switch (t) { + case QBOOLOID: + type = QVariant::Bool; + break; + case QINT8OID: + type = QVariant::LongLong; + break; + case QINT2OID: + case QINT4OID: + case QOIDOID: + case QREGPROCOID: + case QXIDOID: + case QCIDOID: + type = QVariant::Int; + break; + case QNUMERICOID: + case QFLOAT4OID: + case QFLOAT8OID: + type = QVariant::Double; + break; + case QABSTIMEOID: + case QRELTIMEOID: + case QDATEOID: + type = QVariant::Date; + break; + case QTIMEOID: + case QTIMETZOID: + type = QVariant::Time; + break; + case QTIMESTAMPOID: + case QTIMESTAMPTZOID: + type = QVariant::DateTime; + break; + case QBYTEAOID: + type = QVariant::ByteArray; + break; + default: + type = QVariant::String; + break; + } + return type; +} + +static void qDeallocatePreparedStmt(QPSQLResultPrivate *d) +{ + const QString stmt = QLatin1String("DEALLOCATE ") + d->preparedStmtId; + PGresult *result = PQexec(d->driver->connection, + d->driver->isUtf8 ? stmt.toUtf8().constData() + : stmt.toLocal8Bit().constData()); + + if (PQresultStatus(result) != PGRES_COMMAND_OK) + qWarning("Unable to free statement: %s", PQerrorMessage(d->driver->connection)); + PQclear(result); + d->preparedStmtId.clear(); +} + +QPSQLResult::QPSQLResult(const QPSQLDriver* db, const QPSQLDriverPrivate* p) + : QSqlResult(db) +{ + d = new QPSQLResultPrivate(this); + d->driver = p; + d->preparedQueriesEnabled = db->hasFeature(QSqlDriver::PreparedQueries); +} + +QPSQLResult::~QPSQLResult() +{ + cleanup(); + + if (d->preparedQueriesEnabled && !d->preparedStmtId.isNull()) + qDeallocatePreparedStmt(d); + + delete d; +} + +QVariant QPSQLResult::handle() const +{ + return qVariantFromValue(d->result); +} + +void QPSQLResult::cleanup() +{ + if (d->result) + PQclear(d->result); + d->result = 0; + setAt(QSql::BeforeFirstRow); + d->currentSize = -1; + setActive(false); +} + +bool QPSQLResult::fetch(int i) +{ + if (!isActive()) + return false; + if (i < 0) + return false; + if (i >= d->currentSize) + return false; + if (at() == i) + return true; + setAt(i); + return true; +} + +bool QPSQLResult::fetchFirst() +{ + return fetch(0); +} + +bool QPSQLResult::fetchLast() +{ + return fetch(PQntuples(d->result) - 1); +} + +QVariant QPSQLResult::data(int i) +{ + if (i >= PQnfields(d->result)) { + qWarning("QPSQLResult::data: column %d out of range", i); + return QVariant(); + } + int ptype = PQftype(d->result, i); + QVariant::Type type = qDecodePSQLType(ptype); + const char *val = PQgetvalue(d->result, at(), i); + if (PQgetisnull(d->result, at(), i)) + return QVariant(type); + switch (type) { + case QVariant::Bool: + return QVariant((bool)(val[0] == 't')); + case QVariant::String: + return d->driver->isUtf8 ? QString::fromUtf8(val) : QString::fromAscii(val); + case QVariant::LongLong: + if (val[0] == '-') + return QString::fromLatin1(val).toLongLong(); + else + return QString::fromLatin1(val).toULongLong(); + case QVariant::Int: + return atoi(val); + case QVariant::Double: + if (ptype == QNUMERICOID) { + if (numericalPrecisionPolicy() != QSql::HighPrecision) { + QVariant retval; + bool convert; + double dbl=QString::fromAscii(val).toDouble(&convert); + if (numericalPrecisionPolicy() == QSql::LowPrecisionInt64) + retval = (qlonglong)dbl; + else if (numericalPrecisionPolicy() == QSql::LowPrecisionInt32) + retval = (int)dbl; + else if (numericalPrecisionPolicy() == QSql::LowPrecisionDouble) + retval = dbl; + if (!convert) + return QVariant(); + return retval; + } + return QString::fromAscii(val); + } + return strtod(val, 0); + case QVariant::Date: + if (val[0] == '\0') { + return QVariant(QDate()); + } else { +#ifndef QT_NO_DATESTRING + return QVariant(QDate::fromString(QString::fromLatin1(val), Qt::ISODate)); +#else + return QVariant(QString::fromLatin1(val)); +#endif + } + case QVariant::Time: { + const QString str = QString::fromLatin1(val); +#ifndef QT_NO_DATESTRING + if (str.isEmpty()) + return QVariant(QTime()); + if (str.at(str.length() - 3) == QLatin1Char('+') || str.at(str.length() - 3) == QLatin1Char('-')) + // strip the timezone + // TODO: fix this when timestamp support comes into QDateTime + return QVariant(QTime::fromString(str.left(str.length() - 3), Qt::ISODate)); + return QVariant(QTime::fromString(str, Qt::ISODate)); +#else + return QVariant(str); +#endif + } + case QVariant::DateTime: { + QString dtval = QString::fromLatin1(val); +#ifndef QT_NO_DATESTRING + if (dtval.length() < 10) + return QVariant(QDateTime()); + // remove the timezone + // TODO: fix this when timestamp support comes into QDateTime + if (dtval.at(dtval.length() - 3) == QLatin1Char('+') || dtval.at(dtval.length() - 3) == QLatin1Char('-')) + dtval.chop(3); + // milliseconds are sometimes returned with 2 digits only + if (dtval.at(dtval.length() - 3).isPunct()) + dtval += QLatin1Char('0'); + if (dtval.isEmpty()) + return QVariant(QDateTime()); + else + return QVariant(QDateTime::fromString(dtval, Qt::ISODate)); +#else + return QVariant(dtval); +#endif + } + case QVariant::ByteArray: { + size_t len; + unsigned char *data = PQunescapeBytea((unsigned char*)val, &len); + QByteArray ba((const char*)data, len); + qPQfreemem(data); + return QVariant(ba); + } + default: + case QVariant::Invalid: + qWarning("QPSQLResult::data: unknown data type"); + } + return QVariant(); +} + +bool QPSQLResult::isNull(int field) +{ + PQgetvalue(d->result, at(), field); + return PQgetisnull(d->result, at(), field); +} + +bool QPSQLResult::reset (const QString& query) +{ + cleanup(); + if (!driver()) + return false; + if (!driver()->isOpen() || driver()->isOpenError()) + return false; + d->result = PQexec(d->driver->connection, + d->driver->isUtf8 ? query.toUtf8().constData() + : query.toLocal8Bit().constData()); + return d->processResults(); +} + +int QPSQLResult::size() +{ + return d->currentSize; +} + +int QPSQLResult::numRowsAffected() +{ + return QString::fromLatin1(PQcmdTuples(d->result)).toInt(); +} + +QVariant QPSQLResult::lastInsertId() const +{ + if (isActive()) { + Oid id = PQoidValue(d->result); + if (id != InvalidOid) + return QVariant(id); + } + return QVariant(); +} + +QSqlRecord QPSQLResult::record() const +{ + QSqlRecord info; + if (!isActive() || !isSelect()) + return info; + + int count = PQnfields(d->result); + for (int i = 0; i < count; ++i) { + QSqlField f; + if (d->driver->isUtf8) + f.setName(QString::fromUtf8(PQfname(d->result, i))); + else + f.setName(QString::fromLocal8Bit(PQfname(d->result, i))); + f.setType(qDecodePSQLType(PQftype(d->result, i))); + int len = PQfsize(d->result, i); + int precision = PQfmod(d->result, i); + // swap length and precision if length == -1 + if (len == -1 && precision > -1) { + len = precision - 4; + precision = -1; + } + f.setLength(len); + f.setPrecision(precision); + f.setSqlType(PQftype(d->result, i)); + info.append(f); + } + return info; +} + +void QPSQLResult::virtual_hook(int id, void *data) +{ + Q_ASSERT(data); + + switch (id) { + default: + QSqlResult::virtual_hook(id, data); + } +} + +static QString qReplacePlaceholderMarkers(const QString &query) +{ + const int originalLength = query.length(); + bool inQuote = false; + int markerIdx = 0; + QString result; + result.reserve(originalLength + 23); + for (int i = 0; i < originalLength; ++i) { + const QChar ch = query.at(i); + if (ch == QLatin1Char('?') && !inQuote) { + result += QLatin1Char('$'); + result += QString::number(++markerIdx); + } else { + if (ch == QLatin1Char('\'')) + inQuote = !inQuote; + result += ch; + } + } + + result.squeeze(); + return result; +} + +static QString qCreateParamString(const QVector boundValues, const QSqlDriver *driver) +{ + if (boundValues.isEmpty()) + return QString(); + + QString params; + QSqlField f; + for (int i = 0; i < boundValues.count(); ++i) { + const QVariant &val = boundValues.at(i); + + f.setType(val.type()); + if (val.isNull()) + f.clear(); + else + f.setValue(val); + if(!params.isNull()) + params.append(QLatin1String(", ")); + params.append(driver->formatValue(f)); + } + return params; +} + +Q_GLOBAL_STATIC(QMutex, qMutex) +QString qMakePreparedStmtId() +{ + qMutex()->lock(); + static unsigned int qPreparedStmtCount = 0; + QString id = QLatin1String("qpsqlpstmt_") + QString::number(++qPreparedStmtCount, 16); + qMutex()->unlock(); + return id; +} + +bool QPSQLResult::prepare(const QString &query) +{ + if (!d->preparedQueriesEnabled) + return QSqlResult::prepare(query); + + cleanup(); + + if (!d->preparedStmtId.isEmpty()) + qDeallocatePreparedStmt(d); + + const QString stmtId = qMakePreparedStmtId(); + const QString stmt = QString::fromLatin1("PREPARE %1 AS ").arg(stmtId).append(qReplacePlaceholderMarkers(query)); + + PGresult *result = PQexec(d->driver->connection, + d->driver->isUtf8 ? stmt.toUtf8().constData() + : stmt.toLocal8Bit().constData()); + + if (PQresultStatus(result) != PGRES_COMMAND_OK) { + setLastError(qMakeError(QCoreApplication::translate("QPSQLResult", + "Unable to prepare statement"), QSqlError::StatementError, d->driver)); + PQclear(result); + d->preparedStmtId.clear(); + return false; + } + + PQclear(result); + d->preparedStmtId = stmtId; + return true; +} + +bool QPSQLResult::exec() +{ + if (!d->preparedQueriesEnabled) + return QSqlResult::exec(); + + cleanup(); + + QString stmt; + const QString params = qCreateParamString(boundValues(), d->q->driver()); + if (params.isEmpty()) + stmt = QString::fromLatin1("EXECUTE %1").arg(d->preparedStmtId); + else + stmt = QString::fromLatin1("EXECUTE %1 (%2)").arg(d->preparedStmtId).arg(params); + + d->result = PQexec(d->driver->connection, + d->driver->isUtf8 ? stmt.toUtf8().constData() + : stmt.toLocal8Bit().constData()); + + return d->processResults(); +} + +/////////////////////////////////////////////////////////////////// + +static bool setEncodingUtf8(PGconn* connection) +{ + PGresult* result = PQexec(connection, "SET CLIENT_ENCODING TO 'UNICODE'"); + int status = PQresultStatus(result); + PQclear(result); + return status == PGRES_COMMAND_OK; +} + +static void setDatestyle(PGconn* connection) +{ + PGresult* result = PQexec(connection, "SET DATESTYLE TO 'ISO'"); + int status = PQresultStatus(result); + if (status != PGRES_COMMAND_OK) + qWarning("%s", PQerrorMessage(connection)); + PQclear(result); +} + +static QPSQLDriver::Protocol getPSQLVersion(PGconn* connection) +{ + QPSQLDriver::Protocol serverVersion = QPSQLDriver::Version6; + PGresult* result = PQexec(connection, "select version()"); + int status = PQresultStatus(result); + if (status == PGRES_COMMAND_OK || status == PGRES_TUPLES_OK) { + QString val = QString::fromAscii(PQgetvalue(result, 0, 0)); + QRegExp rx(QLatin1String("(\\d+)\\.(\\d+)")); + rx.setMinimal(true); // enforce non-greedy RegExp + if (rx.indexIn(val) != -1) { + int vMaj = rx.cap(1).toInt(); + int vMin = rx.cap(2).toInt(); + + switch (vMaj) { + case 7: + switch (vMin) { + case 0: + serverVersion = QPSQLDriver::Version7; + break; + case 1: + case 2: + serverVersion = QPSQLDriver::Version71; + break; + default: + serverVersion = QPSQLDriver::Version73; + break; + } + break; + case 8: + switch (vMin) { + case 0: + serverVersion = QPSQLDriver::Version8; + break; + case 1: + serverVersion = QPSQLDriver::Version81; + break; + case 2: + default: + serverVersion = QPSQLDriver::Version82; + break; + } + break; + default: + break; + } + } + } + PQclear(result); + + if (serverVersion < QPSQLDriver::Version71) + qWarning("This version of PostgreSQL is not supported and may not work."); + + return serverVersion; +} + +QPSQLDriver::QPSQLDriver(QObject *parent) + : QSqlDriver(parent) +{ + init(); +} + +QPSQLDriver::QPSQLDriver(PGconn *conn, QObject *parent) + : QSqlDriver(parent) +{ + init(); + d->connection = conn; + if (conn) { + d->pro = getPSQLVersion(d->connection); + setOpen(true); + setOpenError(false); + } +} + +void QPSQLDriver::init() +{ + d = new QPSQLDriverPrivate(); +} + +QPSQLDriver::~QPSQLDriver() +{ + if (d->connection) + PQfinish(d->connection); + delete d; +} + +QVariant QPSQLDriver::handle() const +{ + return qVariantFromValue(d->connection); +} + +bool QPSQLDriver::hasFeature(DriverFeature f) const +{ + switch (f) { + case Transactions: + case QuerySize: + case LastInsertId: + case LowPrecisionNumbers: + case EventNotifications: + return true; + case PreparedQueries: + case PositionalPlaceholders: + return d->pro >= QPSQLDriver::Version82; + case BatchOperations: + case NamedPlaceholders: + case SimpleLocking: + case FinishQuery: + case MultipleResultSets: + return false; + case BLOB: + return d->pro >= QPSQLDriver::Version71; + case Unicode: + return d->isUtf8; + } + return false; +} + +/* + Quote a string for inclusion into the connection string + \ -> \\ + ' -> \' + surround string by single quotes + */ +static QString qQuote(QString s) +{ + s.replace(QLatin1Char('\\'), QLatin1String("\\\\")); + s.replace(QLatin1Char('\''), QLatin1String("\\'")); + s.append(QLatin1Char('\'')).prepend(QLatin1Char('\'')); + return s; +} + +bool QPSQLDriver::open(const QString & db, + const QString & user, + const QString & password, + const QString & host, + int port, + const QString& connOpts) +{ + if (isOpen()) + close(); + QString connectString; + if (!host.isEmpty()) + connectString.append(QLatin1String("host=")).append(qQuote(host)); + if (!db.isEmpty()) + connectString.append(QLatin1String(" dbname=")).append(qQuote(db)); + if (!user.isEmpty()) + connectString.append(QLatin1String(" user=")).append(qQuote(user)); + if (!password.isEmpty()) + connectString.append(QLatin1String(" password=")).append(qQuote(password)); + if (port != -1) + connectString.append(QLatin1String(" port=")).append(qQuote(QString::number(port))); + + // add any connect options - the server will handle error detection + if (!connOpts.isEmpty()) { + QString opt = connOpts; + opt.replace(QLatin1Char(';'), QLatin1Char(' '), Qt::CaseInsensitive); + connectString.append(QLatin1Char(' ')).append(opt); + } + + d->connection = PQconnectdb(connectString.toLocal8Bit().constData()); + if (PQstatus(d->connection) == CONNECTION_BAD) { + setLastError(qMakeError(tr("Unable to connect"), QSqlError::ConnectionError, d)); + setOpenError(true); + PQfinish(d->connection); + d->connection = 0; + return false; + } + + d->pro = getPSQLVersion(d->connection); + d->isUtf8 = setEncodingUtf8(d->connection); + setDatestyle(d->connection); + + setOpen(true); + setOpenError(false); + return true; +} + +void QPSQLDriver::close() +{ + if (isOpen()) { + + d->seid.clear(); + if (d->sn) { + disconnect(d->sn, SIGNAL(activated(int)), this, SLOT(_q_handleNotification(int))); + delete d->sn; + d->sn = 0; + } + + if (d->connection) + PQfinish(d->connection); + d->connection = 0; + setOpen(false); + setOpenError(false); + } +} + +QSqlResult *QPSQLDriver::createResult() const +{ + return new QPSQLResult(this, d); +} + +bool QPSQLDriver::beginTransaction() +{ + if (!isOpen()) { + qWarning("QPSQLDriver::beginTransaction: Database not open"); + return false; + } + PGresult* res = PQexec(d->connection, "BEGIN"); + if (!res || PQresultStatus(res) != PGRES_COMMAND_OK) { + PQclear(res); + setLastError(qMakeError(tr("Could not begin transaction"), + QSqlError::TransactionError, d)); + return false; + } + PQclear(res); + return true; +} + +bool QPSQLDriver::commitTransaction() +{ + if (!isOpen()) { + qWarning("QPSQLDriver::commitTransaction: Database not open"); + return false; + } + PGresult* res = PQexec(d->connection, "COMMIT"); + + bool transaction_failed = false; + + // XXX + // This hack is used to tell if the transaction has succeeded for the protocol versions of + // PostgreSQL below. For 7.x and other protocol versions we are left in the dark. + // This hack can dissapear once there is an API to query this sort of information. + if (d->pro == QPSQLDriver::Version8 || + d->pro == QPSQLDriver::Version81 || + d->pro == QPSQLDriver::Version82) { + transaction_failed = qstrcmp(PQcmdStatus(res), "ROLLBACK") == 0; + } + + if (!res || PQresultStatus(res) != PGRES_COMMAND_OK || transaction_failed) { + PQclear(res); + setLastError(qMakeError(tr("Could not commit transaction"), + QSqlError::TransactionError, d)); + return false; + } + PQclear(res); + return true; +} + +bool QPSQLDriver::rollbackTransaction() +{ + if (!isOpen()) { + qWarning("QPSQLDriver::rollbackTransaction: Database not open"); + return false; + } + PGresult* res = PQexec(d->connection, "ROLLBACK"); + if (!res || PQresultStatus(res) != PGRES_COMMAND_OK) { + setLastError(qMakeError(tr("Could not rollback transaction"), + QSqlError::TransactionError, d)); + PQclear(res); + return false; + } + PQclear(res); + return true; +} + +QStringList QPSQLDriver::tables(QSql::TableType type) const +{ + QStringList tl; + if (!isOpen()) + return tl; + QSqlQuery t(createResult()); + t.setForwardOnly(true); + + if (type & QSql::Tables) + d->appendTables(tl, t, QLatin1Char('r')); + if (type & QSql::Views) + d->appendTables(tl, t, QLatin1Char('v')); + if (type & QSql::SystemTables) { + t.exec(QLatin1String("select relname from pg_class where (relkind = 'r') " + "and (relname like 'pg_%') ")); + while (t.next()) + tl.append(t.value(0).toString()); + } + + return tl; +} + +static void qSplitTableName(QString &tablename, QString &schema) +{ + int dot = tablename.indexOf(QLatin1Char('.')); + if (dot == -1) + return; + schema = tablename.left(dot); + tablename = tablename.mid(dot + 1); +} + +QSqlIndex QPSQLDriver::primaryIndex(const QString& tablename) const +{ + QSqlIndex idx(tablename); + if (!isOpen()) + return idx; + QSqlQuery i(createResult()); + QString stmt; + + QString tbl = tablename; + QString schema; + qSplitTableName(tbl, schema); + + if (isIdentifierEscaped(tbl, QSqlDriver::TableName)) + tbl = stripDelimiters(tbl, QSqlDriver::TableName); + else + tbl = tbl.toLower(); + + if (isIdentifierEscaped(schema, QSqlDriver::TableName)) + schema = stripDelimiters(schema, QSqlDriver::TableName); + else + schema = schema.toLower(); + + switch(d->pro) { + case QPSQLDriver::Version6: + stmt = QLatin1String("select pg_att1.attname, int(pg_att1.atttypid), pg_cl.relname " + "from pg_attribute pg_att1, pg_attribute pg_att2, pg_class pg_cl, pg_index pg_ind " + "where pg_cl.relname = '%1_pkey' " + "and pg_cl.oid = pg_ind.indexrelid " + "and pg_att2.attrelid = pg_ind.indexrelid " + "and pg_att1.attrelid = pg_ind.indrelid " + "and pg_att1.attnum = pg_ind.indkey[pg_att2.attnum-1] " + "order by pg_att2.attnum"); + break; + case QPSQLDriver::Version7: + case QPSQLDriver::Version71: + stmt = QLatin1String("select pg_att1.attname, pg_att1.atttypid::int, pg_cl.relname " + "from pg_attribute pg_att1, pg_attribute pg_att2, pg_class pg_cl, pg_index pg_ind " + "where pg_cl.relname = '%1_pkey' " + "and pg_cl.oid = pg_ind.indexrelid " + "and pg_att2.attrelid = pg_ind.indexrelid " + "and pg_att1.attrelid = pg_ind.indrelid " + "and pg_att1.attnum = pg_ind.indkey[pg_att2.attnum-1] " + "order by pg_att2.attnum"); + break; + case QPSQLDriver::Version73: + case QPSQLDriver::Version74: + case QPSQLDriver::Version8: + case QPSQLDriver::Version81: + case QPSQLDriver::Version82: + stmt = QLatin1String("SELECT pg_attribute.attname, pg_attribute.atttypid::int, " + "pg_class.relname " + "FROM pg_attribute, pg_class " + "WHERE %1 pg_class.oid IN " + "(SELECT indexrelid FROM pg_index WHERE indisprimary = true AND indrelid IN " + " (SELECT oid FROM pg_class WHERE relname = '%2')) " + "AND pg_attribute.attrelid = pg_class.oid " + "AND pg_attribute.attisdropped = false " + "ORDER BY pg_attribute.attnum"); + if (schema.isEmpty()) + stmt = stmt.arg(QLatin1String("pg_table_is_visible(pg_class.oid) AND")); + else + stmt = stmt.arg(QString::fromLatin1("pg_class.relnamespace = (select oid from " + "pg_namespace where pg_namespace.nspname = '%1') AND ").arg(schema)); + break; + } + + i.exec(stmt.arg(tbl)); + while (i.isActive() && i.next()) { + QSqlField f(i.value(0).toString(), qDecodePSQLType(i.value(1).toInt())); + idx.append(f); + idx.setName(i.value(2).toString()); + } + return idx; +} + +QSqlRecord QPSQLDriver::record(const QString& tablename) const +{ + QSqlRecord info; + if (!isOpen()) + return info; + + QString tbl = tablename; + QString schema; + qSplitTableName(tbl, schema); + + if (isIdentifierEscaped(tbl, QSqlDriver::TableName)) + tbl = stripDelimiters(tbl, QSqlDriver::TableName); + else + tbl = tbl.toLower(); + + if (isIdentifierEscaped(schema, QSqlDriver::TableName)) + schema = stripDelimiters(schema, QSqlDriver::TableName); + else + schema = schema.toLower(); + + QString stmt; + switch(d->pro) { + case QPSQLDriver::Version6: + stmt = QLatin1String("select pg_attribute.attname, int(pg_attribute.atttypid), " + "pg_attribute.attnotnull, pg_attribute.attlen, pg_attribute.atttypmod, " + "int(pg_attribute.attrelid), pg_attribute.attnum " + "from pg_class, pg_attribute " + "where pg_class.relname = '%1' " + "and pg_attribute.attnum > 0 " + "and pg_attribute.attrelid = pg_class.oid "); + break; + case QPSQLDriver::Version7: + stmt = QLatin1String("select pg_attribute.attname, pg_attribute.atttypid::int, " + "pg_attribute.attnotnull, pg_attribute.attlen, pg_attribute.atttypmod, " + "pg_attribute.attrelid::int, pg_attribute.attnum " + "from pg_class, pg_attribute " + "where pg_class.relname = '%1' " + "and pg_attribute.attnum > 0 " + "and pg_attribute.attrelid = pg_class.oid "); + break; + case QPSQLDriver::Version71: + stmt = QLatin1String("select pg_attribute.attname, pg_attribute.atttypid::int, " + "pg_attribute.attnotnull, pg_attribute.attlen, pg_attribute.atttypmod, " + "pg_attrdef.adsrc " + "from pg_class, pg_attribute " + "left join pg_attrdef on (pg_attrdef.adrelid = " + "pg_attribute.attrelid and pg_attrdef.adnum = pg_attribute.attnum) " + "where pg_class.relname = '%1' " + "and pg_attribute.attnum > 0 " + "and pg_attribute.attrelid = pg_class.oid " + "order by pg_attribute.attnum "); + break; + case QPSQLDriver::Version73: + case QPSQLDriver::Version74: + case QPSQLDriver::Version8: + case QPSQLDriver::Version81: + case QPSQLDriver::Version82: + stmt = QLatin1String("select pg_attribute.attname, pg_attribute.atttypid::int, " + "pg_attribute.attnotnull, pg_attribute.attlen, pg_attribute.atttypmod, " + "pg_attrdef.adsrc " + "from pg_class, pg_attribute " + "left join pg_attrdef on (pg_attrdef.adrelid = " + "pg_attribute.attrelid and pg_attrdef.adnum = pg_attribute.attnum) " + "where %1 " + "and pg_class.relname = '%2' " + "and pg_attribute.attnum > 0 " + "and pg_attribute.attrelid = pg_class.oid " + "and pg_attribute.attisdropped = false " + "order by pg_attribute.attnum "); + if (schema.isEmpty()) + stmt = stmt.arg(QLatin1String("pg_table_is_visible(pg_class.oid)")); + else + stmt = stmt.arg(QString::fromLatin1("pg_class.relnamespace = (select oid from " + "pg_namespace where pg_namespace.nspname = '%1')").arg(schema)); + break; + } + + QSqlQuery query(createResult()); + query.exec(stmt.arg(tbl)); + if (d->pro >= QPSQLDriver::Version71) { + while (query.next()) { + int len = query.value(3).toInt(); + int precision = query.value(4).toInt(); + // swap length and precision if length == -1 + if (len == -1 && precision > -1) { + len = precision - 4; + precision = -1; + } + QString defVal = query.value(5).toString(); + if (!defVal.isEmpty() && defVal.at(0) == QLatin1Char('\'')) + defVal = defVal.mid(1, defVal.length() - 2); + QSqlField f(query.value(0).toString(), qDecodePSQLType(query.value(1).toInt())); + f.setRequired(query.value(2).toBool()); + f.setLength(len); + f.setPrecision(precision); + f.setDefaultValue(defVal); + f.setSqlType(query.value(1).toInt()); + info.append(f); + } + } else { + // Postgres < 7.1 cannot handle outer joins + while (query.next()) { + QString defVal; + QString stmt2 = QLatin1String("select pg_attrdef.adsrc from pg_attrdef where " + "pg_attrdef.adrelid = %1 and pg_attrdef.adnum = %2 "); + QSqlQuery query2(createResult()); + query2.exec(stmt2.arg(query.value(5).toInt()).arg(query.value(6).toInt())); + if (query2.isActive() && query2.next()) + defVal = query2.value(0).toString(); + if (!defVal.isEmpty() && defVal.at(0) == QLatin1Char('\'')) + defVal = defVal.mid(1, defVal.length() - 2); + int len = query.value(3).toInt(); + int precision = query.value(4).toInt(); + // swap length and precision if length == -1 + if (len == -1 && precision > -1) { + len = precision - 4; + precision = -1; + } + QSqlField f(query.value(0).toString(), qDecodePSQLType(query.value(1).toInt())); + f.setRequired(query.value(2).toBool()); + f.setLength(len); + f.setPrecision(precision); + f.setDefaultValue(defVal); + f.setSqlType(query.value(1).toInt()); + info.append(f); + } + } + + return info; +} + +QString QPSQLDriver::formatValue(const QSqlField &field, bool trimStrings) const +{ + QString r; + if (field.isNull()) { + r = QLatin1String("NULL"); + } else { + switch (field.type()) { + case QVariant::DateTime: +#ifndef QT_NO_DATESTRING + if (field.value().toDateTime().isValid()) { + QDate dt = field.value().toDateTime().date(); + QTime tm = field.value().toDateTime().time(); + // msecs need to be right aligned otherwise psql + // interpretes them wrong + r = QLatin1Char('\'') + QString::number(dt.year()) + QLatin1Char('-') + + QString::number(dt.month()) + QLatin1Char('-') + + QString::number(dt.day()) + QLatin1Char(' ') + + tm.toString() + QLatin1Char('.') + + QString::number(tm.msec()).rightJustified(3, QLatin1Char('0')) + + QLatin1Char('\''); + } else { + r = QLatin1String("NULL"); + } +#else + r = QLatin1String("NULL"); +#endif // QT_NO_DATESTRING + break; + case QVariant::Time: +#ifndef QT_NO_DATESTRING + if (field.value().toTime().isValid()) { + r = QLatin1Char('\'') + field.value().toTime().toString(Qt::ISODate) + QLatin1Char('\''); + } else +#endif + { + r = QLatin1String("NULL"); + } + break; + case QVariant::String: + { + // Escape '\' characters + r = QSqlDriver::formatValue(field, trimStrings); + r.replace(QLatin1String("\\"), QLatin1String("\\\\")); + break; + } + case QVariant::Bool: + if (field.value().toBool()) + r = QLatin1String("TRUE"); + else + r = QLatin1String("FALSE"); + break; + case QVariant::ByteArray: { + QByteArray ba(field.value().toByteArray()); + size_t len; +#if defined PG_VERSION_NUM && PG_VERSION_NUM-0 >= 80200 + unsigned char *data = PQescapeByteaConn(d->connection, (unsigned char*)ba.constData(), ba.size(), &len); +#else + unsigned char *data = PQescapeBytea((unsigned char*)ba.constData(), ba.size(), &len); +#endif + r += QLatin1Char('\''); + r += QLatin1String((const char*)data); + r += QLatin1Char('\''); + qPQfreemem(data); + break; + } + case QVariant::Double: { + double val = field.value().toDouble(); + if (isnan(val)) + r = QLatin1String("'NaN'"); + else { + int res = isinf(val); + if (res == 1) + r = QLatin1String("'Infinity'"); + else if (res == -1) + r = QLatin1String("'-Infinity'"); + else + r = QSqlDriver::formatValue(field, trimStrings); + } + break; + } + default: + r = QSqlDriver::formatValue(field, trimStrings); + break; + } + } + return r; +} + +QString QPSQLDriver::escapeIdentifier(const QString &identifier, IdentifierType) const +{ + QString res = identifier; + if(!identifier.isEmpty() && !identifier.startsWith(QLatin1Char('"')) && !identifier.endsWith(QLatin1Char('"')) ) { + res.replace(QLatin1Char('"'), QLatin1String("\"\"")); + res.prepend(QLatin1Char('"')).append(QLatin1Char('"')); + res.replace(QLatin1Char('.'), QLatin1String("\".\"")); + } + return res; +} + +bool QPSQLDriver::isOpen() const +{ + return PQstatus(d->connection) == CONNECTION_OK; +} + +QPSQLDriver::Protocol QPSQLDriver::protocol() const +{ + return d->pro; +} + +bool QPSQLDriver::subscribeToNotificationImplementation(const QString &name) +{ + if (!isOpen()) { + qWarning("QPSQLDriver::subscribeToNotificationImplementation: database not open."); + return false; + } + + if (d->seid.contains(name)) { + qWarning("QPSQLDriver::subscribeToNotificationImplementation: already subscribing to '%s'.", + qPrintable(name)); + return false; + } + + int socket = PQsocket(d->connection); + if (socket) { + QString query = QLatin1String("LISTEN ") + escapeIdentifier(name, QSqlDriver::TableName); + if (PQresultStatus(PQexec(d->connection, + d->isUtf8 ? query.toUtf8().constData() + : query.toLocal8Bit().constData()) + ) != PGRES_COMMAND_OK) { + setLastError(qMakeError(tr("Unable to subscribe"), QSqlError::StatementError, d)); + return false; + } + + if (!d->sn) { + d->sn = new QSocketNotifier(socket, QSocketNotifier::Read); + connect(d->sn, SIGNAL(activated(int)), this, SLOT(_q_handleNotification(int))); + } + } + + d->seid << name; + return true; +} + +bool QPSQLDriver::unsubscribeFromNotificationImplementation(const QString &name) +{ + if (!isOpen()) { + qWarning("QPSQLDriver::unsubscribeFromNotificationImplementation: database not open."); + return false; + } + + if (!d->seid.contains(name)) { + qWarning("QPSQLDriver::unsubscribeFromNotificationImplementation: not subscribed to '%s'.", + qPrintable(name)); + return false; + } + + QString query = QLatin1String("UNLISTEN ") + escapeIdentifier(name, QSqlDriver::TableName); + if (PQresultStatus(PQexec(d->connection, + d->isUtf8 ? query.toUtf8().constData() + : query.toLocal8Bit().constData()) + ) != PGRES_COMMAND_OK) { + setLastError(qMakeError(tr("Unable to unsubscribe"), QSqlError::StatementError, d)); + return false; + } + + d->seid.removeAll(name); + + if (d->seid.isEmpty()) { + disconnect(d->sn, SIGNAL(activated(int)), this, SLOT(_q_handleNotification(int))); + delete d->sn; + d->sn = 0; + } + + return true; +} + +QStringList QPSQLDriver::subscribedToNotificationsImplementation() const +{ + return d->seid; +} + +void QPSQLDriver::_q_handleNotification(int) +{ + PQconsumeInput(d->connection); + + PGnotify *notify = 0; + while((notify = PQnotifies(d->connection)) != 0) { + QString name(QLatin1String(notify->relname)); + if (d->seid.contains(name)) + emit notification(name); + else + qWarning("QPSQLDriver: received notification for '%s' which isn't subscribed to.", + qPrintable(name)); + + qPQfreemem(notify); + } +} + +QT_END_NAMESPACE