From c84d0fc96e5a36b88bdc82dface72ea698f29c87 Mon Sep 17 00:00:00 2001 From: Roland Hieber Date: Thu, 24 Oct 2013 18:30:29 +0200 Subject: [PATCH] new blag post: Splitting overly large hunks in patches --- ...litting-overly-large-hunks-in-patches.mdwn | 36 + .../edit-stages.mdwn | 2087 +++++++++++++++++ 2 files changed, 2123 insertions(+) create mode 100644 blag/post/splitting-overly-large-hunks-in-patches.mdwn create mode 100644 blag/post/splitting-overly-large-hunks-in-patches/edit-stages.mdwn diff --git a/blag/post/splitting-overly-large-hunks-in-patches.mdwn b/blag/post/splitting-overly-large-hunks-in-patches.mdwn new file mode 100644 index 0000000..809be3c --- /dev/null +++ b/blag/post/splitting-overly-large-hunks-in-patches.mdwn @@ -0,0 +1,36 @@ +[[!meta title="Splitting overly large hunks in patches"]] +[[!meta author="rohieb"]] +[[!meta license="CC-BY-SA 3.0"]] +[[!img defaults size=x200]] + +Today I stumbled over a lengthy patch on my harddisk. It was about half a year +old, and consisted of only one hunk, which was about 1000 lines in length. Most +of the contents were indentation changes from tabs to spaces, but I knew that +the patch contained a small useful portion, which I wanted to extract. What was +slightly more annoying was the fact the the patch did not apply cleanly to the +file it was supposed to change, and `patch` only applies hunks atomically, the +whole patch was rejected. + +Since I did not want to compare each of the lines in the patch visually and +decide whether they changed only whitespace, I tried to look for a way to split +the patch into smaller hunks. My first try was looking at the useful tool in the +[patchutils](http://cyberelk.net/tim/software/patchutils/) package, but none of +them did what I wanted, they only allowed me to split patches into single hunks +(but my patch already had only one hunk). + +But after a bit of googling, I found out that Emacs has a +[`diff-split-hunk`][emacs-dsh] command, so I installed Emacs (for the first time +in my life), opened my patch, selected Emacs' Diff mode with `M-x diff-mode`, +and split the patch into smaller hunks by pressing `C-x C-s` on appropriate +context lines. After saving, the patch applied cleanly except for two smaller +hunks, which I could easily identify as containing only whitespace changes. Then +I could compare my patched file with the original file, this time ignoring +whitespace changes with `diff -w`, and, voilà, I got the seven useful lines I +wanted. + +[emacs-dsh]: https://www.gnu.org/software/emacs/manual/html_node/emacs/Diff-Mode.html + +For illustration, see the different [[edit stages of my patch|edit-stages]] on a +separate page. + +[[!tag patch patchutils diff GNU Emacs QDjango C++ howto]] diff --git a/blag/post/splitting-overly-large-hunks-in-patches/edit-stages.mdwn b/blag/post/splitting-overly-large-hunks-in-patches/edit-stages.mdwn new file mode 100644 index 0000000..3a0fdab --- /dev/null +++ b/blag/post/splitting-overly-large-hunks-in-patches/edit-stages.mdwn @@ -0,0 +1,2087 @@ +[[!meta title="Edit stages"]] +[[!meta author="rohieb"]] +[[!meta license="GNU LGPL, v2.1 or later"]] +[[!toc]] + +This page shows the different edit stages of a 1000-line patch which mostly +consisted of white space changes in one single hunk, and which was subsequently +split into smaller hunks. See the [[related blog +post|../splitting-overly-large-hunks-in-patches]] for an explanation what was +done. + +The code below originates from the [QDjango][] project, which is released under +the terms of the [GNU Lesser General Public License][gnu-lgpl], version 2.1 or +later. + +[QDjango]: https://code.google.com/p/qdjango/ +[gnu-lgpl]: http://www.gnu.org/licenses/lgpl.html + +## The original patch + +…with far too much whitespace changes: + +[[!format diff """ +--- qdjango-0.2.6.orig/src/db/QDjangoMetaModel.cpp 2012-09-09 07:34:12.000000000 +0200 ++++ qdjango-0.2.6/src/db/QDjangoMetaModel.cpp 2012-09-09 08:35:54.000000000 +0200 +@@ -27,583 +27,590 @@ + class QDjangoMetaFieldPrivate : public QSharedData + { + public: +- QDjangoMetaFieldPrivate(); ++ QDjangoMetaFieldPrivate(); + +- bool autoIncrement; +- QString db_column; +- QString foreignModel; +- bool index; +- int maxLength; +- QByteArray name; +- bool null; +- QVariant::Type type; +- bool unique; ++ bool autoIncrement; ++ QString db_column; ++ QString foreignModel; ++ bool index; ++ int maxLength; ++ QByteArray name; ++ bool null; ++ QVariant::Type type; ++ bool unique; + }; + + QDjangoMetaFieldPrivate::QDjangoMetaFieldPrivate() +- : autoIncrement(false), +- index(false), +- maxLength(0), +- null(false), +- unique(false) ++ : autoIncrement(false), ++ index(false), ++ maxLength(0), ++ null(false), ++ unique(false) + { + } + + /*! +- Constructs a new QDjangoMetaField. ++ Constructs a new QDjangoMetaField. + */ + QDjangoMetaField::QDjangoMetaField() + { +- d = new QDjangoMetaFieldPrivate; ++ d = new QDjangoMetaFieldPrivate; + } + + /*! +- Constructs a copy of \a other. ++ Constructs a copy of \a other. + */ + QDjangoMetaField::QDjangoMetaField(const QDjangoMetaField &other) +- : d(other.d) ++ : d(other.d) + { + } + + /*! +- Destroys the meta field. ++ Destroys the meta field. + */ + QDjangoMetaField::~QDjangoMetaField() + { + } + + /*! +- Assigns \a other to this meta field. ++ Assigns \a other to this meta field. + */ + QDjangoMetaField& QDjangoMetaField::operator=(const QDjangoMetaField& other) + { +- d = other.d; +- return *this; ++ d = other.d; ++ return *this; + } + + /*! +- Returns the database column for this meta field. ++ Returns the database column for this meta field. + */ + QString QDjangoMetaField::column() const + { +- return d->db_column; ++ return d->db_column; + } + + /*! +- Returns true if this is a valid field. ++ Returns true if this is a valid field. + */ + bool QDjangoMetaField::isValid() const + { +- return !d->name.isEmpty(); ++ return !d->name.isEmpty(); + } + + /*! +- Returns name of this meta field. ++ Returns name of this meta field. + */ + QString QDjangoMetaField::name() const + { +- return QString::fromLatin1(d->name); ++ return QString::fromLatin1(d->name); + } + + /*! +- Transforms the given field value for database storage. ++ Transforms the given field value for database storage. + */ + QVariant QDjangoMetaField::toDatabase(const QVariant &value) const + { +- if (d->type == QVariant::String && !d->null && value.isNull()) +- return QString(""); +- else if (!d->foreignModel.isEmpty() && d->type == QVariant::Int && d->null && !value.toInt()) { +- // store 0 foreign key as NULL if the field is NULL +- return QVariant(); +- } else +- return value; ++ if (d->type == QVariant::String && !d->null && value.isNull()) ++ return QString(""); ++ else if (!d->foreignModel.isEmpty() && d->type == QVariant::Int && d->null && !value.toInt()) { ++ // store 0 foreign key as NULL if the field is NULL ++ return QVariant(); ++ } else ++ return value; + } + + static QMap parseOptions(const char *value) + { +- QMap options; +- QStringList items = QString::fromUtf8(value).split(' '); +- foreach (const QString &item, items) { +- QStringList assign = item.split('='); +- if (assign.size() == 2) { +- options[assign[0].toLower()] = assign[1]; +- } else { +- qWarning() << "Could not parse option" << item; +- } +- } +- return options; ++ QMap options; ++ QStringList items = QString::fromUtf8(value).split(' '); ++ foreach (const QString &item, items) { ++ QStringList assign = item.split('='); ++ if (assign.size() == 2) { ++ options[assign[0].toLower()] = assign[1]; ++ } else { ++ qWarning() << "Could not parse option" << item; ++ } ++ } ++ return options; + } + + class QDjangoMetaModelPrivate : public QSharedData + { + public: +- QList localFields; +- QMap foreignFields; +- QByteArray primaryKey; +- QString table; ++ QList localFields; ++ QMap foreignFields; ++ QByteArray primaryKey; ++ QString table; + }; + + /*! +- Constructs a new QDjangoMetaModel by inspecting the given \a model instance. ++ Constructs a new QDjangoMetaModel by inspecting the given \a model instance. + */ + QDjangoMetaModel::QDjangoMetaModel(const QObject *model) +- : d(new QDjangoMetaModelPrivate) ++ : d(new QDjangoMetaModelPrivate) + { +- if (!model) +- return; ++ if (!model) ++ return; + +- const QMetaObject* meta = model->metaObject(); +- d->table = QString(meta->className()).toLower().toLatin1(); ++ const QMetaObject* meta = model->metaObject(); ++ d->table = QString(meta->className()).toLower().toLatin1(); ++ ++ // parse table options ++ const int optionsIndex = meta->indexOfClassInfo("__meta__"); ++ if (optionsIndex >= 0) { ++ QMap options = parseOptions(meta->classInfo(optionsIndex).value()); ++ QMapIterator option(options); ++ while (option.hasNext()) { ++ option.next(); ++ if (option.key() == "db_table") ++ d->table = option.value(); ++ } ++ } ++ ++ const int count = meta->propertyCount(); ++ for(int i = QObject::staticMetaObject.propertyCount(); i < count; ++i) ++ { ++ QString typeName = meta->property(i).typeName(); ++ if (!qstrcmp(meta->property(i).name(), "pk")) ++ continue; ++ ++ // parse field options ++ bool autoIncrementOption = false; ++ QString dbColumnOption; ++ bool dbIndexOption = false; ++ bool ignoreFieldOption = false; ++ int maxLengthOption = 0; ++ bool primaryKeyOption = false; ++ bool nullOption = false; ++ bool uniqueOption = false; ++ const int infoIndex = meta->indexOfClassInfo(meta->property(i).name()); ++ if (infoIndex >= 0) ++ { ++ QMap options = parseOptions(meta->classInfo(infoIndex).value()); ++ QMapIterator option(options); ++ while (option.hasNext()) { ++ option.next(); ++ const QString value = option.value(); ++ if (option.key() == "auto_increment") ++ autoIncrementOption = (value.toLower() == "true" || value == "1"); ++ else if (option.key() == "db_column") ++ dbColumnOption = value; ++ else if (option.key() == "db_index") ++ dbIndexOption = (value.toLower() == "true" || value == "1"); ++ else if (option.key() == "ignore_field") ++ ignoreFieldOption = (value.toLower() == "true" || value == "1"); ++ else if (option.key() == "max_length") ++ maxLengthOption = value.toInt(); ++ else if (option.key() == "null") ++ nullOption = (value.toLower() == "true" || value == "1"); ++ else if (option.key() == "primary_key") ++ primaryKeyOption = (value.toLower() == "true" || value == "1"); ++ else if (option.key() == "unique") ++ uniqueOption = (value.toLower() == "true" || value == "1"); ++ } ++ } ++ ++ // ignore field ++ if (ignoreFieldOption) ++ continue; ++ ++ // foreign field ++ if (typeName.endsWith("*")) ++ { ++ const QByteArray fkName = meta->property(i).name(); ++ const QString fkModel = typeName.left(typeName.size() - 1); ++ d->foreignFields.insert(fkName, fkModel); ++ ++ QDjangoMetaField field; ++ field.d->name = fkName + "_id"; ++ // FIXME : the key is not necessarily an INTEGER field, we should ++ // probably perform a lookup on the foreign model, but are we sure ++ // it is already registered? ++ field.d->type = QVariant::Int; ++ field.d->foreignModel = fkModel; ++ field.d->db_column = dbColumnOption.isEmpty() ? QString::fromLatin1(field.d->name) : dbColumnOption; ++ field.d->index = true; ++ field.d->null = nullOption; ++ if (primaryKeyOption) { ++ field.d->autoIncrement = autoIncrementOption; ++ field.d->unique = true; ++ d->primaryKey = field.d->name; ++ } else if (uniqueOption) { ++ field.d->unique = true; ++ } ++ d->localFields << field; ++ continue; ++ } ++ ++ // local field ++ QDjangoMetaField field; ++ field.d->name = meta->property(i).name(); ++ field.d->type = meta->property(i).type(); ++ field.d->db_column = dbColumnOption.isEmpty() ? QString::fromLatin1(field.d->name) : dbColumnOption; ++ field.d->index = dbIndexOption; ++ field.d->maxLength = maxLengthOption; ++ field.d->null = nullOption; ++ if (primaryKeyOption) { ++ field.d->autoIncrement = autoIncrementOption; ++ field.d->unique = true; ++ d->primaryKey = field.d->name; ++ } else if (uniqueOption) { ++ field.d->unique = true; ++ } ++ ++ d->localFields << field; ++ } ++ ++ // automatic primary key ++ if (d->primaryKey.isEmpty()) { ++ QDjangoMetaField field; ++ field.d->name = "id"; ++ field.d->type = QVariant::Int; ++ field.d->db_column = "id"; ++ field.d->autoIncrement = true; ++ field.d->unique = true; ++ d->localFields.prepend(field); ++ d->primaryKey = field.d->name; ++ } + +- // parse table options +- const int optionsIndex = meta->indexOfClassInfo("__meta__"); +- if (optionsIndex >= 0) { +- QMap options = parseOptions(meta->classInfo(optionsIndex).value()); +- QMapIterator option(options); +- while (option.hasNext()) { +- option.next(); +- if (option.key() == "db_table") +- d->table = option.value(); +- } +- } +- +- const int count = meta->propertyCount(); +- for(int i = QObject::staticMetaObject.propertyCount(); i < count; ++i) +- { +- QString typeName = meta->property(i).typeName(); +- if (!qstrcmp(meta->property(i).name(), "pk")) +- continue; +- +- // parse field options +- bool autoIncrementOption = false; +- QString dbColumnOption; +- bool dbIndexOption = false; +- bool ignoreFieldOption = false; +- int maxLengthOption = 0; +- bool primaryKeyOption = false; +- bool nullOption = false; +- bool uniqueOption = false; +- const int infoIndex = meta->indexOfClassInfo(meta->property(i).name()); +- if (infoIndex >= 0) +- { +- QMap options = parseOptions(meta->classInfo(infoIndex).value()); +- QMapIterator option(options); +- while (option.hasNext()) { +- option.next(); +- const QString value = option.value(); +- if (option.key() == "auto_increment") +- autoIncrementOption = (value.toLower() == "true" || value == "1"); +- else if (option.key() == "db_column") +- dbColumnOption = value; +- else if (option.key() == "db_index") +- dbIndexOption = (value.toLower() == "true" || value == "1"); +- else if (option.key() == "ignore_field") +- ignoreFieldOption = (value.toLower() == "true" || value == "1"); +- else if (option.key() == "max_length") +- maxLengthOption = value.toInt(); +- else if (option.key() == "null") +- nullOption = (value.toLower() == "true" || value == "1"); +- else if (option.key() == "primary_key") +- primaryKeyOption = (value.toLower() == "true" || value == "1"); +- else if (option.key() == "unique") +- uniqueOption = (value.toLower() == "true" || value == "1"); +- } +- } +- +- // ignore field +- if (ignoreFieldOption) +- continue; +- +- // foreign field +- if (typeName.endsWith("*")) +- { +- const QByteArray fkName = meta->property(i).name(); +- const QString fkModel = typeName.left(typeName.size() - 1); +- d->foreignFields.insert(fkName, fkModel); +- +- QDjangoMetaField field; +- field.d->name = fkName + "_id"; +- // FIXME : the key is not necessarily an INTEGER field, we should +- // probably perform a lookup on the foreign model, but are we sure +- // it is already registered? +- field.d->type = QVariant::Int; +- field.d->foreignModel = fkModel; +- field.d->db_column = dbColumnOption.isEmpty() ? QString::fromLatin1(field.d->name) : dbColumnOption; +- field.d->index = true; +- field.d->null = nullOption; +- d->localFields << field; +- continue; +- } +- +- // local field +- QDjangoMetaField field; +- field.d->name = meta->property(i).name(); +- field.d->type = meta->property(i).type(); +- field.d->db_column = dbColumnOption.isEmpty() ? QString::fromLatin1(field.d->name) : dbColumnOption; +- field.d->index = dbIndexOption; +- field.d->maxLength = maxLengthOption; +- field.d->null = nullOption; +- if (primaryKeyOption) { +- field.d->autoIncrement = autoIncrementOption; +- field.d->unique = true; +- d->primaryKey = field.d->name; +- } else if (uniqueOption) { +- field.d->unique = true; +- } +- +- d->localFields << field; +- } +- +- // automatic primary key +- if (d->primaryKey.isEmpty()) { +- QDjangoMetaField field; +- field.d->name = "id"; +- field.d->type = QVariant::Int; +- field.d->db_column = "id"; +- field.d->autoIncrement = true; +- field.d->unique = true; +- d->localFields.prepend(field); +- d->primaryKey = field.d->name; +- } +- + } + + /*! +- Constructs a copy of \a other. ++ Constructs a copy of \a other. + */ + QDjangoMetaModel::QDjangoMetaModel(const QDjangoMetaModel &other) +- : d(other.d) ++ : d(other.d) + { + } + + /*! +- Destroys the meta model. ++ Destroys the meta model. + */ + QDjangoMetaModel::~QDjangoMetaModel() + { + } + + /*! +- Assigns \a other to this meta model. ++ Assigns \a other to this meta model. + */ + QDjangoMetaModel& QDjangoMetaModel::operator=(const QDjangoMetaModel& other) + { +- d = other.d; +- return *this; ++ d = other.d; ++ return *this; + } + + /*! +- Creates the database table for this QDjangoMetaModel. ++ Creates the database table for this QDjangoMetaModel. + */ + bool QDjangoMetaModel::createTable() const + { +- QSqlDatabase db = QDjango::database(); +- QSqlDriver *driver = db.driver(); +- const QString driverName = db.driverName(); +- +- QStringList propSql; +- const QString quotedTable = db.driver()->escapeIdentifier(d->table, QSqlDriver::TableName); +- foreach (const QDjangoMetaField &field, d->localFields) +- { +- QString fieldSql = driver->escapeIdentifier(field.column(), QSqlDriver::FieldName); +- switch (field.d->type) { +- case QVariant::Bool: +- fieldSql += " BOOLEAN"; +- break; +- case QVariant::ByteArray: +- if (driverName == QLatin1String("QPSQL")) +- fieldSql += " BYTEA"; +- else { +- fieldSql += " BLOB"; +- if (field.d->maxLength > 0) +- fieldSql += QString("(%1)").arg(field.d->maxLength); +- } +- break; +- case QVariant::Date: +- fieldSql += " DATE"; +- break; +- case QVariant::DateTime: +- if (driverName == QLatin1String("QPSQL")) +- fieldSql += " TIMESTAMP"; +- else +- fieldSql += " DATETIME"; +- break; +- case QVariant::Double: +- fieldSql += " REAL"; +- break; +- case QVariant::Int: +- fieldSql += " INTEGER"; +- break; +- case QVariant::LongLong: +- fieldSql += " BIGINT"; +- break; +- case QVariant::String: +- if (field.d->maxLength > 0) +- fieldSql += QString(" VARCHAR(%1)").arg(field.d->maxLength); +- else +- fieldSql += " TEXT"; +- break; +- case QVariant::Time: +- fieldSql += " TIME"; +- break; +- default: +- qWarning() << "Unhandled type" << field.d->type << "for property" << field.d->name; +- continue; +- } +- +- if (!field.d->null) +- fieldSql += " NOT NULL"; +- +- // primary key +- if (field.d->name == d->primaryKey) +- fieldSql += " PRIMARY KEY"; +- +- // auto-increment is backend specific +- if (field.d->autoIncrement) { +- if (driverName == QLatin1String("QSQLITE") || +- driverName == QLatin1String("QSQLITE2")) +- fieldSql += QLatin1String(" AUTOINCREMENT"); +- else if (driverName == QLatin1String("QMYSQL")) +- fieldSql += QLatin1String(" AUTO_INCREMENT"); +- else if (driverName == QLatin1String("QPSQL")) +- fieldSql = driver->escapeIdentifier(field.column(), QSqlDriver::FieldName) + " SERIAL PRIMARY KEY"; +- } +- +- // foreign key +- if (!field.d->foreignModel.isEmpty()) +- { +- const QDjangoMetaModel foreignMeta = QDjango::metaModel(field.d->foreignModel); +- const QDjangoMetaField foreignField = foreignMeta.localField("pk"); +- fieldSql += QString(" REFERENCES %1 (%2)").arg( +- driver->escapeIdentifier(foreignMeta.d->table, QSqlDriver::TableName), +- driver->escapeIdentifier(foreignField.column(), QSqlDriver::FieldName)); +- } +- propSql << fieldSql; +- } +- +- // create table +- QDjangoQuery createQuery(db); +- if (!createQuery.exec(QString("CREATE TABLE %1 (%2)").arg( +- quotedTable, +- propSql.join(", ")))) +- return false; +- +- // create indices +- foreach (const QDjangoMetaField &field, d->localFields) { +- if (field.d->index && !field.d->unique) { +- const QString indexName = d->table + "_" + field.column(); +- if (!createQuery.exec(QString("CREATE INDEX %1 ON %2 (%3)").arg( +- // FIXME : how should we escape an index name? +- driver->escapeIdentifier(indexName, QSqlDriver::FieldName), +- quotedTable, +- driver->escapeIdentifier(field.column(), QSqlDriver::FieldName)))) +- return false; +- } +- } ++ QSqlDatabase db = QDjango::database(); ++ QSqlDriver *driver = db.driver(); ++ const QString driverName = db.driverName(); ++ ++ QStringList propSql; ++ const QString quotedTable = db.driver()->escapeIdentifier(d->table, QSqlDriver::TableName); ++ foreach (const QDjangoMetaField &field, d->localFields) ++ { ++ QString fieldSql = driver->escapeIdentifier(field.column(), QSqlDriver::FieldName); ++ switch (field.d->type) { ++ case QVariant::Bool: ++ fieldSql += " BOOLEAN"; ++ break; ++ case QVariant::ByteArray: ++ if (driverName == QLatin1String("QPSQL")) ++ fieldSql += " BYTEA"; ++ else { ++ fieldSql += " BLOB"; ++ if (field.d->maxLength > 0) ++ fieldSql += QString("(%1)").arg(field.d->maxLength); ++ } ++ break; ++ case QVariant::Date: ++ fieldSql += " DATE"; ++ break; ++ case QVariant::DateTime: ++ if (driverName == QLatin1String("QPSQL")) ++ fieldSql += " TIMESTAMP"; ++ else ++ fieldSql += " DATETIME"; ++ break; ++ case QVariant::Double: ++ fieldSql += " REAL"; ++ break; ++ case QVariant::Int: ++ fieldSql += " INTEGER"; ++ break; ++ case QVariant::LongLong: ++ fieldSql += " BIGINT"; ++ break; ++ case QVariant::String: ++ if (field.d->maxLength > 0) ++ fieldSql += QString(" VARCHAR(%1)").arg(field.d->maxLength); ++ else ++ fieldSql += " TEXT"; ++ break; ++ case QVariant::Time: ++ fieldSql += " TIME"; ++ break; ++ default: ++ qWarning() << "Unhandled type" << field.d->type << "for property" << field.d->name; ++ continue; ++ } ++ ++ if (!field.d->null) ++ fieldSql += " NOT NULL"; ++ ++ // primary key ++ if (field.d->name == d->primaryKey) ++ fieldSql += " PRIMARY KEY"; ++ ++ // auto-increment is backend specific ++ if (field.d->autoIncrement) { ++ if (driverName == QLatin1String("QSQLITE") || ++ driverName == QLatin1String("QSQLITE2")) ++ fieldSql += QLatin1String(" AUTOINCREMENT"); ++ else if (driverName == QLatin1String("QMYSQL")) ++ fieldSql += QLatin1String(" AUTO_INCREMENT"); ++ else if (driverName == QLatin1String("QPSQL")) ++ fieldSql = driver->escapeIdentifier(field.column(), QSqlDriver::FieldName) + " SERIAL PRIMARY KEY"; ++ } ++ ++ // foreign key ++ if (!field.d->foreignModel.isEmpty()) ++ { ++ const QDjangoMetaModel foreignMeta = QDjango::metaModel(field.d->foreignModel); ++ const QDjangoMetaField foreignField = foreignMeta.localField("pk"); ++ fieldSql += QString(" REFERENCES %1 (%2)").arg( ++ driver->escapeIdentifier(foreignMeta.d->table, QSqlDriver::TableName), ++ driver->escapeIdentifier(foreignField.column(), QSqlDriver::FieldName)); ++ } ++ propSql << fieldSql; ++ } ++ ++ // create table ++ QDjangoQuery createQuery(db); ++ if (!createQuery.exec(QString("CREATE TABLE %1 (%2)").arg( ++ quotedTable, ++ propSql.join(", ")))) ++ return false; ++ ++ // create indices ++ foreach (const QDjangoMetaField &field, d->localFields) { ++ if (field.d->index && !field.d->unique) { ++ const QString indexName = d->table + "_" + field.column(); ++ if (!createQuery.exec(QString("CREATE INDEX %1 ON %2 (%3)").arg( ++ // FIXME : how should we escape an index name? ++ driver->escapeIdentifier(indexName, QSqlDriver::FieldName), ++ quotedTable, ++ driver->escapeIdentifier(field.column(), QSqlDriver::FieldName)))) ++ return false; ++ } ++ } + +- return true; ++ return true; + } + + /*! +- Drops the database table for this QDjangoMetaModel. ++ Drops the database table for this QDjangoMetaModel. + */ + bool QDjangoMetaModel::dropTable() const + { +- QSqlDatabase db = QDjango::database(); ++ QSqlDatabase db = QDjango::database(); + +- QDjangoQuery query(db); +- return query.exec(QString("DROP TABLE %1").arg( +- db.driver()->escapeIdentifier(d->table, QSqlDriver::TableName))); ++ QDjangoQuery query(db); ++ return query.exec(QString("DROP TABLE %1").arg( ++ db.driver()->escapeIdentifier(d->table, QSqlDriver::TableName))); + } + + /*! +- Retrieves the QDjangoModel pointed to by the given foreign-key. ++ Retrieves the QDjangoModel pointed to by the given foreign-key. + +- \param model +- \param name ++ \param model ++ \param name + */ + QObject *QDjangoMetaModel::foreignKey(const QObject *model, const char *name) const + { +- const QByteArray prop(name); +- QObject *foreign = model->property(prop + "_ptr").value(); +- if (!foreign) +- return 0; +- +- // if the foreign object was not loaded yet, do it now +- const QString foreignClass = d->foreignFields[prop]; +- const QDjangoMetaModel foreignMeta = QDjango::metaModel(foreignClass); +- const QVariant foreignPk = model->property(prop + "_id"); +- if (foreign->property(foreignMeta.primaryKey()) != foreignPk) +- { +- QDjangoQuerySetPrivate qs(foreignClass); +- qs.addFilter(QDjangoWhere("pk", QDjangoWhere::Equals, foreignPk)); +- qs.sqlFetch(); +- if (qs.properties.size() != 1 || !qs.sqlLoad(foreign, 0)) +- return 0; +- } +- return foreign; ++ const QByteArray prop(name); ++ QObject *foreign = model->property(prop + "_ptr").value(); ++ if (!foreign) ++ return 0; ++ ++ // if the foreign object was not loaded yet, do it now ++ const QString foreignClass = d->foreignFields[prop]; ++ const QDjangoMetaModel foreignMeta = QDjango::metaModel(foreignClass); ++ const QVariant foreignPk = model->property(prop + "_id"); ++ if (foreign->property(foreignMeta.primaryKey()) != foreignPk) ++ { ++ QDjangoQuerySetPrivate qs(foreignClass); ++ qs.addFilter(QDjangoWhere("pk", QDjangoWhere::Equals, foreignPk)); ++ qs.sqlFetch(); ++ if (qs.properties.size() != 1 || !qs.sqlLoad(foreign, 0)) ++ return 0; ++ } ++ return foreign; + } + + /*! +- Sets the QDjangoModel pointed to by the given foreign-key. +- +- \param model +- \param name +- \param value ++ Sets the QDjangoModel pointed to by the given foreign-key. ++ ++ \param model ++ \param name ++ \param value + +- \note The \c model will take ownership of the given \c value. ++ \note The \c model will take ownership of the given \c value. + */ + void QDjangoMetaModel::setForeignKey(QObject *model, const char *name, QObject *value) const + { +- const QByteArray prop(name); +- QObject *old = model->property(prop + "_ptr").value(); +- if (old == value) +- return; +- +- // store the new pointer and update the foreign key +- model->setProperty(prop + "_ptr", qVariantFromValue(value)); +- if (value) +- { +- const QDjangoMetaModel foreignMeta = QDjango::metaModel(d->foreignFields[prop]); +- model->setProperty(prop + "_id", value->property(foreignMeta.primaryKey())); +- } else { +- model->setProperty(prop + "_id", QVariant()); +- } ++ const QByteArray prop(name); ++ QObject *old = model->property(prop + "_ptr").value(); ++ if (old == value) ++ return; ++ ++ // store the new pointer and update the foreign key ++ model->setProperty(prop + "_ptr", qVariantFromValue(value)); ++ if (value) ++ { ++ const QDjangoMetaModel foreignMeta = QDjango::metaModel(d->foreignFields[prop]); ++ model->setProperty(prop + "_id", value->property(foreignMeta.primaryKey())); ++ } else { ++ model->setProperty(prop + "_id", QVariant()); ++ } + } + + /*! +- Loads the given properties into a \a model instance. ++ Loads the given properties into a \a model instance. + */ + void QDjangoMetaModel::load(QObject *model, const QVariantList &properties, int &pos) const + { +- // process local fields +- foreach (const QDjangoMetaField &field, d->localFields) +- model->setProperty(field.d->name, properties.at(pos++)); +- +- // process foreign fields +- if (pos >= properties.size()) +- return; +- foreach (const QByteArray &fkName, d->foreignFields.keys()) +- { +- QObject *object = model->property(fkName + "_ptr").value(); +- if (object) +- { +- const QDjangoMetaModel foreignMeta = QDjango::metaModel(d->foreignFields[fkName]); +- foreignMeta.load(object, properties, pos); +- } +- } ++ // process local fields ++ foreach (const QDjangoMetaField &field, d->localFields) ++ model->setProperty(field.d->name, properties.at(pos++)); ++ ++ // process foreign fields ++ if (pos >= properties.size()) ++ return; ++ foreach (const QByteArray &fkName, d->foreignFields.keys()) ++ { ++ QObject *object = model->property(fkName + "_ptr").value(); ++ if (object) ++ { ++ const QDjangoMetaModel foreignMeta = QDjango::metaModel(d->foreignFields[fkName]); ++ foreignMeta.load(object, properties, pos); ++ } ++ } + } + + /*! +- Returns the foreign field mapping. ++ Returns the foreign field mapping. + */ + QMap QDjangoMetaModel::foreignFields() const + { +- return d->foreignFields; ++ return d->foreignFields; + } + + /*! +- Return the local field with the specified \a name. ++ Return the local field with the specified \a name. + */ + QDjangoMetaField QDjangoMetaModel::localField(const QString &name) const + { +- const QString fieldName = (name == "pk") ? d->primaryKey : name; +- foreach (const QDjangoMetaField &field, d->localFields) { +- if (field.d->name == fieldName) +- return field; +- } +- return QDjangoMetaField(); ++ const QString fieldName = (name == "pk") ? d->primaryKey : name; ++ foreach (const QDjangoMetaField &field, d->localFields) { ++ if (field.d->name == fieldName) ++ return field; ++ } ++ return QDjangoMetaField(); + } + + /*! +- Returns the list of local fields. ++ Returns the list of local fields. + */ + QList QDjangoMetaModel::localFields() const + { +- return d->localFields; ++ return d->localFields; + } + + /*! +- Returns the name of the primary key for the current QDjangoMetaModel. ++ Returns the name of the primary key for the current QDjangoMetaModel. + */ + QByteArray QDjangoMetaModel::primaryKey() const + { +- return d->primaryKey; ++ return d->primaryKey; + } + + /*! +- Returns the name of the database table. ++ Returns the name of the database table. + */ + QString QDjangoMetaModel::table() const + { +- return d->table; ++ return d->table; + } + + /*! +- Removes the given \a model instance from the database. ++ Removes the given \a model instance from the database. + */ + bool QDjangoMetaModel::remove(QObject *model) const + { +- const QVariant pk = model->property(d->primaryKey); +- QDjangoQuerySetPrivate qs(model->metaObject()->className()); +- qs.addFilter(QDjangoWhere("pk", QDjangoWhere::Equals, pk)); +- return qs.sqlDelete(); ++ const QVariant pk = model->property(d->primaryKey); ++ QDjangoQuerySetPrivate qs(model->metaObject()->className()); ++ qs.addFilter(QDjangoWhere("pk", QDjangoWhere::Equals, pk)); ++ return qs.sqlDelete(); + } + + /*! +- Saves the given \a model instance to the database. ++ Saves the given \a model instance to the database. + +- \return true if saving succeeded, false otherwise ++ \return true if saving succeeded, false otherwise + */ + bool QDjangoMetaModel::save(QObject *model) const + { +- // find primary key +- const QDjangoMetaField primaryKey = localField("pk"); +- const QVariant pk = model->property(d->primaryKey); +- if (!pk.isNull() && !(primaryKey.d->type == QVariant::Int && !pk.toInt())) +- { +- QSqlDatabase db = QDjango::database(); +- QDjangoQuery query(db); +- query.prepare(QString("SELECT 1 AS a FROM %1 WHERE %2 = ?").arg( +- db.driver()->escapeIdentifier(d->table, QSqlDriver::FieldName), +- db.driver()->escapeIdentifier(primaryKey.column(), QSqlDriver::FieldName))); +- query.addBindValue(pk); +- if (query.exec() && query.next()) +- { +- // prepare data +- QVariantMap fields; +- foreach (const QDjangoMetaField &field, d->localFields) { +- if (field.d->name != d->primaryKey) { +- const QVariant value = model->property(field.d->name); +- fields.insert(field.d->name, field.toDatabase(value)); +- } +- } +- +- // perform UPDATE +- QDjangoQuerySetPrivate qs(model->metaObject()->className()); +- qs.addFilter(QDjangoWhere("pk", QDjangoWhere::Equals, pk)); +- return qs.sqlUpdate(fields) != -1; +- } +- } +- +- // prepare data +- QVariantMap fields; +- foreach (const QDjangoMetaField &field, d->localFields) { +- if (!field.d->autoIncrement) { +- const QVariant value = model->property(field.d->name); +- fields.insert(field.name(), field.toDatabase(value)); +- } +- } +- +- // perform INSERT +- QVariant insertId; +- QDjangoQuerySetPrivate qs(model->metaObject()->className()); +- if (!qs.sqlInsert(fields, &insertId)) +- return false; +- +- // fetch autoincrement pk +- if (primaryKey.d->autoIncrement) +- model->setProperty(d->primaryKey, insertId); +- return true; ++ // find primary key ++ const QDjangoMetaField primaryKey = localField("pk"); ++ const QVariant pk = model->property(d->primaryKey); ++ if (!pk.isNull() && !(primaryKey.d->type == QVariant::Int && !pk.toInt())) ++ { ++ QSqlDatabase db = QDjango::database(); ++ QDjangoQuery query(db); ++ query.prepare(QString("SELECT 1 AS a FROM %1 WHERE %2 = ?").arg( ++ db.driver()->escapeIdentifier(d->table, QSqlDriver::FieldName), ++ db.driver()->escapeIdentifier(primaryKey.column(), QSqlDriver::FieldName))); ++ query.addBindValue(pk); ++ if (query.exec() && query.next()) ++ { ++ // prepare data ++ QVariantMap fields; ++ foreach (const QDjangoMetaField &field, d->localFields) { ++ if (field.d->name != d->primaryKey) { ++ const QVariant value = model->property(field.d->name); ++ fields.insert(field.d->name, field.toDatabase(value)); ++ } ++ } ++ ++ // perform UPDATE ++ QDjangoQuerySetPrivate qs(model->metaObject()->className()); ++ qs.addFilter(QDjangoWhere("pk", QDjangoWhere::Equals, pk)); ++ return qs.sqlUpdate(fields) != -1; ++ } ++ } ++ ++ // prepare data ++ QVariantMap fields; ++ foreach (const QDjangoMetaField &field, d->localFields) { ++ if (!field.d->autoIncrement) { ++ const QVariant value = model->property(field.d->name); ++ fields.insert(field.name(), field.toDatabase(value)); ++ } ++ } ++ ++ // perform INSERT ++ QVariant insertId; ++ QDjangoQuerySetPrivate qs(model->metaObject()->className()); ++ if (!qs.sqlInsert(fields, &insertId)) ++ return false; ++ ++ // fetch autoincrement pk ++ if (primaryKey.d->autoIncrement) ++ model->setProperty(d->primaryKey, insertId); ++ return true; + } +"""]] + + +## After editing with Emacs + +Note the additional `@@ ... @@` lines: +[[!format diff """ +--- qdjango-0.2.6.orig/src/db/QDjangoMetaModel.cpp 2012-09-09 07:34:12.000000000 +0200 ++++ qdjango-0.2.6/src/db/QDjangoMetaModel.cpp 2012-09-09 08:35:54.000000000 +0200 +@@ -27,15 +27,15 @@ + class QDjangoMetaFieldPrivate : public QSharedData + { + public: +- QDjangoMetaFieldPrivate(); ++ QDjangoMetaFieldPrivate(); + +- bool autoIncrement; +- QString db_column; +- QString foreignModel; +- bool index; +- int maxLength; +- QByteArray name; +- bool null; +- QVariant::Type type; +- bool unique; ++ bool autoIncrement; ++ QString db_column; ++ QString foreignModel; ++ bool index; ++ int maxLength; ++ QByteArray name; ++ bool null; ++ QVariant::Type type; ++ bool unique; + }; +@@ -42,9 +42,9 @@ + + QDjangoMetaFieldPrivate::QDjangoMetaFieldPrivate() +- : autoIncrement(false), +- index(false), +- maxLength(0), +- null(false), +- unique(false) ++ : autoIncrement(false), ++ index(false), ++ maxLength(0), ++ null(false), ++ unique(false) + { + } +@@ -51,8 +51,8 @@ + + /*! +- Constructs a new QDjangoMetaField. ++ Constructs a new QDjangoMetaField. + */ + QDjangoMetaField::QDjangoMetaField() + { +- d = new QDjangoMetaFieldPrivate; ++ d = new QDjangoMetaFieldPrivate; + } +@@ -59,8 +59,8 @@ + + /*! +- Constructs a copy of \a other. ++ Constructs a copy of \a other. + */ + QDjangoMetaField::QDjangoMetaField(const QDjangoMetaField &other) +- : d(other.d) ++ : d(other.d) + { + } +@@ -67,7 +67,7 @@ + + /*! +- Destroys the meta field. ++ Destroys the meta field. + */ + QDjangoMetaField::~QDjangoMetaField() + { + } +@@ -74,9 +74,9 @@ + + /*! +- Assigns \a other to this meta field. ++ Assigns \a other to this meta field. + */ + QDjangoMetaField& QDjangoMetaField::operator=(const QDjangoMetaField& other) + { +- d = other.d; +- return *this; ++ d = other.d; ++ return *this; + } +@@ -83,8 +83,8 @@ + + /*! +- Returns the database column for this meta field. ++ Returns the database column for this meta field. + */ + QString QDjangoMetaField::column() const + { +- return d->db_column; ++ return d->db_column; + } +@@ -91,8 +91,8 @@ + + /*! +- Returns true if this is a valid field. ++ Returns true if this is a valid field. + */ + bool QDjangoMetaField::isValid() const + { +- return !d->name.isEmpty(); ++ return !d->name.isEmpty(); + } +@@ -99,8 +99,8 @@ + + /*! +- Returns name of this meta field. ++ Returns name of this meta field. + */ + QString QDjangoMetaField::name() const + { +- return QString::fromLatin1(d->name); ++ return QString::fromLatin1(d->name); + } +@@ -107,14 +107,14 @@ + + /*! +- Transforms the given field value for database storage. ++ Transforms the given field value for database storage. + */ + QVariant QDjangoMetaField::toDatabase(const QVariant &value) const + { +- if (d->type == QVariant::String && !d->null && value.isNull()) +- return QString(""); +- else if (!d->foreignModel.isEmpty() && d->type == QVariant::Int && d->null && !value.toInt()) { +- // store 0 foreign key as NULL if the field is NULL +- return QVariant(); +- } else +- return value; ++ if (d->type == QVariant::String && !d->null && value.isNull()) ++ return QString(""); ++ else if (!d->foreignModel.isEmpty() && d->type == QVariant::Int && d->null && !value.toInt()) { ++ // store 0 foreign key as NULL if the field is NULL ++ return QVariant(); ++ } else ++ return value; + } +@@ -121,15 +121,15 @@ + + static QMap parseOptions(const char *value) + { +- QMap options; +- QStringList items = QString::fromUtf8(value).split(' '); +- foreach (const QString &item, items) { +- QStringList assign = item.split('='); +- if (assign.size() == 2) { +- options[assign[0].toLower()] = assign[1]; +- } else { +- qWarning() << "Could not parse option" << item; +- } +- } +- return options; ++ QMap options; ++ QStringList items = QString::fromUtf8(value).split(' '); ++ foreach (const QString &item, items) { ++ QStringList assign = item.split('='); ++ if (assign.size() == 2) { ++ options[assign[0].toLower()] = assign[1]; ++ } else { ++ qWarning() << "Could not parse option" << item; ++ } ++ } ++ return options; + } +@@ -136,12 +136,12 @@ + + class QDjangoMetaModelPrivate : public QSharedData + { + public: +- QList localFields; +- QMap foreignFields; +- QByteArray primaryKey; +- QString table; ++ QList localFields; ++ QMap foreignFields; ++ QByteArray primaryKey; ++ QString table; + }; + + /*! +- Constructs a new QDjangoMetaModel by inspecting the given \a model instance. ++ Constructs a new QDjangoMetaModel by inspecting the given \a model instance. +@@ -148,141 +148,148 @@ + */ + QDjangoMetaModel::QDjangoMetaModel(const QObject *model) +- : d(new QDjangoMetaModelPrivate) ++ : d(new QDjangoMetaModelPrivate) + { +- if (!model) +- return; ++ if (!model) ++ return; + +- const QMetaObject* meta = model->metaObject(); +- d->table = QString(meta->className()).toLower().toLatin1(); ++ const QMetaObject* meta = model->metaObject(); ++ d->table = QString(meta->className()).toLower().toLatin1(); ++ ++ // parse table options ++ const int optionsIndex = meta->indexOfClassInfo("__meta__"); ++ if (optionsIndex >= 0) { ++ QMap options = parseOptions(meta->classInfo(optionsIndex).value()); ++ QMapIterator option(options); ++ while (option.hasNext()) { ++ option.next(); ++ if (option.key() == "db_table") ++ d->table = option.value(); ++ } ++ } ++ ++ const int count = meta->propertyCount(); ++ for(int i = QObject::staticMetaObject.propertyCount(); i < count; ++i) ++ { ++ QString typeName = meta->property(i).typeName(); ++ if (!qstrcmp(meta->property(i).name(), "pk")) ++ continue; ++ ++ // parse field options ++ bool autoIncrementOption = false; ++ QString dbColumnOption; ++ bool dbIndexOption = false; ++ bool ignoreFieldOption = false; ++ int maxLengthOption = 0; ++ bool primaryKeyOption = false; ++ bool nullOption = false; ++ bool uniqueOption = false; ++ const int infoIndex = meta->indexOfClassInfo(meta->property(i).name()); ++ if (infoIndex >= 0) ++ { ++ QMap options = parseOptions(meta->classInfo(infoIndex).value()); ++ QMapIterator option(options); ++ while (option.hasNext()) { ++ option.next(); ++ const QString value = option.value(); ++ if (option.key() == "auto_increment") ++ autoIncrementOption = (value.toLower() == "true" || value == "1"); ++ else if (option.key() == "db_column") ++ dbColumnOption = value; ++ else if (option.key() == "db_index") ++ dbIndexOption = (value.toLower() == "true" || value == "1"); ++ else if (option.key() == "ignore_field") ++ ignoreFieldOption = (value.toLower() == "true" || value == "1"); ++ else if (option.key() == "max_length") ++ maxLengthOption = value.toInt(); ++ else if (option.key() == "null") ++ nullOption = (value.toLower() == "true" || value == "1"); ++ else if (option.key() == "primary_key") ++ primaryKeyOption = (value.toLower() == "true" || value == "1"); ++ else if (option.key() == "unique") ++ uniqueOption = (value.toLower() == "true" || value == "1"); ++ } ++ } ++ ++ // ignore field ++ if (ignoreFieldOption) ++ continue; ++ ++ // foreign field ++ if (typeName.endsWith("*")) ++ { ++ const QByteArray fkName = meta->property(i).name(); ++ const QString fkModel = typeName.left(typeName.size() - 1); ++ d->foreignFields.insert(fkName, fkModel); ++ ++ QDjangoMetaField field; ++ field.d->name = fkName + "_id"; ++ // FIXME : the key is not necessarily an INTEGER field, we should ++ // probably perform a lookup on the foreign model, but are we sure ++ // it is already registered? ++ field.d->type = QVariant::Int; ++ field.d->foreignModel = fkModel; ++ field.d->db_column = dbColumnOption.isEmpty() ? QString::fromLatin1(field.d->name) : dbColumnOption; ++ field.d->index = true; ++ field.d->null = nullOption; ++ if (primaryKeyOption) { ++ field.d->autoIncrement = autoIncrementOption; ++ field.d->unique = true; ++ d->primaryKey = field.d->name; ++ } else if (uniqueOption) { ++ field.d->unique = true; ++ } ++ d->localFields << field; ++ continue; ++ } ++ ++ // local field ++ QDjangoMetaField field; ++ field.d->name = meta->property(i).name(); ++ field.d->type = meta->property(i).type(); ++ field.d->db_column = dbColumnOption.isEmpty() ? QString::fromLatin1(field.d->name) : dbColumnOption; ++ field.d->index = dbIndexOption; ++ field.d->maxLength = maxLengthOption; ++ field.d->null = nullOption; ++ if (primaryKeyOption) { ++ field.d->autoIncrement = autoIncrementOption; ++ field.d->unique = true; ++ d->primaryKey = field.d->name; ++ } else if (uniqueOption) { ++ field.d->unique = true; ++ } ++ ++ d->localFields << field; ++ } ++ ++ // automatic primary key ++ if (d->primaryKey.isEmpty()) { ++ QDjangoMetaField field; ++ field.d->name = "id"; ++ field.d->type = QVariant::Int; ++ field.d->db_column = "id"; ++ field.d->autoIncrement = true; ++ field.d->unique = true; ++ d->localFields.prepend(field); ++ d->primaryKey = field.d->name; ++ } + +- // parse table options +- const int optionsIndex = meta->indexOfClassInfo("__meta__"); +- if (optionsIndex >= 0) { +- QMap options = parseOptions(meta->classInfo(optionsIndex).value()); +- QMapIterator option(options); +- while (option.hasNext()) { +- option.next(); +- if (option.key() == "db_table") +- d->table = option.value(); +- } +- } +- +- const int count = meta->propertyCount(); +- for(int i = QObject::staticMetaObject.propertyCount(); i < count; ++i) +- { +- QString typeName = meta->property(i).typeName(); +- if (!qstrcmp(meta->property(i).name(), "pk")) +- continue; +- +- // parse field options +- bool autoIncrementOption = false; +- QString dbColumnOption; +- bool dbIndexOption = false; +- bool ignoreFieldOption = false; +- int maxLengthOption = 0; +- bool primaryKeyOption = false; +- bool nullOption = false; +- bool uniqueOption = false; +- const int infoIndex = meta->indexOfClassInfo(meta->property(i).name()); +- if (infoIndex >= 0) +- { +- QMap options = parseOptions(meta->classInfo(infoIndex).value()); +- QMapIterator option(options); +- while (option.hasNext()) { +- option.next(); +- const QString value = option.value(); +- if (option.key() == "auto_increment") +- autoIncrementOption = (value.toLower() == "true" || value == "1"); +- else if (option.key() == "db_column") +- dbColumnOption = value; +- else if (option.key() == "db_index") +- dbIndexOption = (value.toLower() == "true" || value == "1"); +- else if (option.key() == "ignore_field") +- ignoreFieldOption = (value.toLower() == "true" || value == "1"); +- else if (option.key() == "max_length") +- maxLengthOption = value.toInt(); +- else if (option.key() == "null") +- nullOption = (value.toLower() == "true" || value == "1"); +- else if (option.key() == "primary_key") +- primaryKeyOption = (value.toLower() == "true" || value == "1"); +- else if (option.key() == "unique") +- uniqueOption = (value.toLower() == "true" || value == "1"); +- } +- } +- +- // ignore field +- if (ignoreFieldOption) +- continue; +- +- // foreign field +- if (typeName.endsWith("*")) +- { +- const QByteArray fkName = meta->property(i).name(); +- const QString fkModel = typeName.left(typeName.size() - 1); +- d->foreignFields.insert(fkName, fkModel); +- +- QDjangoMetaField field; +- field.d->name = fkName + "_id"; +- // FIXME : the key is not necessarily an INTEGER field, we should +- // probably perform a lookup on the foreign model, but are we sure +- // it is already registered? +- field.d->type = QVariant::Int; +- field.d->foreignModel = fkModel; +- field.d->db_column = dbColumnOption.isEmpty() ? QString::fromLatin1(field.d->name) : dbColumnOption; +- field.d->index = true; +- field.d->null = nullOption; +- d->localFields << field; +- continue; +- } +- +- // local field +- QDjangoMetaField field; +- field.d->name = meta->property(i).name(); +- field.d->type = meta->property(i).type(); +- field.d->db_column = dbColumnOption.isEmpty() ? QString::fromLatin1(field.d->name) : dbColumnOption; +- field.d->index = dbIndexOption; +- field.d->maxLength = maxLengthOption; +- field.d->null = nullOption; +- if (primaryKeyOption) { +- field.d->autoIncrement = autoIncrementOption; +- field.d->unique = true; +- d->primaryKey = field.d->name; +- } else if (uniqueOption) { +- field.d->unique = true; +- } +- +- d->localFields << field; +- } +- +- // automatic primary key +- if (d->primaryKey.isEmpty()) { +- QDjangoMetaField field; +- field.d->name = "id"; +- field.d->type = QVariant::Int; +- field.d->db_column = "id"; +- field.d->autoIncrement = true; +- field.d->unique = true; +- d->localFields.prepend(field); +- d->primaryKey = field.d->name; +- } +- + } + + /*! +- Constructs a copy of \a other. ++ Constructs a copy of \a other. + */ + QDjangoMetaModel::QDjangoMetaModel(const QDjangoMetaModel &other) +- : d(other.d) ++ : d(other.d) + { + } + + /*! +- Destroys the meta model. ++ Destroys the meta model. + */ + QDjangoMetaModel::~QDjangoMetaModel() + { + } + + /*! +- Assigns \a other to this meta model. ++ Assigns \a other to this meta model. + */ +@@ -289,6 +296,6 @@ + QDjangoMetaModel& QDjangoMetaModel::operator=(const QDjangoMetaModel& other) + { +- d = other.d; +- return *this; ++ d = other.d; ++ return *this; + } + +@@ -295,112 +302,112 @@ + /*! +- Creates the database table for this QDjangoMetaModel. ++ Creates the database table for this QDjangoMetaModel. + */ + bool QDjangoMetaModel::createTable() const + { +- QSqlDatabase db = QDjango::database(); +- QSqlDriver *driver = db.driver(); +- const QString driverName = db.driverName(); +- +- QStringList propSql; +- const QString quotedTable = db.driver()->escapeIdentifier(d->table, QSqlDriver::TableName); +- foreach (const QDjangoMetaField &field, d->localFields) +- { +- QString fieldSql = driver->escapeIdentifier(field.column(), QSqlDriver::FieldName); +- switch (field.d->type) { +- case QVariant::Bool: +- fieldSql += " BOOLEAN"; +- break; +- case QVariant::ByteArray: +- if (driverName == QLatin1String("QPSQL")) +- fieldSql += " BYTEA"; +- else { +- fieldSql += " BLOB"; +- if (field.d->maxLength > 0) +- fieldSql += QString("(%1)").arg(field.d->maxLength); +- } +- break; +- case QVariant::Date: +- fieldSql += " DATE"; +- break; +- case QVariant::DateTime: +- if (driverName == QLatin1String("QPSQL")) +- fieldSql += " TIMESTAMP"; +- else +- fieldSql += " DATETIME"; +- break; +- case QVariant::Double: +- fieldSql += " REAL"; +- break; +- case QVariant::Int: +- fieldSql += " INTEGER"; +- break; +- case QVariant::LongLong: +- fieldSql += " BIGINT"; +- break; +- case QVariant::String: +- if (field.d->maxLength > 0) +- fieldSql += QString(" VARCHAR(%1)").arg(field.d->maxLength); +- else +- fieldSql += " TEXT"; +- break; +- case QVariant::Time: +- fieldSql += " TIME"; +- break; +- default: +- qWarning() << "Unhandled type" << field.d->type << "for property" << field.d->name; +- continue; +- } +- +- if (!field.d->null) +- fieldSql += " NOT NULL"; +- +- // primary key +- if (field.d->name == d->primaryKey) +- fieldSql += " PRIMARY KEY"; +- +- // auto-increment is backend specific +- if (field.d->autoIncrement) { +- if (driverName == QLatin1String("QSQLITE") || +- driverName == QLatin1String("QSQLITE2")) +- fieldSql += QLatin1String(" AUTOINCREMENT"); +- else if (driverName == QLatin1String("QMYSQL")) +- fieldSql += QLatin1String(" AUTO_INCREMENT"); +- else if (driverName == QLatin1String("QPSQL")) +- fieldSql = driver->escapeIdentifier(field.column(), QSqlDriver::FieldName) + " SERIAL PRIMARY KEY"; +- } +- +- // foreign key +- if (!field.d->foreignModel.isEmpty()) +- { +- const QDjangoMetaModel foreignMeta = QDjango::metaModel(field.d->foreignModel); +- const QDjangoMetaField foreignField = foreignMeta.localField("pk"); +- fieldSql += QString(" REFERENCES %1 (%2)").arg( +- driver->escapeIdentifier(foreignMeta.d->table, QSqlDriver::TableName), +- driver->escapeIdentifier(foreignField.column(), QSqlDriver::FieldName)); +- } +- propSql << fieldSql; +- } +- +- // create table +- QDjangoQuery createQuery(db); +- if (!createQuery.exec(QString("CREATE TABLE %1 (%2)").arg( +- quotedTable, +- propSql.join(", ")))) +- return false; +- +- // create indices +- foreach (const QDjangoMetaField &field, d->localFields) { +- if (field.d->index && !field.d->unique) { +- const QString indexName = d->table + "_" + field.column(); +- if (!createQuery.exec(QString("CREATE INDEX %1 ON %2 (%3)").arg( +- // FIXME : how should we escape an index name? +- driver->escapeIdentifier(indexName, QSqlDriver::FieldName), +- quotedTable, +- driver->escapeIdentifier(field.column(), QSqlDriver::FieldName)))) +- return false; +- } +- } ++ QSqlDatabase db = QDjango::database(); ++ QSqlDriver *driver = db.driver(); ++ const QString driverName = db.driverName(); ++ ++ QStringList propSql; ++ const QString quotedTable = db.driver()->escapeIdentifier(d->table, QSqlDriver::TableName); ++ foreach (const QDjangoMetaField &field, d->localFields) ++ { ++ QString fieldSql = driver->escapeIdentifier(field.column(), QSqlDriver::FieldName); ++ switch (field.d->type) { ++ case QVariant::Bool: ++ fieldSql += " BOOLEAN"; ++ break; ++ case QVariant::ByteArray: ++ if (driverName == QLatin1String("QPSQL")) ++ fieldSql += " BYTEA"; ++ else { ++ fieldSql += " BLOB"; ++ if (field.d->maxLength > 0) ++ fieldSql += QString("(%1)").arg(field.d->maxLength); ++ } ++ break; ++ case QVariant::Date: ++ fieldSql += " DATE"; ++ break; ++ case QVariant::DateTime: ++ if (driverName == QLatin1String("QPSQL")) ++ fieldSql += " TIMESTAMP"; ++ else ++ fieldSql += " DATETIME"; ++ break; ++ case QVariant::Double: ++ fieldSql += " REAL"; ++ break; ++ case QVariant::Int: ++ fieldSql += " INTEGER"; ++ break; ++ case QVariant::LongLong: ++ fieldSql += " BIGINT"; ++ break; ++ case QVariant::String: ++ if (field.d->maxLength > 0) ++ fieldSql += QString(" VARCHAR(%1)").arg(field.d->maxLength); ++ else ++ fieldSql += " TEXT"; ++ break; ++ case QVariant::Time: ++ fieldSql += " TIME"; ++ break; ++ default: ++ qWarning() << "Unhandled type" << field.d->type << "for property" << field.d->name; ++ continue; ++ } ++ ++ if (!field.d->null) ++ fieldSql += " NOT NULL"; ++ ++ // primary key ++ if (field.d->name == d->primaryKey) ++ fieldSql += " PRIMARY KEY"; ++ ++ // auto-increment is backend specific ++ if (field.d->autoIncrement) { ++ if (driverName == QLatin1String("QSQLITE") || ++ driverName == QLatin1String("QSQLITE2")) ++ fieldSql += QLatin1String(" AUTOINCREMENT"); ++ else if (driverName == QLatin1String("QMYSQL")) ++ fieldSql += QLatin1String(" AUTO_INCREMENT"); ++ else if (driverName == QLatin1String("QPSQL")) ++ fieldSql = driver->escapeIdentifier(field.column(), QSqlDriver::FieldName) + " SERIAL PRIMARY KEY"; ++ } ++ ++ // foreign key ++ if (!field.d->foreignModel.isEmpty()) ++ { ++ const QDjangoMetaModel foreignMeta = QDjango::metaModel(field.d->foreignModel); ++ const QDjangoMetaField foreignField = foreignMeta.localField("pk"); ++ fieldSql += QString(" REFERENCES %1 (%2)").arg( ++ driver->escapeIdentifier(foreignMeta.d->table, QSqlDriver::TableName), ++ driver->escapeIdentifier(foreignField.column(), QSqlDriver::FieldName)); ++ } ++ propSql << fieldSql; ++ } ++ ++ // create table ++ QDjangoQuery createQuery(db); ++ if (!createQuery.exec(QString("CREATE TABLE %1 (%2)").arg( ++ quotedTable, ++ propSql.join(", ")))) ++ return false; ++ ++ // create indices ++ foreach (const QDjangoMetaField &field, d->localFields) { ++ if (field.d->index && !field.d->unique) { ++ const QString indexName = d->table + "_" + field.column(); ++ if (!createQuery.exec(QString("CREATE INDEX %1 ON %2 (%3)").arg( ++ // FIXME : how should we escape an index name? ++ driver->escapeIdentifier(indexName, QSqlDriver::FieldName), ++ quotedTable, ++ driver->escapeIdentifier(field.column(), QSqlDriver::FieldName)))) ++ return false; ++ } ++ } + +- return true; ++ return true; + } + +@@ -407,12 +414,12 @@ + /*! +- Drops the database table for this QDjangoMetaModel. ++ Drops the database table for this QDjangoMetaModel. + */ + bool QDjangoMetaModel::dropTable() const + { +- QSqlDatabase db = QDjango::database(); ++ QSqlDatabase db = QDjango::database(); + +- QDjangoQuery query(db); +- return query.exec(QString("DROP TABLE %1").arg( +- db.driver()->escapeIdentifier(d->table, QSqlDriver::TableName))); ++ QDjangoQuery query(db); ++ return query.exec(QString("DROP TABLE %1").arg( ++ db.driver()->escapeIdentifier(d->table, QSqlDriver::TableName))); + } + +@@ -419,27 +426,27 @@ + /*! +- Retrieves the QDjangoModel pointed to by the given foreign-key. ++ Retrieves the QDjangoModel pointed to by the given foreign-key. + +- \param model +- \param name ++ \param model ++ \param name + */ + QObject *QDjangoMetaModel::foreignKey(const QObject *model, const char *name) const + { +- const QByteArray prop(name); +- QObject *foreign = model->property(prop + "_ptr").value(); +- if (!foreign) +- return 0; +- +- // if the foreign object was not loaded yet, do it now +- const QString foreignClass = d->foreignFields[prop]; +- const QDjangoMetaModel foreignMeta = QDjango::metaModel(foreignClass); +- const QVariant foreignPk = model->property(prop + "_id"); +- if (foreign->property(foreignMeta.primaryKey()) != foreignPk) +- { +- QDjangoQuerySetPrivate qs(foreignClass); +- qs.addFilter(QDjangoWhere("pk", QDjangoWhere::Equals, foreignPk)); +- qs.sqlFetch(); +- if (qs.properties.size() != 1 || !qs.sqlLoad(foreign, 0)) +- return 0; +- } +- return foreign; ++ const QByteArray prop(name); ++ QObject *foreign = model->property(prop + "_ptr").value(); ++ if (!foreign) ++ return 0; ++ ++ // if the foreign object was not loaded yet, do it now ++ const QString foreignClass = d->foreignFields[prop]; ++ const QDjangoMetaModel foreignMeta = QDjango::metaModel(foreignClass); ++ const QVariant foreignPk = model->property(prop + "_id"); ++ if (foreign->property(foreignMeta.primaryKey()) != foreignPk) ++ { ++ QDjangoQuerySetPrivate qs(foreignClass); ++ qs.addFilter(QDjangoWhere("pk", QDjangoWhere::Equals, foreignPk)); ++ qs.sqlFetch(); ++ if (qs.properties.size() != 1 || !qs.sqlLoad(foreign, 0)) ++ return 0; ++ } ++ return foreign; + } +@@ -446,28 +453,28 @@ + + /*! +- Sets the QDjangoModel pointed to by the given foreign-key. +- +- \param model +- \param name +- \param value ++ Sets the QDjangoModel pointed to by the given foreign-key. ++ ++ \param model ++ \param name ++ \param value + +- \note The \c model will take ownership of the given \c value. ++ \note The \c model will take ownership of the given \c value. + */ + void QDjangoMetaModel::setForeignKey(QObject *model, const char *name, QObject *value) const + { +- const QByteArray prop(name); +- QObject *old = model->property(prop + "_ptr").value(); +- if (old == value) +- return; +- +- // store the new pointer and update the foreign key +- model->setProperty(prop + "_ptr", qVariantFromValue(value)); +- if (value) +- { +- const QDjangoMetaModel foreignMeta = QDjango::metaModel(d->foreignFields[prop]); +- model->setProperty(prop + "_id", value->property(foreignMeta.primaryKey())); +- } else { +- model->setProperty(prop + "_id", QVariant()); +- } ++ const QByteArray prop(name); ++ QObject *old = model->property(prop + "_ptr").value(); ++ if (old == value) ++ return; ++ ++ // store the new pointer and update the foreign key ++ model->setProperty(prop + "_ptr", qVariantFromValue(value)); ++ if (value) ++ { ++ const QDjangoMetaModel foreignMeta = QDjango::metaModel(d->foreignFields[prop]); ++ model->setProperty(prop + "_id", value->property(foreignMeta.primaryKey())); ++ } else { ++ model->setProperty(prop + "_id", QVariant()); ++ } + } + +@@ -474,23 +481,23 @@ + /*! +- Loads the given properties into a \a model instance. ++ Loads the given properties into a \a model instance. + */ + void QDjangoMetaModel::load(QObject *model, const QVariantList &properties, int &pos) const + { +- // process local fields +- foreach (const QDjangoMetaField &field, d->localFields) +- model->setProperty(field.d->name, properties.at(pos++)); +- +- // process foreign fields +- if (pos >= properties.size()) +- return; +- foreach (const QByteArray &fkName, d->foreignFields.keys()) +- { +- QObject *object = model->property(fkName + "_ptr").value(); +- if (object) +- { +- const QDjangoMetaModel foreignMeta = QDjango::metaModel(d->foreignFields[fkName]); +- foreignMeta.load(object, properties, pos); +- } +- } ++ // process local fields ++ foreach (const QDjangoMetaField &field, d->localFields) ++ model->setProperty(field.d->name, properties.at(pos++)); ++ ++ // process foreign fields ++ if (pos >= properties.size()) ++ return; ++ foreach (const QByteArray &fkName, d->foreignFields.keys()) ++ { ++ QObject *object = model->property(fkName + "_ptr").value(); ++ if (object) ++ { ++ const QDjangoMetaModel foreignMeta = QDjango::metaModel(d->foreignFields[fkName]); ++ foreignMeta.load(object, properties, pos); ++ } ++ } + } + +@@ -497,8 +504,8 @@ + /*! +- Returns the foreign field mapping. ++ Returns the foreign field mapping. + */ + QMap QDjangoMetaModel::foreignFields() const + { +- return d->foreignFields; ++ return d->foreignFields; + } + +@@ -505,13 +512,13 @@ + /*! +- Return the local field with the specified \a name. ++ Return the local field with the specified \a name. + */ + QDjangoMetaField QDjangoMetaModel::localField(const QString &name) const + { +- const QString fieldName = (name == "pk") ? d->primaryKey : name; +- foreach (const QDjangoMetaField &field, d->localFields) { +- if (field.d->name == fieldName) +- return field; +- } +- return QDjangoMetaField(); ++ const QString fieldName = (name == "pk") ? d->primaryKey : name; ++ foreach (const QDjangoMetaField &field, d->localFields) { ++ if (field.d->name == fieldName) ++ return field; ++ } ++ return QDjangoMetaField(); + } + +@@ -518,8 +525,8 @@ + /*! +- Returns the list of local fields. ++ Returns the list of local fields. + */ + QList QDjangoMetaModel::localFields() const + { +- return d->localFields; ++ return d->localFields; + } + +@@ -526,8 +533,8 @@ + /*! +- Returns the name of the primary key for the current QDjangoMetaModel. ++ Returns the name of the primary key for the current QDjangoMetaModel. + */ + QByteArray QDjangoMetaModel::primaryKey() const + { +- return d->primaryKey; ++ return d->primaryKey; + } + +@@ -534,8 +541,8 @@ + /*! +- Returns the name of the database table. ++ Returns the name of the database table. + */ + QString QDjangoMetaModel::table() const + { +- return d->table; ++ return d->table; + } + +@@ -542,11 +549,11 @@ + /*! +- Removes the given \a model instance from the database. ++ Removes the given \a model instance from the database. + */ + bool QDjangoMetaModel::remove(QObject *model) const + { +- const QVariant pk = model->property(d->primaryKey); +- QDjangoQuerySetPrivate qs(model->metaObject()->className()); +- qs.addFilter(QDjangoWhere("pk", QDjangoWhere::Equals, pk)); +- return qs.sqlDelete(); ++ const QVariant pk = model->property(d->primaryKey); ++ QDjangoQuerySetPrivate qs(model->metaObject()->className()); ++ qs.addFilter(QDjangoWhere("pk", QDjangoWhere::Equals, pk)); ++ return qs.sqlDelete(); + } + +@@ -553,57 +560,57 @@ + /*! +- Saves the given \a model instance to the database. ++ Saves the given \a model instance to the database. + +- \return true if saving succeeded, false otherwise ++ \return true if saving succeeded, false otherwise + */ + bool QDjangoMetaModel::save(QObject *model) const + { +- // find primary key +- const QDjangoMetaField primaryKey = localField("pk"); +- const QVariant pk = model->property(d->primaryKey); +- if (!pk.isNull() && !(primaryKey.d->type == QVariant::Int && !pk.toInt())) +- { +- QSqlDatabase db = QDjango::database(); +- QDjangoQuery query(db); +- query.prepare(QString("SELECT 1 AS a FROM %1 WHERE %2 = ?").arg( +- db.driver()->escapeIdentifier(d->table, QSqlDriver::FieldName), +- db.driver()->escapeIdentifier(primaryKey.column(), QSqlDriver::FieldName))); +- query.addBindValue(pk); +- if (query.exec() && query.next()) +- { +- // prepare data +- QVariantMap fields; +- foreach (const QDjangoMetaField &field, d->localFields) { +- if (field.d->name != d->primaryKey) { +- const QVariant value = model->property(field.d->name); +- fields.insert(field.d->name, field.toDatabase(value)); +- } +- } +- +- // perform UPDATE +- QDjangoQuerySetPrivate qs(model->metaObject()->className()); +- qs.addFilter(QDjangoWhere("pk", QDjangoWhere::Equals, pk)); +- return qs.sqlUpdate(fields) != -1; +- } +- } +- +- // prepare data +- QVariantMap fields; +- foreach (const QDjangoMetaField &field, d->localFields) { +- if (!field.d->autoIncrement) { +- const QVariant value = model->property(field.d->name); +- fields.insert(field.name(), field.toDatabase(value)); +- } +- } +- +- // perform INSERT +- QVariant insertId; +- QDjangoQuerySetPrivate qs(model->metaObject()->className()); +- if (!qs.sqlInsert(fields, &insertId)) +- return false; +- +- // fetch autoincrement pk +- if (primaryKey.d->autoIncrement) +- model->setProperty(d->primaryKey, insertId); +- return true; ++ // find primary key ++ const QDjangoMetaField primaryKey = localField("pk"); ++ const QVariant pk = model->property(d->primaryKey); ++ if (!pk.isNull() && !(primaryKey.d->type == QVariant::Int && !pk.toInt())) ++ { ++ QSqlDatabase db = QDjango::database(); ++ QDjangoQuery query(db); ++ query.prepare(QString("SELECT 1 AS a FROM %1 WHERE %2 = ?").arg( ++ db.driver()->escapeIdentifier(d->table, QSqlDriver::FieldName), ++ db.driver()->escapeIdentifier(primaryKey.column(), QSqlDriver::FieldName))); ++ query.addBindValue(pk); ++ if (query.exec() && query.next()) ++ { ++ // prepare data ++ QVariantMap fields; ++ foreach (const QDjangoMetaField &field, d->localFields) { ++ if (field.d->name != d->primaryKey) { ++ const QVariant value = model->property(field.d->name); ++ fields.insert(field.d->name, field.toDatabase(value)); ++ } ++ } ++ ++ // perform UPDATE ++ QDjangoQuerySetPrivate qs(model->metaObject()->className()); ++ qs.addFilter(QDjangoWhere("pk", QDjangoWhere::Equals, pk)); ++ return qs.sqlUpdate(fields) != -1; ++ } ++ } ++ ++ // prepare data ++ QVariantMap fields; ++ foreach (const QDjangoMetaField &field, d->localFields) { ++ if (!field.d->autoIncrement) { ++ const QVariant value = model->property(field.d->name); ++ fields.insert(field.name(), field.toDatabase(value)); ++ } ++ } ++ ++ // perform INSERT ++ QVariant insertId; ++ QDjangoQuerySetPrivate qs(model->metaObject()->className()); ++ if (!qs.sqlInsert(fields, &insertId)) ++ return false; ++ ++ // fetch autoincrement pk ++ if (primaryKey.d->autoIncrement) ++ model->setProperty(d->primaryKey, insertId); ++ return true; + } +"""]] + +## The extracted whitespace changes + +[[!format diff """ +--- a/qdjango-0.2.6/src/db/QDjangoMetaModel.cpp ++++ b/qdjango-0.2.6/src/db/QDjangoMetaModel.cpp +@@ -231,6 +231,13 @@ QDjangoMetaModel::QDjangoMetaModel(const QObject *model) + field.d->db_column = dbColumnOption.isEmpty() ? QString::fromLatin1(field.d->name) : dbColumnOption; + field.d->index = true; + field.d->null = nullOption; ++ if (primaryKeyOption) { ++ field.d->autoIncrement = autoIncrementOption; ++ field.d->unique = true; ++ d->primaryKey = field.d->name; ++ } else if (uniqueOption) { ++ field.d->unique = true; ++ } + d->localFields << field; + continue; + } +"""]] -- 2.20.1