diff --git a/kexi/kexidb/alter.cpp b/kexi/kexidb/alter.cpp index 4425d44e721..2edd8d78898 100644 --- a/kexi/kexidb/alter.cpp +++ b/kexi/kexidb/alter.cpp @@ -1,1124 +1,1124 @@ /* This file is part of the KDE project - Copyright (C) 2006-2007 Jarosław Staniek + Copyright (C) 2006-2012 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "alter.h" #include #include #include #include namespace KexiDB { class AlterTableHandler::Private { public: Private() {} ~Private() { qDeleteAll(actions); } ActionList actions; QPointer conn; }; } using namespace KexiDB; //! a global instance used to when returning null is needed AlterTableHandler::ChangeFieldPropertyAction nullChangeFieldPropertyAction(true); AlterTableHandler::RemoveFieldAction nullRemoveFieldAction(true); AlterTableHandler::InsertFieldAction nullInsertFieldAction(true); AlterTableHandler::MoveFieldPositionAction nullMoveFieldPositionAction(true); //-------------------------------------------------------- AlterTableHandler::ActionBase::ActionBase(bool null) : m_alteringRequirements(0) , m_order(-1) , m_null(null) { } AlterTableHandler::ActionBase::~ActionBase() { } AlterTableHandler::ChangeFieldPropertyAction& AlterTableHandler::ActionBase::toChangeFieldPropertyAction() { if (dynamic_cast(this)) return *dynamic_cast(this); return nullChangeFieldPropertyAction; } AlterTableHandler::RemoveFieldAction& AlterTableHandler::ActionBase::toRemoveFieldAction() { if (dynamic_cast(this)) return *dynamic_cast(this); return nullRemoveFieldAction; } AlterTableHandler::InsertFieldAction& AlterTableHandler::ActionBase::toInsertFieldAction() { if (dynamic_cast(this)) return *dynamic_cast(this); return nullInsertFieldAction; } AlterTableHandler::MoveFieldPositionAction& AlterTableHandler::ActionBase::toMoveFieldPositionAction() { if (dynamic_cast(this)) return *dynamic_cast(this); return nullMoveFieldPositionAction; } //-------------------------------------------------------- AlterTableHandler::FieldActionBase::FieldActionBase(const QString& fieldName, int uid) : ActionBase() , m_fieldUID(uid) , m_fieldName(fieldName) { } AlterTableHandler::FieldActionBase::FieldActionBase(bool) : ActionBase(true) , m_fieldUID(-1) { } AlterTableHandler::FieldActionBase::~FieldActionBase() { } //-------------------------------------------------------- //! @internal struct KexiDB_AlterTableHandlerStatic { KexiDB_AlterTableHandlerStatic() { #define I(name, type) \ types.insert(QByteArray(name).toLower(), (int)AlterTableHandler::type) #define I2(name, type1, type2) \ flag = (int)AlterTableHandler::type1|(int)AlterTableHandler::type2; \ if (flag & AlterTableHandler::PhysicalAlteringRequired) \ flag |= AlterTableHandler::MainSchemaAlteringRequired; \ types.insert(QByteArray(name).toLower(), flag) /* useful links: http://dev.mysql.com/doc/refman/5.0/en/create-table.html */ // ExtendedSchemaAlteringRequired is here because when the field is renamed, // we need to do the same rename in extended table schema: int flag; I2("name", PhysicalAlteringRequired, MainSchemaAlteringRequired); I2("type", PhysicalAlteringRequired, DataConversionRequired); I("caption", MainSchemaAlteringRequired); I("description", MainSchemaAlteringRequired); I2("unsigned", PhysicalAlteringRequired, DataConversionRequired); // always? I2("maxLength", PhysicalAlteringRequired, DataConversionRequired); // always? I2("precision", PhysicalAlteringRequired, DataConversionRequired); // always? - I("width", MainSchemaAlteringRequired); + I("defaultWidth", ExtendedSchemaAlteringRequired); // defaultValue: depends on backend, for mysql it can only by a constant or now()... // -- should we look at Driver here? #ifdef KEXI_NO_UNFINISHED //! @todo reenable I("defaultValue", MainSchemaAlteringRequired); #else I2("defaultValue", PhysicalAlteringRequired, MainSchemaAlteringRequired); #endif I2("primaryKey", PhysicalAlteringRequired, DataConversionRequired); I2("unique", PhysicalAlteringRequired, DataConversionRequired); // we may want to add an Index here I2("notNull", PhysicalAlteringRequired, DataConversionRequired); // we may want to add an Index here // allowEmpty: only support it just at kexi level? maybe there is a backend that supports this? I2("allowEmpty", PhysicalAlteringRequired, MainSchemaAlteringRequired); I2("autoIncrement", PhysicalAlteringRequired, DataConversionRequired); // data conversion may be hard here I2("indexed", PhysicalAlteringRequired, DataConversionRequired); // we may want to add an Index here // easier cases follow... I("visibleDecimalPlaces", ExtendedSchemaAlteringRequired); // lookup-field-related properties... /*moved to KexiDB::isExtendedTableFieldProperty() I("boundColumn", ExtendedSchemaAlteringRequired); I("rowSource", ExtendedSchemaAlteringRequired); I("rowSourceType", ExtendedSchemaAlteringRequired); I("rowSourceValues", ExtendedSchemaAlteringRequired); I("visibleColumn", ExtendedSchemaAlteringRequired); I("columnWidths", ExtendedSchemaAlteringRequired); I("showColumnHeaders", ExtendedSchemaAlteringRequired); I("listRows", ExtendedSchemaAlteringRequired); I("limitToList", ExtendedSchemaAlteringRequired); I("displayWidget", ExtendedSchemaAlteringRequired);*/ //more to come... #undef I #undef I2 } QHash types; }; K_GLOBAL_STATIC(KexiDB_AlterTableHandlerStatic, KexiDB_alteringTypeForProperty) //! @internal int AlterTableHandler::alteringTypeForProperty(const QByteArray& propertyName) { const int res = KexiDB_alteringTypeForProperty->types[propertyName.toLower()]; if (res == 0) { if (KexiDB::isExtendedTableFieldProperty(propertyName)) return (int)ExtendedSchemaAlteringRequired; KexiDBWarn << QString("AlterTableHandler::alteringTypeForProperty(): property \"%1\" not found!") .arg(QString(propertyName)); } return res; return KexiDB_alteringTypeForProperty->types[propertyName.toLower()]; } //--- AlterTableHandler::ChangeFieldPropertyAction::ChangeFieldPropertyAction( const QString& fieldName, const QString& propertyName, const QVariant& newValue, int uid) : FieldActionBase(fieldName, uid) , m_propertyName(propertyName) , m_newValue(newValue) { } AlterTableHandler::ChangeFieldPropertyAction::ChangeFieldPropertyAction(bool) : FieldActionBase(true) { } AlterTableHandler::ChangeFieldPropertyAction::~ChangeFieldPropertyAction() { } void AlterTableHandler::ChangeFieldPropertyAction::updateAlteringRequirements() { // m_alteringRequirements = ???; setAlteringRequirements(alteringTypeForProperty(m_propertyName.toLatin1())); } QString AlterTableHandler::ChangeFieldPropertyAction::debugString(const DebugOptions& debugOptions) { QString s = QString("Set \"%1\" property for table field \"%2\" to \"%3\"") .arg(m_propertyName).arg(fieldName()).arg(m_newValue.toString()); if (debugOptions.showUID) s.append(QString(" (UID=%1)").arg(m_fieldUID)); return s; } static AlterTableHandler::ActionDict* createActionDict( AlterTableHandler::ActionDictDict &fieldActions, int forFieldUID) { AlterTableHandler::ActionDict* dict = new AlterTableHandler::ActionDict(); //Qt4 dict->setAutoDelete(true); fieldActions.insert(forFieldUID, dict); return dict; } static void debugAction(AlterTableHandler::ActionBase *action, int nestingLevel, bool simulate, const QString& prependString = QString(), QString * debugTarget = 0) { Q_UNUSED(simulate); Q_UNUSED(nestingLevel); QString debugString; if (!debugTarget) debugString = prependString; if (action) { AlterTableHandler::ActionBase::DebugOptions debugOptions; debugOptions.showUID = debugTarget == 0; debugOptions.showFieldDebug = debugTarget != 0; debugString += action->debugString(debugOptions); } else { if (!debugTarget) debugString += "[No action]"; //hmm } if (debugTarget) { if (!debugString.isEmpty()) *debugTarget += debugString + '\n'; } else { KexiDBDbg << debugString; #ifdef KEXI_DEBUG_GUI if (simulate) KexiDB::alterTableActionDebugGUI(debugString, nestingLevel); #endif } } static void debugActionDict(AlterTableHandler::ActionDict *dict, int fieldUID, bool simulate) { QString fieldName; AlterTableHandler::ActionDictConstIterator it(dict->constBegin()); if (it != dict->constEnd() && dynamic_cast(it.value())) { //retrieve field name from the 1st related action fieldName = dynamic_cast(it.value())->fieldName(); } else { fieldName = "??"; } QString dbg = QString("Action dict for field \"%1\" (%2, UID=%3):") .arg(fieldName).arg(dict->count()).arg(fieldUID); KexiDBDbg << dbg; #ifdef KEXI_DEBUG_GUI if (simulate) KexiDB::alterTableActionDebugGUI(dbg, 1); #endif for (;it != dict->constEnd(); ++it) { debugAction(it.value(), 2, simulate); } } static void debugFieldActions(const AlterTableHandler::ActionDictDict &fieldActions, bool simulate) { #ifdef KEXI_DEBUG_GUI if (simulate) KexiDB::alterTableActionDebugGUI("** Simplified Field Actions:"); #endif for (AlterTableHandler::ActionDictDictConstIterator it(fieldActions.constBegin()); it != fieldActions.constEnd(); ++it) { debugActionDict(it.value(), it.key(), simulate); } } /*! Legend: A,B==fields, P==property, [....]==action, (..,..,..) group of actions, <...> internal operation. Case 1. (special) when new action=[rename A to B] and exists=[rename B to C] => remove [rename B to C] and set result to new [rename A to C] and go to 1b. Case 1b. when new action=[rename A to B] and actions exist like [set property P to C in field B] or like [delete field B] or like [move field B] => change B to A for all these actions Case 2. when new action=[change property in field A] (property != name) and exists=[remove A] or exists=[change property in field A] => do not add [change property in field A] because it will be removed anyway or the property will change */ void AlterTableHandler::ChangeFieldPropertyAction::simplifyActions(ActionDictDict &fieldActions) { ActionDict *actionsLikeThis = fieldActions.value(uid()); if (m_propertyName == "name") { // Case 1. special: name1 -> name2, i.e. rename action QString newName(newValue().toString()); // try to find rename(newName, otherName) action ActionBase *renameActionLikeThis = actionsLikeThis ? actionsLikeThis->value("name") : 0; if (dynamic_cast(renameActionLikeThis)) { // 1. instead of having rename(fieldName(), newValue()) action, // let's have rename(fieldName(), otherName) action dynamic_cast(renameActionLikeThis)->m_newValue = dynamic_cast(renameActionLikeThis)->m_newValue; /* AlterTableHandler::ChangeFieldPropertyAction* newRenameAction = new AlterTableHandler::ChangeFieldPropertyAction( *this ); newRenameAction->m_newValue = dynamic_cast(renameActionLikeThis)->m_newValue; // (m_order is the same as in newAction) // replace prev. rename action (if any) actionsLikeThis->remove( "name" ); ActionDict *adict = fieldActions[ fieldName().toLatin1() ]; if (!adict) adict = createActionDict( fieldActions, fieldName() ); adict->insert(m_propertyName.toLatin1(), newRenameAction);*/ } else { ActionBase *removeActionForThisField = actionsLikeThis ? actionsLikeThis->value(":remove:") : 0; if (removeActionForThisField) { //if this field is going to be removed, jsut change the action's field name // and do not add a new action } else { //just insert a copy of the rename action if (!actionsLikeThis) actionsLikeThis = createActionDict(fieldActions, uid()); //fieldName() ); AlterTableHandler::ChangeFieldPropertyAction* newRenameAction = new AlterTableHandler::ChangeFieldPropertyAction(*this); KexiDBDbg << "insert into" << fieldName() << "dict:" << newRenameAction->debugString(); actionsLikeThis->insert(m_propertyName.toLatin1(), newRenameAction); return; } } if (actionsLikeThis) { // Case 1b. change "field name" information to fieldName() in any action that // is related to newName // e.g. if there is setCaption("B", "captionA") action after rename("A","B"), // replace setCaption action with setCaption("A", "captionA") foreach(ActionBase* action, *actionsLikeThis) { dynamic_cast(action)->setFieldName(fieldName()); } } return; } ActionBase *removeActionForThisField = actionsLikeThis ? actionsLikeThis->value(":remove:") : 0; if (removeActionForThisField) { //if this field is going to be removed, do not add a new action return; } // Case 2. other cases: just give up with adding this "intermediate" action // so, e.g. [ setCaption(A, "captionA"), setCaption(A, "captionB") ] // becomes: [ setCaption(A, "captionB") ] // because adding this action does nothing ActionDict *nextActionsLikeThis = fieldActions.value(uid()); //fieldName().toLatin1() ]; if (!nextActionsLikeThis || !nextActionsLikeThis->value(m_propertyName.toLatin1())) { //no such action, add this AlterTableHandler::ChangeFieldPropertyAction* newAction = new AlterTableHandler::ChangeFieldPropertyAction(*this); if (!nextActionsLikeThis) nextActionsLikeThis = createActionDict(fieldActions, uid()); //fieldName() ); nextActionsLikeThis->insert(m_propertyName.toLatin1(), newAction); } } bool AlterTableHandler::ChangeFieldPropertyAction::shouldBeRemoved(ActionDictDict &fieldActions) { Q_UNUSED(fieldActions); return fieldName().toLower() == m_newValue.toString().toLower(); } tristate AlterTableHandler::ChangeFieldPropertyAction::updateTableSchema(TableSchema &table, Field* field, QHash& fieldHash) { //1. Simpler cases first: changes that do not affect table schema at all // "caption", "description", "width", "visibleDecimalPlaces" if (SchemaAlteringRequired & alteringTypeForProperty(m_propertyName.toLatin1())) { bool result = KexiDB::setFieldProperty(*field, m_propertyName.toLatin1(), newValue()); return result; } if (m_propertyName == "name") { if (fieldHash.value(field->name()) == field->name()) fieldHash.remove(field->name()); fieldHash.insert(newValue().toString(), field->name()); table.renameField(field, newValue().toString()); return true; } return cancelled; } /*! Many of the properties must be applied using a separate algorithm. */ tristate AlterTableHandler::ChangeFieldPropertyAction::execute(Connection &conn, TableSchema &table) { Q_UNUSED(conn); Field *field = table.field(fieldName()); if (!field) { //! @todo errmsg return false; } bool result; //1. Simpler cases first: changes that do not affect table schema at all // "caption", "description", "width", "visibleDecimalPlaces" if (SchemaAlteringRequired & alteringTypeForProperty(m_propertyName.toLatin1())) { result = KexiDB::setFieldProperty(*field, m_propertyName.toLatin1(), newValue()); return result; } //todo return true; //2. Harder cases, that often require special care if (m_propertyName == "name") { /*mysql: A. Get real field type (it's safer): let be the 2nd "Type" column from result of "DESCRIBE tablename oldfieldname" ( http://dev.mysql.com/doc/refman/5.0/en/describe.html ) B. Run "ALTER TABLE tablename CHANGE oldfieldname newfieldname "; ( http://dev.mysql.com/doc/refman/5.0/en/alter-table.html ) */ } if (m_propertyName == "type") { /*mysql: A. Like A. for "name" property above B. Construct string, eg. "varchar(50)" using the driver C. Like B. for "name" property above (mysql then truncate the values for changes like varchar -> integer, and properly convert the values for changes like integer -> varchar) TODO: more cases to check */ } if (m_propertyName == "maxLength") { //use "select max( length(o_name) ) from kexi__Objects" } if (m_propertyName == "primaryKey") { //! @todo } /* "name", "unsigned", "precision", "defaultValue", "primaryKey", "unique", "notNull", "allowEmpty", "autoIncrement", "indexed", bool result = KexiDB::setFieldProperty(*field, m_propertyName.toLatin1(), newValue()); */ return result; } //-------------------------------------------------------- AlterTableHandler::RemoveFieldAction::RemoveFieldAction(const QString& fieldName, int uid) : FieldActionBase(fieldName, uid) { } AlterTableHandler::RemoveFieldAction::RemoveFieldAction(bool) : FieldActionBase(true) { } AlterTableHandler::RemoveFieldAction::~RemoveFieldAction() { } void AlterTableHandler::RemoveFieldAction::updateAlteringRequirements() { //! @todo sometimes add DataConversionRequired (e.g. when relationships require removing orphaned records) ? setAlteringRequirements(PhysicalAlteringRequired); //! @todo } QString AlterTableHandler::RemoveFieldAction::debugString(const DebugOptions& debugOptions) { QString s = QString("Remove table field \"%1\"").arg(fieldName()); if (debugOptions.showUID) s.append(QString(" (UID=%1)").arg(uid())); return s; } /*! Legend: A,B==objects, P==property, [....]==action, (..,..,..) group of actions, <...> internal operation. Preconditions: we assume there cannot be such case encountered: ([remove A], [do something related on A]) (except for [remove A], [insert A]) General Case: it's safe to always insert a [remove A] action. */ void AlterTableHandler::RemoveFieldAction::simplifyActions(ActionDictDict &fieldActions) { //! @todo not checked AlterTableHandler::RemoveFieldAction* newAction = new AlterTableHandler::RemoveFieldAction(*this); ActionDict *actionsLikeThis = fieldActions.value(uid()); //fieldName().toLatin1() ]; if (!actionsLikeThis) actionsLikeThis = createActionDict(fieldActions, uid()); //fieldName() ); actionsLikeThis->insert(":remove:", newAction); //special } tristate AlterTableHandler::RemoveFieldAction::updateTableSchema(TableSchema &table, Field* field, QHash& fieldHash) { fieldHash.remove(field->name()); table.removeField(field); return true; } tristate AlterTableHandler::RemoveFieldAction::execute(Connection& conn, TableSchema& table) { Q_UNUSED(conn); Q_UNUSED(table); //! @todo return true; } //-------------------------------------------------------- AlterTableHandler::InsertFieldAction::InsertFieldAction(int fieldIndex, KexiDB::Field *field, int uid) : FieldActionBase(field->name(), uid) , m_index(fieldIndex) , m_field(0) { Q_ASSERT(field); setField(field); } AlterTableHandler::InsertFieldAction::InsertFieldAction(const InsertFieldAction& action) : FieldActionBase(action) //action.fieldName(), action.uid()) , m_index(action.index()) { m_field = new KexiDB::Field(*action.field()); } AlterTableHandler::InsertFieldAction::InsertFieldAction(bool) : FieldActionBase(true) , m_index(0) , m_field(0) { } AlterTableHandler::InsertFieldAction::~InsertFieldAction() { delete m_field; } void AlterTableHandler::InsertFieldAction::setField(KexiDB::Field* field) { if (m_field) delete m_field; m_field = field; setFieldName(m_field ? m_field->name() : QString()); } void AlterTableHandler::InsertFieldAction::updateAlteringRequirements() { //! @todo sometimes add DataConversionRequired (e.g. when relationships require removing orphaned records) ? setAlteringRequirements(PhysicalAlteringRequired); //! @todo } QString AlterTableHandler::InsertFieldAction::debugString(const DebugOptions& debugOptions) { QString s = QString("Insert table field \"%1\" at position %2") .arg(m_field->name()).arg(m_index); if (debugOptions.showUID) s.append(QString(" (UID=%1)").arg(m_fieldUID)); if (debugOptions.showFieldDebug) s.append(QString(" (%1)").arg(m_field->debugString())); return s; } /*! Legend: A,B==fields, P==property, [....]==action, (..,..,..) group of actions, <...> internal operation. Case 1: there are "change property" actions after the Insert action. -> change the properties in the Insert action itself and remove the "change property" actions. Examples: [Insert A] && [rename A to B] => [Insert B] [Insert A] && [change property P in field A] => [Insert A with P altered] Comment: we need to do this reduction because otherwise we'd need to do psyhical altering right after [Insert A] if [rename A to B] follows. */ void AlterTableHandler::InsertFieldAction::simplifyActions(ActionDictDict &fieldActions) { // Try to find actions related to this action ActionDict *actionsForThisField = fieldActions.value(uid()); //m_field->name().toLatin1() ]; ActionBase *removeActionForThisField = actionsForThisField ? actionsForThisField->value(":remove:") : 0; if (removeActionForThisField) { //if this field is going to be removed, do not add a new action //and remove the "Remove" action actionsForThisField->remove(":remove:"); return; } if (actionsForThisField) { //collect property values that have to be changed in this field - QHash values; + QMap values; ActionDict *newActionsForThisField = new ActionDict(); // this will replace actionsForThisField after the loop QSet actionsToDelete; // used to collect actions taht we soon delete but cannot delete in the loop below for (ActionDictConstIterator it(actionsForThisField->constBegin()); it != actionsForThisField->constEnd();++it) { ChangeFieldPropertyAction* changePropertyAction = dynamic_cast(it.value()); if (changePropertyAction) { //if this field is going to be renamed, also update fieldName() if (changePropertyAction->propertyName() == "name") { setFieldName(changePropertyAction->newValue().toString()); } values.insert(changePropertyAction->propertyName().toLatin1(), changePropertyAction->newValue()); //the subsequent "change property" action is no longer needed actionsToDelete.insert(it.value()); } else { //keep newActionsForThisField->insert(it.key(), it.value()); } } qDeleteAll(actionsToDelete); actionsForThisField->setAutoDelete(false); delete actionsForThisField; actionsForThisField = newActionsForThisField; fieldActions.take(uid()); fieldActions.insert(uid(), actionsForThisField); if (!values.isEmpty()) { //update field, so it will be created as one step KexiDB::Field *f = new KexiDB::Field(*field()); if (KexiDB::setFieldProperties(*f, values)) { //field() = f; setField(f); field()->debug(); #ifdef KEXI_DEBUG_GUI KexiDB::alterTableActionDebugGUI( QString("** Property-set actions moved to field definition itself:\n") + field()->debugString(), 0); #endif } else { #ifdef KEXI_DEBUG_GUI KexiDB::alterTableActionDebugGUI( QString("** Failed to set properties for field ") + field()->debugString(), 0); #endif KexiDBWarn << "AlterTableHandler::InsertFieldAction::simplifyActions(): KexiDB::setFieldProperties() failed!"; delete f; } } } //ok, insert this action //! @todo not checked AlterTableHandler::InsertFieldAction* newAction = new AlterTableHandler::InsertFieldAction(*this); if (!actionsForThisField) actionsForThisField = createActionDict(fieldActions, uid()); actionsForThisField->insert(":insert:", newAction); //special } tristate AlterTableHandler::InsertFieldAction::updateTableSchema(TableSchema &table, Field* field, QHash& fieldMap) { //in most cases we won't add the field to fieldMap Q_UNUSED(field); //! @todo add it only when there should be fixed value (e.g. default) set for this new field... fieldMap.remove(this->field()->name()); table.insertField(index(), new Field(*this->field())); return true; } tristate AlterTableHandler::InsertFieldAction::execute(Connection& conn, TableSchema& table) { Q_UNUSED(conn); Q_UNUSED(table); //! @todo return true; } //-------------------------------------------------------- AlterTableHandler::MoveFieldPositionAction::MoveFieldPositionAction( int fieldIndex, const QString& fieldName, int uid) : FieldActionBase(fieldName, uid) , m_index(fieldIndex) { } AlterTableHandler::MoveFieldPositionAction::MoveFieldPositionAction(bool) : FieldActionBase(true) { } AlterTableHandler::MoveFieldPositionAction::~MoveFieldPositionAction() { } void AlterTableHandler::MoveFieldPositionAction::updateAlteringRequirements() { setAlteringRequirements(MainSchemaAlteringRequired); //! @todo } QString AlterTableHandler::MoveFieldPositionAction::debugString(const DebugOptions& debugOptions) { QString s = QString("Move table field \"%1\" to position %2") .arg(fieldName()).arg(m_index); if (debugOptions.showUID) s.append(QString(" (UID=%1)").arg(uid())); return s; } void AlterTableHandler::MoveFieldPositionAction::simplifyActions(ActionDictDict &fieldActions) { Q_UNUSED(fieldActions); //! @todo } tristate AlterTableHandler::MoveFieldPositionAction::execute(Connection& conn, TableSchema& table) { Q_UNUSED(conn); Q_UNUSED(table); //! @todo return true; } //-------------------------------------------------------- AlterTableHandler::AlterTableHandler(Connection &conn) : Object() , d(new Private()) { d->conn = &conn; } AlterTableHandler::~AlterTableHandler() { delete d; } void AlterTableHandler::addAction(ActionBase* action) { d->actions.append(action); } AlterTableHandler& AlterTableHandler::operator<< (ActionBase* action) { d->actions.append(action); return *this; } const AlterTableHandler::ActionList& AlterTableHandler::actions() const { return d->actions; } void AlterTableHandler::removeAction(int index) { d->actions.removeAt(index); } void AlterTableHandler::clear() { d->actions.clear(); } void AlterTableHandler::setActions(const ActionList& actions) { qDeleteAll(d->actions); d->actions = actions; } void AlterTableHandler::debug() { KexiDBDbg << "AlterTableHandler's actions:"; foreach(ActionBase* action, d->actions) { action->debug(); } } TableSchema* AlterTableHandler::execute(const QString& tableName, ExecutionArguments& args) { args.result = false; if (!d->conn) { //! @todo err msg? return 0; } if (d->conn->isReadOnly()) { //! @todo err msg? return 0; } if (!d->conn->isDatabaseUsed()) { //! @todo err msg? return 0; } TableSchema *oldTable = d->conn->tableSchema(tableName); if (!oldTable) { //! @todo err msg? return 0; } if (!args.debugString) debug(); // Find a sum of requirements... int allActionsCount = 0; foreach(ActionBase* action, d->actions) { action->updateAlteringRequirements(); action->m_order = allActionsCount++; } /* Simplify actions list if possible and check for errors How to do it? - track property changes/deletions in reversed order - reduce intermediate actions Trivial example 1: *action1: "rename field a to b" *action2: "rename field b to c" *action3: "rename field c to d" After reduction: *action1: "rename field a to d" Summing up: we have tracked what happens to field curently named "d" and eventually discovered that it was originally named "a". Trivial example 2: *action1: "rename field a to b" *action2: "rename field b to c" *action3: "remove field b" After reduction: *action3: "remove field b" Summing up: we have noticed that field "b" has beed eventually removed so we needed to find all actions related to this field and remove them. This is good optimization, as some of the eventually removed actions would be difficult to perform and/or costly, what would be a waste of resources and a source of unwanted questions sent to the user. */ // Fields-related actions. ActionDictDict fieldActions; //Qt 4 fieldActions.setAutoDelete(true); ActionBase* action; for (int i = d->actions.count() - 1; i >= 0; i--) { d->actions[i]->simplifyActions(fieldActions); } if (!args.debugString) debugFieldActions(fieldActions, args.simulate); // Prepare actions for execution ---- // - Sort actions by order ActionsVector actionsVector(allActionsCount); int currentActionsCount = 0; //some actions may be removed args.requirements = 0; QSet fieldsWithChangedMainSchema; // Used to collect fields with changed main schema. // This will be used when recreateTable is false to update kexi__fields for (ActionDictDictConstIterator it(fieldActions.constBegin()); it != fieldActions.constEnd(); ++it) { for (AlterTableHandler::ActionDictConstIterator it2(it.value()->constBegin()); it2 != it.value()->constEnd(); ++it2, ++currentActionsCount) { if (it2.value()->shouldBeRemoved(fieldActions)) continue; actionsVector[ it2.value()->m_order ] = it2.value(); // a sum of requirements... const int r = it2.value()->alteringRequirements(); args.requirements |= r; if (r & MainSchemaAlteringRequired && dynamic_cast(it2.value())) { // Remember, this will be used when recreateTable is false to update kexi__fields, below. fieldsWithChangedMainSchema.insert( dynamic_cast(it2.value())->fieldName()); } } } // - Debug QString dbg = QString("** Overall altering requirements: %1").arg(args.requirements); KexiDBDbg << dbg; if (args.onlyComputeRequirements) { args.result = true; return 0; } const bool recreateTable = (args.requirements & PhysicalAlteringRequired); #ifdef KEXI_DEBUG_GUI if (args.simulate) KexiDB::alterTableActionDebugGUI(dbg, 0); #endif dbg = QString("** Ordered, simplified actions (%1, was %2):") .arg(currentActionsCount).arg(allActionsCount); KexiDBDbg << dbg; #ifdef KEXI_DEBUG_GUI if (args.simulate) KexiDB::alterTableActionDebugGUI(dbg, 0); #endif for (int i = 0; i < allActionsCount; i++) { debugAction(actionsVector.at(i), 1, args.simulate, QString("%1: ").arg(i + 1), args.debugString); } if (args.requirements == 0) {//nothing to do args.result = true; return oldTable; } if (args.simulate) {//do not execute args.result = true; return oldTable; } // @todo transaction! // Create new TableSchema TableSchema *newTable = recreateTable ? new TableSchema(*oldTable, false/*!copy id*/) : oldTable; // find nonexisting temp name for new table schema if (recreateTable) { QString tempDestTableName; while (true) { tempDestTableName = QString("%1_temp%2%3").arg(newTable->name()).arg(QString::number(rand(), 16)).arg(QString::number(rand(), 16)); if (!d->conn->tableSchema(tempDestTableName)) break; } newTable->setName(tempDestTableName); } oldTable->debug(); if (recreateTable && !args.debugString) newTable->debug(); // Update table schema in memory ---- int lastUID = -1; Field *currentField = 0; QHash fieldHash; // a map from new value to old value foreach(Field* f, *newTable->fields()) { fieldHash.insert(f->name(), f->name()); } for (int i = 0; i < allActionsCount; i++) { action = actionsVector.at(i); if (!action) continue; //remember the current Field object because soon we may be unable to find it by name: FieldActionBase *fieldAction = dynamic_cast(action); if (!fieldAction) { currentField = 0; } else { if (lastUID != fieldAction->uid()) { currentField = newTable->field(fieldAction->fieldName()); lastUID = currentField ? fieldAction->uid() : -1; } InsertFieldAction *insertFieldAction = dynamic_cast(action); if (insertFieldAction && insertFieldAction->index() > (int)newTable->fieldCount()) { //update index: there can be empty rows insertFieldAction->setIndex(newTable->fieldCount()); } } //if (!currentField) // continue; args.result = action->updateTableSchema(*newTable, currentField, fieldHash); if (args.result != true) { if (recreateTable) delete newTable; return 0; } } if (recreateTable) { // Create the destination table with temporary name if (!d->conn->createTable(newTable, false)) { setError(d->conn); delete newTable; args.result = false; return 0; } } #if 0//todo // Execute actions ---- for (int i = 0; i < allActionsCount; i++) { action = actionsVector.at(i); if (!action) continue; args.result = action->execute(*d->conn, *newTable); if (!args.result || ~args.result) { //! @todo delete newTable... args.result = false; return 0; } } #endif // update extended table schema after executing the actions if (!d->conn->storeExtendedTableSchemaData(*newTable)) { //! @todo better errmsg? setError(d->conn); //! @todo delete newTable... args.result = false; return 0; } if (recreateTable) { // Copy the data: // Build "INSERT INTO ... SELECT FROM ..." SQL statement // The order is based on the order of the source table fields. // Notes: // -Some source fields can be skipped in case when there are deleted fields. // -Some destination fields can be skipped in case when there // are new empty fields without fixed/default value. QString sql = QString("INSERT INTO %1 (").arg(d->conn->escapeIdentifier(newTable->name())); //insert list of dest. fields bool first = true; QString sourceFields; foreach(Field* f, *newTable->fields()) { QString renamedFieldName(fieldHash.value(f->name())); QString sourceSQLString; if (!renamedFieldName.isEmpty()) { //this field should be renamed sourceSQLString = d->conn->escapeIdentifier(renamedFieldName); } else if (!f->defaultValue().isNull()) { //this field has a default value defined //! @todo support expressions (eg. TODAY()) as a default value //! @todo this field can be notNull or notEmpty - check whether the default is ok //! (or do this checking also in the Table Designer?) sourceSQLString = d->conn->driver()->valueToSQL(f->type(), f->defaultValue()); } else if (f->isNotNull()) { //this field cannot be null sourceSQLString = d->conn->driver()->valueToSQL( f->type(), KexiDB::emptyValueForType(f->type())); } else if (f->isNotEmpty()) { //this field cannot be empty - use any nonempty value..., e.g. " " for text or 0 for number sourceSQLString = d->conn->driver()->valueToSQL( f->type(), KexiDB::notEmptyValueForType(f->type())); } //! @todo support unique, validatationRule, unsigned flags... //! @todo check for foreignKey values... if (!sourceSQLString.isEmpty()) { if (first) { first = false; } else { sql.append(", "); sourceFields.append(", "); } sql.append(d->conn->escapeIdentifier(f->name())); sourceFields.append(sourceSQLString); } } sql.append(QString(") SELECT ") + sourceFields + " FROM " + oldTable->name()); KexiDBDbg << " ** " << sql; if (!d->conn->executeSQL(sql)) { setError(d->conn); //! @todo delete newTable... args.result = false; return 0; } const QString oldTableName = oldTable->name(); /* args.result = d->conn->dropTable( oldTable ); if (!args.result || ~args.result) { setError(d->conn); //! @todo delete newTable... return 0; } oldTable = 0;*/ // Replace the old table with the new one (oldTable will be destroyed) if (!d->conn->alterTableName(*newTable, oldTableName, true /*replace*/)) { setError(d->conn); //! @todo delete newTable... args.result = false; return 0; } oldTable = 0; } if (!recreateTable) { if ((MainSchemaAlteringRequired & args.requirements) && !fieldsWithChangedMainSchema.isEmpty()) { //update main schema (kexi__fields) for changed fields foreach(const QString& changeFieldPropertyActionName, fieldsWithChangedMainSchema) { Field *f = newTable->field(changeFieldPropertyActionName); if (f) { if (!d->conn->storeMainFieldSchema(f)) { setError(d->conn); //! @todo delete newTable... args.result = false; return 0; } } } } } args.result = true; return newTable; } /*TableSchema* AlterTableHandler::execute(const QString& tableName, tristate &result, bool simulate) { return executeInternal( tableName, result, simulate, 0 ); } tristate AlterTableHandler::simulateExecution(const QString& tableName, QString& debugString) { tristate result; (void)executeInternal( tableName, result, true//simulate , &debugString ); return result; } */ diff --git a/kexi/kexidb/alter.h b/kexi/kexidb/alter.h index 9d62561826f..e331331fc4f 100644 --- a/kexi/kexidb/alter.h +++ b/kexi/kexidb/alter.h @@ -1,510 +1,510 @@ /* This file is part of the KDE project - Copyright (C) 2006-2007 Jarosław Staniek + Copyright (C) 2006-2012 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KEXIDB_ALTER_H #define KEXIDB_ALTER_H #include "kexidb_export.h" #include #include #include #include #include namespace KexiDB { class Connection; //! @short A tool for handling altering database table schema. /*! In relational (and other) databases, table schema altering is not an easy task. It may be considered as easy if there is no data that user wants to keep while the table schema is altered. Otherwise, if the table is alredy filled with data, there could be no easy algorithm like: 1. Drop existing table 2. Create new one with altered schema. Instead, more complex algorithm is needed. To perform the table schema alteration, a list of well defined atomic operations is used as a "recipe". 1. Look at the current data, and: 1.1. analyze what values will be removed (in case of impossible conversion or table field removal); 1.2. analyze what values can be converted (e.g. from numeric types to text), and so on. 2. Optimize the atomic actions knowing that sometimes a compilation of one action and another that's opposite to the first means "do nothing". The optimization is a simulating of actions' execution. For example, when both action A="change field name from 'city' to 'town'" and action B="change field name from 'town' to 'city'" is specified, the compilation of the actions means "change field name from 'city' to 'city'", what is a NULL action. On the other hand, we need to execute all the actions on the destination table in proper order, and not just drop them. For the mentioned example, between actions A and B there can be an action like C="change the type of field 'city' to LongText". If A and B were simply removed, C would become invalid (there is no 'city' field). 3. Ask user whether she agrees with the results of analysis mentioned in 1. 3.2. Additionally, it may be possible to get some hints from the user, as humans usually know more about logic behind the altered table schema than any machine. If the user provided hints about the altering, apply them to the actions list. 4. Create (empty) destination table schema with temporary name, using the information collected so far. 5. Copy the data from the source to destionation table. Convert values, move them between fields, using the information collected. 6. Remove the source table. 7. Rename the destination table to the name previously assigned for the source table. Notes: * The actions 4 to 7 should be performed within a database transaction. * [todo] We want to take care about database relationships as well. For example, is a table field is removed, relationships related to this field should be also removed (similar rules as in the Query Designer). * Especially, care about primary keys and uniquess (indices). Recreate them when needed. The problem could be if such analysis may require to fetch the entire table data to the client side. Use "SELECT INTO" statments if possible to avoid such a treat. The AlterTableHandler is used in Kexi's Table Designer. Already opened Connection object is needed. Use case: \code Connection *conn = ... // add some actions (in reality this is performed by tracking user's actions) // Actions 1, 2 will require physical table altering PhysicalAltering // Action 3 will only require changes in kexi__fields // Action 4 will only require changes in extended table schema written in kexi__objectdata AlterTable::ActionList list; // 1. rename the "city" field to "town" list << new ChangeFieldPropertyAction("city", "name", "town") // 2. change type of "town" field to "LongText" << new ChangeFieldPropertyAction("town", "type", "LongText") // 3. set caption of "town" field to "Town" << new ChangeFieldPropertyAction("town", "caption", "Town") // 4. set visible decimal places to 4 for "cost" field << new ChangeFieldPropertyAction("cost", "visibleDecimalPlaces", 4) AlterTableHandler::execute( *conn ); \endcode Actions for Alter */ class KEXI_DB_EXPORT AlterTableHandler : public Object { public: class ChangeFieldPropertyAction; class RemoveFieldAction; class InsertFieldAction; class MoveFieldPositionAction; //! Defines flags for possible altering requirements; can be combined. enum AlteringRequirements { /*! Physical table altering is required; e.g. ALTER TABLE ADD COLUMN. */ PhysicalAlteringRequired = 1, /*! Data conversion is required; e.g. converting integer values to string after changing column type from integer to text. */ DataConversionRequired = 2, - /* Changes to the main table schema (in kexi__fields) required, + /*! Changes to the main table schema (in kexi__fields) required, this does not require physical changes for the table; e.g. changing value of the "caption" or "description" property. */ MainSchemaAlteringRequired = 4, - /* Only changes to extended table schema required, + /*! Only changes to extended table schema required, this does not require physical changes for the table; e.g. changing value of the "visibleDecimalPlaces" property or any of the custom properties. */ ExtendedSchemaAlteringRequired = 8, /*! Convenience flag, changes to the main or extended schema is required. */ SchemaAlteringRequired = ExtendedSchemaAlteringRequired | MainSchemaAlteringRequired }; class ActionBase; //! For collecting actions related to a single field typedef KexiDB::AutodeletedHash ActionDict; typedef KexiDB::AutodeletedHash ActionDictDict; //!< for collecting groups of actions by field UID typedef QHash::Iterator ActionDictIterator; typedef QHash::ConstIterator ActionDictConstIterator; typedef QHash::Iterator ActionDictDictIterator; typedef QHash::ConstIterator ActionDictDictConstIterator; typedef QVector ActionsVector; //!< for collecting actions related to a single field //! Defines a type for action list. typedef QList ActionList; //! Defines a type for action list's iterator. typedef QList::ConstIterator ActionListIterator; //! Abstract base class used for implementing all the AlterTable actions. class KEXI_DB_EXPORT ActionBase { public: ActionBase(bool null = false); virtual ~ActionBase(); ChangeFieldPropertyAction& toChangeFieldPropertyAction(); RemoveFieldAction& toRemoveFieldAction(); InsertFieldAction& toInsertFieldAction(); MoveFieldPositionAction& toMoveFieldPositionAction(); //! \return true if the action is NULL; used in the Table Designer //! for temporarily collecting actions that have no effect at all. bool isNull() const { return m_null; } //! Controls debug options for actions. Used in debugString() and debug(). class DebugOptions { public: DebugOptions() : showUID(true), showFieldDebug(false) {} //! true if UID should be added to the action debug string (the default) bool showUID; //! true if the field associated with the action (if exists) should //! be appended to the debug string (default is false) bool showFieldDebug; }; virtual QString debugString(const DebugOptions& debugOptions = DebugOptions()) { Q_UNUSED(debugOptions); return "ActionBase"; } void debug(const DebugOptions& debugOptions = DebugOptions()) { KexiDBDbg << debugString(debugOptions) << " (req = " << alteringRequirements() << ")"; } protected: //! Sets requirements for altering; used internally by AlterTableHandler object void setAlteringRequirements(int alteringRequirements) { m_alteringRequirements = alteringRequirements; } int alteringRequirements() const { return m_alteringRequirements; } virtual void updateAlteringRequirements() {} /*! Simplifies \a fieldActions dictionary. If this action has to be inserted Into the dictionary, an ActionDict is created first and then a copy of this action is inserted into it. */ virtual void simplifyActions(ActionDictDict &fieldActions) { Q_UNUSED(fieldActions); } /*! After calling simplifyActions() for each action, shouldBeRemoved() is called for them as an additional step. This is used for ChangeFieldPropertyAction items so actions that do not change property values are removed. */ virtual bool shouldBeRemoved(ActionDictDict &fieldActions) { Q_UNUSED(fieldActions); return false; } virtual tristate updateTableSchema(TableSchema &table, Field* field, QHash& fieldHash) { Q_UNUSED(table); Q_UNUSED(field); Q_UNUSED(fieldHash); return true; } private: //! Performs physical execution of this action. virtual tristate execute(Connection& /*conn*/, TableSchema& /*table*/) { return true; } //! requirements for altering; used internally by AlterTableHandler object int m_alteringRequirements; //! @internal used for "simplify" algorithm int m_order; bool m_null; friend class AlterTableHandler; }; //! Abstract base class used for implementing table field-related actions. class KEXI_DB_EXPORT FieldActionBase : public ActionBase { public: FieldActionBase(const QString& fieldName, int uid); FieldActionBase(bool); virtual ~FieldActionBase(); //! \return field name for this action QString fieldName() const { return m_fieldName; } /*! \return field's unique identifier This id is needed because in the meantime there can be more than one field sharing the same name, so we need to identify them unambiguously. After the (valid) altering is completed all the names will be unique. Example scenario when user exchanged the field names: 1. At the beginning: [field A], [field B] 2. Rename the 1st field to B: [field B], [field B] 3. Rename the 2nd field to A: [field B], [field A] */ int uid() const { return m_fieldUID; } //! Sets field name for this action void setFieldName(const QString& fieldName) { m_fieldName = fieldName; } protected: //! field's unique identifier, @see uid() int m_fieldUID; private: QString m_fieldName; }; /*! Defines an action for changing a single property value of a table field. Supported properties are currently: "name", "type", "caption", "description", "unsigned", "maxLength", "precision", "width", "defaultValue", "primaryKey", "unique", "notNull", "allowEmpty", "autoIncrement", "indexed", "visibleDecimalPlaces" More to come. */ class KEXI_DB_EXPORT ChangeFieldPropertyAction : public FieldActionBase { public: ChangeFieldPropertyAction(const QString& fieldName, const QString& propertyName, const QVariant& newValue, int uid); //! @internal, used for constructing null action ChangeFieldPropertyAction(bool null); virtual ~ChangeFieldPropertyAction(); QString propertyName() const { return m_propertyName; } QVariant newValue() const { return m_newValue; } virtual QString debugString(const DebugOptions& debugOptions = DebugOptions()); virtual void simplifyActions(ActionDictDict &fieldActions); virtual bool shouldBeRemoved(ActionDictDict &fieldActions); virtual tristate updateTableSchema(TableSchema &table, Field* field, QHash& fieldHash); protected: virtual void updateAlteringRequirements(); //! Performs physical execution of this action. virtual tristate execute(Connection &conn, TableSchema &table); QString m_propertyName; QVariant m_newValue; }; //! Defines an action for removing a single table field. class KEXI_DB_EXPORT RemoveFieldAction : public FieldActionBase { public: RemoveFieldAction(const QString& fieldName, int uid); RemoveFieldAction(bool); virtual ~RemoveFieldAction(); virtual QString debugString(const DebugOptions& debugOptions = DebugOptions()); virtual void simplifyActions(ActionDictDict &fieldActions); virtual tristate updateTableSchema(TableSchema &table, Field* field, QHash& fieldHash); protected: virtual void updateAlteringRequirements(); //! Performs physical execution of this action. virtual tristate execute(Connection &conn, TableSchema &table); }; //! Defines an action for inserting a single table field. class KEXI_DB_EXPORT InsertFieldAction : public FieldActionBase { public: InsertFieldAction(int fieldIndex, KexiDB::Field *newField, int uid); //copy ctor InsertFieldAction(const InsertFieldAction& action); InsertFieldAction(bool); virtual ~InsertFieldAction(); int index() const { return m_index; } void setIndex(int index) { m_index = index; } const KexiDB::Field* field() const { return m_field; } void setField(KexiDB::Field* field); virtual QString debugString(const DebugOptions& debugOptions = DebugOptions()); virtual void simplifyActions(ActionDictDict &fieldActions); virtual tristate updateTableSchema(TableSchema &table, Field* field, QHash& fieldHash); protected: virtual void updateAlteringRequirements(); //! Performs physical execution of this action. virtual tristate execute(Connection &conn, TableSchema &table); int m_index; private: KexiDB::Field *m_field; }; /*! Defines an action for moving a single table field to a different position within table schema. */ class KEXI_DB_EXPORT MoveFieldPositionAction : public FieldActionBase { public: MoveFieldPositionAction(int fieldIndex, const QString& fieldName, int uid); MoveFieldPositionAction(bool); virtual ~MoveFieldPositionAction(); int index() const { return m_index; } virtual QString debugString(const DebugOptions& debugOptions = DebugOptions()); virtual void simplifyActions(ActionDictDict &fieldActions); protected: virtual void updateAlteringRequirements(); //! Performs physical execution of this action. virtual tristate execute(Connection &conn, TableSchema &table); int m_index; }; AlterTableHandler(Connection &conn); virtual ~AlterTableHandler(); /*! Appends \a action for the alter table tool. */ void addAction(ActionBase* action); /*! Provided for convenience, @see addAction(const ActionBase& action). */ AlterTableHandler& operator<< (ActionBase* action); /*! Removes an action from the alter table tool at index \a index. */ void removeAction(int index); /*! Removes all actions from the alter table tool. */ void clear(); /*! Sets \a actions for the alter table tool. Previous actions are cleared. \a actions will be owned by the AlterTableHandler object. */ void setActions(const ActionList& actions); /*! \return a list of actions for this AlterTable object. Use ActionBase::ListIterator to iterate over the list items. */ const ActionList& actions() const; //! Arguments for AlterTableHandler::execute(). class ExecutionArguments { public: ExecutionArguments() : debugString(0) , requirements(0) , result(false) , simulate(false) , onlyComputeRequirements(false) { } /*! If not 0, debug is directed here. Used only in the alter table test suite. */ QString* debugString; /*! Requrements computed, a combination of AlteringRequirements values. */ int requirements; /*! Set to true on success, to false on failure. */ tristate result; /*! Used only in the alter table test suite. */ bool simulate; /*! Set to true if requirements should be computed and the execute() method should return afterwards. */ bool onlyComputeRequirements; }; /*! Performs table alteration using predefined actions for table named \a tableName, assuming it already exists. The Connection object passed to the constructor must exist, must be connected and a database must be used. The connection must not be read-only. If args.simulate is true, the execution is only simulated, i.e. al lactions are processed like for regular execution but no changes are performed physically. This mode is used only for debugging purposes. @todo For some cases, table schema can completely change, so it will be needed to refresh all objects depending on it. Implement this! Sets args.result to true on success, to false on failure or when the above requirements are not met (then, you can get a detailed error message from KexiDB::Object). When the action has been cancelled (stopped), args.result is set to cancelled value. If args.debugString is not 0, it will be filled with debugging output. \return the new table schema object created as a result of schema altering. The old table is returned if recreating table schema was not necessary or args.simulate is true. 0 is returned if args.result is not true. */ TableSchema* execute(const QString& tableName, ExecutionArguments & args); //! Displays debug information about all actions collected by the handler. void debug(); /*! Like execute() with simulate set to true, but debug is directed to debugString. This function is used only in the alter table test suite. */ // tristate simulateExecution(const QString& tableName, QString& debugString); /*! Helper. \return a combination of AlteringRequirements values decribing altering type required when a given property field's \a propertyName is altered. Used internally AlterTableHandler. Moreover it can be also used in the Table Designer's code as a temporary replacement before AlterTableHandler is fully implemented. Thus, it is possible to identify properties that have no PhysicalAlteringRequired flag set (e.g. caption or extended properties like visibleDecimalPlaces. */ static int alteringTypeForProperty(const QByteArray& propertyName); protected: // TableSchema* executeInternal(const QString& tableName, tristate& result, bool simulate = false, // QString* debugString = 0); class Private; Private * const d; }; } #endif diff --git a/kexi/plugins/tables/kexitabledesignerview.cpp b/kexi/plugins/tables/kexitabledesignerview.cpp index 79284f1a3b2..ec92bde9242 100644 --- a/kexi/plugins/tables/kexitabledesignerview.cpp +++ b/kexi/plugins/tables/kexitabledesignerview.cpp @@ -1,1939 +1,1940 @@ /* This file is part of the KDE project Copyright (C) 2004-2012 Jarosław Staniek This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kexitabledesignerview.h" #include "kexitabledesignerview_p.h" #include "kexilookupcolumnpage.h" #include "kexitabledesignercommands.h" #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#define MAX_FIELDS 101 //nice prime number //! used only for BLOBs #define DEFAULT_OBJECT_TYPE_VALUE "image" //#define KexiTableDesignerView_DEBUG //! @todo remove this when BLOBs are implemented //#define KEXI_NO_BLOB_FIELDS using namespace KexiTableDesignerCommands; //! @internal Used in tryCastQVariant() anf canCastQVariant() static bool isIntegerQVariant(QVariant::Type t) { return t == QVariant::LongLong || t == QVariant::ULongLong || t == QVariant::Int || t == QVariant::UInt; } //! @internal Used in tryCastQVariant() static bool canCastQVariant(QVariant::Type fromType, QVariant::Type toType) { return (fromType == QVariant::Int && toType == QVariant::UInt) || (fromType == QVariant::ByteArray && toType == QVariant::String) || (fromType == QVariant::LongLong && toType == QVariant::ULongLong) || ((fromType == QVariant::String || fromType == QVariant::ByteArray) && (isIntegerQVariant(toType) || toType == QVariant::Double)); } /*! @internal \return a variant value converted from \a fromVal to \a toType type. Null QVariant is returned if \a fromVal's type and \a toType type are incompatible. */ static QVariant tryCastQVariant(const QVariant& fromVal, QVariant::Type toType) { const QVariant::Type fromType = fromVal.type(); if (fromType == toType) return fromVal; if (canCastQVariant(fromType, toType) || canCastQVariant(toType, fromType) || (isIntegerQVariant(fromType) && toType == QVariant::Double)) { QVariant res(fromVal); if (res.convert(toType)) return res; } return QVariant(); } KexiTableDesignerView::KexiTableDesignerView(QWidget *parent) : KexiDataTable(parent, false/*not db-aware*/) , KexiTableDesignerInterface() , d(new KexiTableDesignerViewPrivate(this)) { setObjectName("KexiTableDesignerView"); //needed for custom "identifier" property editor widget KexiCustomPropertyFactory::init(); KexiDB::Connection *conn = KexiMainWindowIface::global()->project()->dbConnection(); d->view = dynamic_cast(mainWidget()); d->data = new KexiTableViewData(); if (conn->isReadOnly()) d->data->setReadOnly(true); d->data->setInsertingEnabled(false); KexiTableViewColumn *col = new KexiTableViewColumn("pk", KexiDB::Field::Text, QString(), i18n("Additional information about the field")); col->setIcon(KexiUtils::colorizeIconToTextColor(koSmallIcon("help-about"), d->view->palette())); col->setHeaderTextVisible(false); col->field()->setSubType("KIcon"); col->setReadOnly(true); d->data->addColumn(col); // col = new KexiTableViewColumn("name", KexiDB::Field::Text, i18n("Field Name"), col = new KexiTableViewColumn("caption", KexiDB::Field::Text, i18n("Field Caption"), i18n("Describes caption for the field")); // KexiUtils::Validator *vd = new KexiUtils::IdentifierValidator(); // vd->setAcceptsEmptyValue(true); // col->setValidator( vd ); d->data->addColumn(col); col = new KexiTableViewColumn("type", KexiDB::Field::Enum, i18n("Data Type"), i18n("Describes data type for the field")); d->data->addColumn(col); #ifdef KEXI_NO_BLOB_FIELDS //! @todo remove this later QVector types(KexiDB::Field::LastTypeGroup - 1); //don't show last type (BLOB) #else QVector types(KexiDB::Field::LastTypeGroup); #endif d->maxTypeNameTextWidth = 0; QFontMetrics fm(font()); for (uint i = 1; i <= (uint)types.count(); i++) { types[i-1] = KexiDB::Field::typeGroupName(i); d->maxTypeNameTextWidth = qMax(d->maxTypeNameTextWidth, fm.width(types[i-1])); } col->field()->setEnumHints(types); d->data->addColumn(col = new KexiTableViewColumn("comments", KexiDB::Field::Text, i18n("Comments"), i18n("Describes additional comments for the field"))); d->view->setSpreadSheetMode(); connect(d->data, SIGNAL(aboutToChangeCell(KexiDB::RecordData*, int, QVariant&, KexiDB::ResultInfo*)), this, SLOT(slotBeforeCellChanged(KexiDB::RecordData*, int, QVariant&, KexiDB::ResultInfo*))); connect(d->data, SIGNAL(rowUpdated(KexiDB::RecordData*)), this, SLOT(slotRowUpdated(KexiDB::RecordData*))); //connect(d->data, SIGNAL(aboutToInsertRow(KexiDB::RecordData*,KexiDB::ResultInfo*,bool)), // this, SLOT(slotAboutToInsertRow(KexiDB::RecordData*,KexiDB::ResultInfo*,bool))); connect(d->data, SIGNAL(aboutToDeleteRow(KexiDB::RecordData&, KexiDB::ResultInfo*, bool)), this, SLOT(slotAboutToDeleteRow(KexiDB::RecordData&, KexiDB::ResultInfo*, bool))); setMinimumSize(d->view->minimumSizeHint().width(), d->view->minimumSizeHint().height()); d->view->setFocus(); d->sets = new KexiDataAwarePropertySet(this, d->view); connect(d->sets, SIGNAL(rowDeleted()), this, SLOT(updateActions())); connect(d->sets, SIGNAL(rowInserted()), this, SLOT(slotRowInserted())); //d->contextMenuTitle = new KPopupTitle(d->view->contextMenu()); //d->view->contextMenu()->insertItem(d->contextMenuTitle, -1, 0); // d->contextMenuTitle = d->view->contextMenu()->addTitle(QString()); connect(d->view->contextMenu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShowContextMenu())); // - setup local actions QList viewActions; QAction* a; viewActions << (d->action_toggle_pkey = new KToggleAction(koIcon("key"), i18n("Primary Key"), this)); a = d->action_toggle_pkey; a->setObjectName("tablepart_toggle_pkey"); a->setToolTip(i18n("Sets or removes primary key")); a->setWhatsThis(i18n("Sets or removes primary key for currently selected field.")); connect(a, SIGNAL(triggered()), this, SLOT(slotTogglePrimaryKey())); setViewActions(viewActions); d->view->contextMenu()->insertAction( d->view->contextMenu()->actions()[1], d->action_toggle_pkey); //add at the beginning as 2nd // d->action_toggle_pkey->plug(d->view->contextMenu(), 1); //add at the beginning as 2nd d->view->contextMenu()->insertSeparator(d->view->contextMenu()->actions()[2]); //as 3rd setAvailable("tablepart_toggle_pkey", !conn->isReadOnly()); #ifndef KEXI_NO_UNDOREDO_ALTERTABLE plugSharedAction("edit_undo", this, SLOT(slotUndo())); plugSharedAction("edit_redo", this, SLOT(slotRedo())); setAvailable("edit_undo", false); setAvailable("edit_redo", false); //!todo qundo connect(d->history, SIGNAL(commandExecuted(K3Command*)), this, SLOT(slotCommandExecuted(K3Command*))); #endif #ifdef KEXI_DEBUG_GUI KexiDB::alterTableActionDebugGUI(QString()); //to create the tab KexiUtils::connectPushButtonActionForDebugWindow( "simulateAlterTableExecution", this, SLOT(slotSimulateAlterTableExecution())); KexiUtils::connectPushButtonActionForDebugWindow( "executeRealAlterTable", this, SLOT(executeRealAlterTable())); #endif } KexiTableDesignerView::~KexiTableDesignerView() { // eraseCurrentPropertySet(); delete d; } void KexiTableDesignerView::initData() { //add column data // d->data->clear(); d->data->deleteAllRows(); int tableFieldCount = 0; d->primaryKeyExists = false; if (tempData()->table) { tableFieldCount = tempData()->table->fieldCount(); //not needed d->sets->clear(tableFieldCount); //recreate table data rows for (int i = 0; i < tableFieldCount; i++) { KexiDB::Field *field = tempData()->table->field(i); KexiDB::RecordData *record = d->data->createItem(); if (field->isPrimaryKey()) { (*record)[COLUMN_ID_ICON] = "key"; d->primaryKeyExists = true; } else { KexiDB::LookupFieldSchema *lookupFieldSchema = field->table() ? field->table()->lookupFieldSchema(*field) : 0; if (lookupFieldSchema && lookupFieldSchema->rowSource().type() != KexiDB::LookupFieldSchema::RowSource::NoType && !lookupFieldSchema->rowSource().name().isEmpty()) { (*record)[COLUMN_ID_ICON] = "combo"; } } (*record)[COLUMN_ID_CAPTION] = field->captionOrName(); (*record)[COLUMN_ID_TYPE] = field->typeGroup() - 1; //-1 because type groups are counted from 1 (*record)[COLUMN_ID_DESC] = field->description(); d->data->append(record); //later! createPropertySet( i, field ); } } // else { // d->sets->clear();//default size // } //add empty space // const int columnsCount = d->data->columnsCount(); for (int i = tableFieldCount; i < (int)d->sets->size(); i++) { // KexiDB::RecordData *item = new KexiDB::RecordData(columnsCount);//3 empty fields d->data->append(d->data->createItem()); } //set data for our spreadsheet: this will clear our sets d->view->setData(d->data); //now recreate property sets if (tempData()->table) { for (int i = 0; i < tableFieldCount; i++) { KexiDB::Field *field = tempData()->table->field(i); createPropertySet(i, *field); } } //column widths d->view->setColumnWidth(COLUMN_ID_ICON, IconSize(KIconLoader::Small) + 10); d->view->adjustColumnWidthToContents(COLUMN_ID_CAPTION); //adjust column width d->view->setColumnWidth(COLUMN_ID_TYPE, d->maxTypeNameTextWidth + 2 * d->view->rowHeight()); d->view->setColumnStretchEnabled(true, COLUMN_ID_DESC); //last column occupies the rest of the area const int minCaptionColumnWidth = d->view->fontMetrics().width("wwwwwwwwwww"); if (minCaptionColumnWidth > d->view->columnWidth(COLUMN_ID_CAPTION)) d->view->setColumnWidth(COLUMN_ID_CAPTION, minCaptionColumnWidth); setDirty(false); d->view->setCursorPosition(0, COLUMN_ID_CAPTION); //set @ name column propertySetSwitched(); } //! Gets subtype strings and names for type \a fieldType void KexiTableDesignerView::getSubTypeListData(KexiDB::Field::TypeGroup fieldTypeGroup, QStringList& stringsList, QStringList& namesList) { /* disabled - "mime" is moved from subType to "objectType" custom property if (fieldTypeGroup==KexiDB::Field::BLOBGroup) { // special case: BLOB type uses "mime-based" subtypes //! @todo hardcoded! stringsList << "image"; namesList << i18n("Image object type", "Image"); } else {*/ stringsList = KexiDB::typeStringsForGroup(fieldTypeGroup); namesList = KexiDB::typeNamesForGroup(fieldTypeGroup); // } kDebug() << "subType strings: " << stringsList.join("|") << "\nnames: " << namesList.join("|"); } KoProperty::Set * KexiTableDesignerView::createPropertySet(int row, const KexiDB::Field& field, bool newOne) { QString typeName = "KexiDB::Field::" + field.typeGroupString(); KoProperty::Set *set = new KoProperty::Set(d->sets, typeName); if (KexiMainWindowIface::global()->project()->dbConnection()->isReadOnly()) set->setReadOnly(true); // connect(buff,SIGNAL(propertyChanged(KexiPropertyBuffer&,KexiProperty&)), // this, SLOT(slotPropertyChanged(KexiPropertyBuffer&,KexiProperty&))); KoProperty::Property *prop; set->addProperty(prop = new KoProperty::Property("uid", d->generateUniqueId(), "")); prop->setVisible(false); //meta-info for property editor set->addProperty(prop = new KoProperty::Property("this:classString", i18n("Table field"))); prop->setVisible(false); set->addProperty(prop = new KoProperty::Property("this:iconName", //! \todo add table_field icon "lineedit" //"table_field" )); prop->setVisible(false); set->addProperty(prop = new KoProperty::Property("this:useCaptionAsObjectName", QVariant(true), QString())); //we want "caption" to be displayed in the header, not name prop->setVisible(false); //name set->addProperty(prop = new KoProperty::Property( "name", QVariant(field.name()), i18n("Name"), QString(), KexiCustomPropertyFactory::Identifier)); //type set->addProperty(prop = new KoProperty::Property("type", QVariant(field.type()), i18n("Type"))); #ifndef KexiTableDesignerView_DEBUG prop->setVisible(false);//always hidden #endif //subtype QStringList typeStringList, typeNameList; getSubTypeListData(field.typeGroup(), typeStringList, typeNameList); /* disabled - "mime" is moved from subType to "objectType" custom property QString subTypeValue; if (field.typeGroup()==KexiDB::Field::BLOBGroup) { // special case: BLOB type uses "mime-based" subtypes //! @todo this should be retrieved from KexiDB::Field when BLOB supports many different mimetypes subTypeValue = slist.first(); } else {*/ QString subTypeValue = field.typeString(); //} set->addProperty(prop = new KoProperty::Property("subType", typeStringList, typeNameList, subTypeValue, i18n("Subtype"))); // objectType QStringList objectTypeStringList, objectTypeNameList; //! @todo this should be retrieved from KexiDB::Field when BLOB supports many different mimetypes objectTypeStringList << "image"; objectTypeNameList << i18nc("Image object type", "Image"); QString objectTypeValue(field.customProperty("objectType").toString()); if (objectTypeValue.isEmpty()) objectTypeValue = DEFAULT_OBJECT_TYPE_VALUE; set->addProperty(prop = new KoProperty::Property("objectType", objectTypeStringList, objectTypeNameList, objectTypeValue, i18n("Subtype")/*! @todo other i18n string?*/)); set->addProperty(prop = new KoProperty::Property("caption", QVariant(field.caption()), i18n("Caption"))); prop->setVisible(false);//always hidden set->addProperty(prop = new KoProperty::Property("description", QVariant(field.description()))); prop->setVisible(false);//always hidden set->addProperty(prop = new KoProperty::Property("unsigned", QVariant(field.isUnsigned()), i18n("Unsigned Number"))); set->addProperty(prop = new KoProperty::Property("maxLength", (uint)field.maxLength(), i18n("Max Length"))); set->addProperty(prop = new KoProperty::Property("maxLengthIsDefault", field.maxLengthStrategy() == KexiDB::Field::DefaultMaxLength)); prop->setVisible(false); //always hidden set->addProperty(prop = new KoProperty::Property("precision", (int)field.precision()/*200?*/, i18n("Precision"))); #ifdef KEXI_NO_UNFINISHED prop->setVisible(false); #endif set->addProperty(prop = new KoProperty::Property("visibleDecimalPlaces", field.visibleDecimalPlaces(), i18n("Visible Decimal Places"))); prop->setOption("min", -1); prop->setOption("minValueText", i18nc("Auto Decimal Places", "Auto")); //! @todo set reasonable default for column width - set->addProperty(prop = new KoProperty::Property("width", QVariant(0) /*field.width()*//*200?*/, - i18n("Column Width"))); + set->addProperty(prop = new KoProperty::Property("defaultWidth", QVariant(0) /*field.width()*//*200?*/, + i18n("Default Width"))); #ifdef KEXI_NO_UNFINISHED prop->setVisible(false); #endif set->addProperty(prop = new KoProperty::Property("defaultValue", field.defaultValue(), i18n("Default Value"), QString(), //! @todo use "Variant" type here when supported by KoProperty (KoProperty::PropertyType)field.variantType())); prop->setOption("3rdState", i18n("None")); // prop->setVisible(false); set->addProperty(prop = new KoProperty::Property("primaryKey", QVariant(field.isPrimaryKey()), i18n("Primary Key"))); prop->setIcon("key"); set->addProperty(prop = new KoProperty::Property("unique", QVariant(field.isUniqueKey()), i18n("Unique"))); set->addProperty(prop = new KoProperty::Property("notNull", QVariant(field.isNotNull()), i18n("Required"))); set->addProperty(prop = new KoProperty::Property("allowEmpty", QVariant(!field.isNotEmpty()), i18n("Allow Zero\nSize"))); set->addProperty(prop = new KoProperty::Property("autoIncrement", QVariant(field.isAutoIncrement()), i18n("Autonumber"))); prop->setIcon("autonumber"); set->addProperty(prop = new KoProperty::Property("indexed", QVariant(field.isIndexed()), i18n("Indexed"))); //- properties related to lookup columns (used and set by the "lookup column" // tab in the property pane) KexiDB::LookupFieldSchema *lookupFieldSchema = field.table() ? field.table()->lookupFieldSchema(field) : 0; set->addProperty(prop = new KoProperty::Property("rowSource", lookupFieldSchema ? lookupFieldSchema->rowSource().name() : QString(), i18n("Record Source"))); prop->setVisible(false); set->addProperty(prop = new KoProperty::Property("rowSourceType", lookupFieldSchema ? lookupFieldSchema->rowSource().typeName() : QString(), i18n("Record Source\nType"))); prop->setVisible(false); set->addProperty(prop = new KoProperty::Property("boundColumn", lookupFieldSchema ? lookupFieldSchema->boundColumn() : -1, i18n("Bound Column"))); prop->setVisible(false); //! @todo this is backward-compatible code for "single visible column" implementation //! for multiple columns, only the first is displayed, so there is a data loss is GUI is used //! -- special koproperty editor needed int visibleColumn = -1; if (lookupFieldSchema && !lookupFieldSchema->visibleColumns().isEmpty()) visibleColumn = lookupFieldSchema->visibleColumns().first(); set->addProperty(prop = new KoProperty::Property("visibleColumn", visibleColumn, i18n("Visible Column"))); prop->setVisible(false); //! @todo support columnWidths(), columnHeadersVisible(), maximumListRows(), limitToList(), displayWidget() //---- d->updatePropertiesVisibility(field.type(), *set); connect(set, SIGNAL(propertyChanged(KoProperty::Set&, KoProperty::Property&)), this, SLOT(slotPropertyChanged(KoProperty::Set&, KoProperty::Property&))); d->sets->set(row, set, newOne); return set; } void KexiTableDesignerView::updateActions(bool activated) { Q_UNUSED(activated); /*! \todo check if we can set pkey for this column type (eg. BLOB?) */ setAvailable("tablepart_toggle_pkey", propertySet() != 0 && !KexiMainWindowIface::global()->project()->dbConnection()->isReadOnly()); if (!propertySet()) return; KoProperty::Set &set = *propertySet(); d->slotTogglePrimaryKeyCalled = true; d->action_toggle_pkey->setChecked(set["primaryKey"].value().toBool()); d->slotTogglePrimaryKeyCalled = false; } void KexiTableDesignerView::slotUpdateRowActions(int row) { KexiDataTable::slotUpdateRowActions(row); updateActions(); } void KexiTableDesignerView::slotTogglePrimaryKey() { if (d->slotTogglePrimaryKeyCalled) return; d->slotTogglePrimaryKeyCalled = true; if (!propertySet()) return; KoProperty::Set &set = *propertySet(); bool isSet = !set["primaryKey"].value().toBool(); set.changeProperty("primaryKey", QVariant(isSet)); //this will update all related properties as well /* CommandGroup *setPrimaryKeyCommand; if (isSet) { setPrimaryKeyCommand = new CommandGroup(i18n("Set primary key for field \"%1\"") .arg(set["name"].value().toString()) ); } else { setPrimaryKeyCommand = new CommandGroup(i18n("Unset primary key for field \"%1\"") .arg(set["name"].value().toString()) ); } switchPrimaryKey(set, isSet, false, setPrimaryKeyCommand);*/ //addHistoryCommand( setPrimaryKeyCommand, false /* !execute */ ); d->slotTogglePrimaryKeyCalled = false; } void KexiTableDesignerView::switchPrimaryKey(KoProperty::Set &propertySet, bool set, bool aWasPKey, Command* commandGroup) { const bool was_pkey = aWasPKey || propertySet["primaryKey"].value().toBool(); // propertySet["primaryKey"] = QVariant(set); d->setPropertyValueIfNeeded(propertySet, "primaryKey", QVariant(set), commandGroup); if (&propertySet == this->propertySet()) { //update action and icon @ column 0 (only if we're changing current property set) d->action_toggle_pkey->setChecked(set); if (d->view->selectedItem()) { //show key in the table d->view->data()->clearRowEditBuffer(); d->view->data()->updateRowEditBuffer(d->view->selectedItem(), COLUMN_ID_ICON, QVariant(set ? "key" : "")); d->view->data()->saveRowChanges(*d->view->selectedItem(), true); } if (was_pkey || set) //change flag only if we're setting pk or really clearing it d->primaryKeyExists = set; } if (set) { //primary key is set, remove old pkey if exists KoProperty::Set *s = 0; int i; const int count = (int)d->sets->size(); for (i = 0; i < count; i++) { s = d->sets->at(i); if ( s && s != &propertySet && (*s)["primaryKey"].value().toBool() && i != d->view->currentRow()) { break; } } if (i < count) {//remove //(*s)["autoIncrement"] = QVariant(false, 0); d->setPropertyValueIfNeeded(*s, "autoIncrement", QVariant(false), commandGroup); //(*s)["primaryKey"] = QVariant(false, 0); d->setPropertyValueIfNeeded(*s, "primaryKey", QVariant(false), commandGroup); //remove key from table d->view->data()->clearRowEditBuffer(); KexiDB::RecordData *record = d->view->itemAt(i); if (record) { d->view->data()->updateRowEditBuffer(record, COLUMN_ID_ICON, QVariant()); d->view->data()->saveRowChanges(*record, true); } } //set unsigned big-integer type // d->view->data()->saveRowChanges(*d->view->selectedItem()); d->slotBeforeCellChanged_enabled = false; d->view->data()->clearRowEditBuffer(); d->view->data()->updateRowEditBuffer(d->view->selectedItem(), COLUMN_ID_TYPE, QVariant(KexiDB::Field::IntegerGroup - 1/*counting from 0*/)); // QVariant(KexiDB::Field::typeGroupName(KexiDB::Field::IntegerGroup))); d->view->data()->saveRowChanges(*d->view->selectedItem(), true); //propertySet["subType"] = KexiDB::Field::typeString(KexiDB::Field::BigInteger); d->setPropertyValueIfNeeded(propertySet, "subType", KexiDB::Field::typeString(KexiDB::Field::BigInteger), commandGroup); //propertySet["unsigned"] = QVariant(true); d->setPropertyValueIfNeeded(propertySet, "unsigned", QVariant(true), commandGroup); /*todo*/ d->slotBeforeCellChanged_enabled = true; } updateActions(); } /*void KexiTableDesignerView::slotCellSelected(int, int row) { kDebug(); if(row == m_row) return; m_row = row; propertyBufferSwitched(); }*/ tristate KexiTableDesignerView::beforeSwitchTo(Kexi::ViewMode mode, bool &dontStore) { if (!d->view->acceptRowEdit()) return false; /* if (mode==Kexi::DesignViewMode) { initData(); return true; } else */ tristate res = true; if (mode == Kexi::DataViewMode) { if (!isDirty() && window()->neverSaved()) { KMessageBox::sorry(this, i18n("Cannot switch to data view, because table design is empty.\n" "First, please create your design.")); return cancelled; } // else if (isDirty() && !window()->neverSaved()) { // cancelled = (KMessageBox::No == KMessageBox::questionYesNo(this, i18n("Saving changes for existing table design is not yet supported.\nDo you want to discard your changes now?"))); // KexiDB::Connection *conn = KexiMainWindowIface::global()->project()->dbConnection(); bool emptyTable; int r = KMessageBox::warningYesNoCancel(this, i18n("Saving changes for existing table design is now required.") + "\n" + d->messageForSavingChanges(emptyTable, /*skip warning?*/!isPhysicalAlteringNeeded()), QString(), KStandardGuiItem::save(), KStandardGuiItem::discard(), KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (r == KMessageBox::Cancel) res = cancelled; else res = true; dontStore = (r != KMessageBox::Yes); if (!dontStore) d->dontAskOnStoreData = true; // if (dontStore) // setDirty(false); } // //todo return res; } else if (mode == Kexi::TextViewMode) { //todo } return res; } tristate KexiTableDesignerView::afterSwitchFrom(Kexi::ViewMode mode) { if (mode == Kexi::NoViewMode || mode == Kexi::DataViewMode) { initData(); } return true; } KoProperty::Set *KexiTableDesignerView::propertySet() { return d->sets ? d->sets->currentPropertySet() : 0; } void KexiTableDesignerView::slotBeforeCellChanged( KexiDB::RecordData *record, int colnum, QVariant& newValue, KexiDB::ResultInfo* /*result*/) { if (!d->slotBeforeCellChanged_enabled) return; // kDebug() << d->view->selectedItem() << " " << item //<< " " << d->sets->at( d->view->currentRow() ) << " " << propertySet(); if (colnum == COLUMN_ID_CAPTION) {//'caption' // if (!item->at(1).toString().isEmpty() && item->at(1).isNull()) { //if 'type' is not filled yet if (record->at(COLUMN_ID_TYPE).isNull()) { //auto select 1st row of 'type' column d->view->data()->updateRowEditBuffer(record, COLUMN_ID_TYPE, QVariant((int)0)); } KoProperty::Set *propertySetForRecord = d->sets->findPropertySetForItem(*record); if (propertySetForRecord) { d->addHistoryCommand_in_slotPropertyChanged_enabled = false; // because we'll add // the two changes as one group QString oldName(propertySetForRecord->property("name").value().toString()); QString oldCaption(propertySetForRecord->property("caption").value().toString()); //remember this action containing 2 subactions //Parent command is a Command containing 2 child commands Command *changeCaptionAndNameCommand = new Command( i18n( "Change \"%1\" field's name to \"%2\" and caption from \"%3\" to \"%4\"", oldName, propertySetForRecord->property("name").value().toString(), oldCaption, newValue.toString()), 0, this ); //we need to create the action now as set["name"] will be changed soon. //Child 1 is the caption /*ChangeFieldPropertyCommand *changeCaptionCommand = */ (void)new ChangeFieldPropertyCommand(changeCaptionAndNameCommand, this, *propertySetForRecord, "caption", oldCaption, newValue); //update field caption and name propertySetForRecord->changeProperty("caption", newValue); propertySetForRecord->changeProperty("name", KexiUtils::string2Identifier(newValue.toString())); //Child 2 is the name /*ChangeFieldPropertyCommand *changeNameCommand =*/ (void)new ChangeFieldPropertyCommand( changeCaptionAndNameCommand, this, *propertySetForRecord, "name", oldName, propertySetForRecord->property("name").value().toString()); addHistoryCommand(changeCaptionAndNameCommand, false /* !execute */); d->addHistoryCommand_in_slotPropertyChanged_enabled = true; } } else if (colnum == COLUMN_ID_TYPE) {//'type' if (newValue.isNull()) { //'type' col will be cleared: clear all other columns as well d->slotBeforeCellChanged_enabled = false; d->view->data()->updateRowEditBuffer(record, COLUMN_ID_ICON, QVariant()); d->view->data()->updateRowEditBuffer(record, COLUMN_ID_CAPTION, QVariant(QString())); d->view->data()->updateRowEditBuffer(record, COLUMN_ID_DESC, QVariant()); d->slotBeforeCellChanged_enabled = true; return; } KoProperty::Set *propertySetForRecord = d->sets->findPropertySetForItem(*record); if (!propertySetForRecord) return; KoProperty::Set &set = *propertySetForRecord; //propertySet(); //'type' col is changed (existed before) //-get type group number KexiDB::Field::TypeGroup fieldTypeGroup; int i_fieldTypeGroup = newValue.toInt() + 1/*counting from 1*/; if (i_fieldTypeGroup < 1 || i_fieldTypeGroup > #ifdef KEXI_NO_BLOB_FIELDS //! @todo remove this later (int)KexiDB::Field::LastTypeGroup - 1) //don't show last (BLOB) type #else (int)KexiDB::Field::LastTypeGroup) #endif return; fieldTypeGroup = static_cast(i_fieldTypeGroup); //-get 1st type from this group, and update 'type' property KexiDB::Field::Type fieldType = KexiDB::defaultTypeForGroup(fieldTypeGroup); if (fieldType == KexiDB::Field::InvalidType) fieldType = KexiDB::Field::Text; //-get subtypes for this type: keys (slist) and names (nlist) QStringList slist, nlist; getSubTypeListData(fieldTypeGroup, slist, nlist); QString subTypeValue; /* disabled - "mime" is moved from subType to "objectType" custom property if (fieldType==KexiDB::Field::BLOB) { // special case: BLOB type uses "mime-based" subtypes subTypeValue = slist.first(); } else {*/ subTypeValue = KexiDB::Field::typeString(fieldType); //} KoProperty::Property *subTypeProperty = &set["subType"]; kDebug() << subTypeProperty->value(); // *** this action contains subactions *** Command *changeDataTypeCommand = new Command( i18n("Change data type for field \"%1\" to \"%2\"", set["name"].value().toString(), KexiDB::Field::typeName(fieldType)), 0, this); //kDebug() << "++++++++++" << slist << nlist; //update subtype list and value const bool forcePropertySetReload = KexiDB::Field::typeGroup( KexiDB::Field::typeForString(subTypeProperty->value().toString())) != fieldTypeGroup; //<-- ????? // const bool forcePropertySetReload = set["type"].value().toInt() != (int)fieldTypeGroup; const bool useListData = slist.count() > 1; //disabled-> || fieldType==KexiDB::Field::BLOB; if (!useListData) { slist.clear(); //empty list will be passed nlist.clear(); } d->setPropertyValueIfNeeded(set, "type", (int)fieldType, changeDataTypeCommand, false /*!forceAddCommand*/, true /*rememberOldValue*/); // notNull and defaultValue=false is reasonable for boolean type if (fieldType == KexiDB::Field::Boolean) { //! @todo maybe this is good for other data types as well? d->setPropertyValueIfNeeded(set, "notNull", QVariant(true), changeDataTypeCommand, false /*!forceAddCommand*/, false /*!rememberOldValue*/); d->setPropertyValueIfNeeded(set, "defaultValue", QVariant(false), changeDataTypeCommand, false /*!forceAddCommand*/, false /*!rememberOldValue*/); } /* if (useListData) { { subTypeProperty->setListData( slist, nlist ); } else { subTypeProperty->setListData( 0 ); }*/ if (set["primaryKey"].value().toBool() == true) { //primary keys require big int, so if selected type is not integer- remove PK if (fieldTypeGroup != KexiDB::Field::IntegerGroup) { /*not needed, line below will do the work d->view->data()->updateRowEditBuffer(record, COLUMN_ID_ICON, QVariant()); d->view->data()->saveRowChanges(*record); */ //set["primaryKey"] = QVariant(false); d->setPropertyValueIfNeeded(set, "primaryKey", QVariant(false), changeDataTypeCommand); //! @todo should we display (passive?) dialog informing about cleared pkey? } } // if (useListData) // subTypeProperty->setValue( subTypeValue, false/*!rememberOldValue*/ ); d->setPropertyValueIfNeeded(set, "subType", subTypeValue, changeDataTypeCommand, false, false /*!rememberOldValue*/, &slist, &nlist); if (d->updatePropertiesVisibility(fieldType, set, changeDataTypeCommand) || forcePropertySetReload) { //properties' visiblility changed: refresh prop. set propertySetReloaded(true); } addHistoryCommand(changeDataTypeCommand, false /* !execute */); } else if (colnum == COLUMN_ID_DESC) {//'description' KoProperty::Set *propertySetForRecord = d->sets->findPropertySetForItem(*record); if (!propertySetForRecord) return; //update field desc. QVariant oldValue((*propertySetForRecord)["description"].value()); kDebug() << oldValue; propertySetForRecord->changeProperty("description", newValue); } } void KexiTableDesignerView::slotRowUpdated(KexiDB::RecordData *record) { const int row = d->view->data()->indexOf(record); if (row < 0) return; setDirty(); //-check if the row was empty before updating //if yes: we want to add a property set for this new row (field) QString fieldCaption(record->at(COLUMN_ID_CAPTION).toString()); const bool prop_set_allowed = !record->at(COLUMN_ID_TYPE).isNull(); if (!prop_set_allowed && d->sets->at(row)/*propertySet()*/) { //there is a property set, but it's not allowed - remove it: d->sets->eraseAt(row); //d->sets->eraseCurrentPropertySet(); //clear 'type' column: d->view->data()->clearRowEditBuffer(); // d->view->data()->updateRowEditBuffer(d->view->selectedItem(), COLUMN_ID_TYPE, QVariant()); d->view->data()->updateRowEditBuffer(record, COLUMN_ID_TYPE, QVariant()); d->view->data()->saveRowChanges(*record); } else if (prop_set_allowed && !d->sets->at(row)/*propertySet()*/) { //-- create a new field: KexiDB::Field::TypeGroup fieldTypeGroup = static_cast( record->at(COLUMN_ID_TYPE).toInt() + 1/*counting from 1*/); int intFieldType = KexiDB::defaultTypeForGroup(fieldTypeGroup); if (intFieldType == 0) return; QString description(record->at(COLUMN_ID_DESC).toString()); //! @todo check uniqueness: QString fieldName(KexiUtils::string2Identifier(fieldCaption)); KexiDB::Field::Type fieldType = KexiDB::intToFieldType(intFieldType); uint maxLength = 0; if (fieldType == KexiDB::Field::Text) { maxLength = KexiDB::Field::defaultMaxLength(); } KexiDB::Field field( //tmp fieldName, fieldType, KexiDB::Field::NoConstraints, KexiDB::Field::NoOptions, maxLength, /*precision*/0, /*defaultValue*/QVariant(), fieldCaption, description); // m_newTable->addField( field ); // reasonable case for boolean type: set notNull flag and "false" as default value switch (fieldType) { case KexiDB::Field::Boolean: field.setNotNull(true); field.setDefaultValue(QVariant(false)); break; case KexiDB::Field::Text: field.setMaxLengthStrategy(KexiDB::Field::DefaultMaxLength); break; default:; } kDebug() << field.debugString(); //create a new property set: KoProperty::Set *newSet = createPropertySet(row, field, true); //refresh property editor: propertySetSwitched(); if (row >= 0) { if (d->addHistoryCommand_in_slotRowUpdated_enabled) { addHistoryCommand(new InsertFieldCommand(0, this, row, *newSet /*propertySet()*/), //, field /*will be copied*/ false /* !execute */); } } else { kWarning() << "record # not found !"; } } } void KexiTableDesignerView::updateActions() { updateActions(false); } void KexiTableDesignerView::slotPropertyChanged(KoProperty::Set& set, KoProperty::Property& property) { // if (!d->slotPropertyChanged_enabled) // return; const QByteArray pname(property.name()); kDebug() << pname << " = " << property.value() << " (oldvalue = " << property.oldValue() << ")"; // true is PK should be altered bool changePrimaryKey = false; // true is PK should be set to true, otherwise unset bool setPrimaryKey = false; if (pname == "primaryKey" && d->slotPropertyChanged_primaryKey_enabled) { changePrimaryKey = true; setPrimaryKey = property.value().toBool(); } // update "lookup column" icon if (pname == "rowSource" || pname == "rowSourceType") { //! @todo indicate invalid definitions of lookup columns as well using a special icon //! (e.g. due to missing data source) const int row = d->sets->findRowForPropertyValue("uid", set["uid"].value().toInt()); KexiDB::RecordData *record = d->view->itemAt(row); if (record) d->updateIconForRecord(*record, set); } //setting autonumber requires setting PK as well Command *setAutonumberCommand = 0; Command *toplevelCommand = 0; if (pname == "autoIncrement" && property.value().toBool() == true) { if (set["primaryKey"].value().toBool() == false) {//we need PKEY here! QString msg = QLatin1String("

") + i18n("Setting autonumber requires primary key to be set for current field.") + "

"; if (d->primaryKeyExists) msg += (QLatin1String("

") + i18n("Previous primary key will be removed.") + "

"); msg += (QLatin1String("

") + i18n("Do you want to create primary key for current field? " "Click \"Cancel\" to cancel setting autonumber.") + "

"); if (KMessageBox::Yes == KMessageBox::questionYesNo(this, msg, i18n("Setting Autonumber Field"), KGuiItem(i18n("Create &Primary Key"), koIconName("key")), KStandardGuiItem::cancel())) { changePrimaryKey = true; setPrimaryKey = true; //switchPrimaryKey(set, true); // this will be toplevel command setAutonumberCommand = new Command( i18n("Assign autonumber for field \"%1\"", set["name"].value().toString()), 0, this); toplevelCommand = setAutonumberCommand; d->setPropertyValueIfNeeded(set, "autoIncrement", QVariant(true), setAutonumberCommand); } else { setAutonumberCommand = new Command( i18n("Remove autonumber from field \"%1\"", set["name"].value().toString()), 0, this); //d->slotPropertyChanged_enabled = false; // set["autoIncrement"].setValue( QVariant(false), false/*don't save old*/); // d->slotPropertyChanged_enabled = true; d->setPropertyValueIfNeeded(set, "autoIncrement", QVariant(false), setAutonumberCommand, true /*forceAddCommand*/, false/*rememberOldValue*/); addHistoryCommand(setAutonumberCommand, false /* !execute */); return; } } } //clear PK when these properties were set to false: if ((pname == "indexed" || pname == "unique" || pname == "notNull") && set["primaryKey"].value().toBool() && property.value().toBool() == false) { //! @todo perhaps show a hint in help panel telling what happens? changePrimaryKey = true; setPrimaryKey = false; // this will be toplevel command Command *unsetIndexedOrUniquOrNotNullCommand = new Command( i18n("Set \"%1\" property for field \"%2\"", property.caption(), set["name"].value().toString()), 0, this); toplevelCommand = unsetIndexedOrUniquOrNotNullCommand; d->setPropertyValueIfNeeded(set, pname, QVariant(false), unsetIndexedOrUniquOrNotNullCommand); if (pname == "notNull") { //? d->setPropertyValueIfNeeded( set, "notNull", QVariant(true), unsetIndexedOrUniquOrNotNullCommand ); d->setPropertyValueIfNeeded(set, "unique", QVariant(false), unsetIndexedOrUniquOrNotNullCommand); } } if (pname == "defaultValue") { KexiDB::Field::Type type = KexiDB::intToFieldType(set["type"].value().toInt()); set["defaultValue"].setType((KoProperty::PropertyType)KexiDB::Field::variantType(type)); } if (pname == "subType" && d->slotPropertyChanged_subType_enabled) { d->slotPropertyChanged_subType_enabled = false; if (set["primaryKey"].value().toBool() == true && property.value().toString() != KexiDB::Field::typeString(KexiDB::Field::BigInteger)) { kDebug() << "INVALID " << property.value().toString(); // if (KMessageBox::Yes == KMessageBox::questionYesNo(this, msg, // i18n("This field has promary key assigned. Setting autonumber field"), // KGuiItem(i18n("Create &Primary Key"), koIconName("key")), KStandardGuiItem::cancel() )) } KexiDB::Field::Type type = KexiDB::intToFieldType(set["type"].value().toInt()); QString typeName; /* disabled - "mime" is moved from subType to "objectType" custom property if (type==KexiDB::Field::BLOB) { //special case //find i18n'd text QStringList stringsList, namesList; getSubTypeListData(KexiDB::Field::BLOBGroup, stringsList, namesList); const int stringIndex = stringsList.findIndex( property.value().toString() ); if (-1 == stringIndex || stringIndex>=(int)namesList.count()) typeName = property.value().toString(); //for sanity else typeName = namesList[stringIndex]; } else {*/ typeName = KexiDB::Field::typeName(KexiDB::Field::typeForString(property.value().toString())); // } // kDebug() << property.value().toString(); // kDebug() << set["type"].value(); // if (KexiDB::Field::typeGroup( set["type"].value().toInt() ) == (int)KexiDB::Field::TextGroup) { Command* changeFieldTypeCommand = new Command( i18n( "Change type for field \"%1\" to \"%2\"", set["name"].value().toString(), typeName), 0, this); d->setPropertyValueIfNeeded(set, "subType", property.value(), property.oldValue(), changeFieldTypeCommand); kDebug() << set["type"].value(); const KexiDB::Field::Type newType = KexiDB::Field::typeForString(property.value().toString()); set["type"].setValue(newType); // cast "defaultValue" property value to a new type QVariant oldDefVal(set["defaultValue"].value()); QVariant newDefVal(tryCastQVariant(oldDefVal, KexiDB::Field::variantType(type))); if (oldDefVal.type() != newDefVal.type()) set["defaultValue"].setType(newDefVal.type()); d->setPropertyValueIfNeeded(set, "defaultValue", newDefVal, newDefVal, changeFieldTypeCommand); d->updatePropertiesVisibility(newType, set); //properties' visiblility changed: refresh prop. set propertySetReloaded(true); d->slotPropertyChanged_subType_enabled = true; addHistoryCommand(changeFieldTypeCommand, false /* !execute */); return; // } // d->slotPropertyChanged_subType_enabled = true; // return; } //! @todo add command text if ( d->addHistoryCommand_in_slotPropertyChanged_enabled && !changePrimaryKey/*we'll add multiple commands for PK*/) { addHistoryCommand(new ChangeFieldPropertyCommand(0, this, set, property.name(), property.oldValue() /* ??? */, property.value()), false /* !execute */); } if (changePrimaryKey) { d->slotPropertyChanged_primaryKey_enabled = false; if (setPrimaryKey) { //primary key implies some rules //const bool prev_addHistoryCommand_in_slotPropertyChanged_enabled = d->addHistoryCommand_in_slotPropertyChanged_enabled; // d->addHistoryCommand_in_slotPropertyChanged_enabled = false; //this action contains subactions Command * setPrimaryKeyCommand = new Command( i18n("Set primary key for field \"%1\"", set["name"].value().toString()), toplevelCommand, this); if (!toplevelCommand) { toplevelCommand = setPrimaryKeyCommand; } d->setPropertyValueIfNeeded(set, "primaryKey", QVariant(true), setPrimaryKeyCommand, true /*forceAddCommand*/); d->setPropertyValueIfNeeded(set, "unique", QVariant(true), setPrimaryKeyCommand); d->setPropertyValueIfNeeded(set, "notNull", QVariant(true), setPrimaryKeyCommand); d->setPropertyValueIfNeeded(set, "allowEmpty", QVariant(false), setPrimaryKeyCommand); d->setPropertyValueIfNeeded(set, "indexed", QVariant(true), setPrimaryKeyCommand); //! \todo: add setting for this: "Integer PKeys have autonumber set by default" d->setPropertyValueIfNeeded(set, "autoIncrement", QVariant(true), setPrimaryKeyCommand); /* set["unique"] = QVariant(true); set["notNull"] = QVariant(true); set["allowEmpty"] = QVariant(false); set["indexed"] = QVariant(true); set["autoIncrement"] = QVariant(true);*/ // d->addHistoryCommand_in_slotPropertyChanged_enabled = prev_addHistoryCommand_in_slotPropertyChanged_enabled; //down addHistoryCommand( toplevelCommand, false /* !execute */ ); } else {//! set PK to false //remember this action containing 2 subactions Command *setPrimaryKeyCommand = new Command( i18n("Unset primary key for field \"%1\"", set["name"].value().toString()), toplevelCommand, this); if (!toplevelCommand) { toplevelCommand = setPrimaryKeyCommand; } d->setPropertyValueIfNeeded(set, "primaryKey", QVariant(false), setPrimaryKeyCommand, true /*forceAddCommand*/); d->setPropertyValueIfNeeded(set, "autoIncrement", QVariant(false), setPrimaryKeyCommand); // set["autoIncrement"] = QVariant(false); } switchPrimaryKey(set, setPrimaryKey, true/*wasPKey*/, toplevelCommand); d->updatePropertiesVisibility( KexiDB::Field::typeForString(set["subType"].value().toString()), set, toplevelCommand); addHistoryCommand(toplevelCommand, false /* !execute */); //properties' visiblility changed: refresh prop. set propertySetReloaded(true/*preservePrevSelection*/); d->slotPropertyChanged_primaryKey_enabled = true; } } void KexiTableDesignerView::slotRowInserted() { updateActions(); if (d->addHistoryCommand_in_slotRowInserted_enabled) { const int row = d->view->currentRow(); if (row >= 0) { addHistoryCommand(new InsertEmptyRowCommand(0, this, row), false /* !execute */); } } //TODO? } void KexiTableDesignerView::slotAboutToDeleteRow( KexiDB::RecordData& record, KexiDB::ResultInfo* result, bool repaint) { Q_UNUSED(result) Q_UNUSED(repaint) if (record[COLUMN_ID_ICON].toString() == "key") d->primaryKeyExists = false; if (d->addHistoryCommand_in_slotAboutToDeleteRow_enabled) { const int row = d->view->data()->indexOf(&record); KoProperty::Set *set = row >= 0 ? d->sets->at(row) : 0; //set can be 0 here, what means "removing empty row" addHistoryCommand( new RemoveFieldCommand(0, this, row, set), false /* !execute */ ); } } KexiDB::Field * KexiTableDesignerView::buildField(const KoProperty::Set &set) const { //create a map of property values kDebug() << set["type"].value(); - QHash values(KoProperty::propertyValues(set)); + QMap values(KoProperty::propertyValues(set)); //remove internal values, to avoid creating custom field's properties KexiDB::Field *field = new KexiDB::Field(); - for (QMutableHashIterator it(values); it.hasNext();) { + for (QMutableMapIterator it(values); it.hasNext();) { it.next(); const QByteArray propName(it.key()); if (d->internalPropertyNames.contains(propName) || propName.startsWith("this:") || (/*sanity*/propName == "objectType" && KexiDB::Field::BLOB != KexiDB::intToFieldType(set["type"].value().toInt())) ) { it.remove(); } } //assign properties to the field // (note that "objectType" property will be saved as custom property) if (!KexiDB::setFieldProperties(*field, values)) { delete field; return 0; } return field; } tristate KexiTableDesignerView::buildSchema(KexiDB::TableSchema &schema, bool beSilent) { if (!d->view->acceptRowEdit()) return cancelled; tristate res = true; //check for pkey; automatically add a pkey if user wanted if (!d->primaryKeyExists) { if (beSilent) { kDebug() << "no primay key defined..."; } else { const int questionRes = KMessageBox::questionYesNoCancel(this, i18n("

Table \"%1\" has no primary key defined.

" "

Although a primary key is not required, it is needed " "for creating relations between database tables. " "Do you want to add primary key automatically now?

" "

If you want to add a primary key by hand, press \"Cancel\" " "to cancel saving table design.

", schema.name()), QString(), KGuiItem(i18n("&Add Primary Key"), koIconName("key")), KStandardGuiItem::no(), KStandardGuiItem::cancel(), "autogeneratePrimaryKeysOnTableDesignSaving"); if (questionRes == KMessageBox::Cancel) { return cancelled; } else if (questionRes == KMessageBox::Yes) { //-find unique name, starting with, "id", "id2", .... int i = 0; int idIndex = 1; //means "id" QString pkFieldName("id%1"); KLocalizedString pkFieldCaption(ki18nc("Identifier%1", "Id%1")); while (i < (int)d->sets->size()) { KoProperty::Set *set = d->sets->at(i); if (set) { if ( (*set)["name"].value().toString() == pkFieldName.arg(idIndex == 1 ? QString() : QString::number(idIndex)) || (*set)["caption"].value().toString() == pkFieldCaption.subs(idIndex == 1 ? QString() : QString::number(idIndex)).toString() ) { //try next id index i = 0; idIndex++; continue; } } i++; } pkFieldName = pkFieldName.arg(idIndex == 1 ? QString() : QString::number(idIndex)); //ok, add PK with such unique name d->view->insertEmptyRow(0); d->view->setCursorPosition(0, COLUMN_ID_CAPTION); d->view->data()->updateRowEditBuffer(d->view->selectedItem(), COLUMN_ID_CAPTION, pkFieldCaption.subs(idIndex == 1 ? QString() : QString::number(idIndex)).toString() ); d->view->data()->updateRowEditBuffer(d->view->selectedItem(), COLUMN_ID_TYPE, QVariant(KexiDB::Field::IntegerGroup - 1/*counting from 0*/)); if (!d->view->data()->saveRowChanges(*d->view->selectedItem(), true)) { return cancelled; } slotTogglePrimaryKey(); } } } //check for duplicates KoProperty::Set *b = 0; bool no_fields = true; int i; QSet names; for (i = 0; i < (int)d->sets->size(); i++) { b = d->sets->at(i); if (b) { no_fields = false; const QString name((*b)["name"].value().toString()); if (name.isEmpty()) { if (beSilent) { kWarning() << QString("no field caption entered at record %1...").arg(i + 1); } else { d->view->setCursorPosition(i, COLUMN_ID_CAPTION); d->view->startEditCurrentCell(); KMessageBox::information(this, i18n("You should enter field caption.")); } res = cancelled; break; } if (names.contains(name.toLower())) { break; } names.insert(name.toLower()); //remember } } if (res == true && no_fields) {//no fields added if (beSilent) { kWarning() << "no field defined..."; } else { KMessageBox::sorry(this, i18n("You have added no fields.\nEvery table should have at least one field.")); } res = cancelled; } if (res == true && b && i < (int)d->sets->size()) {//found a duplicate if (beSilent) { kWarning() << QString("duplicated field name '%1'") .arg((*b)["name"].value().toString()); } else { d->view->setCursorPosition(i, COLUMN_ID_CAPTION); d->view->startEditCurrentCell(); //! @todo for "names hidden" mode we won't get this error because user is unable to change names KMessageBox::sorry(this, i18n( "You have added \"%1\" field name twice.\nField names cannot be repeated. " "Correct name of the field.", (*b)["name"].value().toString())); } res = cancelled; } if (res == true) { //for every field, create KexiDB::Field definition for (i = 0;i < (int)d->sets->size();++i) { KoProperty::Set *s = d->sets->at(i); if (!s) continue; KexiDB::Field * f = buildField(*s); if (!f) continue; //hmm? schema.addField(f); if ( !(*s)["rowSource"].value().toString().isEmpty() && !(*s)["rowSourceType"].value().toString().isEmpty()) { //add lookup column KexiDB::LookupFieldSchema *lookupFieldSchema = new KexiDB::LookupFieldSchema(); lookupFieldSchema->rowSource().setTypeByName((*s)["rowSourceType"].value().toString()); lookupFieldSchema->rowSource().setName((*s)["rowSource"].value().toString()); lookupFieldSchema->setBoundColumn((*s)["boundColumn"].value().toInt()); //! @todo this is backward-compatible code for "single visible column" implementation //! for multiple columns, only the first is displayed, so there is a data loss is GUI is used //! -- special koproperty editor needed QList visibleColumns; const int visibleColumn = (*s)["visibleColumn"].value().toInt(); if (visibleColumn >= 0) visibleColumns.append((uint)visibleColumn); lookupFieldSchema->setVisibleColumns(visibleColumns); //! @todo support columnWidths(), columnHeadersVisible(), maximumListRows(), limitToList(), displayWidget() if (!schema.setLookupFieldSchema(f->name(), lookupFieldSchema)) { kWarning() << "!schema.setLookupFieldSchema()"; delete lookupFieldSchema; return false; } } } } return res; } //! @internal //! A recursive function for copying alter table actions from undo/redo commands. static void copyAlterTableActions(const KUndo2Command* command, KexiDB::AlterTableHandler::ActionList &actions) { for (int i = 0; i < command->childCount(); ++i) { copyAlterTableActions(command->child(i), actions); } const Command* cmd = dynamic_cast(command); if (!cmd) { kWarning() << "cmd is not of type 'Command'!"; return; } KexiDB::AlterTableHandler::ActionBase* action = cmd->createAction(); //some commands can contain null actions, e.g. "set visibility" command if (action) actions.append(action); } tristate KexiTableDesignerView::buildAlterTableActions( KexiDB::AlterTableHandler::ActionList &actions) { actions.clear(); kDebug() << d->history->count() << " top-level command(s) to process..."; for (int i = 0; i < d->history->count(); ++i) { copyAlterTableActions(d->history->command(i), actions); } return true; } KexiDB::SchemaData* KexiTableDesignerView::storeNewData(const KexiDB::SchemaData& sdata, bool &cancel) { if (tempData()->table || window()->schemaData()) //must not be return 0; //create table schema definition tempData()->table = new KexiDB::TableSchema(sdata.name()); tempData()->table->setName(sdata.name()); tempData()->table->setCaption(sdata.caption()); tempData()->table->setDescription(sdata.description()); tristate res = buildSchema(*tempData()->table); cancel = ~res; //FINALLY: create table: if (res == true) { //todo KexiDB::Connection *conn = KexiMainWindowIface::global()->project()->dbConnection(); res = conn->createTable(tempData()->table); if (res != true) window()->setStatus(conn, ""); } if (res == true) { //we've current schema tempData()->tableSchemaChangedInPreviousView = true; //not needed; KexiProject emits newItemStored signal //let project know the table is created // KexiMainWindowIface::global()->project()->emitTableCreated(*tempData()->table); } else { delete tempData()->table; tempData()->table = 0; } return tempData()->table; } tristate KexiTableDesignerView::storeData(bool dontAsk) { if (!tempData()->table || !window()->schemaData()) { d->recentResultOfStoreData = false; return false; } KexiDB::Connection *conn = KexiMainWindowIface::global()->project()->dbConnection(); KexiDB::AlterTableHandler *alterTableHandler = 0; KexiDB::TableSchema *newTable = 0; //- create action list for the alter table handler KexiDB::AlterTableHandler::ActionList actions; tristate res = buildAlterTableActions(actions); //!< @todo this is temporary flag before we switch entirely to real alter table bool realAlterTableCanBeUsed = false; if (res == true) { alterTableHandler = new KexiDB::AlterTableHandler(*conn); alterTableHandler->setActions(actions); if (!d->tempStoreDataUsingRealAlterTable) { //only compute requirements KexiDB::AlterTableHandler::ExecutionArguments args; args.onlyComputeRequirements = true; (void)alterTableHandler->execute(tempData()->table->name(), args); res = args.result; if ( res == true && 0 == (args.requirements & (0xffff ^ KexiDB::AlterTableHandler::SchemaAlteringRequired))) { realAlterTableCanBeUsed = true; } } } if (res == true) { res = KexiTablePart::askForClosingObjectsUsingTableSchema( this, *conn, *tempData()->table, i18n("You are about to change the design of table \"%1\" " "but following objects using this table are opened:", tempData()->table->name())); } if (res == true) { if (!d->tempStoreDataUsingRealAlterTable && !realAlterTableCanBeUsed) { //! @todo temp; remove this case: delete alterTableHandler; alterTableHandler = 0; // - inform about removing the current table and ask for confirmation if (!d->dontAskOnStoreData && !dontAsk) { bool emptyTable; const QString msg = d->messageForSavingChanges(emptyTable); if (!emptyTable) { if (KMessageBox::No == KMessageBox::questionYesNo(this, msg)) res = cancelled; } } d->dontAskOnStoreData = false; //one-time use if (~res) { d->recentResultOfStoreData = res; return res; } // keep old behaviour: newTable = new KexiDB::TableSchema(); // copy the schema data static_cast(*newTable) = static_cast(*tempData()->table); res = buildSchema(*newTable); kDebug() << "BUILD SCHEMA:"; newTable->debug(); res = conn->alterTable(*tempData()->table, *newTable); if (res != true) window()->setStatus(conn, ""); } else { KexiDB::AlterTableHandler::ExecutionArguments args; newTable = alterTableHandler->execute(tempData()->table->name(), args); res = args.result; kDebug() << "ALTER TABLE EXECUTE: " << res.toString(); if (true != res) { alterTableHandler->debugError(); window()->setStatus(alterTableHandler, ""); } } } if (res == true) { //change current schema tempData()->table = newTable; tempData()->tableSchemaChangedInPreviousView = true; d->history->clear(); } else { delete newTable; } delete alterTableHandler; d->recentResultOfStoreData = res; return res; } tristate KexiTableDesignerView::simulateAlterTableExecution(QString *debugTarget) { #ifndef KEXI_NO_UNDOREDO_ALTERTABLE # ifdef KEXI_DEBUG_GUI if (KexiMainWindowIface::global()->currentWindow() != window()) { //to avoid executing for multiple alter table views return false; } if (!tempData()->table || !window()->schemaData()) return false; KexiDB::Connection *conn = KexiMainWindowIface::global()->project()->dbConnection(); KexiDB::AlterTableHandler::ActionList actions; tristate res = buildAlterTableActions(actions); //todo: result? KexiDB::AlterTableHandler alterTableHandler(*conn); alterTableHandler.setActions(actions); KexiDB::AlterTableHandler::ExecutionArguments args; if (debugTarget) { args.debugString = debugTarget; } else { args.simulate = true; } (void)alterTableHandler.execute(tempData()->table->name(), args); return args.result; # else Q_UNUSED(debugTarget); return false; # endif #else Q_UNUSED(debugTarget); return false; #endif } void KexiTableDesignerView::slotSimulateAlterTableExecution() { (void)simulateAlterTableExecution(0); } tristate KexiTableDesignerView::executeRealAlterTable() { //ported QSignal signal; //ported signal.connect( KexiMainWindowIface::global()->thisWidget(), SLOT(slotProjectSave()) ); d->tempStoreDataUsingRealAlterTable = true; d->recentResultOfStoreData = false; // will call KexiMainWindow::slotProjectSaveAs() and thus storeData(): QMetaObject::invokeMethod( KexiMainWindowIface::global()->thisWidget(), "slotProjectSave"); d->tempStoreDataUsingRealAlterTable = false; return d->recentResultOfStoreData; } KexiTablePart::TempData* KexiTableDesignerView::tempData() const { return static_cast(window()->data()); } #ifdef KEXI_DEBUG_GUI void KexiTableDesignerView::debugCommand(const KUndo2Command* command, int nestingLevel) { if (dynamic_cast(command)) { KexiDB::alterTableActionDebugGUI( dynamic_cast(command)->debugString(), nestingLevel); } else { KexiDB::alterTableActionDebugGUI(command->text(), nestingLevel); } //show subcommands for (int i = 0; i < command->childCount(); ++i) { debugCommand(command->child(i), nestingLevel + 1); } } #endif void KexiTableDesignerView::addHistoryCommand(KexiTableDesignerCommands::Command* command, bool execute) { #ifndef KEXI_NO_UNDOREDO_ALTERTABLE # ifdef KEXI_DEBUG_GUI debugCommand(command, 0); # endif if (!execute) { command->setRedoEnabled(false); } d->history->push(command); if (!execute) { command->setRedoEnabled(true); } updateUndoRedoActions(); #endif } void KexiTableDesignerView::updateUndoRedoActions() { #ifndef KEXI_NO_UNDOREDO_ALTERTABLE setAvailable("edit_undo", d->historyActionCollection->action("edit_undo")->isEnabled()); setAvailable("edit_redo", d->historyActionCollection->action("edit_redo")->isEnabled()); #endif } void KexiTableDesignerView::slotUndo() { #ifndef KEXI_NO_UNDOREDO_ALTERTABLE # ifdef KEXI_DEBUG_GUI KexiDB::alterTableActionDebugGUI(QString("UNDO:")); # endif d->history->undo(); updateUndoRedoActions(); #endif } void KexiTableDesignerView::slotRedo() { #ifndef KEXI_NO_UNDOREDO_ALTERTABLE # ifdef KEXI_DEBUG_GUI KexiDB::alterTableActionDebugGUI(QString("REDO:")); # endif d->history->redo(); updateUndoRedoActions(); #endif } void KexiTableDesignerView::slotAboutToShowContextMenu() { //update title QString title; if (propertySet()) { const KoProperty::Set &set = *propertySet(); QString captionOrName(set["caption"].value().toString()); if (captionOrName.isEmpty()) captionOrName = set["name"].value().toString(); title = i18n("Table field \"%1\"", captionOrName); } else { title = i18nc("Empty table row", "Empty Row"); } //! \todo replace lineedit with table_field icon d->view->setContextMenuTitle(koIcon("lineedit"), title); } QString KexiTableDesignerView::debugStringForCurrentTableSchema(tristate& result) { KexiDB::TableSchema tempTable; //copy schema data static_cast(tempTable) = static_cast(*tempData()->table); result = buildSchema(tempTable, true /*beSilent*/); if (true != result) return QString(); return tempTable.debugString(false /*without name*/); } // -- low-level actions used by undo/redo framework void KexiTableDesignerView::clearRow(int row, bool addCommand) { if (!d->view->acceptRowEdit()) return; KexiDB::RecordData *record = d->view->itemAt(row); if (!record) return; //clear from prop. set d->sets->eraseAt(row); //clear row in table view (just clear value in COLUMN_ID_TYPE column) // for (int i=0; i < (int)d->view->data()->columnsCount(); i++) { if (!addCommand) { d->addHistoryCommand_in_slotRowUpdated_enabled = false; d->addHistoryCommand_in_slotPropertyChanged_enabled = false; d->slotBeforeCellChanged_enabled = false; } d->view->data()->updateRowEditBuffer(record, COLUMN_ID_TYPE, QVariant()); if (!addCommand) { d->addHistoryCommand_in_slotRowUpdated_enabled = true; d->addHistoryCommand_in_slotPropertyChanged_enabled = true; d->slotBeforeCellChanged_enabled = true; } d->view->data()->saveRowChanges(*record, true); } void KexiTableDesignerView::insertField(int row, const QString& caption, bool addCommand) { insertFieldInternal(row, 0, caption, addCommand); } void KexiTableDesignerView::insertField(int row, KoProperty::Set& set, bool addCommand) { insertFieldInternal(row, &set, QString(), addCommand); } void KexiTableDesignerView::insertFieldInternal(int row, KoProperty::Set* set, //const KexiDB::Field& field, const QString& caption, bool addCommand) { if (set && (!set->contains("type") || !set->contains("caption"))) { kWarning() << "no 'type' or 'caption' property in set!"; return; } if (!d->view->acceptRowEdit()) return; KexiDB::RecordData *record = d->view->itemAt(row); if (!record) return; if (!addCommand) { d->addHistoryCommand_in_slotRowUpdated_enabled = false; d->addHistoryCommand_in_slotPropertyChanged_enabled = false; d->slotBeforeCellChanged_enabled = false; } d->view->data()->updateRowEditBuffer(record, COLUMN_ID_CAPTION, set ? (*set)["caption"].value() : QVariant(caption));//field.caption()); d->view->data()->updateRowEditBuffer(record, COLUMN_ID_TYPE, set ? (int)KexiDB::Field::typeGroup((*set)["type"].value().toInt()) - 1/*counting from 0*/ : (((int)KexiDB::Field::TextGroup) - 1)/*default type, counting from 0*/ ); d->view->data()->updateRowEditBuffer(record, COLUMN_ID_DESC, set ? (*set)["description"].value() : QVariant());//field.description()); if (!addCommand) { d->slotBeforeCellChanged_enabled = true; } //this will create a new property set: d->view->data()->saveRowChanges(*record); if (set) { KoProperty::Set *newSet = d->sets->at(row); if (newSet) { *newSet = *set; //deep copy } else { kWarning() << "!newSet, row==" << row; } } if (!addCommand) { d->addHistoryCommand_in_slotPropertyChanged_enabled = true; d->addHistoryCommand_in_slotRowUpdated_enabled = true; } d->view->updateRow(row); propertySetReloaded(true); } void KexiTableDesignerView::insertEmptyRow(int row, bool addCommand) { if (!addCommand) { d->addHistoryCommand_in_slotRowInserted_enabled = false; } d->view->insertEmptyRow(row); if (!addCommand) { d->addHistoryCommand_in_slotRowInserted_enabled = true; } } void KexiTableDesignerView::deleteRow(int row, bool addCommand) { KexiDB::RecordData *record = d->view->itemAt(row); if (!record) return; if (!addCommand) { d->addHistoryCommand_in_slotAboutToDeleteRow_enabled = false; } const bool res = d->view->deleteItem(record); if (!addCommand) { d->addHistoryCommand_in_slotAboutToDeleteRow_enabled = true; } if (!res) return; } void KexiTableDesignerView::changeFieldPropertyForRow(int row, const QByteArray& propertyName, const QVariant& newValue, KoProperty::Property::ListData* const listData, bool addCommand) { #ifdef KEXI_DEBUG_GUI KexiDB::alterTableActionDebugGUI(QString("** changeFieldProperty: \"") + QString(propertyName) + "\" to \"" + newValue.toString() + "\"", 2/*nestingLevel*/); #endif if (!d->view->acceptRowEdit()) return; KoProperty::Set* set = d->sets->at(row); if (!set || !set->contains(propertyName)) return; KoProperty::Property &property = set->property(propertyName); if (listData) { if (listData->keys.isEmpty()) property.setListData(0); else property.setListData(new KoProperty::Property::ListData(*listData)); } if (propertyName != "type") //delayed type update (we need to have subtype set properly) property.setValue(newValue); KexiDB::RecordData *record = d->view->itemAt(row); Q_ASSERT(record); if (propertyName == "type") { // d->addHistoryCommand_in_slotRowUpdated_enabled = false; // d->addHistoryCommand_in_slotPropertyChanged_enabled = false; d->slotPropertyChanged_subType_enabled = false; d->view->data()->updateRowEditBuffer(record, COLUMN_ID_TYPE, int(KexiDB::Field::typeGroup(newValue.toInt())) - 1); d->view->data()->saveRowChanges(*record); d->addHistoryCommand_in_slotRowUpdated_enabled = true; // d->addHistoryCommand_in_slotPropertyChanged_enabled = true; // d->slotPropertyChanged_subType_enabled = true; property.setValue(newValue); //delayed type update (we needed to have subtype set properly) } if (!addCommand) { d->addHistoryCommand_in_slotRowUpdated_enabled = false; d->addHistoryCommand_in_slotPropertyChanged_enabled = false; d->slotPropertyChanged_subType_enabled = false; } //special cases: properties displayed within the data grid: if (propertyName == "caption") { if (!addCommand) { d->slotBeforeCellChanged_enabled = false; } d->view->data()->updateRowEditBuffer(record, COLUMN_ID_CAPTION, newValue); d->view->data()->saveRowChanges(*record); if (!addCommand) { d->slotBeforeCellChanged_enabled = true; } } else if (propertyName == "description") { if (!addCommand) { d->slotBeforeCellChanged_enabled = false; } d->view->data()->updateRowEditBuffer(record, COLUMN_ID_DESC, newValue); if (!addCommand) { d->slotBeforeCellChanged_enabled = true; } d->view->data()->saveRowChanges(*record); } if (!addCommand) { d->addHistoryCommand_in_slotPropertyChanged_enabled = true; d->addHistoryCommand_in_slotRowUpdated_enabled = true; d->slotPropertyChanged_subType_enabled = true; } d->view->updateRow(row); } void KexiTableDesignerView::changeFieldProperty(int fieldUID, const QByteArray& propertyName, const QVariant& newValue, KoProperty::Property::ListData* const listData, bool addCommand) { //find a property by UID const int row = d->sets->findRowForPropertyValue("uid", fieldUID); if (row < 0) { kWarning() << "field with uid=" << fieldUID << " not found!"; return; } changeFieldPropertyForRow(row, propertyName, newValue, listData, addCommand); } void KexiTableDesignerView::changePropertyVisibility( int fieldUID, const QByteArray& propertyName, bool visible) { #ifdef KEXI_DEBUG_GUI KexiDB::alterTableActionDebugGUI(QString("** changePropertyVisibility: \"") + QString(propertyName) + "\" to \"" + (visible ? "true" : "false") + "\"", 2/*nestingLevel*/); #endif if (!d->view->acceptRowEdit()) return; //find a property by name const int row = d->sets->findRowForPropertyValue("uid", fieldUID); if (row < 0) return; KoProperty::Set* set = d->sets->at(row); if (!set || !set->contains(propertyName)) return; KoProperty::Property &property = set->property(propertyName); if (property.isVisible() != visible) { property.setVisible(visible); propertySetReloaded(true); } } void KexiTableDesignerView::propertySetSwitched() { KexiDataTable::propertySetSwitched(); - KexiLookupColumnPage *page = static_cast(window()->part())->lookupColumnPage(); + KexiLookupColumnPage *page = qobject_cast(window()->part())->lookupColumnPage(); if (page) page->assignPropertySet(propertySet()); } bool KexiTableDesignerView::isPhysicalAlteringNeeded() { //- create action list for the alter table handler KexiDB::AlterTableHandler::ActionList actions; tristate res = buildAlterTableActions(actions); if (res != true) return true; KexiDB::Connection *conn = KexiMainWindowIface::global()->project()->dbConnection(); KexiDB::AlterTableHandler *alterTableHandler = new KexiDB::AlterTableHandler(*conn); alterTableHandler->setActions(actions); //only compute requirements KexiDB::AlterTableHandler::ExecutionArguments args; args.onlyComputeRequirements = true; (void)alterTableHandler->execute(tempData()->table->name(), args); res = args.result; delete alterTableHandler; if ( res == true && 0 == (args.requirements & (0xffff ^ KexiDB::AlterTableHandler::SchemaAlteringRequired))) { return false; } return true; } #include "kexitabledesignerview.moc" diff --git a/libs/db/connection.cpp b/libs/db/connection.cpp index 1f85b616674..6ceb8889412 100644 --- a/libs/db/connection.cpp +++ b/libs/db/connection.cpp @@ -1,3722 +1,3725 @@ /* This file is part of the KDE project Copyright (C) 2003-2012 Jarosław Staniek This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "connection.h" #include "error.h" #include "connection_p.h" #include "connectiondata.h" #include "driver.h" #include "driver_p.h" #include "schemadata.h" #include "tableschema.h" #include "relationship.h" #include "transaction.h" #include "cursor.h" #include "global.h" #include "calligradb_global.h" #include "roweditbuffer.h" #include "utils.h" #include "dbproperties.h" #include "lookupfieldschema.h" #include "parser/parser.h" #include #include #include #include #include #include /*! Version number of extended table schema. List of changes: * 2: (Kexi 2.5.0) Added maxLengthIsDefault property (type: bool, if true, Field::maxLengthStrategy() == Field::DefaultMaxLength) * 1: (Kexi 1.x) Initial version */ #define KEXIDB_EXTENDED_TABLE_SCHEMA_VERSION 2 //#define KEXIDB_LOOKUP_FIELD_TEST namespace KexiDB { Connection::SelectStatementOptions::SelectStatementOptions() : identifierEscaping(Driver::EscapeDriver | Driver::EscapeAsNecessary) , alsoRetrieveROWID(false) , addVisibleLookupColumns(true) { } Connection::SelectStatementOptions::~SelectStatementOptions() { } //================================================ ConnectionInternal::ConnectionInternal(Connection *conn) : connection(conn) { } ConnectionInternal::~ConnectionInternal() { } //================================================ //! @internal class ConnectionPrivate { public: ConnectionPrivate(Connection* const conn, ConnectionData &conn_data) : conn(conn) , conn_data(&conn_data) , m_parser(0) , dont_remove_transactions(false) , skip_databaseExists_check_in_useDatabase(false) , default_trans_started_inside(false) , isConnected(false) , autoCommit(true) , takeTableEnabled(true) { //Qt 4 tableSchemaChangeListeners.setAutoDelete(true); //Qt 4 obsoleteQueries.setAutoDelete(true); //Qt 4 tables.setAutoDelete(true); //Qt 4 kexiDBSystemTables.setAutoDelete(true);//only system tables //Qt 4 queries.setAutoDelete(true); //Qt 4 queries_byname.setAutoDelete(false);//queries is owner, not me //reasonable sizes: TODO //Qt 4 tables.resize(101); //Qt 4 queries.resize(101); } ~ConnectionPrivate() { qDeleteAll(cursors); delete m_parser; qDeleteAll(tableSchemaChangeListeners); qDeleteAll(obsoleteQueries); } void errorInvalidDBContents(const QString& details) { conn->setError(ERR_INVALID_DATABASE_CONTENTS, i18n("Invalid database contents. ") + details); } QString strItIsASystemObject() const { return i18n("It is a system object."); } inline Parser *parser() { return m_parser ? m_parser : (m_parser = new Parser(conn)); } inline TableSchema* table(const QString& name) const { return tables_byname.value(name); } inline TableSchema* table(int id) const { return tables.value(id); } //! used just for removing system TableSchema objects on db close. inline const QSet& kexiDBSystemTables() const { return _kexiDBSystemTables; } inline void insertTable(TableSchema& tableSchema) { tables.insert(tableSchema.id(), &tableSchema); tables_byname.insert(tableSchema.name(), &tableSchema); } /*! @internal. Inserts internal table to Connection's structures, so it can be found by name. Used by Connection::insertInternalTable(TableSchema&) */ inline void insertInternalTable(TableSchema& tableSchema) { tableSchema.setKexiDBSystem(true); _kexiDBSystemTables.insert(&tableSchema); tables_byname.insert(tableSchema.name().toLower(), &tableSchema); } /*! @internal Removes table schema pointed by tableSchema.id() and tableSchema.name() from internal structures and destroys it. Does not make any change at the backend. Note that the table schema being removed may be not the same as @a tableSchema. */ inline void removeTable(const TableSchema& tableSchema) { tables_byname.remove(tableSchema.name()); TableSchema *toDelete = tables.take(tableSchema.id()); delete toDelete; } inline void takeTable(TableSchema& tableSchema) { if (!takeTableEnabled) return; tables.take(tableSchema.id()); tables_byname.take(tableSchema.name()); } inline void renameTable(TableSchema& tableSchema, const QString& newName) { tables_byname.take(tableSchema.name()); tableSchema.setName(newName); tables_byname.insert(tableSchema.name(), &tableSchema); } inline void changeTableId(TableSchema& tableSchema, int newId) { tables.take(tableSchema.id()); tables.insert(newId, &tableSchema); } inline void clearTables() { tables_byname.clear(); qDeleteAll(_kexiDBSystemTables); _kexiDBSystemTables.clear(); takeTableEnabled = false; //!< needed because otherwise 'tables' hash will //!< be touched by takeTable() what's not allowed during qDeleteAll() qDeleteAll(tables); takeTableEnabled = true; tables.clear(); } inline QuerySchema* query(const QString& name) const { return queries_byname.value(name); } inline QuerySchema* query(int id) const { return queries.value(id); } inline void insertQuery(QuerySchema& query) { queries.insert(query.id(), &query); queries_byname.insert(query.name(), &query); } /*! @internal Removes \a querySchema from internal structures and destroys it. Does not make any change at the backend. */ inline void removeQuery(QuerySchema &querySchema) { queries_byname.remove(querySchema.name()); queries.remove(querySchema.id()); delete &querySchema; } inline void setQueryObsolete(QuerySchema& query) { obsoleteQueries.insert(&query); queries_byname.take(query.name()); queries.take(query.id()); } inline void clearQueries() { qDeleteAll(queries); queries.clear(); } Connection* const conn; //!< The \a Connection instance this \a ConnectionPrivate belongs to. QPointer conn_data; //!< the \a ConnectionData used within that connection. /*! Default transaction handle. If transactions are supported: Any operation on database (e.g. inserts) that is started without specifying transaction context, will be performed in the context of this transaction. */ Transaction default_trans; QList transactions; QHash* > tableSchemaChangeListeners; //! Used in Connection::setQuerySchemaObsolete( const QString& queryName ) //! to collect obsolete queries. THese are deleted on connection deleting. QSet obsoleteQueries; //! server version information for this connection. KexiDB::ServerVersionInfo serverVersion; //! Daabase version information for this connection. KexiDB::DatabaseVersionInfo databaseVersion; Parser *m_parser; //! cursors created for this connection QSet cursors; //! Database properties DatabaseProperties* dbProperties; QString availableDatabaseName; //!< used by anyAvailableDatabaseName() QString usedDatabase; //!< database name that is opened now (the currentDatabase() name) //! true if rollbackTransaction() and commitTransaction() shouldn't remove //! the transaction object from 'transactions' list; used by closeDatabase() bool dont_remove_transactions : 1; //! used to avoid endless recursion between useDatabase() and databaseExists() //! when useTemporaryDatabaseIfNeeded() works bool skip_databaseExists_check_in_useDatabase : 1; /*! Used when single transactions are only supported (Driver::SingleTransactions). True value means default transaction has been started inside connection object (by beginAutoCommitTransaction()), otherwise default transaction has been started outside of the object (e.g. before createTable()), so we shouldn't autocommit the transaction in commitAutoCommitTransaction(). Also, beginAutoCommitTransaction() doesn't restarts transaction if default_trans_started_inside is false. Such behaviour allows user to execute a sequence of actions like CREATE TABLE...; INSERT DATA...; within a single transaction and commit it or rollback by hand. */ bool default_trans_started_inside : 1; bool isConnected : 1; bool autoCommit : 1; /*! True for read only connection. Used especially for file-based drivers. */ bool readOnly : 1; private: //! Table schemas retrieved on demand with tableSchema() QHash tables; QHash tables_byname; //! used just for removing system TableSchema objects on db close. QSet _kexiDBSystemTables; //! Query schemas retrieved on demand with querySchema() QHash queries; QHash queries_byname; bool takeTableEnabled : 1; //!< used by takeTable() needed because otherwise 'tables' hash will //!< be touched by takeTable() what's not allowed during qDeleteAll() }; }//namespace KexiDB //================================================ using namespace KexiDB; //! static: list of internal KexiDB system table names QStringList KexiDB_kexiDBSystemTableNames; Connection::Connection(Driver *driver, ConnectionData &conn_data) : QObject() , KexiDB::Object() , d(new ConnectionPrivate(this, conn_data)) , m_driver(driver) , m_destructor_started(false) , m_insideCloseDatabase(false) { d->dbProperties = new DatabaseProperties(this); m_sql.reserve(0x4000); } void Connection::destroy() { disconnect(); //do not allow the driver to touch me: I will kill myself. m_driver->d->connections.remove(this); } Connection::~Connection() { m_destructor_started = true; // KexiDBDbg << "Connection::~Connection()"; delete d->dbProperties; delete d; d = 0; /* if (m_driver) { if (m_is_connected) { //delete own table schemas d->tables.clear(); //delete own cursors: m_cursors.clear(); } //do not allow the driver to touch me: I will kill myself. m_driver->m_connections.take( this ); }*/ } ConnectionData* Connection::data() const { return d->conn_data; } bool Connection::connect() { clearError(); if (d->isConnected) { setError(ERR_ALREADY_CONNECTED, i18n("Connection already established.")); return false; } d->serverVersion.clear(); if (!(d->isConnected = drv_connect(d->serverVersion))) { setError(m_driver->isFileDriver() ? i18n("Could not open \"%1\" project file.", QDir::convertSeparators(d->conn_data->fileName())) : i18n("Could not connect to \"%1\" database server.", d->conn_data->serverInfoString())); } return d->isConnected; } bool Connection::isDatabaseUsed() const { return !d->usedDatabase.isEmpty() && d->isConnected && drv_isDatabaseUsed(); } void Connection::clearError() { Object::clearError(); m_sql.clear(); } bool Connection::disconnect() { clearError(); if (!d->isConnected) return true; if (!closeDatabase()) return false; bool ok = drv_disconnect(); if (ok) d->isConnected = false; return ok; } bool Connection::isConnected() const { return d->isConnected; } bool Connection::checkConnected() { if (d->isConnected) { clearError(); return true; } setError(ERR_NO_CONNECTION, i18n("Not connected to the database server.")); return false; } bool Connection::checkIsDatabaseUsed() { if (isDatabaseUsed()) { clearError(); return true; } setError(ERR_NO_DB_USED, i18n("Currently no database is used.")); return false; } QStringList Connection::databaseNames(bool also_system_db) { KexiDBDbg << "Connection::databaseNames(" << also_system_db << ")"; if (!checkConnected()) return QStringList(); QString tmpdbName; //some engines need to have opened any database before executing "create database" if (!useTemporaryDatabaseIfNeeded(tmpdbName)) return QStringList(); QStringList list, non_system_list; bool ret = drv_getDatabasesList(list); if (!tmpdbName.isEmpty()) { //whatever result is - now we have to close temporary opened database: if (!closeDatabase()) return QStringList(); } if (!ret) return QStringList(); if (also_system_db) return list; //filter system databases: for (QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { KexiDBDbg << "Connection::databaseNames(): " << *it; if (!m_driver->isSystemDatabaseName(*it)) { KexiDBDbg << "add " << *it; non_system_list << (*it); } } return non_system_list; } bool Connection::drv_getDatabasesList(QStringList &list) { list.clear(); return true; } bool Connection::drv_databaseExists(const QString &dbName, bool ignoreErrors) { QStringList list = databaseNames(true);//also system if (error()) { return false; } if (list.indexOf(dbName) == -1) { if (!ignoreErrors) setError(ERR_OBJECT_NOT_FOUND, i18n("The database \"%1\" does not exist.", dbName)); return false; } return true; } bool Connection::databaseExists(const QString &dbName, bool ignoreErrors) { // KexiDBDbg << "Connection::databaseExists(" << dbName << "," << ignoreErrors << ")"; if (!checkConnected()) return false; clearError(); if (m_driver->isFileDriver()) { //for file-based db: file must exists and be accessible //js: moved from useDatabase(): QFileInfo file(d->conn_data->fileName()); if (!file.exists() || (!file.isFile() && !file.isSymLink())) { if (!ignoreErrors) setError(ERR_OBJECT_NOT_FOUND, i18n("Database file \"%1\" does not exist.", QDir::convertSeparators(d->conn_data->fileName()))); return false; } if (!file.isReadable()) { if (!ignoreErrors) setError(ERR_ACCESS_RIGHTS, i18n("Database file \"%1\" is not readable.", QDir::convertSeparators(d->conn_data->fileName()))); return false; } if (!file.isWritable()) { if (!ignoreErrors) setError(ERR_ACCESS_RIGHTS, i18n("Database file \"%1\" is not writable.", QDir::convertSeparators(d->conn_data->fileName()))); return false; } return true; } QString tmpdbName; //some engines need to have opened any database before executing "create database" const bool orig_skip_databaseExists_check_in_useDatabase = d->skip_databaseExists_check_in_useDatabase; d->skip_databaseExists_check_in_useDatabase = true; bool ret = useTemporaryDatabaseIfNeeded(tmpdbName); d->skip_databaseExists_check_in_useDatabase = orig_skip_databaseExists_check_in_useDatabase; if (!ret) return false; ret = drv_databaseExists(dbName, ignoreErrors); if (!tmpdbName.isEmpty()) { //whatever result is - now we have to close temporary opened database: if (!closeDatabase()) return false; } return ret; } #define createDatabase_CLOSE \ { if (!closeDatabase()) { \ setError(i18n("Database \"%1\" created but could not be closed after creation.", dbName) ); \ return false; \ } } #define createDatabase_ERROR \ { createDatabase_CLOSE; return false; } bool Connection::createDatabase(const QString &dbName) { if (!checkConnected()) return false; if (databaseExists(dbName)) { setError(ERR_OBJECT_EXISTS, i18n("Database \"%1\" already exists.", dbName)); return false; } if (m_driver->isSystemDatabaseName(dbName)) { setError(ERR_SYSTEM_NAME_RESERVED, i18n("Cannot create database \"%1\". This name is reserved for system database.", dbName)); return false; } if (m_driver->isFileDriver()) { //update connection data if filename differs if (QFileInfo(dbName).isAbsolute()) { d->conn_data->setFileName(dbName); } else { d->conn_data->setFileName( d->conn_data->dbPath() + QDir::separator() + QFileInfo(dbName).fileName()); } } QString tmpdbName; //some engines need to have opened any database before executing "create database" if (!useTemporaryDatabaseIfNeeded(tmpdbName)) return false; //low-level create if (!drv_createDatabase(dbName)) { setError(i18n("Error creating database \"%1\" on the server.", dbName)); closeDatabase();//sanity return false; } if (!tmpdbName.isEmpty()) { //whatever result is - now we have to close temporary opened database: if (!closeDatabase()) return false; } if (!tmpdbName.isEmpty() || !m_driver->d->isDBOpenedAfterCreate) { //db need to be opened if (!useDatabase(dbName, false/*not yet kexi compatible!*/)) { setError(i18n("Database \"%1\" created but could not be opened.", dbName)); return false; } } else { //just for the rule d->usedDatabase = dbName; } Transaction trans; if (m_driver->transactionsSupported()) { trans = beginTransaction(); if (!trans.active()) return false; } //not needed since closeDatabase() rollbacks transaction: TransactionGuard trans_g(this); // if (error()) // return false; //-create system tables schema objects if (!setupKexiDBSystemSchema()) return false; //-physically create system tables foreach(TableSchema* t, d->kexiDBSystemTables()) { if (!drv_createTable(t->name())) createDatabase_ERROR; } //-insert KexiDB version info: TableSchema *t_db = d->table("kexi__db"); if (!t_db) createDatabase_ERROR; if (!insertRecord(*t_db, "kexidb_major_ver", KexiDB::version().major) || !insertRecord(*t_db, "kexidb_minor_ver", KexiDB::version().minor)) createDatabase_ERROR; if (trans.active() && !commitTransaction(trans)) createDatabase_ERROR; createDatabase_CLOSE; return true; } #undef createDatabase_CLOSE #undef createDatabase_ERROR bool Connection::useDatabase(const QString &dbName, bool kexiCompatible, bool *cancelled, MessageHandler* msgHandler) { if (cancelled) *cancelled = false; KexiDBDbg << "Connection::useDatabase(" << dbName << "," << kexiCompatible << ")"; if (!checkConnected()) return false; if (dbName.isEmpty()) return false; QString my_dbName = dbName; // if (my_dbName.isEmpty()) { // const QStringList& db_lst = databaseNames(); // if (!db_lst.isEmpty()) // my_dbName = db_lst.first(); // } if (d->usedDatabase == my_dbName) return true; //already used if (!d->skip_databaseExists_check_in_useDatabase) { if (!databaseExists(my_dbName, false /*don't ignore errors*/)) return false; //database must exist } if (!d->usedDatabase.isEmpty() && !closeDatabase()) //close db if already used return false; d->usedDatabase = ""; if (!drv_useDatabase(my_dbName, cancelled, msgHandler)) { if (cancelled && *cancelled) return false; QString msg(i18n("Opening database \"%1\" failed.", my_dbName)); if (error()) setError(this, msg); else setError(msg); return false; } //-create system tables schema objects if (!setupKexiDBSystemSchema()) return false; if (kexiCompatible && my_dbName.toLower() != anyAvailableDatabaseName().toLower()) { //-get global database information int num; bool ok; // static QString notfound_str = i18n("\"%1\" database property not found"); num = d->dbProperties->value("kexidb_major_ver").toInt(&ok); if (!ok) return false; d->databaseVersion.major = num; /* if (true!=querySingleNumber( "select db_value from kexi__db where db_property=" + m_driver->escapeString(QString("kexidb_major_ver")), num)) { d->errorInvalidDBContents(notfound_str.arg("kexidb_major_ver")); return false; }*/ num = d->dbProperties->value("kexidb_minor_ver").toInt(&ok); if (!ok) return false; d->databaseVersion.minor = num; /* if (true!=querySingleNumber( "select db_value from kexi__db where db_property=" + m_driver->escapeString(QString("kexidb_minor_ver")), num)) { d->errorInvalidDBContents(notfound_str.arg("kexidb_minor_ver")); return false; }*/ #if 0 //this is already checked in DriverManagerInternal::lookupDrivers() //** error if major version does not match if (m_driver->versionMajor() != KexiDB::versionMajor()) { setError(ERR_INCOMPAT_DATABASE_VERSION, i18n("Database version (%1) does not match Kexi application's version (%2)", QString::fromLatin1("%1.%2").arg(versionMajor()).arg(versionMinor()), QString::fromLatin1("%1.%2").arg(KexiDB::versionMajor()).arg(KexiDB::versionMinor()))); return false; } if (m_driver->versionMinor() != KexiDB::versionMinor()) { //js TODO: COMPATIBILITY CODE HERE! //js TODO: CONVERSION CODE HERE (or signal that conversion is needed) } #endif } d->usedDatabase = my_dbName; return true; } bool Connection::closeDatabase() { if (d->usedDatabase.isEmpty()) return true; //no db used if (!checkConnected()) return true; bool ret = true; /*! \todo (js) add CLEVER algorithm here for nested transactions */ if (m_driver->transactionsSupported()) { //rollback all transactions d->dont_remove_transactions = true; //lock! foreach(const Transaction& tr, d->transactions) { if (!rollbackTransaction(tr)) {//rollback as much as you can, don't stop on prev. errors ret = false; } else { KexiDBDbg << "Connection::closeDatabase(): transaction rolled back!"; KexiDBDbg << "Connection::closeDatabase(): trans.refcount==" << (tr.m_data ? QString::number(tr.m_data->refcount) : "(null)"); } } d->dont_remove_transactions = false; //unlock! d->transactions.clear(); //free trans. data } m_insideCloseDatabase = true; //delete own cursors: qDeleteAll(d->cursors); d->cursors.clear(); //delete own schemas d->clearTables(); d->clearQueries(); m_insideCloseDatabase = false; if (!drv_closeDatabase()) return false; d->usedDatabase = ""; // KexiDBDbg << "Connection::closeDatabase(): " << ret; return ret; } QString Connection::currentDatabase() const { return d->usedDatabase; } bool Connection::useTemporaryDatabaseIfNeeded(QString &tmpdbName) { if (!m_driver->isFileDriver() && m_driver->beh->USING_DATABASE_REQUIRED_TO_CONNECT && !isDatabaseUsed()) { //we have no db used, but it is required by engine to have used any! tmpdbName = anyAvailableDatabaseName(); if (tmpdbName.isEmpty()) { setError(ERR_NO_DB_USED, i18n("Cannot find any database for temporary connection.")); return false; } const bool orig_skip_databaseExists_check_in_useDatabase = d->skip_databaseExists_check_in_useDatabase; d->skip_databaseExists_check_in_useDatabase = true; bool ret = useDatabase(tmpdbName, false); d->skip_databaseExists_check_in_useDatabase = orig_skip_databaseExists_check_in_useDatabase; if (!ret) { setError(errorNum(), i18n("Error during starting temporary connection using \"%1\" database name.", tmpdbName)); return false; } } return true; } bool Connection::dropDatabase(const QString &dbName) { if (!checkConnected()) return false; QString dbToDrop; if (dbName.isEmpty() && d->usedDatabase.isEmpty()) { if (!m_driver->isFileDriver() || (m_driver->isFileDriver() && d->conn_data->fileName().isEmpty())) { setError(ERR_NO_NAME_SPECIFIED, i18n("Cannot drop database - name not specified.")); return false; } //this is a file driver so reuse previously passed filename dbToDrop = d->conn_data->fileName(); } else { if (dbName.isEmpty()) { dbToDrop = d->usedDatabase; } else { if (m_driver->isFileDriver()) //lets get full path dbToDrop = QFileInfo(dbName).absoluteFilePath(); else dbToDrop = dbName; } } if (dbToDrop.isEmpty()) { setError(ERR_NO_NAME_SPECIFIED, i18n("Cannot delete database - name not specified.")); return false; } if (m_driver->isSystemDatabaseName(dbToDrop)) { setError(ERR_SYSTEM_NAME_RESERVED, i18n("Cannot delete system database \"%1\".", dbToDrop)); return false; } if (isDatabaseUsed() && d->usedDatabase == dbToDrop) { //we need to close database because cannot drop used this database if (!closeDatabase()) return false; } QString tmpdbName; //some engines need to have opened any database before executing "drop database" if (!useTemporaryDatabaseIfNeeded(tmpdbName)) return false; //ok, now we have access to dropping bool ret = drv_dropDatabase(dbToDrop); if (!tmpdbName.isEmpty()) { //whatever result is - now we have to close temporary opened database: if (!closeDatabase()) return false; } return ret; } QStringList Connection::objectNames(int objType, bool* ok) { QStringList list; if (!checkIsDatabaseUsed()) { if (ok) *ok = false; return list; } QString sql; if (objType == KexiDB::AnyObjectType) sql = "SELECT o_name FROM kexi__objects"; else sql = QString::fromLatin1("SELECT o_name FROM kexi__objects WHERE o_type=%1 ORDER BY o_id").arg(objType); Cursor *c = executeQuery(sql); if (!c) { if (ok) *ok = false; return list; } for (c->moveFirst(); !c->eof(); c->moveNext()) { QString name = c->value(0).toString(); if (KexiDB::isIdentifier(name)) { list.append(name); } } if (!deleteCursor(c)) { if (ok) *ok = false; return list; } if (ok) *ok = true; return list; } QStringList Connection::tableNames(bool also_system_tables) { bool ok = true; QStringList list = objectNames(TableObjectType, &ok); if (also_system_tables && ok) { list += Connection::kexiDBSystemTableNames(); } return list; } //! \todo (js): this will depend on KexiDB lib version const QStringList& Connection::kexiDBSystemTableNames() { if (KexiDB_kexiDBSystemTableNames.isEmpty()) { KexiDB_kexiDBSystemTableNames << "kexi__objects" << "kexi__objectdata" << "kexi__fields" // << "kexi__querydata" // << "kexi__queryfields" // << "kexi__querytables" << "kexi__db" ; } return KexiDB_kexiDBSystemTableNames; } KexiDB::ServerVersionInfo* Connection::serverVersion() const { return isConnected() ? &d->serverVersion : 0; } KexiDB::DatabaseVersionInfo* Connection::databaseVersion() const { return isDatabaseUsed() ? &d->databaseVersion : 0; } DatabaseProperties& Connection::databaseProperties() { return *d->dbProperties; } QList Connection::tableIds() { return objectIds(KexiDB::TableObjectType); } QList Connection::queryIds() { return objectIds(KexiDB::QueryObjectType); } QList Connection::objectIds(int objType) { QList list; if (!checkIsDatabaseUsed()) return list; QString sql; if (objType == KexiDB::AnyObjectType) sql = "SELECT o_id, o_name FROM kexi__objects ORDER BY o_id"; else sql = QString::fromLatin1("SELECT o_id, o_name FROM kexi__objects WHERE o_type=%1 ORDER BY o_id").arg(objType); Cursor *c = executeQuery(sql); if (!c) return list; for (c->moveFirst(); !c->eof(); c->moveNext()) { QString tname = c->value(1).toString(); //kexi__objects.o_name if (KexiDB::isIdentifier(tname)) { list.append(c->value(0).toInt()); //kexi__objects.o_id } } deleteCursor(c); return list; } QString Connection::createTableStatement(const KexiDB::TableSchema& tableSchema) const { // Each SQL identifier needs to be escaped in the generated query. QString sql; sql.reserve(4096); sql = "CREATE TABLE " + escapeIdentifier(tableSchema.name()) + " ("; bool first = true; foreach(Field *field, tableSchema.m_fields) { if (first) first = false; else sql += ", "; QString v = escapeIdentifier(field->name()) + " "; const bool autoinc = field->isAutoIncrement(); const bool pk = field->isPrimaryKey() || (autoinc && m_driver->beh->AUTO_INCREMENT_REQUIRES_PK); //TODO: warning: ^^^^^ this allows only one autonumber per table when AUTO_INCREMENT_REQUIRES_PK==true! if (autoinc && m_driver->beh->SPECIAL_AUTO_INCREMENT_DEF) { if (pk) v += m_driver->beh->AUTO_INCREMENT_TYPE + " " + m_driver->beh->AUTO_INCREMENT_PK_FIELD_OPTION; else v += m_driver->beh->AUTO_INCREMENT_TYPE + " " + m_driver->beh->AUTO_INCREMENT_FIELD_OPTION; } else { if (autoinc && !m_driver->beh->AUTO_INCREMENT_TYPE.isEmpty()) v += m_driver->beh->AUTO_INCREMENT_TYPE; else v += m_driver->sqlTypeName(field->type(), field->precision()); if (field->isUnsigned()) v += (" " + m_driver->beh->UNSIGNED_TYPE_KEYWORD); if (field->isFPNumericType() && field->precision() > 0) { if (field->scale() > 0) v += QString::fromLatin1("(%1,%2)").arg(field->precision()).arg(field->scale()); else v += QString::fromLatin1("(%1)").arg(field->precision()); } else if (field->type() == Field::Text) { uint realMaxLen; if (m_driver->beh->TEXT_TYPE_MAX_LENGTH == 0) { realMaxLen = field->maxLength(); // allow to skip (N) } else { // max length specified by driver if (field->maxLength() == 0) { // as long as possible realMaxLen = m_driver->beh->TEXT_TYPE_MAX_LENGTH; } else { // not longer than specified by driver realMaxLen = qMin(m_driver->beh->TEXT_TYPE_MAX_LENGTH, field->maxLength()); } } if (realMaxLen > 0) { v += QString::fromLatin1("(%1)").arg(realMaxLen); } } if (autoinc) v += (" " + (pk ? m_driver->beh->AUTO_INCREMENT_PK_FIELD_OPTION : m_driver->beh->AUTO_INCREMENT_FIELD_OPTION)); else //TODO: here is automatically a single-field key created if (pk) v += " PRIMARY KEY"; if (!pk && field->isUniqueKey()) v += " UNIQUE"; ///@todo IS this ok for all engines?: if (!autoinc && !field->isPrimaryKey() && field->isNotNull()) if (!autoinc && !pk && field->isNotNull()) v += " NOT NULL"; //only add not null option if no autocommit is set if (field->defaultValue().isValid()) { QString valToSQL(m_driver->valueToSQL(field, field->defaultValue())); if (!valToSQL.isEmpty()) //for sanity v += QLatin1String(" DEFAULT ") + valToSQL; } } sql += v; } sql += ")"; return sql; } //yeah, it is very efficient: #define C_A(a) , const QVariant& c ## a #define V_A0 m_driver->valueToSQL( tableSchema.field(0), c0 ) #define V_A(a) +","+m_driver->valueToSQL( \ tableSchema.field(a) ? tableSchema.field(a)->type() : Field::Text, c ## a ) // KexiDBDbg << "******** " << QString("INSERT INTO ") + // escapeIdentifier(tableSchema.name()) + // " VALUES (" + vals + ")"; #define C_INS_REC(args, vals) \ bool Connection::insertRecord(KexiDB::TableSchema &tableSchema args) {\ if ( !drv_beforeInsert( tableSchema.name(), tableSchema ) ) \ return false; \ \ bool res = executeSQL( \ QLatin1String("INSERT INTO ") + escapeIdentifier(tableSchema.name()) \ + " (" + tableSchema.sqlFieldsList(m_driver) + ") VALUES (" + vals + ")" \ ); \ \ if ( !drv_afterInsert( tableSchema.name(),tableSchema ) ) \ return false; \ \ return res; \ } #define C_INS_REC_ALL \ C_INS_REC( C_A(0), V_A0 ) \ C_INS_REC( C_A(0) C_A(1), V_A0 V_A(1) ) \ C_INS_REC( C_A(0) C_A(1) C_A(2), V_A0 V_A(1) V_A(2) ) \ C_INS_REC( C_A(0) C_A(1) C_A(2) C_A(3), V_A0 V_A(1) V_A(2) V_A(3) ) \ C_INS_REC( C_A(0) C_A(1) C_A(2) C_A(3) C_A(4), V_A0 V_A(1) V_A(2) V_A(3) V_A(4) ) \ C_INS_REC( C_A(0) C_A(1) C_A(2) C_A(3) C_A(4) C_A(5), V_A0 V_A(1) V_A(2) V_A(3) V_A(4) V_A(5) ) \ C_INS_REC( C_A(0) C_A(1) C_A(2) C_A(3) C_A(4) C_A(5) C_A(6), V_A0 V_A(1) V_A(2) V_A(3) V_A(4) V_A(5) V_A(6) ) \ C_INS_REC( C_A(0) C_A(1) C_A(2) C_A(3) C_A(4) C_A(5) C_A(6) C_A(7), V_A0 V_A(1) V_A(2) V_A(3) V_A(4) V_A(5) V_A(6) V_A(7) ) C_INS_REC_ALL #undef V_A0 #undef V_A #undef C_INS_REC #define V_A0 value += m_driver->valueToSQL( it.next(), c0 ); #define V_A( a ) value += ("," + m_driver->valueToSQL( it.next(), c ## a )); #define C_INS_REC(args, vals) \ bool Connection::insertRecord(FieldList& fields args) \ { \ QString value; \ const Field::List *flist = fields.fields(); \ QListIterator it(*flist); \ vals \ it.toFront(); \ QString tableName( (it.hasNext() && it.peekNext()->table()) ? it.next()->table()->name() : "??" ); \ if ( !drv_beforeInsert( tableName, fields ) ) \ return false; \ bool res = executeSQL( \ QLatin1String("INSERT INTO ") + escapeIdentifier(tableName) \ + "(" + fields.sqlFieldsList(m_driver) + ") VALUES (" + value + ")" \ ); \ if ( !drv_afterInsert( tableName, fields ) ) \ return false; \ return res; \ } C_INS_REC_ALL #undef C_A #undef V_A #undef V_ALAST #undef C_INS_REC #undef C_INS_REC_ALL bool Connection::insertRecord(TableSchema &tableSchema, const QList& values) { // Each SQL identifier needs to be escaped in the generated query. const Field::List *flist = tableSchema.fields(); if (flist->isEmpty()) return false; Field::ListIterator fieldsIt(flist->constBegin()); // QString s_val; // s_val.reserve(4096); m_sql.clear(); QList::ConstIterator it = values.constBegin(); // int i=0; while (fieldsIt != flist->constEnd() && (it != values.end())) { Field *f = *fieldsIt; if (m_sql.isEmpty()) m_sql = QLatin1String("INSERT INTO ") + escapeIdentifier(tableSchema.name()) + QLatin1String(" VALUES ("); else m_sql += ","; m_sql += m_driver->valueToSQL(f, *it); // KexiDBDbg << "val" << i++ << ": " << m_driver->valueToSQL( f, *it ); ++it; ++fieldsIt; } m_sql += ")"; // KexiDBDbg<<"******** "<< m_sql; if (!drv_beforeInsert(tableSchema.name(), tableSchema)) return false; bool res = executeSQL(m_sql); if (!drv_afterInsert(tableSchema.name(), tableSchema)) return false; return res; } bool Connection::insertRecord(FieldList& fields, const QList& values) { // Each SQL identifier needs to be escaped in the generated query. const Field::List *flist = fields.fields(); if (flist->isEmpty()) return false; Field::ListIterator fieldsIt(flist->constBegin()); // QString s_val; // s_val.reserve(4096); m_sql.clear(); QList::ConstIterator it = values.constBegin(); // int i=0; QString tableName = escapeIdentifier(flist->first()->table()->name()); while (fieldsIt != flist->constEnd() && it != values.constEnd()) { Field *f = *fieldsIt; if (m_sql.isEmpty()) m_sql = QLatin1String("INSERT INTO ") + tableName + "(" + fields.sqlFieldsList(m_driver) + ") VALUES ("; else m_sql += ","; m_sql += m_driver->valueToSQL(f, *it); // KexiDBDbg << "val" << i++ << ": " << m_driver->valueToSQL( f, *it ); ++it; ++fieldsIt; } m_sql += ")"; if (!drv_beforeInsert(tableName, fields)) return false; bool res = executeSQL(m_sql); if (!drv_afterInsert(tableName, fields)) return false; return res; } bool Connection::executeSQL(const QString& statement) { m_sql = statement; //remember for error handling if (!drv_executeSQL(m_sql)) { m_errMsg.clear(); //clear as this could be most probably jsut "Unknown error" string. m_errorSql = statement; setError(this, ERR_SQL_EXECUTION_ERROR, i18n("Error while executing SQL statement.")); return false; } return true; } QString Connection::selectStatement(KexiDB::QuerySchema& querySchema, const QList& params, const SelectStatementOptions& options) const { return KexiDB::selectStatement(driver(), querySchema, params, options); } // static QString KexiDB::selectStatement(const KexiDB::Driver *driver, KexiDB::QuerySchema& querySchema, const QList& params, const KexiDB::Connection::SelectStatementOptions& options) { //"SELECT FROM ..." is theoretically allowed " //if (querySchema.fieldCount()<1) // return QString(); // Each SQL identifier needs to be escaped in the generated query. if (!querySchema.statement().isEmpty()) return querySchema.statement(); //! @todo looking at singleTable is visually nice but a field name can conflict //! with function or variable name... uint number = 0; bool singleTable = querySchema.tables()->count() <= 1; if (singleTable) { //make sure we will have single table: foreach(Field *f, *querySchema.fields()) { if (querySchema.isColumnVisible(number) && f->table() && f->table()->lookupFieldSchema(*f)) { //uups, no, there's at least one left join singleTable = false; break; } number++; } } QString sql; //final sql string sql.reserve(4096); //unused QString s_from_additional; //additional tables list needed for lookup fields QString s_additional_joins; //additional joins needed for lookup fields QString s_additional_fields; //additional fields to append to the fields list uint internalUniqueTableAliasNumber = 0; //used to build internalUniqueTableAliases uint internalUniqueQueryAliasNumber = 0; //used to build internalUniqueQueryAliases number = 0; QList subqueries_for_lookup_data; // subqueries will be added to FROM section QString kexidb_subquery_prefix("__kexidb_subquery_"); foreach(Field *f, *querySchema.fields()) { if (querySchema.isColumnVisible(number)) { if (!sql.isEmpty()) sql += QLatin1String(", "); if (f->isQueryAsterisk()) { if (!singleTable && static_cast(f)->isSingleTableAsterisk()) //single-table * sql += escapeIdentifier(f->table()->name(), options.identifierEscaping) + QLatin1String(".*"); else //all-tables * (or simplified table.* when there's only one table) sql += QLatin1String("*"); } else { if (f->isExpression()) { sql += f->expression()->toString(); } else { if (!f->table()) //sanity check return QString(); QString tableName; int tablePosition = querySchema.tableBoundToColumn(number); if (tablePosition >= 0) tableName = querySchema.tableAlias(tablePosition); if (tableName.isEmpty()) tableName = f->table()->name(); if (!singleTable) { sql += (escapeIdentifier(tableName, options.identifierEscaping) + "."); } sql += escapeIdentifier(f->name(), options.identifierEscaping); } QString aliasString( querySchema.columnAlias(number) ); if (!aliasString.isEmpty()) sql += (QLatin1String(" AS ") + aliasString); //! @todo add option that allows to omit "AS" keyword } LookupFieldSchema *lookupFieldSchema = (options.addVisibleLookupColumns && f->table()) ? f->table()->lookupFieldSchema(*f) : 0; if (lookupFieldSchema && lookupFieldSchema->boundColumn() >= 0) { // Lookup field schema found // Now we also need to fetch "visible" value from the lookup table, not only the value of binding. // -> build LEFT OUTER JOIN clause for this purpose (LEFT, not INNER because the binding can be broken) // "LEFT OUTER JOIN lookupTable ON thisTable.thisField=lookupTable.boundField" LookupFieldSchema::RowSource& rowSource = lookupFieldSchema->rowSource(); if (rowSource.type() == LookupFieldSchema::RowSource::Table) { TableSchema *lookupTable = querySchema.connection()->tableSchema(rowSource.name()); FieldList* visibleColumns = 0; Field *boundField = 0; if (lookupTable && (uint)lookupFieldSchema->boundColumn() < lookupTable->fieldCount() && (visibleColumns = lookupTable->subList(lookupFieldSchema->visibleColumns())) && (boundField = lookupTable->field(lookupFieldSchema->boundColumn()))) { //add LEFT OUTER JOIN if (!s_additional_joins.isEmpty()) s_additional_joins += QLatin1String(" "); QString internalUniqueTableAlias(QLatin1String("__kexidb_") + lookupTable->name() + "_" + QString::number(internalUniqueTableAliasNumber++)); s_additional_joins += QString::fromLatin1("LEFT OUTER JOIN %1 AS %2 ON %3.%4=%5.%6") .arg(escapeIdentifier(lookupTable->name(), options.identifierEscaping)) .arg(internalUniqueTableAlias) .arg(escapeIdentifier(f->table()->name(), options.identifierEscaping)) .arg(escapeIdentifier(f->name(), options.identifierEscaping)) .arg(internalUniqueTableAlias) .arg(escapeIdentifier(boundField->name(), options.identifierEscaping)); //add visibleField to the list of SELECTed fields //if it is not yet present there //not needed if (!querySchema.findTableField( visibleField->table()->name()+"."+visibleField->name() )) { #if 0 if (!querySchema.table(visibleField->table()->name())) { /* not true //table should be added after FROM if (!s_from_additional.isEmpty()) s_from_additional += QString::fromLatin1(", "); s_from_additional += escapeIdentifier(visibleField->table()->name(), options.identifierEscaping); */ } #endif if (!s_additional_fields.isEmpty()) s_additional_fields += QLatin1String(", "); // s_additional_fields += (internalUniqueTableAlias + "." //escapeIdentifier(visibleField->table()->name(), options.identifierEscaping) + "." // escapeIdentifier(visibleField->name(), options.identifierEscaping)); //! @todo Add lookup schema option for separator other than ' ' or even option for placeholders like "Name ? ?" //! @todo Add possibility for joining the values at client side. s_additional_fields += visibleColumns->sqlFieldsList( driver, " || ' ' || ", internalUniqueTableAlias, options.identifierEscaping); } delete visibleColumns; } else if (rowSource.type() == LookupFieldSchema::RowSource::Query) { QuerySchema *lookupQuery = querySchema.connection()->querySchema(rowSource.name()); if (!lookupQuery) { KexiDBWarn << "Connection::selectStatement(): !lookupQuery"; return QString(); } const QueryColumnInfo::Vector fieldsExpanded(lookupQuery->fieldsExpanded()); if (lookupFieldSchema->boundColumn() >= fieldsExpanded.count()) { KexiDBWarn << "Connection::selectStatement(): (uint)lookupFieldSchema->boundColumn() >= fieldsExpanded.count()"; return QString(); } QueryColumnInfo *boundColumnInfo = fieldsExpanded.at(lookupFieldSchema->boundColumn()); if (!boundColumnInfo) { KexiDBWarn << "Connection::selectStatement(): !boundColumnInfo"; return QString(); } Field *boundField = boundColumnInfo->field; if (!boundField) { KexiDBWarn << "Connection::selectStatement(): !boundField"; return QString(); } //add LEFT OUTER JOIN if (!s_additional_joins.isEmpty()) s_additional_joins += QLatin1String(" "); QString internalUniqueQueryAlias( kexidb_subquery_prefix + lookupQuery->name() + "_" + QString::number(internalUniqueQueryAliasNumber++)); s_additional_joins += QString::fromLatin1("LEFT OUTER JOIN (%1) AS %2 ON %3.%4=%5.%6") .arg(KexiDB::selectStatement(driver, *lookupQuery, params, options)) .arg(internalUniqueQueryAlias) .arg(KexiDB::escapeIdentifier(driver, f->table()->name(), options.identifierEscaping)) .arg(KexiDB::escapeIdentifier(driver, f->name(), options.identifierEscaping)) .arg(internalUniqueQueryAlias) .arg(QString(KexiDB::escapeIdentifier(driver, boundColumnInfo->aliasOrName(), options.identifierEscaping))); if (!s_additional_fields.isEmpty()) s_additional_fields += QLatin1String(", "); const QList visibleColumns(lookupFieldSchema->visibleColumns()); QString expression; foreach(uint visibleColumnIndex, visibleColumns) { //! @todo Add lookup schema option for separator other than ' ' or even option for placeholders like "Name ? ?" //! @todo Add possibility for joining the values at client side. if ((uint)fieldsExpanded.count() <= visibleColumnIndex) { KexiDBWarn << "Connection::selectStatement(): fieldsExpanded.count() <= (*visibleColumnsIt) : " << fieldsExpanded.count() << " <= " << visibleColumnIndex; return QString(); } if (!expression.isEmpty()) expression += " || ' ' || "; expression += (internalUniqueQueryAlias + "." + escapeIdentifier(fieldsExpanded.value(visibleColumnIndex)->aliasOrName(), options.identifierEscaping)); } s_additional_fields += expression; //subqueries_for_lookup_data.append(lookupQuery); } else { KexiDBWarn << "Connection::selectStatement(): unsupported record source type " << rowSource.typeName(); return QString(); } } } number++; } //add lookup fields if (!s_additional_fields.isEmpty()) sql += (QLatin1String(", ") + s_additional_fields); if (driver && options.alsoRetrieveROWID) { //append rowid column QString s; if (!sql.isEmpty()) s = QLatin1String(", "); if (querySchema.masterTable()) s += (escapeIdentifier(querySchema.masterTable()->name()) + "."); s += driver->behaviour()->ROW_ID_FIELD_NAME; sql += s; } sql.prepend("SELECT "); TableSchema::List* tables = querySchema.tables(); if ((tables && !tables->isEmpty()) || !subqueries_for_lookup_data.isEmpty()) { sql += QLatin1String(" FROM "); QString s_from; if (tables) { number = 0; foreach(TableSchema *table, *tables) { if (!s_from.isEmpty()) s_from += QLatin1String(", "); s_from += escapeIdentifier(table->name(), options.identifierEscaping); QString aliasString = QString(querySchema.tableAlias(number)); if (!aliasString.isEmpty()) s_from += (QLatin1String(" AS ") + aliasString); number++; } /*unused if (!s_from_additional.isEmpty()) {//additional tables list needed for lookup fields if (!s_from.isEmpty()) s_from += QLatin1String(", "); s_from += s_from_additional; }*/ } // add subqueries for lookup data uint subqueries_for_lookup_data_counter = 0; foreach(QuerySchema* subQuery, subqueries_for_lookup_data) { if (!s_from.isEmpty()) s_from += QLatin1String(", "); s_from += QLatin1String("("); s_from += selectStatement(driver, *subQuery, params, options); s_from += QString::fromLatin1(") AS %1%2") .arg(kexidb_subquery_prefix).arg(subqueries_for_lookup_data_counter++); } sql += s_from; } QString s_where; s_where.reserve(4096); //JOINS if (!s_additional_joins.isEmpty()) { sql += QLatin1String(" ") + s_additional_joins + QLatin1String(" "); } //@todo: we're using WHERE for joins now; use INNER/LEFT/RIGHT JOIN later //WHERE bool wasWhere = false; //for later use foreach(Relationship *rel, *querySchema.relationships()) { if (s_where.isEmpty()) { wasWhere = true; } else s_where += QLatin1String(" AND "); QString s_where_sub; foreach(Field::Pair pair, *rel->fieldPairs()) { if (!s_where_sub.isEmpty()) s_where_sub += QLatin1String(" AND "); s_where_sub += ( escapeIdentifier(pair.first->table()->name(), options.identifierEscaping) + QLatin1String(".") + escapeIdentifier(pair.first->name(), options.identifierEscaping) + QLatin1String(" = ") + escapeIdentifier(pair.second->table()->name(), options.identifierEscaping) + QLatin1String(".") + escapeIdentifier(pair.second->name(), options.identifierEscaping)); } if (rel->fieldPairs()->count() > 1) { s_where_sub.prepend("("); s_where_sub += QLatin1String(")"); } s_where += s_where_sub; } //EXPLICITLY SPECIFIED WHERE EXPRESSION if (querySchema.whereExpression()) { QuerySchemaParameterValueListIterator paramValuesIt(driver, params); QuerySchemaParameterValueListIterator *paramValuesItPtr = params.isEmpty() ? 0 : ¶mValuesIt; if (wasWhere) { //TODO: () are not always needed s_where = "(" + s_where + ") AND (" + querySchema.whereExpression()->toString(paramValuesItPtr) + ")"; } else { s_where = querySchema.whereExpression()->toString(paramValuesItPtr); } } if (!s_where.isEmpty()) sql += QLatin1String(" WHERE ") + s_where; //! \todo (js) add other sql parts //(use wasWhere here) // ORDER BY QString orderByString( querySchema.orderByColumnList().toSQLString(!singleTable/*includeTableName*/, driver, options.identifierEscaping)); const QVector pkeyFieldsOrder(querySchema.pkeyFieldsOrder()); if (orderByString.isEmpty() && !pkeyFieldsOrder.isEmpty()) { //add automatic ORDER BY if there is no explicitly defined (especially helps when there are complex JOINs) OrderByColumnList automaticPKOrderBy; const QueryColumnInfo::Vector fieldsExpanded(querySchema.fieldsExpanded()); foreach(int pkeyFieldsIndex, pkeyFieldsOrder) { if (pkeyFieldsIndex < 0) // no field mentioned in this query continue; if (pkeyFieldsIndex >= (int)fieldsExpanded.count()) { KexiDBWarn << "Connection::selectStatement(): ORDER BY: (*it) >= fieldsExpanded.count() - " << pkeyFieldsIndex << " >= " << fieldsExpanded.count(); continue; } QueryColumnInfo *ci = fieldsExpanded[ pkeyFieldsIndex ]; automaticPKOrderBy.appendColumn(*ci); } orderByString = automaticPKOrderBy.toSQLString(!singleTable/*includeTableName*/, driver, options.identifierEscaping); } if (!orderByString.isEmpty()) sql += (" ORDER BY " + orderByString); //KexiDBDbg << sql; return sql; } QString Connection::selectStatement(KexiDB::TableSchema& tableSchema, const SelectStatementOptions& options) const { return selectStatement(*tableSchema.query(), options); } Field* Connection::findSystemFieldName(const KexiDB::FieldList& fieldlist) { for (Field::ListIterator it(fieldlist.fieldsIterator()); it != fieldlist.fieldsIteratorConstEnd(); ++it) { if (m_driver->isSystemFieldName((*it)->name())) return *it; } return 0; } quint64 Connection::lastInsertedAutoIncValue(const QString& aiFieldName, const QString& tableName, quint64* ROWID) { quint64 row_id = drv_lastInsertRowID(); if (ROWID) *ROWID = row_id; if (m_driver->beh->ROW_ID_FIELD_RETURNS_LAST_AUTOINCREMENTED_VALUE) { return row_id; } RecordData rdata; if (row_id <= 0 || true != querySingleRecord( QLatin1String("SELECT ") + tableName + QLatin1String(".") + aiFieldName + QLatin1String(" FROM ") + tableName + QLatin1String(" WHERE ") + m_driver->beh->ROW_ID_FIELD_NAME + QLatin1String("=") + QString::number(row_id), rdata)) { // KexiDBDbg << "Connection::lastInsertedAutoIncValue(): row_id<=0 || true!=querySingleRecord()"; return (quint64) - 1; //ULL; } return rdata[0].toULongLong(); } quint64 Connection::lastInsertedAutoIncValue(const QString& aiFieldName, const KexiDB::TableSchema& table, quint64* ROWID) { return lastInsertedAutoIncValue(aiFieldName, table.name(), ROWID); } //! Creates a Field list for kexi__fields, for sanity. Used by createTable() static FieldList* createFieldListForKexi__Fields(TableSchema *kexi__fieldsSchema) { if (!kexi__fieldsSchema) return 0; return kexi__fieldsSchema->subList( "t_id", "f_type", "f_name", "f_length", "f_precision", "f_constraints", "f_options", "f_default", "f_order", "f_caption", "f_help" ); } static QVariant buildLengthValue(const Field& f) { if (f.isFPNumericType()) { return f.scale(); } return f.maxLength(); } //! builds a list of values for field's \a f properties. Used by createTable(). void buildValuesForKexi__Fields(QList& vals, Field* f) { vals.clear(); vals << QVariant(f->table()->id()) << QVariant(f->type()) << QVariant(f->name()) << buildLengthValue(*f) << QVariant(f->isFPNumericType() ? f->precision() : 0) << QVariant(f->constraints()) << QVariant(f->options()) // KexiDB::variantToString() is needed here because the value can be of any QVariant type, // depending on f->type() << (f->defaultValue().isNull() ? QVariant() : QVariant(KexiDB::variantToString(f->defaultValue()))) << QVariant(f->order()) << QVariant(f->caption()) << QVariant(f->description()); } bool Connection::storeMainFieldSchema(Field *field) { if (!field || !field->table()) return false; FieldList *fl = createFieldListForKexi__Fields(d->table("kexi__fields")); if (!fl) return false; QList vals; buildValuesForKexi__Fields(vals, field); QList::ConstIterator valsIt = vals.constBegin(); bool first = true; QString sql = "UPDATE kexi__fields SET "; foreach(Field *f, *fl->fields()) { sql.append((first ? QString() : QLatin1String(", ")) + f->name() + "=" + m_driver->valueToSQL(f, *valsIt)); if (first) first = false; ++valsIt; } delete fl; sql.append(QLatin1String(" WHERE t_id=") + QString::number(field->table()->id()) + " AND f_name=" + m_driver->valueToSQL(Field::Text, field->name())); return executeSQL(sql); } #define createTable_ERR \ { KexiDBDbg << "Connection::createTable(): ERROR!"; \ setError(this, i18n("Creating table failed.")); \ rollbackAutoCommitTransaction(tg.transaction()); \ return false; } //setError( errorNum(), i18n("Creating table failed.") + " " + errorMsg()); //! Creates a table according to the given schema /*! Creates a table according to the given TableSchema, adding the table and column definitions to kexi__* tables. Checks that a database is in use, that the table name is not that of a system table, and that the schema defines at least one column. If the table exists, and replaceExisting is true, the table is replaced. Otherwise, the table is not replaced. */ bool Connection::createTable(KexiDB::TableSchema* tableSchema, bool replaceExisting) { if (!tableSchema || !checkIsDatabaseUsed()) return false; //check if there are any fields if (tableSchema->fieldCount() < 1) { clearError(); setError(ERR_CANNOT_CREATE_EMPTY_OBJECT, i18n("Cannot create table without fields.")); return false; } const bool internalTable = dynamic_cast(tableSchema); const QString tableName = tableSchema->name(); if (!internalTable) { if (m_driver->isSystemObjectName(tableName)) { clearError(); setError(ERR_SYSTEM_NAME_RESERVED, i18n("System name \"%1\" cannot be used as table name.", tableSchema->name())); return false; } Field *sys_field = findSystemFieldName(*tableSchema); if (sys_field) { clearError(); setError(ERR_SYSTEM_NAME_RESERVED, i18n("System name \"%1\" cannot be used as one of fields in \"%2\" table.", sys_field->name(), tableName)); return false; } } bool previousSchemaStillKept = false; KexiDB::TableSchema *existingTable = 0; if (replaceExisting) { //get previous table (do not retrieve, though) existingTable = d->table(tableName); if (existingTable) { if (existingTable == tableSchema) { clearError(); setError(ERR_OBJECT_EXISTS, i18n("Could not create the same table \"%1\" twice.", tableSchema->name())); return false; } //TODO(js): update any structure (e.g. queries) that depend on this table! if (existingTable->id() > 0) tableSchema->m_id = existingTable->id(); //copy id from existing table previousSchemaStillKept = true; if (!dropTable(existingTable, false /*alsoRemoveSchema*/)) return false; } } else { if (this->tableSchema(tableSchema->name()) != 0) { clearError(); setError(ERR_OBJECT_EXISTS, i18n("Table \"%1\" already exists.", tableSchema->name())); return false; } } /* if (replaceExisting) { //get previous table (do not retrieve, though) KexiDB::TableSchema *existingTable = d->tables_byname.take(name); if (oldTable) { }*/ TransactionGuard tg; if (!beginAutoCommitTransaction(tg)) return false; if (!drv_createTable(*tableSchema)) createTable_ERR; //add schema data to kexi__* tables if (!internalTable) { //update kexi__objects if (!storeObjectSchemaData(*tableSchema, true)) createTable_ERR; TableSchema *ts = d->table("kexi__fields"); if (!ts) return false; //for sanity: remove field info (if any) for this table id if (!KexiDB::deleteRow(*this, ts, "t_id", tableSchema->id())) return false; FieldList *fl = createFieldListForKexi__Fields(d->table("kexi__fields")); if (!fl) return false; foreach(Field *f, *tableSchema->fields()) { QList vals; buildValuesForKexi__Fields(vals, f); if (!insertRecord(*fl, vals)) createTable_ERR; } delete fl; if (!storeExtendedTableSchemaData(*tableSchema)) createTable_ERR; } //finally: /* if (replaceExisting) { if (existingTable) { d->tables.take(existingTable->id()); delete existingTable; } }*/ bool res = commitAutoCommitTransaction(tg.transaction()); if (res) { if (internalTable) { //insert the internal table into structures insertInternalTable(*tableSchema); } else { if (previousSchemaStillKept) { //remove previous table schema d->removeTable(*tableSchema); } //store one schema object locally: d->insertTable(*tableSchema); } //ok, this table is not created by the connection tableSchema->m_conn = this; } return res; } bool Connection::removeObject(uint objId) { clearError(); //remove table schema from kexi__* tables if (!KexiDB::deleteRow(*this, d->table("kexi__objects"), "o_id", objId) //schema entry || !KexiDB::deleteRow(*this, d->table("kexi__objectdata"), "o_id", objId)) {//data blocks setError(ERR_DELETE_SERVER_ERROR, i18n("Could not remove object's data.")); return false; } return true; } bool Connection::drv_dropTable(const QString& name) { m_sql = "DROP TABLE " + escapeIdentifier(name); return executeSQL(m_sql); } //! Drops a table corresponding to the name in the given schema /*! Drops a table according to the name given by the TableSchema, removing the table and column definitions to kexi__* tables. Checks first that the table is not a system table. TODO: Should check that a database is currently in use? (c.f. createTable) */ tristate Connection::dropTable(KexiDB::TableSchema* tableSchema) { return dropTable(tableSchema, true); } tristate Connection::dropTable(KexiDB::TableSchema* tableSchema, bool alsoRemoveSchema) { // Each SQL identifier needs to be escaped in the generated query. clearError(); if (!tableSchema) return false; KLocalizedString errmsg = ki18n("Table \"%1\" cannot be removed.\n"); //be sure that we handle the correct TableSchema object: if (tableSchema->id() < 0 || this->tableSchema(tableSchema->name()) != tableSchema || this->tableSchema(tableSchema->id()) != tableSchema) { setError(ERR_OBJECT_NOT_FOUND, errmsg.subs(tableSchema->name()).toString() + i18n("Unexpected name or identifier.")); return false; } tristate res = closeAllTableSchemaChangeListeners(*tableSchema); if (true != res) return res; //sanity checks: if (m_driver->isSystemObjectName(tableSchema->name())) { setError(ERR_SYSTEM_NAME_RESERVED, errmsg.subs(tableSchema->name()).toString() + d->strItIsASystemObject()); return false; } TransactionGuard tg; if (!beginAutoCommitTransaction(tg)) return false; //for sanity we're checking if this table exists physically if (drv_containsTable(tableSchema->name())) { if (!drv_dropTable(tableSchema->name())) return false; } TableSchema *ts = d->table("kexi__fields"); if (!KexiDB::deleteRow(*this, ts, "t_id", tableSchema->id())) //field entries return false; //remove table schema from kexi__objects table if (!removeObject(tableSchema->id())) { return false; } if (alsoRemoveSchema) { //! \todo js: update any structure (e.g. queries) that depend on this table! tristate res = removeDataBlock(tableSchema->id(), "extended_schema"); if (!res) return false; d->removeTable(*tableSchema); } return commitAutoCommitTransaction(tg.transaction()); } tristate Connection::dropTable(const QString& table) { clearError(); TableSchema* ts = tableSchema(table); if (!ts) { setError(ERR_OBJECT_NOT_FOUND, i18n("Table \"%1\" does not exist.", table)); return false; } return dropTable(ts); } tristate Connection::alterTable(TableSchema& tableSchema, TableSchema& newTableSchema) { clearError(); tristate res = closeAllTableSchemaChangeListeners(tableSchema); if (true != res) return res; if (&tableSchema == &newTableSchema) { setError(ERR_OBJECT_THE_SAME, i18n("Could not alter table \"%1\" using the same table.", tableSchema.name())); return false; } //TODO(js): implement real altering //TODO(js): update any structure (e.g. query) that depend on this table! bool ok, empty; #if 0//TODO ucomment: empty = isEmpty(tableSchema, ok) && ok; #else empty = true; #endif if (empty) { ok = createTable(&newTableSchema, true/*replace*/); } return ok; } bool Connection::alterTableName(TableSchema& tableSchema, const QString& newName, bool replace) { clearError(); if (&tableSchema != d->table(tableSchema.id())) { setError(ERR_OBJECT_NOT_FOUND, i18n("Unknown table \"%1\"", tableSchema.name())); return false; } if (newName.isEmpty() || !KexiDB::isIdentifier(newName)) { setError(ERR_INVALID_IDENTIFIER, i18n("Invalid table name \"%1\"", newName)); return false; } const QString oldTableName = tableSchema.name(); const QString newTableName = newName.trimmed(); if (oldTableName.trimmed() == newTableName) { setError(ERR_OBJECT_THE_SAME, i18n("Could not rename table \"%1\" using the same name.", newTableName)); return false; } //TODO: alter table name for server DB backends! //TODO: what about objects (queries/forms) that use old name? //TODO TableSchema *tableToReplace = this->tableSchema(newName); const bool destTableExists = tableToReplace != 0; const int origID = destTableExists ? tableToReplace->id() : -1; //will be reused in the new table if (!replace && destTableExists) { setError(ERR_OBJECT_EXISTS, i18n("Could not rename table \"%1\" to \"%2\". Table \"%3\" already exists.", tableSchema.name(), newName, newName)); return false; } //helper: #define alterTableName_ERR \ tableSchema.setName(oldTableName) //restore old name TransactionGuard tg; if (!beginAutoCommitTransaction(tg)) return false; // drop the table replaced (with schema) if (destTableExists) { if (!replace) { return false; } if (!dropTable(newName)) { return false; } // the new table owns the previous table's id: if (!executeSQL( QString::fromLatin1("UPDATE kexi__objects SET o_id=%1 WHERE o_id=%2 AND o_type=%3") .arg(origID).arg(tableSchema.id()).arg((int)TableObjectType))) { return false; } if (!executeSQL(QString::fromLatin1("UPDATE kexi__fields SET t_id=%1 WHERE t_id=%2") .arg(origID).arg(tableSchema.id()))) { return false; } //maintain table ID d->changeTableId(tableSchema, origID); tableSchema.m_id = origID; } if (!drv_alterTableName(tableSchema, newTableName)) { alterTableName_ERR; return false; } // Update kexi__objects //TODO if (!executeSQL(QString::fromLatin1("UPDATE kexi__objects SET o_name=%1 WHERE o_id=%2") .arg(m_driver->escapeString(tableSchema.name())).arg(tableSchema.id()))) { alterTableName_ERR; return false; } //TODO what about caption? //restore old name: it will be changed soon! tableSchema.setName(oldTableName); if (!commitAutoCommitTransaction(tg.transaction())) { alterTableName_ERR; return false; } //update tableSchema: d->renameTable(tableSchema, newTableName); return true; } bool Connection::drv_alterTableName(TableSchema& tableSchema, const QString& newName) { const QString oldTableName = tableSchema.name(); tableSchema.setName(newName); if (!executeSQL(QString::fromLatin1("ALTER TABLE %1 RENAME TO %2") .arg(escapeIdentifier(oldTableName)).arg(escapeIdentifier(newName)))) { tableSchema.setName(oldTableName); //restore old name return false; } return true; } bool Connection::dropQuery(KexiDB::QuerySchema* querySchema) { clearError(); if (!querySchema) return false; TransactionGuard tg; if (!beginAutoCommitTransaction(tg)) return false; /* TableSchema *ts = d->tables_byname["kexi__querydata"]; if (!KexiDB::deleteRow(*this, ts, "q_id", querySchema->id())) return false; ts = d->tables_byname["kexi__queryfields"]; if (!KexiDB::deleteRow(*this, ts, "q_id", querySchema->id())) return false; ts = d->tables_byname["kexi__querytables"]; if (!KexiDB::deleteRow(*this, ts, "q_id", querySchema->id())) return false;*/ //remove query schema from kexi__objects table if (!removeObject(querySchema->id())) { return false; } //TODO(js): update any structure that depend on this table! d->removeQuery(*querySchema); return commitAutoCommitTransaction(tg.transaction()); } bool Connection::dropQuery(const QString& query) { clearError(); QuerySchema* qs = querySchema(query); if (!qs) { setError(ERR_OBJECT_NOT_FOUND, i18n("Query \"%1\" does not exist.", query)); return false; } return dropQuery(qs); } bool Connection::drv_createTable(const KexiDB::TableSchema& tableSchema) { m_sql = createTableStatement(tableSchema); KexiDBDbg << "******** " << m_sql; return executeSQL(m_sql); } bool Connection::drv_createTable(const QString& tableSchemaName) { TableSchema *ts = d->table(tableSchemaName); if (!ts) return false; return drv_createTable(*ts); } bool Connection::beginAutoCommitTransaction(TransactionGuard &tg) { if ((m_driver->d->features & Driver::IgnoreTransactions) || !d->autoCommit) { tg.setTransaction(Transaction()); return true; } // commit current transaction (if present) for drivers // that allow single transaction per connection if (m_driver->d->features & Driver::SingleTransactions) { if (d->default_trans_started_inside) //only commit internally started transaction if (!commitTransaction(d->default_trans, true)) { tg.setTransaction(Transaction()); return false; //we have a real error } d->default_trans_started_inside = d->default_trans.isNull(); if (!d->default_trans_started_inside) { tg.setTransaction(d->default_trans); tg.doNothing(); return true; //reuse externally started transaction } } else if (!(m_driver->d->features & Driver::MultipleTransactions)) { tg.setTransaction(Transaction()); return true; //no trans. supported at all - just return } tg.setTransaction(beginTransaction()); return !error(); } bool Connection::commitAutoCommitTransaction(const Transaction& trans) { if (m_driver->d->features & Driver::IgnoreTransactions) return true; if (trans.isNull() || !m_driver->transactionsSupported()) return true; if (m_driver->d->features & Driver::SingleTransactions) { if (!d->default_trans_started_inside) //only commit internally started transaction return true; //give up } return commitTransaction(trans, true); } bool Connection::rollbackAutoCommitTransaction(const Transaction& trans) { if (trans.isNull() || !m_driver->transactionsSupported()) return true; return rollbackTransaction(trans); } #define SET_ERR_TRANS_NOT_SUPP \ { setError(ERR_UNSUPPORTED_DRV_FEATURE, \ i18n("Transactions are not supported for \"%1\" driver.", m_driver->name() )); } #define SET_BEGIN_TR_ERROR \ { if (!error()) \ setError(ERR_ROLLBACK_OR_COMMIT_TRANSACTION, i18n("Begin transaction failed")); } Transaction Connection::beginTransaction() { if (!checkIsDatabaseUsed()) return Transaction(); Transaction trans; if (m_driver->d->features & Driver::IgnoreTransactions) { //we're creating dummy transaction data here, //so it will look like active trans.m_data = new TransactionData(this); d->transactions.append(trans); return trans; } if (m_driver->d->features & Driver::SingleTransactions) { if (d->default_trans.active()) { setError(ERR_TRANSACTION_ACTIVE, i18n("Transaction already started.")); return Transaction(); } if (!(trans.m_data = drv_beginTransaction())) { SET_BEGIN_TR_ERROR; return Transaction(); } d->default_trans = trans; d->transactions.append(trans); return d->default_trans; } if (m_driver->d->features & Driver::MultipleTransactions) { if (!(trans.m_data = drv_beginTransaction())) { SET_BEGIN_TR_ERROR; return Transaction(); } d->transactions.append(trans); return trans; } SET_ERR_TRANS_NOT_SUPP; return Transaction(); } bool Connection::commitTransaction(const Transaction trans, bool ignore_inactive) { if (!isDatabaseUsed()) return false; // if (!checkIsDatabaseUsed()) //return false; if (!m_driver->transactionsSupported() && !(m_driver->d->features & Driver::IgnoreTransactions)) { SET_ERR_TRANS_NOT_SUPP; return false; } Transaction t = trans; if (!t.active()) { //try default tr. if (!d->default_trans.active()) { if (ignore_inactive) return true; clearError(); setError(ERR_NO_TRANSACTION_ACTIVE, i18n("Transaction not started.")); return false; } t = d->default_trans; d->default_trans = Transaction(); //now: no default tr. } bool ret = true; if (!(m_driver->d->features & Driver::IgnoreTransactions)) ret = drv_commitTransaction(t.m_data); if (t.m_data) t.m_data->m_active = false; //now this transaction if inactive if (!d->dont_remove_transactions) //true=transaction obj will be later removed from list d->transactions.removeAt(d->transactions.indexOf(t)); if (!ret && !error()) setError(ERR_ROLLBACK_OR_COMMIT_TRANSACTION, i18n("Error on commit transaction")); return ret; } bool Connection::rollbackTransaction(const Transaction trans, bool ignore_inactive) { if (!isDatabaseUsed()) return false; // if (!checkIsDatabaseUsed()) // return false; if (!m_driver->transactionsSupported() && !(m_driver->d->features & Driver::IgnoreTransactions)) { SET_ERR_TRANS_NOT_SUPP; return false; } Transaction t = trans; if (!t.active()) { //try default tr. if (!d->default_trans.active()) { if (ignore_inactive) return true; clearError(); setError(ERR_NO_TRANSACTION_ACTIVE, i18n("Transaction not started.")); return false; } t = d->default_trans; d->default_trans = Transaction(); //now: no default tr. } bool ret = true; if (!(m_driver->d->features & Driver::IgnoreTransactions)) ret = drv_rollbackTransaction(t.m_data); if (t.m_data) t.m_data->m_active = false; //now this transaction if inactive if (!d->dont_remove_transactions) //true=transaction obj will be later removed from list d->transactions.removeAt(d->transactions.indexOf(t)); if (!ret && !error()) setError(ERR_ROLLBACK_OR_COMMIT_TRANSACTION, i18n("Error on rollback transaction")); return ret; } #undef SET_ERR_TRANS_NOT_SUPP #undef SET_BEGIN_TR_ERROR /*bool Connection::duringTransaction() { return drv_duringTransaction(); }*/ Transaction& Connection::defaultTransaction() const { return d->default_trans; } void Connection::setDefaultTransaction(const Transaction& trans) { if (!isDatabaseUsed()) return; // if (!checkIsDatabaseUsed()) // return; if (!(m_driver->d->features & Driver::IgnoreTransactions) && (!trans.active() || !m_driver->transactionsSupported())) { return; } d->default_trans = trans; } const QList& Connection::transactions() { return d->transactions; } bool Connection::autoCommit() const { return d->autoCommit; } bool Connection::setAutoCommit(bool on) { if (d->autoCommit == on || m_driver->d->features & Driver::IgnoreTransactions) return true; if (!drv_setAutoCommit(on)) return false; d->autoCommit = on; return true; } TransactionData* Connection::drv_beginTransaction() { QString old_sql = m_sql; //don't if (!executeSQL("BEGIN")) return 0; return new TransactionData(this); } bool Connection::drv_commitTransaction(TransactionData *) { return executeSQL("COMMIT"); } bool Connection::drv_rollbackTransaction(TransactionData *) { return executeSQL("ROLLBACK"); } bool Connection::drv_setAutoCommit(bool /*on*/) { return true; } Cursor* Connection::executeQuery(const QString& statement, uint cursor_options) { if (statement.isEmpty()) return 0; Cursor *c = prepareQuery(statement, cursor_options); if (!c) return 0; if (!c->open()) {//err - kill that setError(c); delete c; return 0; } return c; } Cursor* Connection::executeQuery(QuerySchema& query, const QList& params, uint cursor_options) { Cursor *c = prepareQuery(query, params, cursor_options); if (!c) return 0; if (!c->open()) {//err - kill that setError(c); delete c; return 0; } return c; } Cursor* Connection::executeQuery(QuerySchema& query, uint cursor_options) { return executeQuery(query, QList(), cursor_options); } Cursor* Connection::executeQuery(TableSchema& table, uint cursor_options) { return executeQuery(*table.query(), cursor_options); } Cursor* Connection::prepareQuery(TableSchema& table, uint cursor_options) { return prepareQuery(*table.query(), cursor_options); } Cursor* Connection::prepareQuery(QuerySchema& query, const QList& params, uint cursor_options) { Cursor* cursor = prepareQuery(query, cursor_options); if (cursor) cursor->setQueryParameters(params); return cursor; } bool Connection::deleteCursor(Cursor *cursor) { if (!cursor) return false; if (cursor->connection() != this) {//illegal call KexiDBWarn << "Connection::deleteCursor(): Cannot delete the cursor not owned by the same connection!"; return false; } const bool ret = cursor->close(); delete cursor; return ret; } bool Connection::setupObjectSchemaData(const RecordData &data, SchemaData &sdata) { //not found: retrieve schema /* KexiDB::Cursor *cursor; if (!(cursor = executeQuery( QString("select * from kexi__objects where o_id='%1'").arg(objId) ))) return false; if (!cursor->moveFirst()) { deleteCursor(cursor); return false; }*/ //if (!ok) { //deleteCursor(cursor); //return 0; // } if (data.count() < 5) { KexiDBWarn << "Aborting, schema data should have 5 elements, found" << data.count(); return false; } bool ok; sdata.m_id = data[0].toInt(&ok); if (!ok) { return false; } sdata.m_name = data[2].toString(); if (!KexiDB::isIdentifier(sdata.m_name)) { setError(ERR_INVALID_IDENTIFIER, i18n("Invalid object name \"%1\"", sdata.m_name)); return false; } sdata.m_caption = data[3].toString(); sdata.m_desc = data[4].toString(); // KexiDBDbg<<"@@@ Connection::setupObjectSchemaData() == " << sdata.schemaDataDebugString(); return true; } tristate Connection::loadObjectSchemaData(int objectID, SchemaData &sdata) { RecordData data; if (true != querySingleRecord(QString::fromLatin1( "SELECT o_id, o_type, o_name, o_caption, o_desc FROM kexi__objects WHERE o_id=%1") .arg(objectID), data)) return cancelled; return setupObjectSchemaData(data, sdata); } tristate Connection::loadObjectSchemaData(int objectType, const QString& objectName, SchemaData &sdata) { RecordData data; if (true != querySingleRecord(QString::fromLatin1("SELECT o_id, o_type, o_name, o_caption, o_desc " "FROM kexi__objects WHERE o_type=%1 AND lower(o_name)=%2") .arg(objectType).arg(m_driver->valueToSQL(Field::Text, objectName)), data)) return cancelled; return setupObjectSchemaData(data, sdata); } bool Connection::storeObjectSchemaData(SchemaData &sdata, bool newObject) { TableSchema *ts = d->table("kexi__objects"); if (!ts) return false; if (newObject) { int existingID; if (true == querySingleNumber(QString::fromLatin1( "SELECT o_id FROM kexi__objects WHERE o_type=%1 AND lower(o_name)=%2") .arg(sdata.type()).arg(m_driver->valueToSQL(Field::Text, sdata.name().toLower())), existingID)) { //we already have stored a schema data with the same name and type: //just update it's properties as it would be existing object sdata.m_id = existingID; newObject = false; } } if (newObject) { FieldList *fl; bool ok; if (sdata.id() <= 0) {//get new ID fl = ts->subList("o_type", "o_name", "o_caption", "o_desc"); ok = fl != 0; if (ok && !insertRecord(*fl, QVariant(sdata.type()), QVariant(sdata.name()), QVariant(sdata.caption()), QVariant(sdata.description()))) ok = false; delete fl; if (!ok) return false; //fetch newly assigned ID //! @todo safe to cast it? int obj_id = (int)lastInsertedAutoIncValue("o_id", *ts); KexiDBDbg << "######## NEW obj_id == " << obj_id; if (obj_id <= 0) return false; sdata.m_id = obj_id; return true; } else { fl = ts->subList("o_id", "o_type", "o_name", "o_caption", "o_desc"); ok = fl != 0; if (ok && !insertRecord(*fl, QVariant(sdata.id()), QVariant(sdata.type()), QVariant(sdata.name()), QVariant(sdata.caption()), QVariant(sdata.description()))) ok = false; delete fl; return ok; } } //existing object: return executeSQL( QString::fromLatin1("UPDATE kexi__objects SET o_type=%2, o_caption=%3, o_desc=%4 WHERE o_id=%1") .arg(sdata.id()).arg(sdata.type()) .arg(m_driver->valueToSQL(KexiDB::Field::Text, sdata.caption())) .arg(m_driver->valueToSQL(KexiDB::Field::Text, sdata.description()))); } tristate Connection::querySingleRecordInternal(RecordData &data, const QString* sql, QuerySchema* query, bool addLimitTo1) { Q_ASSERT(sql || query); //! @todo does not work with non-SQL data sources if (sql) m_sql = m_driver->addLimitTo1(*sql, addLimitTo1); KexiDB::Cursor *cursor; if (!(cursor = sql ? executeQuery(m_sql) : executeQuery(*query))) { KexiDBWarn << "Connection::querySingleRecord(): !executeQuery() " << m_sql; return false; } if (!cursor->moveFirst() || cursor->eof() || !cursor->storeCurrentRow(data)) { const tristate result = cursor->error() ? false : cancelled; KexiDBWarn << "Connection::querySingleRecord(): !cursor->moveFirst() || cursor->eof() || cursor->storeCurrentRow(data) m_sql=" << m_sql; setError(cursor); deleteCursor(cursor); return result; } return deleteCursor(cursor); } tristate Connection::querySingleRecord(const QString& sql, RecordData &data, bool addLimitTo1) { return querySingleRecordInternal(data, &sql, 0, addLimitTo1); } tristate Connection::querySingleRecord(QuerySchema& query, RecordData &data, bool addLimitTo1) { return querySingleRecordInternal(data, 0, &query, addLimitTo1); } bool Connection::checkIfColumnExists(Cursor *cursor, uint column) { if (column >= cursor->fieldCount()) { setError(ERR_CURSOR_RECORD_FETCHING, i18n("Column %1 does not exist for the query.", column)); return false; } return true; } tristate Connection::querySingleString(const QString& sql, QString &value, uint column, bool addLimitTo1) { KexiDB::Cursor *cursor; m_sql = m_driver->addLimitTo1(sql, addLimitTo1); if (!(cursor = executeQuery(m_sql))) { KexiDBWarn << "Connection::querySingleRecord(): !executeQuery() " << m_sql; return false; } if (!cursor->moveFirst() || cursor->eof()) { const tristate result = cursor->error() ? false : cancelled; KexiDBWarn << "Connection::querySingleRecord(): !cursor->moveFirst() || cursor->eof() " << m_sql; deleteCursor(cursor); return result; } if (!checkIfColumnExists(cursor, column)) { deleteCursor(cursor); return false; } value = cursor->value(column).toString(); return deleteCursor(cursor); } tristate Connection::querySingleNumber(const QString& sql, int &number, uint column, bool addLimitTo1) { static QString str; static bool ok; const tristate result = querySingleString(sql, str, column, addLimitTo1); if (result != true) return result; number = str.toInt(&ok); return ok; } bool Connection::queryStringList(const QString& sql, QStringList& list, uint column) { KexiDB::Cursor *cursor; clearError(); m_sql = sql; if (!(cursor = executeQuery(m_sql))) { KexiDBWarn << "Connection::queryStringList(): !executeQuery() " << m_sql; return false; } cursor->moveFirst(); if (cursor->error()) { setError(cursor); deleteCursor(cursor); return false; } if (!cursor->eof() && !checkIfColumnExists(cursor, column)) { deleteCursor(cursor); return false; } list.clear(); while (!cursor->eof()) { list.append(cursor->value(column).toString()); if (!cursor->moveNext() && cursor->error()) { setError(cursor); deleteCursor(cursor); return false; } } return deleteCursor(cursor); } bool Connection::resultExists(const QString& sql, bool &success, bool addLimitTo1) { KexiDB::Cursor *cursor; //optimization if (m_driver->beh->SELECT_1_SUBQUERY_SUPPORTED) { //this is at least for sqlite if (addLimitTo1 && sql.left(6).toUpper() == "SELECT") m_sql = m_driver->addLimitTo1(QString::fromLatin1("SELECT 1 FROM (") + sql + ")", addLimitTo1); else m_sql = sql; } else { if (addLimitTo1 && sql.left(6).toUpper() == "SELECT") m_sql = m_driver->addLimitTo1(sql, addLimitTo1); else m_sql = sql; } if (!(cursor = executeQuery(m_sql))) { KexiDBWarn << "Connection::querySingleRecord(): !executeQuery() " << m_sql; success = false; return false; } if (!cursor->moveFirst() || cursor->eof()) { success = !cursor->error(); KexiDBWarn << "Connection::querySingleRecord(): !cursor->moveFirst() || cursor->eof() " << m_sql; setError(cursor); deleteCursor(cursor); return false; } success = deleteCursor(cursor); return true; } bool Connection::isEmpty(TableSchema& table, bool &success) { return !resultExists(selectStatement(*table.query()), success); } //! Used by addFieldPropertyToExtendedTableSchemaData() static void createExtendedTableSchemaMainElementIfNeeded( QDomDocument& doc, QDomElement& extendedTableSchemaMainEl, bool& extendedTableSchemaStringIsEmpty) { if (!extendedTableSchemaStringIsEmpty) return; //init document extendedTableSchemaMainEl = doc.createElement("EXTENDED_TABLE_SCHEMA"); doc.appendChild(extendedTableSchemaMainEl); extendedTableSchemaMainEl.setAttribute("version", QString::number(KEXIDB_EXTENDED_TABLE_SCHEMA_VERSION)); extendedTableSchemaStringIsEmpty = false; } //! Used by addFieldPropertyToExtendedTableSchemaData() static void createExtendedTableSchemaFieldElementIfNeeded(QDomDocument& doc, QDomElement& extendedTableSchemaMainEl, const QString& fieldName, QDomElement& extendedTableSchemaFieldEl, bool append = true) { if (!extendedTableSchemaFieldEl.isNull()) return; extendedTableSchemaFieldEl = doc.createElement("field"); if (append) extendedTableSchemaMainEl.appendChild(extendedTableSchemaFieldEl); extendedTableSchemaFieldEl.setAttribute("name", fieldName); } /*! @internal used by storeExtendedTableSchemaData() Creates DOM node for \a propertyName and \a propertyValue. Creates enclosing EXTENDED_TABLE_SCHEMA element if EXTENDED_TABLE_SCHEMA is true. Updates extendedTableSchemaStringIsEmpty and extendedTableSchemaMainEl afterwards. If extendedTableSchemaFieldEl is null, creates element (with optional "custom" attribute is \a custom is false). */ static void addFieldPropertyToExtendedTableSchemaData( Field *f, const char* propertyName, const QVariant& propertyValue, QDomDocument& doc, QDomElement& extendedTableSchemaMainEl, QDomElement& extendedTableSchemaFieldEl, bool& extendedTableSchemaStringIsEmpty, bool custom = false) { createExtendedTableSchemaMainElementIfNeeded(doc, extendedTableSchemaMainEl, extendedTableSchemaStringIsEmpty); createExtendedTableSchemaFieldElementIfNeeded( doc, extendedTableSchemaMainEl, f->name(), extendedTableSchemaFieldEl); //create QDomElement extendedTableSchemaFieldPropertyEl = doc.createElement("property"); extendedTableSchemaFieldEl.appendChild(extendedTableSchemaFieldPropertyEl); if (custom) extendedTableSchemaFieldPropertyEl.setAttribute("custom", "true"); extendedTableSchemaFieldPropertyEl.setAttribute("name", propertyName); QDomElement extendedTableSchemaFieldPropertyValueEl; switch (propertyValue.type()) { case QVariant::String: extendedTableSchemaFieldPropertyValueEl = doc.createElement("string"); break; case QVariant::ByteArray: extendedTableSchemaFieldPropertyValueEl = doc.createElement("cstring"); break; case QVariant::Int: case QVariant::Double: case QVariant::UInt: case QVariant::LongLong: case QVariant::ULongLong: extendedTableSchemaFieldPropertyValueEl = doc.createElement("number"); break; case QVariant::Bool: extendedTableSchemaFieldPropertyValueEl = doc.createElement("bool"); break; default: //! @todo add more QVariant types KexiDBFatal << "addFieldPropertyToExtendedTableSchemaData(): impl. error"; } extendedTableSchemaFieldPropertyEl.appendChild(extendedTableSchemaFieldPropertyValueEl); extendedTableSchemaFieldPropertyValueEl.appendChild( doc.createTextNode(propertyValue.toString())); } bool Connection::storeExtendedTableSchemaData(TableSchema& tableSchema) { //! @todo future: save in older versions if neeed QDomDocument doc("EXTENDED_TABLE_SCHEMA"); QDomElement extendedTableSchemaMainEl; bool extendedTableSchemaStringIsEmpty = true; //for each field: foreach(Field* f, *tableSchema.fields()) { QDomElement extendedTableSchemaFieldEl; if (f->visibleDecimalPlaces() >= 0/*nondefault*/ && KexiDB::supportsVisibleDecimalPlacesProperty(f->type())) { addFieldPropertyToExtendedTableSchemaData( f, "visibleDecimalPlaces", f->visibleDecimalPlaces(), doc, extendedTableSchemaMainEl, extendedTableSchemaFieldEl, extendedTableSchemaStringIsEmpty); } if (f->type() == Field::Text) { if (f->maxLengthStrategy() == Field::DefaultMaxLength) { addFieldPropertyToExtendedTableSchemaData( f, "maxLengthIsDefault", true, doc, extendedTableSchemaMainEl, extendedTableSchemaFieldEl, extendedTableSchemaStringIsEmpty); } } // boolean field with "not null" // add custom properties const Field::CustomPropertiesMap customProperties(f->customProperties()); for (Field::CustomPropertiesMap::ConstIterator itCustom = customProperties.constBegin(); itCustom != customProperties.constEnd(); ++itCustom) { addFieldPropertyToExtendedTableSchemaData( f, itCustom.key(), itCustom.value(), doc, extendedTableSchemaMainEl, extendedTableSchemaFieldEl, extendedTableSchemaStringIsEmpty, /*custom*/true); } + // save lookup table specification, if present LookupFieldSchema *lookupFieldSchema = tableSchema.lookupFieldSchema(*f); if (lookupFieldSchema) { createExtendedTableSchemaFieldElementIfNeeded( - doc, extendedTableSchemaMainEl, f->name(), extendedTableSchemaFieldEl, false/* !append */); - LookupFieldSchema::saveToDom(*lookupFieldSchema, doc, extendedTableSchemaFieldEl); + doc, extendedTableSchemaMainEl, f->name(), extendedTableSchemaFieldEl, + false/* !append */); + lookupFieldSchema->saveToDom(&doc, &extendedTableSchemaFieldEl); if (extendedTableSchemaFieldEl.hasChildNodes()) { // this element provides the definition, so let's append it now createExtendedTableSchemaMainElementIfNeeded(doc, extendedTableSchemaMainEl, extendedTableSchemaStringIsEmpty); extendedTableSchemaMainEl.appendChild(extendedTableSchemaFieldEl); } } + //KexiDBDbg << doc.toString(1); } // Store extended schema information (see ExtendedTableSchemaInformation in Kexi Wiki) if (extendedTableSchemaStringIsEmpty) { #ifdef CALLIGRADB_DEBUG_GUI KexiDB::alterTableActionDebugGUI(QString("** Extended table schema REMOVED.")); #endif if (!removeDataBlock(tableSchema.id(), "extended_schema")) return false; } else { #ifdef CALLIGRADB_DEBUG_GUI KexiDB::alterTableActionDebugGUI(QString("** Extended table schema set to:\n") + doc.toString(4)); #endif if (!storeDataBlock(tableSchema.id(), doc.toString(1), "extended_schema")) return false; } return true; } bool Connection::loadExtendedTableSchemaData(TableSchema& tableSchema) { #define loadExtendedTableSchemaData_ERR \ { setError(i18n("Error while loading extended table schema information.")); \ return false; } #define loadExtendedTableSchemaData_ERR2(details) \ { setError(i18n("Error while loading extended table schema information."), details); \ return false; } #define loadExtendedTableSchemaData_ERR3(data) \ { setError(i18n("Error while loading extended table schema information."), \ i18n("Invalid XML data: ") + data.left(1024) ); \ return false; } // Load extended schema information, if present (see ExtendedTableSchemaInformation in Kexi Wiki) QString extendedTableSchemaString; tristate res = loadDataBlock(tableSchema.id(), extendedTableSchemaString, "extended_schema"); if (!res) loadExtendedTableSchemaData_ERR; // extendedTableSchemaString will be just empty if there is no such data block #ifdef KEXIDB_LOOKUP_FIELD_TEST // if (tableSchema.name() == "cars") { LookupFieldSchema *lookupFieldSchema = new LookupFieldSchema(); lookupFieldSchema->rowSource().setType(LookupFieldSchema::RowSource::Table); lookupFieldSchema->rowSource().setName("persons"); lookupFieldSchema->setBoundColumn(0); //id lookupFieldSchema->setVisibleColumn(3); //surname tableSchema.setLookupFieldSchema("owner", lookupFieldSchema); } // #endif if (extendedTableSchemaString.isEmpty()) return true; QDomDocument doc; QString errorMsg; int errorLine, errorColumn; if (!doc.setContent(extendedTableSchemaString, &errorMsg, &errorLine, &errorColumn)) loadExtendedTableSchemaData_ERR2(i18n("Error in XML data: \"%1\" in line %2, column %3.\nXML data: ", errorMsg, errorLine, errorColumn) + extendedTableSchemaString.left(1024)); //! @todo look at the current format version (KEXIDB_EXTENDED_TABLE_SCHEMA_VERSION) if (doc.doctype().name() != "EXTENDED_TABLE_SCHEMA") loadExtendedTableSchemaData_ERR3(extendedTableSchemaString); QDomElement docEl = doc.documentElement(); if (docEl.tagName() != "EXTENDED_TABLE_SCHEMA") loadExtendedTableSchemaData_ERR3(extendedTableSchemaString); for (QDomNode n = docEl.firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement fieldEl = n.toElement(); if (fieldEl.tagName() == "field") { Field *f = tableSchema.field(fieldEl.attribute("name")); if (f) { //set properties of the field: //! @todo more properties for (QDomNode propNode = fieldEl.firstChild(); !propNode.isNull(); propNode = propNode.nextSibling()) { QDomElement propEl = propNode.toElement(); bool ok; int intValue; if (propEl.tagName() == "property") { QByteArray propertyName = propEl.attribute("name").toLatin1(); if (propEl.attribute("custom") == "true") { //custom property const QVariant v(KexiDB::loadPropertyValueFromDom(propEl.firstChild(), &ok)); if (ok) { f->setCustomProperty(propertyName, v); } } else if (propertyName == "visibleDecimalPlaces") { if (KexiDB::supportsVisibleDecimalPlacesProperty(f->type())) { intValue = KexiDB::loadIntPropertyValueFromDom(propEl.firstChild(), &ok); if (ok) f->setVisibleDecimalPlaces(intValue); } } else if (propertyName == "maxLengthIsDefault") { if (f->type() == Field::Text) { const bool maxLengthIsDefault = KexiDB::loadPropertyValueFromDom(propEl.firstChild(), &ok).toBool(); if (ok) { f->setMaxLengthStrategy( maxLengthIsDefault ? Field::DefaultMaxLength : Field::DefinedMaxLength); } } } //! @todo more properties... } else if (propEl.tagName() == "lookup-column") { LookupFieldSchema *lookupFieldSchema = LookupFieldSchema::loadFromDom(propEl); if (lookupFieldSchema) { - lookupFieldSchema->debug(); + lookupFieldSchema->debug(f->name()); tableSchema.setLookupFieldSchema(f->name(), lookupFieldSchema); } } } } else { KexiDBWarn << "Connection::loadExtendedTableSchemaData(): no such field \"" << fieldEl.attribute("name") << "\" in table \"" << tableSchema.name() << "\""; } } } return true; } KexiDB::Field* Connection::setupField(const RecordData &data) { bool ok = true; int f_int_type = data.at(1).toInt(&ok); if (f_int_type <= Field::InvalidType || f_int_type > Field::LastType) ok = false; if (!ok) return 0; Field::Type f_type = (Field::Type)f_int_type; int f_len = qMax(0, data.at(3).toInt(&ok)); // defined limit if (!ok) { return 0; } if (f_len < 0) { f_len = 0; } //! @todo load maxLengthStrategy info to see if the maxLength is the default int f_prec = data.at(4).toInt(&ok); if (!ok) return 0; int f_constr = data.at(5).toInt(&ok); if (!ok) return 0; int f_opts = data.at(6).toInt(&ok); if (!ok) return 0; QString name(data.at(2).toString().toLower()); if (!KexiDB::isIdentifier(name)) { setError(ERR_INVALID_IDENTIFIER, i18n("Invalid object name \"%1\"", data.at(2).toString())); ok = false; return 0; } Field *f = new Field( data.at(2).toString(), f_type, f_constr, f_opts, f_len, f_prec); f->setDefaultValue(KexiDB::stringToVariant(data.at(7).toString(), Field::variantType(f_type), ok)); if (!ok) { KexiDBWarn << "Connection::setupTableSchema() problem with KexiDB::stringToVariant(" << data.at(7).toString() << ")"; } ok = true; //problem with defaultValue is not critical f->m_caption = data.at(9).toString(); f->m_desc = data.at(10).toString(); return f; } KexiDB::TableSchema* Connection::setupTableSchema(const RecordData &data) { TableSchema *t = new TableSchema(this); if (!setupObjectSchemaData(data, *t)) { delete t; return 0; } KexiDB::Cursor *cursor; if (!(cursor = executeQuery( QString::fromLatin1("SELECT t_id, f_type, f_name, f_length, f_precision, f_constraints, " "f_options, f_default, f_order, f_caption, f_help" " FROM kexi__fields WHERE t_id=%1 ORDER BY f_order").arg(t->m_id)))) { delete t; return 0; } if (!cursor->moveFirst()) { if (!cursor->error() && cursor->eof()) { setError(i18n("Table has no fields defined.")); } deleteCursor(cursor); delete t; return 0; } // For each field: load its schema RecordData fieldData; bool ok = true; while (!cursor->eof()) { // KexiDBDbg<<"@@@ f_name=="<value(2).asCString(); if (!cursor->storeCurrentRow(fieldData)) { ok = false; break; } Field *f = setupField(fieldData); if (!f) { ok = false; break; } t->addField(f); cursor->moveNext(); } if (!ok) {//error: deleteCursor(cursor); delete t; return 0; } if (!deleteCursor(cursor)) { delete t; return 0; } if (!loadExtendedTableSchemaData(*t)) { delete t; return 0; } //store locally: d->insertTable(*t); return t; } TableSchema* Connection::tableSchema(const QString& tableName) { TableSchema *t = d->table(tableName); if (t) return t; //not found: retrieve schema RecordData data; if (true != querySingleRecord(QString::fromLatin1( "SELECT o_id, o_type, o_name, o_caption, o_desc FROM kexi__objects WHERE lower(o_name)='%1'" " AND o_type=%2") .arg(tableName).arg(KexiDB::TableObjectType), data)) return 0; return setupTableSchema(data); } TableSchema* Connection::tableSchema(int tableId) { TableSchema *t = d->table(tableId); if (t) return t; //not found: retrieve schema RecordData data; if (true != querySingleRecord(QString::fromLatin1( "SELECT o_id, o_type, o_name, o_caption, o_desc FROM kexi__objects WHERE o_id=%1") .arg(tableId), data)) return 0; return setupTableSchema(data); } tristate Connection::loadDataBlock(int objectID, QString &dataString, const QString& dataID) { if (objectID <= 0) return false; return querySingleString( QString::fromLatin1("SELECT o_data FROM kexi__objectdata WHERE o_id=") + QString::number(objectID) + " AND " + KexiDB::sqlWhere(m_driver, KexiDB::Field::Text, "o_sub_id", dataID.isEmpty() ? QVariant() : QVariant(dataID)), dataString); } bool Connection::storeDataBlock(int objectID, const QString &dataString, const QString& dataID) { if (objectID <= 0) return false; QString sql(QString::fromLatin1( "SELECT kexi__objectdata.o_id FROM kexi__objectdata WHERE o_id=%1").arg(objectID)); QString sql_sub(KexiDB::sqlWhere(m_driver, KexiDB::Field::Text, "o_sub_id", dataID.isEmpty() ? QVariant() : QVariant(dataID))); bool ok, exists; exists = resultExists(sql + " and " + sql_sub, ok); if (!ok) return false; if (exists) { return executeSQL("UPDATE kexi__objectdata SET o_data=" + m_driver->valueToSQL(KexiDB::Field::LongText, dataString) + " WHERE o_id=" + QString::number(objectID) + " AND " + sql_sub); } return executeSQL( QString::fromLatin1("INSERT INTO kexi__objectdata (o_id, o_data, o_sub_id) VALUES (") + QString::number(objectID) + "," + m_driver->valueToSQL(KexiDB::Field::LongText, dataString) + "," + m_driver->valueToSQL(KexiDB::Field::Text, dataID) + ")"); } bool Connection::removeDataBlock(int objectID, const QString& dataID) { if (objectID <= 0) return false; if (dataID.isEmpty()) return KexiDB::deleteRow(*this, "kexi__objectdata", "o_id", QString::number(objectID)); else return KexiDB::deleteRow(*this, "kexi__objectdata", "o_id", KexiDB::Field::Integer, objectID, "o_sub_id", KexiDB::Field::Text, dataID); } KexiDB::QuerySchema* Connection::setupQuerySchema(const RecordData &data) { bool ok = true; const int objID = data[0].toInt(&ok); if (!ok) return 0; QString sqlText; if (!loadDataBlock(objID, sqlText, "sql")) { setError(ERR_OBJECT_NOT_FOUND, i18n("Could not find definition for query \"%1\". Removing this query is recommended.", data[2].toString())); return 0; } d->parser()->parse(sqlText); KexiDB::QuerySchema *query = d->parser()->query(); //error? if (!query) { setError(ERR_SQL_PARSE_ERROR, i18n("

Could not load definition for query \"%1\". " "SQL statement for this query is invalid:
%2

\n" "

You can open this query in Text View and correct it.

", data[2].toString(), d->parser()->statement())); return 0; } if (!setupObjectSchemaData(data, *query)) { delete query; return 0; } d->insertQuery(*query); return query; } QuerySchema* Connection::querySchema(const QString& queryName) { QString m_queryName = queryName.toLower(); QuerySchema *q = d->query(m_queryName); if (q) return q; //not found: retrieve schema RecordData data; if (true != querySingleRecord(QString::fromLatin1( "SELECT o_id, o_type, o_name, o_caption, o_desc FROM kexi__objects WHERE lower(o_name)='%1'" " AND o_type=%2") .arg(m_queryName).arg(KexiDB::QueryObjectType), data)) return 0; return setupQuerySchema(data); } QuerySchema* Connection::querySchema(int queryId) { QuerySchema *q = d->query(queryId); if (q) return q; //not found: retrieve schema clearError(); RecordData data; if (true != querySingleRecord(QString::fromLatin1( "SELECT o_id, o_type, o_name, o_caption, o_desc FROM kexi__objects WHERE o_id=%1") .arg(queryId), data)) return 0; return setupQuerySchema(data); } bool Connection::setQuerySchemaObsolete(const QString& queryName) { QuerySchema* oldQuery = querySchema(queryName); if (!oldQuery) return false; d->setQueryObsolete(*oldQuery); return true; } void Connection::insertInternalTable(TableSchema& tableSchema) { d->insertInternalTable(tableSchema); } TableSchema* Connection::newKexiDBSystemTableSchema(const QString& tsname) { TableSchema *ts = new TableSchema(tsname.toLower()); insertInternalTable(*ts); return ts; } bool Connection::isInternalTableSchema(const QString& tableName) { return (d->kexiDBSystemTables().contains(d->table(tableName))) // these are here for compatiblility because we're no longer instantiate // them but can exist in projects created with previous Kexi versions: || tableName == "kexi__final" || tableName == "kexi__useractions"; } //! Creates kexi__* tables. bool Connection::setupKexiDBSystemSchema() { if (!d->kexiDBSystemTables().isEmpty()) return true; //already set up TableSchema *t_objects = newKexiDBSystemTableSchema("kexi__objects"); t_objects->addField(new Field("o_id", Field::Integer, Field::PrimaryKey | Field::AutoInc, Field::Unsigned)) .addField(new Field("o_type", Field::Byte, 0, Field::Unsigned)) .addField(new Field("o_name", Field::Text)) .addField(new Field("o_caption", Field::Text)) .addField(new Field("o_desc", Field::LongText)); t_objects->debug(); TableSchema *t_objectdata = newKexiDBSystemTableSchema("kexi__objectdata"); t_objectdata->addField(new Field("o_id", Field::Integer, Field::NotNull, Field::Unsigned)) .addField(new Field("o_data", Field::LongText)) .addField(new Field("o_sub_id", Field::Text)); TableSchema *t_fields = newKexiDBSystemTableSchema("kexi__fields"); t_fields->addField(new Field("t_id", Field::Integer, 0, Field::Unsigned)) .addField(new Field("f_type", Field::Byte, 0, Field::Unsigned)) .addField(new Field("f_name", Field::Text)) .addField(new Field("f_length", Field::Integer)) .addField(new Field("f_precision", Field::Integer)) .addField(new Field("f_constraints", Field::Integer)) .addField(new Field("f_options", Field::Integer)) .addField(new Field("f_default", Field::Text)) //these are additional properties: .addField(new Field("f_order", Field::Integer)) .addField(new Field("f_caption", Field::Text)) .addField(new Field("f_help", Field::LongText)); /* TableSchema *t_querydata = newKexiDBSystemTableSchema("kexi__querydata"); t_querydata->addField( new Field("q_id", Field::Integer, 0, Field::Unsigned) ) .addField( new Field("q_sql", Field::LongText ) ) .addField( new Field("q_valid", Field::Boolean ) ); TableSchema *t_queryfields = newKexiDBSystemTableSchema("kexi__queryfields"); t_queryfields->addField( new Field("q_id", Field::Integer, 0, Field::Unsigned) ) .addField( new Field("f_order", Field::Integer ) ) .addField( new Field("f_id", Field::Integer ) ) .addField( new Field("f_tab_asterisk", Field::Integer, 0, Field::Unsigned) ) .addField( new Field("f_alltab_asterisk", Field::Boolean) ); TableSchema *t_querytables = newKexiDBSystemTableSchema("kexi__querytables"); t_querytables->addField( new Field("q_id", Field::Integer, 0, Field::Unsigned) ) .addField( new Field("t_id", Field::Integer, 0, Field::Unsigned) ) .addField( new Field("t_order", Field::Integer, 0, Field::Unsigned) );*/ TableSchema *t_db = newKexiDBSystemTableSchema("kexi__db"); t_db->addField(new Field("db_property", Field::Text, Field::NoConstraints, Field::NoOptions, 32)) .addField(new Field("db_value", Field::LongText)); /* moved to KexiProject... TableSchema *t_parts = newKexiDBSystemTableSchema("kexi__parts"); t_parts->addField( new Field("p_id", Field::Integer, Field::PrimaryKey | Field::AutoInc, Field::Unsigned) ) .addField( new Field("p_name", Field::Text) ) .addField( new Field("p_mime", Field::Text ) ) .addField( new Field("p_url", Field::Text ) ); */ /*UNUSED TableSchema *t_final = newKexiDBSystemTableSchema("kexi__final"); t_final->addField( new Field("p_id", Field::Integer, 0, Field::Unsigned) ) .addField( new Field("property", Field::LongText ) ) .addField( new Field("value", Field::BLOB) ); TableSchema *t_useractions = newKexiDBSystemTableSchema("kexi__useractions"); t_useractions->addField( new Field("p_id", Field::Integer, 0, Field::Unsigned) ) .addField( new Field("scope", Field::Integer ) ) .addField( new Field("name", Field::LongText ) ) .addField( new Field("text", Field::LongText ) ) .addField( new Field("icon", Field::LongText ) ) .addField( new Field("method", Field::Integer ) ) .addField( new Field("arguments", Field::LongText) ); */ return true; } void Connection::removeMe(TableSchema *ts) { if (ts && !m_destructor_started) d->takeTable(*ts); } QString Connection::anyAvailableDatabaseName() { if (!d->availableDatabaseName.isEmpty()) { return d->availableDatabaseName; } return m_driver->beh->ALWAYS_AVAILABLE_DATABASE_NAME; } void Connection::setAvailableDatabaseName(const QString& dbName) { d->availableDatabaseName = dbName; } //! @internal used in updateRow(), insertRow(), inline void updateRowDataWithNewValues(QuerySchema &query, RecordData& data, KexiDB::RowEditBuffer::DBMap& b, QHash& columnsOrderExpanded) { columnsOrderExpanded = query.columnsOrder(QuerySchema::ExpandedList); QHash::ConstIterator columnsOrderExpandedIt; for (KexiDB::RowEditBuffer::DBMap::ConstIterator it = b.constBegin();it != b.constEnd();++it) { columnsOrderExpandedIt = columnsOrderExpanded.constFind(it.key()); if (columnsOrderExpandedIt == columnsOrderExpanded.constEnd()) { KexiDBWarn << "(Connection) updateRowDataWithNewValues(): \"now also assign new value in memory\" step " "- could not find item '" << it.key()->aliasOrName() << "'"; continue; } data[ columnsOrderExpandedIt.value()] = it.value(); } } bool Connection::updateRow(QuerySchema &query, RecordData& data, RowEditBuffer& buf, bool useROWID) { // Each SQL identifier needs to be escaped in the generated query. // query.debug(); KexiDBDbg << "Connection::updateRow.."; clearError(); //--get PKEY if (buf.dbBuffer().isEmpty()) { KexiDBDbg << " -- NO CHANGES DATA!"; return true; } TableSchema *mt = query.masterTable(); if (!mt) { KexiDBWarn << " -- NO MASTER TABLE!"; setError(ERR_UPDATE_NO_MASTER_TABLE, i18n("Could not update record because there is no master table defined.")); return false; } IndexSchema *pkey = (mt->primaryKey() && !mt->primaryKey()->fields()->isEmpty()) ? mt->primaryKey() : 0; if (!useROWID && !pkey) { KexiDBWarn << " -- NO MASTER TABLE's PKEY!"; setError(ERR_UPDATE_NO_MASTER_TABLES_PKEY, i18n("Could not update record because master table has no primary key defined.")); //! @todo perhaps we can try to update without using PKEY? return false; } //update the record: m_sql = "UPDATE " + escapeIdentifier(mt->name()) + " SET "; QString sqlset, sqlwhere; sqlset.reserve(1024); sqlwhere.reserve(1024); KexiDB::RowEditBuffer::DBMap b = buf.dbBuffer(); //gather the fields which are updated ( have values in RowEditBuffer) FieldList affectedFields; for (KexiDB::RowEditBuffer::DBMap::ConstIterator it = b.constBegin();it != b.constEnd();++it) { if (it.key()->field->table() != mt) continue; // skip values for fields outside of the master table (e.g. a "visible value" of the lookup field) if (!sqlset.isEmpty()) sqlset += ","; Field* currentField = it.key()->field; affectedFields.addField(currentField); sqlset += (escapeIdentifier(currentField->name()) + "=" + m_driver->valueToSQL(currentField, it.value())); } if (pkey) { const QVector pkeyFieldsOrder(query.pkeyFieldsOrder()); KexiDBDbg << pkey->fieldCount() << " ? " << query.pkeyFieldsCount(); if (pkey->fieldCount() != query.pkeyFieldsCount()) { //sanity check KexiDBWarn << " -- NO ENTIRE MASTER TABLE's PKEY SPECIFIED!"; setError(ERR_UPDATE_NO_ENTIRE_MASTER_TABLES_PKEY, i18n("Could not update record because it does not contain entire master table's primary key.")); return false; } if (!pkey->fields()->isEmpty()) { uint i = 0; foreach(Field *f, *pkey->fields()) { if (!sqlwhere.isEmpty()) sqlwhere += " AND "; QVariant val(data.at(pkeyFieldsOrder.at(i))); if (val.isNull() || !val.isValid()) { setError(ERR_UPDATE_NULL_PKEY_FIELD, i18n("Primary key's field \"%1\" cannot be empty.", f->name())); //js todo: pass the field's name somewhere! return false; } sqlwhere += (escapeIdentifier(f->name()) + "=" + m_driver->valueToSQL(f, val)); i++; } } } else {//use ROWID sqlwhere = (escapeIdentifier(m_driver->beh->ROW_ID_FIELD_NAME) + "=" + m_driver->valueToSQL(Field::BigInteger, data[data.size()-1])); } m_sql += (sqlset + " WHERE " + sqlwhere); KexiDBDbg << " -- SQL == " << ((m_sql.length() > 400) ? (m_sql.left(400) + "[.....]") : m_sql); // preprocessing before update if (!drv_beforeUpdate(mt->name(), affectedFields)) return false; bool res = executeSQL(m_sql); // postprocessing after update if (!drv_afterUpdate(mt->name(), affectedFields)) return false; if (!res) { setError(ERR_UPDATE_SERVER_ERROR, i18n("Record updating on the server failed.")); return false; } //success: now also assign new values in memory: QHash columnsOrderExpanded; updateRowDataWithNewValues(query, data, b, columnsOrderExpanded); return true; } bool Connection::insertRow(QuerySchema &query, RecordData& data, RowEditBuffer& buf, bool getROWID) { // Each SQL identifier needs to be escaped in the generated query. KexiDBDbg << "Connection::updateRow.."; clearError(); //--get PKEY /*disabled: there may be empty rows (with autoinc) if (buf.dbBuffer().isEmpty()) { KexiDBDbg << " -- NO CHANGES DATA!"; return true; }*/ TableSchema *mt = query.masterTable(); if (!mt) { KexiDBWarn << " -- NO MASTER TABLE!"; setError(ERR_INSERT_NO_MASTER_TABLE, i18n("Could not insert record because there is no master table defined.")); return false; } IndexSchema *pkey = (mt->primaryKey() && !mt->primaryKey()->fields()->isEmpty()) ? mt->primaryKey() : 0; if (!getROWID && !pkey) KexiDBWarn << " -- WARNING: NO MASTER TABLE's PKEY"; QString sqlcols, sqlvals; sqlcols.reserve(1024); sqlvals.reserve(1024); //insert the record: m_sql = "INSERT INTO " + escapeIdentifier(mt->name()) + " ("; KexiDB::RowEditBuffer::DBMap b = buf.dbBuffer(); // add default values, if available (for any column without value explicitly set) const QueryColumnInfo::Vector fieldsExpanded(query.fieldsExpanded(QuerySchema::Unique)); uint fieldsExpandedCount = fieldsExpanded.count(); for (uint i = 0; i < fieldsExpandedCount; i++) { QueryColumnInfo *ci = fieldsExpanded.at(i); if (ci->field && KexiDB::isDefaultValueAllowed(ci->field) && !ci->field->defaultValue().isNull() && !b.contains(ci)) { KexiDBDbg << "Connection::insertRow(): adding default value '" << ci->field->defaultValue().toString() << "' for column '" << ci->field->name() << "'"; b.insert(ci, ci->field->defaultValue()); } } //collect fields which have values in RowEditBuffer FieldList affectedFields; if (b.isEmpty()) { // empty record inserting requested: if (!getROWID && !pkey) { KexiDBWarn << "MASTER TABLE's PKEY REQUIRED FOR INSERTING EMPTY RECORDS: INSERT CANCELLED"; setError(ERR_INSERT_NO_MASTER_TABLES_PKEY, i18n("Could not insert record because master table has no primary key defined.")); return false; } if (pkey) { const QVector pkeyFieldsOrder(query.pkeyFieldsOrder()); // KexiDBDbg << pkey->fieldCount() << " ? " << query.pkeyFieldsCount(); if (pkey->fieldCount() != query.pkeyFieldsCount()) { //sanity check KexiDBWarn << "NO ENTIRE MASTER TABLE's PKEY SPECIFIED!"; setError(ERR_INSERT_NO_ENTIRE_MASTER_TABLES_PKEY, i18n("Could not insert record because it does not contain entire master table's primary key.")); return false; } } //at least one value is needed for VALUES section: find it and set to NULL: Field *anyField = mt->anyNonPKField(); if (!anyField) { if (!pkey) { KexiDBWarn << "WARNING: NO FIELD AVAILABLE TO SET IT TO NULL"; return false; } else { //try to set NULL in pkey field (could not work for every SQL engine!) anyField = pkey->fields()->first(); } } sqlcols += escapeIdentifier(anyField->name()); sqlvals += m_driver->valueToSQL(anyField, QVariant()/*NULL*/); affectedFields.addField(anyField); } else { // non-empty record inserting requested: for (KexiDB::RowEditBuffer::DBMap::ConstIterator it = b.constBegin();it != b.constEnd();++it) { if (it.key()->field->table() != mt) continue; // skip values for fields outside of the master table (e.g. a "visible value" of the lookup field) if (!sqlcols.isEmpty()) { sqlcols += ","; sqlvals += ","; } Field* currentField = it.key()->field; affectedFields.addField(currentField); sqlcols += escapeIdentifier(currentField->name()); sqlvals += m_driver->valueToSQL(currentField, it.value()); } } m_sql += (sqlcols + ") VALUES (" + sqlvals + ")"); // KexiDBDbg << " -- SQL == " << m_sql; // do driver specific pre-processing if (!drv_beforeInsert(mt->name(), affectedFields)) return false; bool res = executeSQL(m_sql); // do driver specific post-processing if (!drv_afterInsert(mt->name(), affectedFields)) return false; if (!res) { setError(ERR_INSERT_SERVER_ERROR, i18n("Record inserting on the server failed.")); return false; } //success: now also assign a new value in memory: QHash columnsOrderExpanded; updateRowDataWithNewValues(query, data, b, columnsOrderExpanded); //fetch autoincremented values QueryColumnInfo::List *aif_list = query.autoIncrementFields(); quint64 ROWID = 0; if (pkey && !aif_list->isEmpty()) { //! @todo now only if PKEY is present, this should also work when there's no PKEY QueryColumnInfo *id_columnInfo = aif_list->first(); //! @todo safe to cast it? quint64 last_id = lastInsertedAutoIncValue( id_columnInfo->field->name(), id_columnInfo->field->table()->name(), &ROWID); if (last_id == (quint64) - 1 || last_id <= 0) { //! @todo show error //! @todo remove just inserted record. How? Using ROLLBACK? return false; } RecordData aif_data; QString getAutoIncForInsertedValue = QLatin1String("SELECT ") + query.autoIncrementSQLFieldsList(m_driver) + QLatin1String(" FROM ") + escapeIdentifier(id_columnInfo->field->table()->name()) + QLatin1String(" WHERE ") + escapeIdentifier(id_columnInfo->field->name()) + "=" + QString::number(last_id); if (true != querySingleRecord(getAutoIncForInsertedValue, aif_data)) { //! @todo show error return false; } uint i = 0; foreach(QueryColumnInfo *ci, *aif_list) { // KexiDBDbg << "Connection::insertRow(): AUTOINCREMENTED FIELD " << fi->field->name() << " == " // << aif_data[i].toInt(); (data[ columnsOrderExpanded.value(ci)] = aif_data.value(i)).convert(ci->field->variantType()); //cast to get proper type i++; } } else { ROWID = drv_lastInsertRowID(); // KexiDBDbg << "Connection::insertRow(): new ROWID == " << (uint)ROWID; if (m_driver->beh->ROW_ID_FIELD_RETURNS_LAST_AUTOINCREMENTED_VALUE) { KexiDBWarn << "Connection::insertRow(): m_driver->beh->ROW_ID_FIELD_RETURNS_LAST_AUTOINCREMENTED_VALUE"; return false; } } if (getROWID && /*sanity check*/data.size() > (int)fieldsExpanded.size()) { // KexiDBDbg << "Connection::insertRow(): new ROWID == " << (uint)ROWID; data[data.size()-1] = ROWID; } return true; } bool Connection::deleteRow(QuerySchema &query, RecordData& data, bool useROWID) { // Each SQL identifier needs to be escaped in the generated query. KexiDBWarn << "Connection::deleteRow.."; clearError(); TableSchema *mt = query.masterTable(); if (!mt) { KexiDBWarn << " -- NO MASTER TABLE!"; setError(ERR_DELETE_NO_MASTER_TABLE, i18n("Could not delete record because there is no master table defined.")); return false; } IndexSchema *pkey = (mt->primaryKey() && !mt->primaryKey()->fields()->isEmpty()) ? mt->primaryKey() : 0; //! @todo allow to delete from a table without pkey if (!useROWID && !pkey) { KexiDBWarn << " -- WARNING: NO MASTER TABLE's PKEY"; setError(ERR_DELETE_NO_MASTER_TABLES_PKEY, i18n("Could not delete record because there is no primary key for master table defined.")); return false; } //update the record: m_sql = "DELETE FROM " + escapeIdentifier(mt->name()) + " WHERE "; QString sqlwhere; sqlwhere.reserve(1024); if (pkey) { const QVector pkeyFieldsOrder(query.pkeyFieldsOrder()); KexiDBDbg << pkey->fieldCount() << " ? " << query.pkeyFieldsCount(); if (pkey->fieldCount() != query.pkeyFieldsCount()) { //sanity check KexiDBWarn << " -- NO ENTIRE MASTER TABLE's PKEY SPECIFIED!"; setError(ERR_DELETE_NO_ENTIRE_MASTER_TABLES_PKEY, i18n("Could not delete record because it does not contain entire master table's primary key.")); return false; } uint i = 0; foreach(Field *f, *pkey->fields()) { if (!sqlwhere.isEmpty()) sqlwhere += " AND "; QVariant val(data.at(pkeyFieldsOrder.at(i))); if (val.isNull() || !val.isValid()) { setError(ERR_DELETE_NULL_PKEY_FIELD, i18n("Primary key's field \"%1\" cannot be empty.", f->name())); //js todo: pass the field's name somewhere! return false; } sqlwhere += (escapeIdentifier(f->name()) + "=" + m_driver->valueToSQL(f, val)); i++; } } else {//use ROWID sqlwhere = (escapeIdentifier(m_driver->beh->ROW_ID_FIELD_NAME) + "=" + m_driver->valueToSQL(Field::BigInteger, data[data.size()-1])); } m_sql += sqlwhere; KexiDBDbg << " -- SQL == " << m_sql; if (!executeSQL(m_sql)) { setError(ERR_DELETE_SERVER_ERROR, i18n("Record deletion on the server failed.")); return false; } return true; } bool Connection::deleteAllRows(QuerySchema &query) { clearError(); TableSchema *mt = query.masterTable(); if (!mt) { KexiDBWarn << " -- NO MASTER TABLE!"; return false; } IndexSchema *pkey = mt->primaryKey(); if (!pkey || pkey->fields()->isEmpty()) KexiDBWarn << "Connection::deleteAllRows -- WARNING: NO MASTER TABLE's PKEY"; m_sql = "DELETE FROM " + escapeIdentifier(mt->name()); KexiDBDbg << " -- SQL == " << m_sql; if (!executeSQL(m_sql)) { setError(ERR_DELETE_SERVER_ERROR, i18n("Record deletion on the server failed.")); return false; } return true; } void Connection::registerForTableSchemaChanges(TableSchemaChangeListenerInterface& listener, TableSchema &schema) { QSet* listeners = d->tableSchemaChangeListeners.value(&schema); if (!listeners) { listeners = new QSet(); d->tableSchemaChangeListeners.insert(&schema, listeners); } listeners->insert(&listener); } void Connection::unregisterForTableSchemaChanges(TableSchemaChangeListenerInterface& listener, TableSchema &schema) { QSet* listeners = d->tableSchemaChangeListeners.value(&schema); if (!listeners) return; listeners->remove(&listener); } void Connection::unregisterForTablesSchemaChanges(TableSchemaChangeListenerInterface& listener) { foreach(QSet *listeners, d->tableSchemaChangeListeners) { listeners->remove(&listener); } } QSet* Connection::tableSchemaChangeListeners(TableSchema& tableSchema) const { KexiDBDbg << d->tableSchemaChangeListeners.count(); return d->tableSchemaChangeListeners.value(&tableSchema); } tristate Connection::closeAllTableSchemaChangeListeners(TableSchema& tableSchema) { QSet *listeners = d->tableSchemaChangeListeners.value(&tableSchema); if (!listeners) return true; //Qt4??? QSet::ConstIterator tmpListeners(*listeners); //safer copy tristate res = true; //try to close every window QList list(listeners->toList()); KexiDBDbg << list.count(); foreach (Connection::TableSchemaChangeListenerInterface* listener, list) { res = listener->closeListener(); } return res; } /*PreparedStatement::Ptr Connection::prepareStatement(PreparedStatement::StatementType, TableSchema&) { //safe? return 0; }*/ void Connection::setReadOnly(bool set) { if (d->isConnected) return; //sanity d->readOnly = set; } bool Connection::isReadOnly() const { return d->readOnly; } void Connection::addCursor(KexiDB::Cursor& cursor) { d->cursors.insert(&cursor); } void Connection::takeCursor(KexiDB::Cursor& cursor) { d->cursors.remove(&cursor); } #include "connection.moc" diff --git a/libs/db/field.h b/libs/db/field.h index 66fa7997ab6..11e3fc50ba6 100644 --- a/libs/db/field.h +++ b/libs/db/field.h @@ -1,765 +1,765 @@ /* This file is part of the KDE project Copyright (C) 2002 Lucijan Busch Copyright (C) 2002 Joseph Wenninger Copyright (C) 2003-2012 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KEXIDB_FIELD_H #define KEXIDB_FIELD_H #include #include #include #include #include #include #include #include "calligradb_export.h" namespace KexiDB { class TableSchema; class QuerySchema; class FieldList; class BaseExpr; //! Meta-data for a field /*! KexiDB::Field provides information about single database field. Field class has defined following members: - name - type - database constraints - additional options - maxLength (makes sense mostly for string types) - maxLengthStrategy (makes sense mostly for string types) - precision (for floating-point type) - defaultValue - caption (user readable name that can be e.g. translated) - description (user readable name additional text, can be useful for developers) - - width (a hint for displaying in tabular mode or as text box) + - defaultWidth (a hint for displaying in tabular mode or as text box) Field can also have assigned expression (see KexiDB::BaseExpr class, and expression() method). If an expression is defined, then field's name is Note that aliases for fields are defined within query, not in Field object, because the same field can be used in different queries with different alias. Notes for advanced use: Field obeject is designed to be owned by a parent object. Such a parent object can be KexiDB::TableSchema, if the field defines single table column, or KexiDB::QuerySchema, if the field defines an expression (KexiDB::BaseExpr class). Using expression class for fields allos to define expressions within queries like "SELECT AVG(price) FROM products" You can choose whether your field is owned by query or table, using appropriate constructor, or using parameterless constructor and calling setTable() or setQuery() later. */ class CALLIGRADB_EXPORT Field { public: typedef KexiDB::AutodeletedList List; //!< list of fields typedef QVector Vector; //!< vector of fields typedef QList::ConstIterator ListIterator; //!< iterator for list of fields typedef QPair Pair; //!< fields pair typedef QList PairList; //!< list of fields pair /*! Unified (most common used) types of fields. */ enum Type { InvalidType = 0, /*!< Unsupported/Unimplemented type */ FirstType = 1, /*! First type */ Byte = 1, /*!< 1 byte, signed or unsigned */ ShortInteger = 2,/*!< 2 bytes, signed or unsigned */ Integer = 3, /*!< 4 bytes, signed or unsigned */ BigInteger = 4, /*!< 8 bytes, signed or unsigned */ Boolean = 5, /*!< 0 or 1 */ Date = 6, /*!< */ DateTime = 7, /*!< */ Time = 8, /*!< */ Float = 9, /*!< 4 bytes */ Double = 10, /*!< 8 bytes */ Text = 11, /*!< Other name: Varchar */ LongText = 12, /*!< Other name: Memo */ BLOB = 13, /*!< Large binary object */ LastType = 13, /*!< This line should be at the end of the list of types! */ Null = 64, /*!< Used for fields that are "NULL" expressions. */ //! Special, internal types: Asterisk = 128, /*!< Used in QueryAsterisk subclass objects only, not used in table definitions, but only in query definitions */ Enum = 129, /*!< An integer internal with a string list of hints */ Map = 130 /*!< Mapping from string to string list (more generic than Enum */ }; /*! Type groups for fields. */ enum TypeGroup { InvalidGroup = 0, TextGroup = 1, IntegerGroup = 2, FloatGroup = 3, BooleanGroup = 4, DateTimeGroup = 5, BLOBGroup = 6, /* large binary object */ LastTypeGroup = 6 // This line should be at the end of the enum! }; /*! Possible constraints defined for a field. */ enum Constraints { NoConstraints = 0, AutoInc = 1, Unique = 2, PrimaryKey = 4, ForeignKey = 8, NotNull = 16, NotEmpty = 32, //!< only legal for string-like and blob fields Indexed = 64 }; /*! Possible options defined for a field. */ enum Options { NoOptions = 0, Unsigned = 1 }; /*! Creates a database field as a child of \a tableSchema table. maxLength property is set to 0 (unlimited length). No other properties are set (even the name), so these should be set later. */ Field(TableSchema *tableSchema); /*! Creates a database field. maxLength property is set to 0 (unlimited length). No other properties are set (even the name), so these should be set later. */ Field(); /*! Creates a database field with specified properties. For meaning of @a maxLength argument please refer to setMaxLength(). */ Field(const QString& name, Type ctype, uint cconst = NoConstraints, uint options = NoOptions, uint maxLength = 0, uint precision = 0, QVariant defaultValue = QVariant(), const QString& caption = QString(), const QString& description = QString()); /*! Copy constructor. */ Field(const Field& f); virtual ~Field(); //! Converts type \a type to QVariant equivalent as accurate as possible static QVariant::Type variantType(uint type); /*! \return a i18n'd type name for \a type (\a type has to be an element from Field::Type, not greater than Field::LastType) */ static QString typeName(uint type); /*! \return list of all available i18n'd type names. */ static QStringList typeNames(); /*! \return type string for \a type, e.g. "Integer" for Integer type (not-i18n'd, \a type has to be an element from Field::Type, not greater than Field::LastType) */ static QString typeString(uint type); /*! \return type for a given \a typeString */ static Type typeForString(const QString& typeString); /*! \return type group for a given \a typeGroupString */ static TypeGroup typeGroupForString(const QString& typeGroupString); /*! \return group for \a type */ static TypeGroup typeGroup(uint type); /*! \return a i18n'd group name for \a typeGroup (\a typeGroup has to be an element from Field::TypeGroup) */ static QString typeGroupName(uint typeGroup); /*! \return list of all available i18n'd type group names. */ static QStringList typeGroupNames(); /*! \return type group string for \a typeGroup, e.g. "IntegerGroup" for IntegerGroup type (not-i18n'd, \a type has to be an element from Field::Type, not greater than Field::LastType) */ static QString typeGroupString(uint typeGroup); /* ! \return the name of this field */ inline QString name() const { return m_name; } /*! \return table schema of table that owns this field or null if it has no table assigned. @see query() */ virtual TableSchema* table() const; /*! Sets \a table schema of table that owns this field. This does not adds the field to \a table object. You do not need to call this method by hand. Call TableSchema::addField(Field *field) instead. @see setQuery() */ virtual void setTable(TableSchema *table); /*! For special use when the field defines expression. \return query schema of query that owns this field or null if it has no query assigned. @see table() */ QuerySchema* query() const; /*! For special use when field defines expression. Sets \a query schema of query that owns this field. This does not adds the field to \a query object. You do not need to call this method by hand. Call QuerySchema::addField() instead. @see setQuery() */ void setQuery(QuerySchema *query); /*! \return true if the field is autoincrement (e.g. integer/numeric) */ inline bool isAutoIncrement() const { return constraints() & AutoInc; } /*! \return true if the field is member of single-field primary key */ inline bool isPrimaryKey() const { return constraints() & PrimaryKey; } /*! \return true if the field is member of single-field unique key */ inline bool isUniqueKey() const { return constraints() & Unique; } /*! \return true if the field is member of single-field foreign key */ inline bool isForeignKey() const { return constraints() & ForeignKey; } /*! \return true if the field is not allowed to be null */ inline bool isNotNull() const { return constraints() & NotNull; } /*! \return true if the field is not allowed to be null */ inline bool isNotEmpty() const { return constraints() & NotEmpty; } /*! \return true if the field is indexed using single-field database index. */ inline bool isIndexed() const { return constraints() & Indexed; } /*! \return true if the field is of any numeric type (integer or floating point) */ inline bool isNumericType() const { return Field::isNumericType(type()); } /*! static version of isNumericType() method *! \return true if the field is of any numeric type (integer or floating point)*/ static bool isNumericType(uint type); /*! \return true if the field is of any integer type */ inline bool isIntegerType() const { return Field::isIntegerType(type()); } /*! static version of isIntegerType() method *! \return true if the field is of any integer type */ static bool isIntegerType(uint type); /*! \return true if the field is of any floating point numeric type */ inline bool isFPNumericType() const { return Field::isFPNumericType(type()); } /*! static version of isFPNumericType() method *! \return true if the field is of any floating point numeric type */ static bool isFPNumericType(uint type); /*! \return true if the field is of any date or time related type */ inline bool isDateTimeType() const { return Field::isDateTimeType(type()); } /*! static version of isDateTimeType() method *! \return true if the field is of any date or time related type */ static bool isDateTimeType(uint type); /*! @return true if the field is of any text type */ inline bool isTextType() const { return Field::isTextType(type()); } /*! static version of isTextType() method *! \return true if the field is of any text type */ static bool isTextType(uint type); uint options() const { return m_options; } void setOptions(uint options) { m_options = options; } //! Converts field's type to QVariant equivalent as accurate as possible inline QVariant::Type variantType() const { return variantType(type()); } /*! \return a type for this field. If there's expression assigned, type of the expression is returned instead. */ Type type() const; //! \return a i18n'd type name for this field inline QString typeName() const { return Field::typeName(type()); } //! \return type group for this field inline TypeGroup typeGroup() const { return Field::typeGroup(type()); } //! \return a i18n'd type group name for this field inline QString typeGroupName() const { return Field::typeGroupName(typeGroup()); } //! \return a type string for this field, //! for example "Integer" string for Field::Integer type. inline QString typeString() const { return Field::typeString(type()); } //! \return a type group string for this field, //! for example "Integer" string for Field::IntegerGroup. inline QString typeGroupString() const { return Field::typeGroupString(typeGroup()); } /*! \return (optional) subtype for this field. Subtype is a string providing additional hint for field's type. E.g. for BLOB type, it can be a MIME type or certain QVariant type name, for example: "QPixmap", "QColor" or "QFont" */ inline QString subType() const { return m_subType; } /*! Sets (optional) subtype for this field. \sa subType() */ inline void setSubType(const QString& subType) { m_subType = subType; } //! \return default value for this field. Null value means there //! is no default value declared. The variant value is compatible with field's type. inline QVariant defaultValue() const { return m_defaultValue; } /*! @return default maximum length of text. Default is 0, i.e unlimited length (if the engine supports it). */ static uint defaultMaxLength(); /*! Sets default maximum length of text. 0 means unlimited length, greater than 0 means specific maximum length. */ static void setDefaultMaxLength(uint maxLength); /*! Strategy for defining maximum length of text for this field. Only makes sense if the field type is of Text type. Default strategy is DefinedMaxLength. */ enum MaxLengthStrategy { DefaultMaxLength, //!< Default maximum text length defined globally by the application. //!< @see defaultMaxLength() DefinedMaxLength //!< Used if setMaxLength() was called to set specific maximum value //!< or to unlimited (0). }; /*! \return a hint that indicates if the maximum length of text for this field is based on default setting (defaultMaxLength()) or was explicitly set. Only makes sense if the field type is Text. */ MaxLengthStrategy maxLengthStrategy() const; /*! Sets strategy for defining maximum length of text for this field. Only makes sense if the field type is Text. Default strategy is DefinedMaxLength. Changing this value does not affect maxLength property. Fields with DefaultMaxLength strategy does not follow changes made by calling setDefaultMaxLength() so to update the default maximum lengths in fields, the app has to iterate over all fields of type Text, and reset to the new default as explained in setMaxLength() documentation. See documentation for setMaxLength() for information how to reset maxLength to default value. @see maxLengthStrategy(), setMaxLength() */ void setMaxLengthStrategy(MaxLengthStrategy strategy); /*! \return maximum length of text allowed for this field. Only meaningful if the type is Text. @see setMaxLength() */ uint maxLength() const; /*! Sets maximum length for this field. Only works for Text type. It can be specific maximum value or 0 for unlimited length (which will work if engine supports). Resets maxLengthStrategy property to DefinedMaxLength. To reset to default maximum length, call setMaxLength(defaultMaxLength()) and then to indicate this is based on default setting, call setMaxLengthStrategy(DefaultMaxLength). @see maxLength(), maxLengthStrategy() */ void setMaxLength(uint maxLength); /*! \return precision for numeric and other fields that have both length (scale) and precision (floating point types). */ inline uint precision() const { return m_precision; } /*! \return scale for numeric and other fields that have both length (scale) and precision (floating point types). The scale of a numeric is the count of decimal digits in the fractional part, to the right of the decimal point. The precision of a numeric is the total count of significant digits in the whole number, that is, the number of digits to both sides of the decimal point. So the number 23.5141 has a precision of 6 and a scale of 4. Integers can be considered to have a scale of zero. */ inline uint scale() const { return m_maxLength; } //! @todo should we keep extended properties here or move them to a QVariant dictionary? /*! \return number of decimal places that should be visible to the user, e.g. within table view widget, form or printout. Only meaningful if the field type is floating point or (in the future: decimal or currency). - Any value less than 0 (-1 is the default) means that there should be displayed all digits of the fractional part, except the ending zeros. This is known as "auto" mode. For example, 12.345000 becomes 12.345. - Value of 0 means that all the fractional part should be hidden (as well as the dot or comma). For example, 12.345000 becomes 12. - Value N > 0 means that the fractional part should take exactly N digits. If the fractional part is shorter than N, additional zeros are appended. For example, "12.345" becomes "12.345000" if N=6. */ inline int visibleDecimalPlaces() const { return m_visibleDecimalPlaces; } /*! \return the constraints defined for this field. */ inline uint constraints() const { return m_constraints; } /*! \return order of this field in containing table (counting starts from 0) (-1 if unspecified). */ inline int order() const { return m_order; } /*! \return caption of this field. */ inline QString caption() const { return m_caption; } /*! \return caption of this field or - if empty - return its name. */ inline QString captionOrName() const { return m_caption.isEmpty() ? m_name : m_caption; } /*! \return description text for this field. */ inline QString description() const { return m_desc; } //! if the type has the unsigned attribute inline bool isUnsigned() const { return m_options & Unsigned; } /*! \return true if this field has EMPTY property (i.e. it is of type string or is a BLOB). */ inline bool hasEmptyProperty() const { return Field::hasEmptyProperty(type()); } /*! static version of hasEmptyProperty() method \return true if this field type has EMPTY property (i.e. it is string or BLOB type) */ static bool hasEmptyProperty(uint type); /*! \return true if this field can be auto-incremented. Actually, returns true for integer field type. \sa IntegerType, isAutoIncrement() */ inline bool isAutoIncrementAllowed() const { return Field::isAutoIncrementAllowed(type()); } /*! static version of isAutoIncrementAllowed() method \return true if this field type can be auto-incremented. */ static bool isAutoIncrementAllowed(uint type); /*! Sets type \a t for this field. This does nothing if there's already expression assigned, see expression(). */ void setType(Type t); /*! Sets name \a name for this field. */ void setName(const QString& name); /*! Sets constraints to \a c. If PrimaryKey is set in \a c, also constraits implied by being primary key are enforced (see setPrimaryKey()). If Indexed is not set in \a c, constraits implied by not being are enforced as well (see setIndexed()). */ void setConstraints(uint c); /*! Sets scale for this field. Only works for floating-point types. @see scale() */ void setScale(uint s); /*! Sets number of decimal places that should be visible to the user. @see visibleDecimalPlaces() */ void setVisibleDecimalPlaces(int p); /*! Sets scale for this field. Only works for floating-point types. */ void setPrecision(uint p); /*! Sets unsigned flag for this field. Only works for integer types. */ void setUnsigned(bool u); /*! Sets default value for this field. Setting null value removes the default value. @see defaultValue() */ void setDefaultValue(const QVariant& def); /*! Sets default value decoded from QByteArray. Decoding errors are detected (value is strictly checked against field type) - if one is encountered, default value is cleared (defaultValue()==QVariant()). \return true if given value was valid for field type. */ bool setDefaultValue(const QByteArray& def); /*! Sets auto increment flag. Only available to set true, if isAutoIncrementAllowed() is true. */ void setAutoIncrement(bool a); /*! Specifies whether the field is single-field primary key or not (KexiDB::PrimeryKey item). Use this with caution. Setting this to true implies setting: - setUniqueKey(true) - setNotNull(true) - setNotEmpty(true) - setIndexed(true) Setting this to false implies setting setAutoIncrement(false). */ void setPrimaryKey(bool p); /*! Specifies whether the field has single-field unique constraint or not (KexiDB::Unique item). Setting this to true implies setting Indexed flag to true (setIndexed(true)), because index is required it control unique constraint. */ void setUniqueKey(bool u); /*! Sets whether the field has to be declared with single-field foreign key. Used in IndexSchema::setForeigKey(). */ void setForeignKey(bool f); /*! Specifies whether the field has single-field unique constraint or not (KexiDB::NotNull item). Setting this to true implies setting Indexed flag to true (setIndexed(true)), because index is required it control not null constraint. */ void setNotNull(bool n); /*! Specifies whether the field has single-field unique constraint or not (KexiDB::NotEmpty item). Setting this to true implies setting Indexed flag to true (setIndexed(true)), because index is required it control not empty constraint. */ void setNotEmpty(bool n); /*! Specifies whether the field is indexed (KexiDB::Indexed item) (by single-field implicit index) or not. Use this with caution. Since index is used to control unique, not null/empty constratins, setting this to false implies setting: - setPrimaryKey(false) - setUniqueKey(false) - setNotNull(false) - setNotEmpty(false) because above flags need index to be present. Similarly, setting one of the above flags to true, will automatically do setIndexed(true) for the same reason. */ void setIndexed(bool s); /*! Sets caption for this field to \a caption. */ void setCaption(const QString& caption) { m_caption = caption; } /*! Sets description for this field to \a description. */ void setDescription(const QString& description) { m_desc = description; } /*! There can be added asterisks (QueryAsterisk objects) to query schemas' field list. QueryAsterisk subclasses Field class, and to check if the given object (pointed by Field*) is asterisk or just ordinary field definition, you can call this method. This is just effective version of QObject::isA(). Every QueryAsterisk object returns true here, and every Field object returns false. */ inline bool isQueryAsterisk() const { return m_type == Field::Asterisk; } /*! \return string for debugging purposes. */ virtual QString debugString() const; /*! Shows debug information about this field. */ void debug() const; /*! \return KexiDB::BaseExpr object if the field value is an expression. Unless the expression is set with setExpression(), it is null. */ inline KexiDB::BaseExpr *expression() { return m_expr; } /*! Sets expression data \a expr. If there was already expression set, it is destroyed before new assignment. This Field object becames owner of \a expr object, so you do not have to worry about deleting it later. If the \a expr is null, current field's expression is deleted, if exists. Because the field defines an expression, it should be assigned to a query, not to a table. */ void setExpression(KexiDB::BaseExpr *expr); /*! \return true if there is expression defined for this field. This method is provided for better readibility - does the same as expression()!=NULL but */ inline bool isExpression() const { return m_expr != NULL; } // /*! \return the hints for enum fields. */ QVector enumHints() const { return m_hints; } QString enumHint(uint num) { return (num < (uint)m_hints.size()) ? m_hints.at(num) : QString(); } /*! sets the hint for enum fields */ void setEnumHints(const QVector &l) { m_hints = l; } // /*! \return custom property \a propertyName. If there is no such a property, \a defaultValue is returned. */ QVariant customProperty(const QByteArray& propertyName, const QVariant& defaultValue = QVariant()) const; //! Sets value \a value for custom property \a propertyName void setCustomProperty(const QByteArray& propertyName, const QVariant& value); //! A data type used for handling custom properties of a field typedef QHash CustomPropertiesMap; //! \return all custom properties inline const CustomPropertiesMap customProperties() const { return m_customProperties ? *m_customProperties : CustomPropertiesMap(); } protected: /*! Creates a database field as a child of \a querySchema table Assigns \a expr expression to this field, if present. Used internally by query schemas, e.g. to declare asterisks or to add expression columns. No other properties are set, so these should be set later. */ Field(QuerySchema *querySchema, BaseExpr* expr = 0); /*! @internal Used by constructors. */ void init(); //! \return a deep copy of this object. Used in @ref FieldList(const FieldList& fl). virtual Field* copy() const; FieldList *m_parent; //!< In most cases this points to a TableSchema //!< object that field is assigned. QString m_name; QString m_subType; uint m_constraints; MaxLengthStrategy m_maxLengthStrategy; uint m_maxLength; //!< also used for storing scale for floating point types uint m_precision; int m_visibleDecimalPlaces; //!< used in visibleDecimalPlaces() uint m_options; QVariant m_defaultValue; int m_order; QString m_caption; QString m_desc; QVector m_hints; KexiDB::BaseExpr *m_expr; CustomPropertiesMap* m_customProperties; //! @internal Used in m_typeNames member to handle i18n'd type names class CALLIGRADB_EXPORT FieldTypeNames : public QVector { public: FieldTypeNames(); void init(); QHash str2num; QStringList names; protected: bool m_initialized : 1; }; //! @internal Used in m_typeGroupNames member to handle i18n'd type group names class CALLIGRADB_EXPORT FieldTypeGroupNames : public QVector { public: FieldTypeGroupNames(); void init(); QHash str2num; QStringList names; protected: bool m_initialized : 1; }; //! real i18n'd type names (and not-i18n'd type name strings) static FieldTypeNames m_typeNames; //! real i18n'd type group names (and not-i18n'd group name strings) static FieldTypeGroupNames m_typeGroupNames; private: Type m_type; friend class Connection; friend class FieldList; friend class TableSchema; friend class QuerySchema; }; } //namespace KexiDB #endif diff --git a/libs/db/lookupfieldschema.cpp b/libs/db/lookupfieldschema.cpp index e326254f3e6..30ca55f5ad9 100644 --- a/libs/db/lookupfieldschema.cpp +++ b/libs/db/lookupfieldschema.cpp @@ -1,560 +1,680 @@ /* This file is part of the KDE project - Copyright (C) 2006-2007 Jarosław Staniek + Copyright (C) 2006-2012 Jarosław Staniek This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "lookupfieldschema.h" #include "utils.h" #include #include #include #include namespace KexiDB { //! @internal class LookupFieldSchema::RowSource::Private { public: Private() : type(LookupFieldSchema::RowSource::NoType) { } LookupFieldSchema::RowSource::Type type; QString name; QStringList values; }; //! @internal class LookupFieldSchema::Private { public: Private() : boundColumn(-1) , maximumListRows(KEXIDB_LOOKUP_FIELD_DEFAULT_LIST_ROWS) , displayWidget(KEXIDB_LOOKUP_FIELD_DEFAULT_DISPLAY_WIDGET) , columnHeadersVisible(KEXIDB_LOOKUP_FIELD_DEFAULT_HEADERS_VISIBLE) , limitToList(KEXIDB_LOOKUP_FIELD_DEFAULT_LIMIT_TO_LIST) { } RowSource rowSource; int boundColumn; QList visibleColumns; QList columnWidths; uint maximumListRows; DisplayWidget displayWidget; bool columnHeadersVisible : 1; bool limitToList : 1; }; } //---------------------------- using namespace KexiDB; LookupFieldSchema::RowSource::RowSource() : d(new Private) { } LookupFieldSchema::RowSource::RowSource(const RowSource& other) : d(new Private) { *d = *other.d; } LookupFieldSchema::RowSource::~RowSource() { delete d; } LookupFieldSchema::RowSource::Type LookupFieldSchema::RowSource::type() const { return d->type; } void LookupFieldSchema::RowSource::setType(Type type) { d->type = type; } QString LookupFieldSchema::RowSource::name() const { return d->name; } void LookupFieldSchema::RowSource::setName(const QString& name) { d->name = name; d->values.clear(); } QString LookupFieldSchema::RowSource::typeName() const { switch (d->type) { case Table: return "table"; case Query: return "query"; case SQLStatement: return "sql"; case ValueList: return "valuelist"; case FieldList: return "fieldlist"; default:; } return QString(); } void LookupFieldSchema::RowSource::setTypeByName(const QString& typeName) { if (typeName == "table") setType(Table); else if (typeName == "query") setType(Query); else if (typeName == "sql") setType(SQLStatement); else if (typeName == "valuelist") setType(ValueList); else if (typeName == "fieldlist") setType(FieldList); else setType(NoType); } QStringList LookupFieldSchema::RowSource::values() const { return d->values; } void LookupFieldSchema::RowSource::setValues(const QStringList& values) { d->name.clear(); d->values = values; } LookupFieldSchema::RowSource& LookupFieldSchema::RowSource::operator=(const RowSource & other) { if (this != &other) { *d = *other.d; } return *this; } QString LookupFieldSchema::RowSource::debugString() const { - return QString("rowSourceType:'%1' rowSourceName:'%2' rowSourceValues:'%3'\n") + return QString("rowSourceType:'%1' rowSourceName:'%2' rowSourceValues:'%3'") .arg(typeName()).arg(name()).arg(d->values.join("|")); } void LookupFieldSchema::RowSource::debug() const { KexiDBDbg << debugString(); } //---------------------------- LookupFieldSchema::LookupFieldSchema() : d(new Private) { } +LookupFieldSchema::LookupFieldSchema(const LookupFieldSchema &schema) +: d(new Private) +{ + *d = *schema.d; +} + LookupFieldSchema::~LookupFieldSchema() { delete d; } LookupFieldSchema::RowSource& LookupFieldSchema::rowSource() const { return d->rowSource; } void LookupFieldSchema::setRowSource(const LookupFieldSchema::RowSource& rowSource) { d->rowSource = rowSource; } void LookupFieldSchema::setMaximumListRows(uint rows) { if (rows == 0) d->maximumListRows = KEXIDB_LOOKUP_FIELD_DEFAULT_LIST_ROWS; else if (rows > KEXIDB_LOOKUP_FIELD_MAX_LIST_ROWS) d->maximumListRows = KEXIDB_LOOKUP_FIELD_MAX_LIST_ROWS; else d->maximumListRows = rows; } -QString LookupFieldSchema::debugString() const +QString LookupFieldSchema::debugString(const QString &fieldName) const { QString columnWidthsStr; for (QList::ConstIterator it = d->columnWidths.constBegin(); it != d->columnWidths.constEnd();++it) { if (!columnWidthsStr.isEmpty()) columnWidthsStr.append(";"); columnWidthsStr.append(QString::number(*it)); } QString visibleColumnsString; foreach(uint visibleColumn, d->visibleColumns) { if (!visibleColumnsString.isEmpty()) visibleColumnsString.append(";"); visibleColumnsString.append(QString::number(visibleColumn)); } - return QString("LookupFieldSchema( %1\n" - " boundColumn:%2 visibleColumns:%3 maximumListRows:%4 displayWidget:%5\n" - " columnHeadersVisible:%6 limitToList:%7\n" - " columnWidths:%8 )") + return QString("LookupFieldSchema( field:'%1 %2" + " boundColumn:%3 visibleColumns:%4 maximumListRows:%5 displayWidget:%6" + " columnHeadersVisible:%7 limitToList:%8" + " columnWidths:%9 )") + .arg(fieldName) .arg(d->rowSource.debugString()) .arg(d->boundColumn).arg(visibleColumnsString).arg(d->maximumListRows) .arg(d->displayWidget == ComboBox ? "ComboBox" : "ListBox") .arg(d->columnHeadersVisible).arg(d->limitToList) .arg(columnWidthsStr); } -void LookupFieldSchema::debug() const +void LookupFieldSchema::debug(const QString &fieldName) const { - KexiDBDbg << debugString(); + KexiDBDbg << debugString(fieldName); +} + +static bool setBoundColumn(LookupFieldSchema *lookup, const QVariant &val) +{ + if (val.isNull()) { + lookup->setBoundColumn(-1); + } + else { + bool ok; + const int ival = val.toInt(&ok); + if (!ok) + return false; + lookup->setBoundColumn(ival); + } + return true; +} + +static bool setVisibleColumns(LookupFieldSchema *lookup, const QVariant &val) +{ + QList variantList; + if (val.canConvert(QVariant::Int)) { + //! @todo Remove this case: it's for backward compatibility with Kexi's 1.1.2 table designer GUI + //! supporting only single lookup column. + variantList.append(val); + } + else { + variantList = val.toList(); + } + QList visibleColumns; + foreach(const QVariant& variant, variantList) { + bool ok; + const uint ival = variant.toUInt(&ok); + if (!ok) { + return false; + } + visibleColumns.append(ival); + } + lookup->setVisibleColumns(visibleColumns); + return true; +} + +static bool setColumnWidths(LookupFieldSchema *lookup, const QVariant &val) +{ + QList widths; + foreach(const QVariant& variant, val.toList()) { + bool ok; + const uint ival = variant.toInt(&ok); + if (!ok) + return false; + widths.append(ival); + } + lookup->setColumnWidths(widths); + return true; +} + +static bool setDisplayWidget(LookupFieldSchema *lookup, const QVariant &val) +{ + bool ok; + const uint ival = val.toUInt(&ok); + if (!ok || ival > LookupFieldSchema::ListBox) + return false; + lookup->setDisplayWidget(static_cast(ival)); + return true; } /* static */ LookupFieldSchema *LookupFieldSchema::loadFromDom(const QDomElement& lookupEl) { LookupFieldSchema *lookupFieldSchema = new LookupFieldSchema(); for (QDomNode node = lookupEl.firstChild(); !node.isNull(); node = node.nextSibling()) { QDomElement el = node.toElement(); QString name(el.tagName()); if (name == "row-source") { /* empty | table|query|sql|valuelist|fieldlist #required because there can be table and query with the same name "fieldlist" (basically a list of column names of a table/query, "Field List" as in MSA) string #table/query name, etc. or KEXISQL SELECT QUERY ... #for "valuelist" type ... */ for (el = el.firstChild().toElement(); !el.isNull(); el = el.nextSibling().toElement()) { if (el.tagName() == "type") lookupFieldSchema->rowSource().setTypeByName(el.text()); else if (el.tagName() == "name") lookupFieldSchema->rowSource().setName(el.text()); //! @todo handle fieldlist (retrieve from external table or so?), use lookupFieldSchema.rowSource().setValues() } } else if (name == "bound-column") { /* number #in later implementation there can be more columns */ bool ok; const QVariant val = KexiDB::loadPropertyValueFromDom(el.firstChild(), &ok); - if (!ok) { + if (!ok || !::setBoundColumn(lookupFieldSchema, val)) { delete lookupFieldSchema; return 0; } - if (val.type() == QVariant::Int) - lookupFieldSchema->setBoundColumn(val.toInt()); } else if (name == "visible-column") { /* #a column that has to be visible in the combo box number 1 number 2 [..] */ - QList list; + QVariantList list; for (QDomNode childNode = el.firstChild(); !childNode.isNull(); childNode = childNode.nextSibling()) { bool ok; const QVariant val = KexiDB::loadPropertyValueFromDom(childNode, &ok); if (!ok) { delete lookupFieldSchema; return 0; } - if (val.type() == QVariant::Int) - list.append(val.toUInt()); + list.append(val); + } + if (!::setVisibleColumns(lookupFieldSchema, list)) { + delete lookupFieldSchema; + return 0; } - lookupFieldSchema->setVisibleColumns(list); } else if (name == "column-widths") { /* #column widths, -1 means 'default' int ... int */ - QVariant val; - QList columnWidths; + QVariantList columnWidths; for (el = el.firstChild().toElement(); !el.isNull(); el = el.nextSibling().toElement()) { bool ok; QVariant val = KexiDB::loadPropertyValueFromDom(el, &ok); if (!ok) { delete lookupFieldSchema; return 0; } - if (val.type() == QVariant::Int) - columnWidths.append(val.toInt()); + columnWidths.append(val); + } + if (!::setColumnWidths(lookupFieldSchema, columnWidths)) { + delete lookupFieldSchema; + return 0; } - lookupFieldSchema->setColumnWidths(columnWidths); } else if (name == "show-column-headers") { /* true/false */ bool ok; const QVariant val = KexiDB::loadPropertyValueFromDom(el.firstChild(), &ok); if (!ok) { delete lookupFieldSchema; return 0; } if (val.type() == QVariant::Bool) lookupFieldSchema->setColumnHeadersVisible(val.toBool()); } else if (name == "list-rows") { /* 1..100 */ bool ok; const QVariant val = KexiDB::loadPropertyValueFromDom(el.firstChild(), &ok); if (!ok) { delete lookupFieldSchema; return 0; } if (val.type() == QVariant::Int) lookupFieldSchema->setMaximumListRows(val.toUInt()); } else if (name == "limit-to-list") { /* true/false */ bool ok; const QVariant val = KexiDB::loadPropertyValueFromDom(el.firstChild(), &ok); if (!ok) { delete lookupFieldSchema; return 0; } if (val.type() == QVariant::Bool) lookupFieldSchema->setLimitToList(val.toBool()); } else if (name == "display-widget") { if (el.text() == "combobox") lookupFieldSchema->setDisplayWidget(LookupFieldSchema::ComboBox); else if (el.text() == "listbox") lookupFieldSchema->setDisplayWidget(LookupFieldSchema::ListBox); } } return lookupFieldSchema; } -/* static */ -void LookupFieldSchema::saveToDom(LookupFieldSchema& lookupSchema, QDomDocument& doc, QDomElement& parentEl) +void LookupFieldSchema::saveToDom(QDomDocument *doc, QDomElement *parentEl) const { QDomElement lookupColumnEl, rowSourceEl, rowSourceTypeEl, nameEl; - if (!lookupSchema.rowSource().name().isEmpty()) { - lookupColumnEl = doc.createElement("lookup-column"); - parentEl.appendChild(lookupColumnEl); + if (!rowSource().name().isEmpty()) { + lookupColumnEl = doc->createElement("lookup-column"); + parentEl->appendChild(lookupColumnEl); - rowSourceEl = doc.createElement("row-source"); + rowSourceEl = doc->createElement("row-source"); lookupColumnEl.appendChild(rowSourceEl); - rowSourceTypeEl = doc.createElement("type"); + rowSourceTypeEl = doc->createElement("type"); rowSourceEl.appendChild(rowSourceTypeEl); - rowSourceTypeEl.appendChild(doc.createTextNode(lookupSchema.rowSource().typeName())); //can be empty + rowSourceTypeEl.appendChild(doc->createTextNode(rowSource().typeName())); //can be empty - nameEl = doc.createElement("name"); + nameEl = doc->createElement("name"); rowSourceEl.appendChild(nameEl); - nameEl.appendChild(doc.createTextNode(lookupSchema.rowSource().name())); + nameEl.appendChild(doc->createTextNode(rowSource().name())); } - const QStringList& values(lookupSchema.rowSource().values()); + const QStringList values(rowSource().values()); if (!values.isEmpty()) { - QDomElement valuesEl(doc.createElement("values")); + QDomElement valuesEl(doc->createElement("values")); rowSourceEl.appendChild(valuesEl); for (QStringList::ConstIterator it = values.constBegin(); it != values.constEnd(); ++it) { - QDomElement valueEl(doc.createElement("value")); + QDomElement valueEl(doc->createElement("value")); valuesEl.appendChild(valueEl); - valueEl.appendChild(doc.createTextNode(*it)); + valueEl.appendChild(doc->createTextNode(*it)); } } - if (lookupSchema.boundColumn() >= 0) - KexiDB::saveNumberElementToDom(doc, lookupColumnEl, "bound-column", lookupSchema.boundColumn()); + if (boundColumn() >= 0) + KexiDB::saveNumberElementToDom(*doc, lookupColumnEl, "bound-column", boundColumn()); - QList visibleColumns(lookupSchema.visibleColumns()); + QList visibleColumns(this->visibleColumns()); if (!visibleColumns.isEmpty()) { - QDomElement visibleColumnEl(doc.createElement("visible-column")); + QDomElement visibleColumnEl(doc->createElement("visible-column")); lookupColumnEl.appendChild(visibleColumnEl); foreach(uint visibleColumn, visibleColumns) { - QDomElement numberEl(doc.createElement("number")); + QDomElement numberEl(doc->createElement("number")); visibleColumnEl.appendChild(numberEl); - numberEl.appendChild(doc.createTextNode(QString::number(visibleColumn))); + numberEl.appendChild(doc->createTextNode(QString::number(visibleColumn))); } } - const QList columnWidths(lookupSchema.columnWidths()); + const QList columnWidths(this->columnWidths()); if (!columnWidths.isEmpty()) { - QDomElement columnWidthsEl(doc.createElement("column-widths")); + QDomElement columnWidthsEl(doc->createElement("column-widths")); lookupColumnEl.appendChild(columnWidthsEl); foreach(int columnWidth, columnWidths) { - QDomElement columnWidthEl(doc.createElement("number")); + QDomElement columnWidthEl(doc->createElement("number")); columnWidthsEl.appendChild(columnWidthEl); - columnWidthEl.appendChild(doc.createTextNode(QString::number(columnWidth))); + columnWidthEl.appendChild(doc->createTextNode(QString::number(columnWidth))); } } - if (lookupSchema.columnHeadersVisible() != KEXIDB_LOOKUP_FIELD_DEFAULT_HEADERS_VISIBLE) - KexiDB::saveBooleanElementToDom(doc, lookupColumnEl, "show-column-headers", lookupSchema.columnHeadersVisible()); - if (lookupSchema.maximumListRows() != KEXIDB_LOOKUP_FIELD_DEFAULT_LIST_ROWS) - KexiDB::saveNumberElementToDom(doc, lookupColumnEl, "list-rows", lookupSchema.maximumListRows()); - if (lookupSchema.limitToList() != KEXIDB_LOOKUP_FIELD_DEFAULT_LIMIT_TO_LIST) - KexiDB::saveBooleanElementToDom(doc, lookupColumnEl, "limit-to-list", lookupSchema.limitToList()); + if (columnHeadersVisible() != KEXIDB_LOOKUP_FIELD_DEFAULT_HEADERS_VISIBLE) + KexiDB::saveBooleanElementToDom(*doc, lookupColumnEl, "show-column-headers", columnHeadersVisible()); + if (maximumListRows() != KEXIDB_LOOKUP_FIELD_DEFAULT_LIST_ROWS) + KexiDB::saveNumberElementToDom(*doc, lookupColumnEl, "list-rows", maximumListRows()); + if (limitToList() != KEXIDB_LOOKUP_FIELD_DEFAULT_LIMIT_TO_LIST) + KexiDB::saveBooleanElementToDom(*doc, lookupColumnEl, "limit-to-list", limitToList()); - if (lookupSchema.displayWidget() != KEXIDB_LOOKUP_FIELD_DEFAULT_DISPLAY_WIDGET) { - QDomElement displayWidgetEl(doc.createElement("display-widget")); + if (displayWidget() != KEXIDB_LOOKUP_FIELD_DEFAULT_DISPLAY_WIDGET) { + QDomElement displayWidgetEl(doc->createElement("display-widget")); lookupColumnEl.appendChild(displayWidgetEl); displayWidgetEl.appendChild( - doc.createTextNode((lookupSchema.displayWidget() == ListBox) ? "listbox" : "combobox")); + doc->createTextNode((displayWidget() == ListBox) ? "listbox" : "combobox")); } } -//static -bool LookupFieldSchema::setProperty( - LookupFieldSchema& lookup, const QByteArray& propertyName, const QVariant& value) +namespace KexiDB { +void getProperties(const LookupFieldSchema *lookup, QMap *values); +} + +void LookupFieldSchema::getProperties(QMap *values) const +{ + values->clear(); + KexiDB::getProperties(this, values); +} + +bool LookupFieldSchema::setProperty(const QByteArray& propertyName, const QVariant& value) { bool ok; - if ("rowSource" == propertyName - || "rowSourceType" == propertyName - || "rowSourceValues" == propertyName) { - LookupFieldSchema::RowSource rowSource(lookup.rowSource()); + if ( "rowSource" == propertyName + || "rowSourceType" == propertyName + || "rowSourceValues" == propertyName) + { + LookupFieldSchema::RowSource rowSource(this->rowSource()); if ("rowSource" == propertyName) rowSource.setName(value.toString()); else if ("rowSourceType" == propertyName) rowSource.setTypeByName(value.toString()); - else if ("rowSourceValues" == propertyName) + else if ("rowSourceValues" == propertyName) { + if (value.isNull()) { + return true; + } rowSource.setValues(value.toStringList()); - lookup.setRowSource(rowSource); + } + setRowSource(rowSource); } else if ("boundColumn" == propertyName) { - const int ival = value.toInt(&ok); - if (!ok) + if (!::setBoundColumn(this, value)) { return false; - lookup.setBoundColumn(ival); - } else if ("visibleColumn" == propertyName) { - QList variantList; - if (value.type() == QVariant::Int) { -//! @todo Remove this case: it's for backward compatibility with Kexi's 1.1.2 table designer GUI -//! supporting only single lookup column. - variantList.append(value.toInt()); - } else { - variantList = value.toList(); } - QList visibleColumns; - foreach(const QVariant& variant, variantList) { - const uint ival = variant.toUInt(&ok); - if (!ok) - return false; - visibleColumns.append(ival); + } else if ("visibleColumn" == propertyName) { + if (!::setVisibleColumns(this, value)) { + return false; } - lookup.setVisibleColumns(visibleColumns); } else if ("columnWidths" == propertyName) { - QList variantList(value.toList()); - QList widths; - foreach(const QVariant& variant, variantList) { - const uint ival = variant.toInt(&ok); - if (!ok) - return false; - widths.append(ival); + if (!::setColumnWidths(this, value)) { + return false; } - lookup.setColumnWidths(widths); } else if ("showColumnHeaders" == propertyName) { - lookup.setColumnHeadersVisible(value.toBool()); + setColumnHeadersVisible(value.toBool()); } else if ("listRows" == propertyName) { const uint ival = value.toUInt(&ok); if (!ok) return false; - lookup.setMaximumListRows(ival); + setMaximumListRows(ival); } else if ("limitToList" == propertyName) { - lookup.setLimitToList(value.toBool()); + setLimitToList(value.toBool()); } else if ("displayWidget" == propertyName) { - const uint ival = value.toUInt(&ok); - if (!ok || ival > LookupFieldSchema::ListBox) + if (!::setDisplayWidget(this, value)) { + return false; + } + } + return true; +} + +bool LookupFieldSchema::setProperties(const QMap& values) +{ + QMap::ConstIterator it; + LookupFieldSchema::RowSource rowSource(this->rowSource()); + bool ok; + bool updateRowSource = false; + if ((it = values.find("rowSource")) != values.constEnd()) { + rowSource.setName(it.value().toString()); + updateRowSource = true; + } + if ((it = values.find("rowSourceType")) != values.constEnd()) { + rowSource.setTypeByName(it.value().toString()); + updateRowSource = true; + } + if ((it = values.find("rowSourceValues")) != values.constEnd()) { + if (!it.value().isNull()) { + rowSource.setValues(it.value().toStringList()); + updateRowSource = true; + } + } + if (updateRowSource) { + setRowSource(rowSource); + } + if ((it = values.find("boundColumn")) != values.constEnd()) { + if (!::setBoundColumn(this, it.value())) { return false; - lookup.setDisplayWidget((LookupFieldSchema::DisplayWidget)ival); + } + } + if ((it = values.find("visibleColumn")) != values.constEnd()) { + if (!::setVisibleColumns(this, it.value())) { + return false; + } + } + if ((it = values.find("columnWidths")) != values.constEnd()) { + if (!::setColumnWidths(this, it.value())) { + return false; + } + } + if ((it = values.find("showColumnHeaders")) != values.constEnd()) { + setColumnHeadersVisible(it.value().toBool()); + } + if ((it = values.find("listRows")) != values.constEnd()) { + int ival = it.value().toInt(&ok); + if (!ok) + return false; + setMaximumListRows(ival); + } + if ((it = values.find("limitToList")) != values.constEnd()) { + setLimitToList(it.value().toBool()); + } + if ((it = values.find("displayWidget")) != values.constEnd()) { + if (!::setDisplayWidget(this, it.value())) { + return false; + } } return true; } int LookupFieldSchema::boundColumn() const { return d->boundColumn; } void LookupFieldSchema::setBoundColumn(int column) { d->boundColumn = column >= 0 ? column : -1; } QList LookupFieldSchema::visibleColumns() const { return d->visibleColumns; } void LookupFieldSchema::setVisibleColumns(const QList& list) { d->visibleColumns = list; } int LookupFieldSchema::visibleColumn(uint fieldsCount) const { if (d->visibleColumns.count() == 1) return (d->visibleColumns.first() < fieldsCount) ? (int)d->visibleColumns.first() : -1; if (d->visibleColumns.isEmpty()) return -1; return fieldsCount - 1; } QList LookupFieldSchema::columnWidths() const { return d->columnWidths; } void LookupFieldSchema::setColumnWidths(const QList& widths) { d->columnWidths = widths; } bool LookupFieldSchema::columnHeadersVisible() const { return d->columnHeadersVisible; } void LookupFieldSchema::setColumnHeadersVisible(bool set) { d->columnHeadersVisible = set; } uint LookupFieldSchema::maximumListRows() const { return d->maximumListRows; } bool LookupFieldSchema::limitToList() const { return d->limitToList; } void LookupFieldSchema::setLimitToList(bool set) { d->limitToList = set; } LookupFieldSchema::DisplayWidget LookupFieldSchema::displayWidget() const { return d->displayWidget; } void LookupFieldSchema::setDisplayWidget(DisplayWidget widget) { d->displayWidget = widget; } diff --git a/libs/db/lookupfieldschema.h b/libs/db/lookupfieldschema.h index 460567c15ba..d6d96ce606e 100644 --- a/libs/db/lookupfieldschema.h +++ b/libs/db/lookupfieldschema.h @@ -1,230 +1,243 @@ /* This file is part of the KDE project - Copyright (C) 2006-2007 Jarosław Staniek + Copyright (C) 2006-2012 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KEXIDB_LOOKUPFIELDSCHEMA_H #define KEXIDB_LOOKUPFIELDSCHEMA_H +#include + #include "global.h" class QStringList; class QDomElement; class QDomDocument; class QVariant; namespace KexiDB { //! default value for LookupFieldSchema::columnHeadersVisible() #define KEXIDB_LOOKUP_FIELD_DEFAULT_HEADERS_VISIBLE false //! default value for LookupFieldSchema::maximumListRows() #define KEXIDB_LOOKUP_FIELD_DEFAULT_LIST_ROWS 8 //! maximum value for LookupFieldSchema::maximumListRows() #define KEXIDB_LOOKUP_FIELD_MAX_LIST_ROWS 100 //! default value for LookupFieldSchema::limitToList() #define KEXIDB_LOOKUP_FIELD_DEFAULT_LIMIT_TO_LIST true //! default value for LookupFieldSchema::displayWidget() #define KEXIDB_LOOKUP_FIELD_DEFAULT_DISPLAY_WIDGET KexiDB::LookupFieldSchema::ComboBox //! @short Provides information about lookup field's setup. /*! LookupFieldSchema object is owned by TableSchema and created upon creating or retrieving the table schema from the database metadata. @see LookupFieldSchema *TableSchema::lookupFieldSchema( Field& field ) const */ class CALLIGRADB_EXPORT LookupFieldSchema { public: //! Row source information that can be specified for the lookup field schema class CALLIGRADB_EXPORT RowSource { public: //! Row source type enum Type { NoType, //!< used for invalid schema Table, //!< table as lookup row source Query, //!< named query as lookup row source SQLStatement, //!< anonymous query as lookup row source ValueList, //!< a fixed list of values as lookup row source FieldList //!< a list of column names from a table/query will be displayed }; RowSource(); RowSource(const RowSource& other); ~RowSource(); /*! @return row source type: table, query, anonymous; in the future it will be also fixed value list and field list. The latter is basically a list of column names of a table/query, "Field List" in MSA. */ Type type() const; /*! Sets row source type to \a type. */ void setType(Type type); /*! @return row source type name. @see setTypeByName() */ QString typeName() const; /*! Sets row source type by name using \a typeName. Accepted (case sensitive) names are "table", "query", "sql", "valuelist", "fieldlist". For other value NoType type is set. */ void setTypeByName(const QString& typeName); /*! @return a string for row source: table name, query name or anonymous query provided as KEXISQL string. If rowSourceType() is a ValueList, rowSourceValues() should be used instead. If rowSourceType() is a FieldList, rowSource() should return table or query name. */ QString name() const; /*! Sets row source value. @see value() */ void setName(const QString& name); /*! @return row source values specified if type() is ValueList. */ QStringList values() const; /*! Sets row source values used if type() is ValueList. Using it clears name (see name()). */ void setValues(const QStringList& values); //! Assigns other to this row source and returns a reference to this row source. RowSource& operator=(const RowSource& other); /*! \return String for debugging purposes. */ QString debugString() const; /*! Shows debug information. */ void debug() const; private: class Private; Private * const d; }; LookupFieldSchema(); + LookupFieldSchema(const LookupFieldSchema &schema); + ~LookupFieldSchema(); /*! @return row source information for the lookup field schema */ RowSource& rowSource() const; /*! Sets row source for the lookup field schema */ void setRowSource(const RowSource& rowSource); /*! @return bound column: an integer specifying a column that is bound (counted from 0). -1 means unspecified value. */ //! @todo in later implementation there can be more columns int boundColumn() const; /*! Sets bound column number to \a column. @see boundColumn() */ void setBoundColumn(int column); /*! @return a list of visible column: a list of integers specifying a column that has to be visible in the combo box (counted from 0). Empty list means unspecified value. */ QList visibleColumns() const; /*! Sets a list of visible columns to \a list. Columns will be separated with a single space character when displayed. */ void setVisibleColumns(const QList& list); /*! A helper method. If visibleColumns() contains one item, this item is returned (a typical case). If visibleColumns() contains no item, -1 is returned. If visibleColumns() multiple items, \a fieldsCount - 1 is returned. */ int visibleColumn(uint fieldsCount) const; /*! @return a number of ordered integers specifying column widths; -1 means 'default width' for a given column. */ QList columnWidths() const; /*! Sets column widths. @see columnWidths */ void setColumnWidths(const QList& widths); /*! @return true if column headers are visible in the associated combo box popup or the list view. The default is false. */ bool columnHeadersVisible() const; /*! Sets "column headers visibility" flag. @see columnHeadersVisible() */ void setColumnHeadersVisible(bool set); /*! @return integer property specifying a maximum number of rows that can be displayed in a combo box popup or a list box. The default is equal to KEXIDB_LOOKUP_FIELD_DEFAULT_LIST_ROWS constant. */ uint maximumListRows() const; /*! Sets maximum number of rows that can be displayed in a combo box popup or a list box. If \a rows is 0, KEXIDB_LOOKUP_FIELD_DEFAULT_LIST_ROWS is set. If \a rows is greater than KEXIDB_LOOKUP_FIELD_MAX_LIST_ROWS, KEXIDB_LOOKUP_FIELD_MAX_LIST_ROWS is set. */ void setMaximumListRows(uint rows); /*! @return true if , only values present on the list can be selected using the combo box. The default is true. */ bool limitToList() const; /*! Sets "limit to list" flag. @see limitToList() */ void setLimitToList(bool set); //! used in displayWidget() enum DisplayWidget { ComboBox = 0, //!< (the default) combobox widget should be displayed in forms for this lookup field ListBox = 1 //!< listbox widget should be displayed in forms for this lookup field }; /*! @return the widget type that should be displayed within the forms for this lookup field. The default is ComboBox. For the Table View, combo box is always displayed. */ DisplayWidget displayWidget() const; /*! Sets type of widget to display within the forms for this lookup field. @see displayWidget() */ void setDisplayWidget(DisplayWidget widget); - /*! \return String for debugging purposes. */ - QString debugString() const; + /*! \return String for debugging purposes. \a fieldName will be printed if provided. */ + QString debugString(const QString &fieldName = QString()) const; - /*! Shows debug information. */ - void debug() const; + /*! Shows debug information. \a fieldName will be printed if provided. */ + void debug(const QString &fieldName = QString()) const; /*! Loads data of lookup column schema from DOM tree. The data can be outdated or invalid, so the app should handle such cases. @return a new LookupFieldSchema object even if lookupEl contains no valid contents. */ static LookupFieldSchema* loadFromDom(const QDomElement& lookupEl); /*! Saves data of lookup column schema to \a parentEl DOM element of \a doc document. */ - static void saveToDom(LookupFieldSchema& lookupSchema, QDomDocument& doc, QDomElement& parentEl); + void saveToDom(QDomDocument *doc, QDomElement *parentEl) const; + + /*! Gets property values for the lookup schema. + \a values is cleared before filling. + This function is used e.g. for altering table design. */ + void getProperties(QMap *values) const; /*! Sets property of name \a propertyName and value \a value for the lookup schema \a lookup \return true on successful set and false on failure because of invalid value or invalid property name. */ - static bool setProperty( - LookupFieldSchema& lookup, const QByteArray& propertyName, - const QVariant& value); + bool setProperty(const QByteArray& propertyName, const QVariant& value); + + /*! Sets property values for the lookup schema. + Properties coming from extended schema are also supported. + Properties not listed are kept untouched. + This function is used e.g. for altering table design. + \return true on successful set and false on failure because of invalid value or invalid property name. */ + bool setProperties(const QMap& values); private: - Q_DISABLE_COPY(LookupFieldSchema) class Private; Private * const d; }; } //namespace KexiDB #endif diff --git a/libs/db/tableschema.cpp b/libs/db/tableschema.cpp index 4e5951687a4..35946f047c7 100644 --- a/libs/db/tableschema.cpp +++ b/libs/db/tableschema.cpp @@ -1,454 +1,454 @@ /* This file is part of the KDE project Copyright (C) 2003 Joseph Wenninger Copyright (C) 2003-2007 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "tableschema.h" #include "driver.h" #include "connection.h" #include "lookupfieldschema.h" #include #include namespace KexiDB { //! @internal class TableSchema::Private { public: Private() : anyNonPKField(0) { } ~Private() { clearLookupFields(); } void clearLookupFields() { qDeleteAll(lookupFields); lookupFields.clear(); } Field *anyNonPKField; QHash lookupFields; QVector lookupFieldsList; }; } //------------------------------------- using namespace KexiDB; TableSchema::TableSchema(const QString& name) : FieldList(true) , SchemaData(KexiDB::TableObjectType) , m_query(0) , d( new Private ) , m_isKexiDBSystem(false) { m_name = name; init(); } TableSchema::TableSchema(const SchemaData& sdata) : FieldList(true) , SchemaData(sdata) , m_query(0) , d( new Private ) , m_isKexiDBSystem(false) { init(); } TableSchema::TableSchema() : FieldList(true) , SchemaData(KexiDB::TableObjectType) , m_query(0) , d( new Private ) , m_isKexiDBSystem(false) { init(); } TableSchema::TableSchema(const TableSchema& ts, bool copyId) : FieldList(static_cast(ts)) , SchemaData(static_cast(ts)) , d( new Private ) { init(ts, copyId); } TableSchema::TableSchema(const TableSchema& ts, int setId) : FieldList(static_cast(ts)) , SchemaData(static_cast(ts)) , d( new Private ) { init(ts, false); m_id = setId; } // used by Connection TableSchema::TableSchema(Connection *conn, const QString & name) : FieldList(true) , SchemaData(KexiDB::TableObjectType) , m_conn(conn) , m_query(0) , d( new Private ) , m_isKexiDBSystem(false) { assert(conn); m_name = name; init(); } TableSchema::~TableSchema() { if (m_conn) m_conn->removeMe(this); qDeleteAll(m_indices); delete m_query; delete d; } void TableSchema::init() { //Qt 4 m_indices.setAutoDelete( true ); m_pkey = new IndexSchema(this); m_indices.append(m_pkey); } void TableSchema::init(const TableSchema& ts, bool copyId) { m_conn = ts.m_conn; m_query = 0; //not cached m_isKexiDBSystem = false; m_name = ts.m_name; //Qt 4 m_indices.setAutoDelete( true ); m_pkey = 0; //will be copied if (!copyId) m_id = -1; //deep copy all members foreach(IndexSchema* otherIdx, ts.m_indices) { IndexSchema *idx = new IndexSchema( *otherIdx, *this /*fields from _this_ table will be assigned to the index*/); if (idx->isPrimaryKey()) {//assign pkey m_pkey = idx; } m_indices.append(idx); } } void TableSchema::setPrimaryKey(IndexSchema *pkey) { if (m_pkey && m_pkey != pkey) { if (m_pkey->fieldCount() == 0) {//this is empty key, probably default - remove it m_indices.removeOne(m_pkey); delete m_pkey; } else { m_pkey->setPrimaryKey(false); //there can be only one pkey.. //thats ok, the old pkey is still on indices list, if not empty } // m_pkey=0; } if (!pkey) {//clearing - set empty pkey pkey = new IndexSchema(this); } m_pkey = pkey; //todo m_pkey->setPrimaryKey(true); d->anyNonPKField = 0; //for safety } FieldList& TableSchema::insertField(uint index, Field *field) { assert(field); FieldList::insertField(index, field); if (!field || index > (uint)m_fields.count()) return *this; field->setTable(this); field->m_order = index; //m_fields.count(); //update order for next next fields uint fieldsCount = m_fields.count(); for (uint i = index + 1; i < fieldsCount; i++) m_fields.at(i)->m_order = i; //Check for auto-generated indices: IndexSchema *idx = 0; if (field->isPrimaryKey()) {// this is auto-generated single-field unique index idx = new IndexSchema(this); idx->setAutoGenerated(true); idx->addField(field); setPrimaryKey(idx); } if (field->isUniqueKey()) { if (!idx) { idx = new IndexSchema(this); idx->setAutoGenerated(true); idx->addField(field); } idx->setUnique(true); } if (field->isIndexed()) {// this is auto-generated single-field if (!idx) { idx = new IndexSchema(this); idx->setAutoGenerated(true); idx->addField(field); } } if (idx) m_indices.append(idx); return *this; } bool TableSchema::removeField(KexiDB::Field *field) { LookupFieldSchema* lookup = d->lookupFields.take(field); if (!FieldList::removeField(field)) { return false; } if (d->anyNonPKField && field == d->anyNonPKField) //d->anyNonPKField will be removed! d->anyNonPKField = 0; delete lookup; return true; } #if 0 //original KexiDB::FieldList& TableSchema::addField(KexiDB::Field* field) { assert(field); FieldList::addField(field); field->setTable(this); field->m_order = m_fields.count(); //Check for auto-generated indices: IndexSchema *idx = 0; if (field->isPrimaryKey()) {// this is auto-generated single-field unique index idx = new IndexSchema(this); idx->setAutoGenerated(true); idx->addField(field); setPrimaryKey(idx); } if (field->isUniqueKey()) { if (!idx) { idx = new IndexSchema(this); idx->setAutoGenerated(true); idx->addField(field); } idx->setUnique(true); } if (field->isIndexed()) {// this is auto-generated single-field if (!idx) { idx = new IndexSchema(this); idx->setAutoGenerated(true); idx->addField(field); } } if (idx) m_indices.append(idx); return *this; } #endif void TableSchema::clear() { m_indices.clear(); d->clearLookupFields(); FieldList::clear(); SchemaData::clear(); m_conn = 0; } /* void TableSchema::addPrimaryKey(const QString& key) { m_primaryKeys.append(key); }*/ /*QStringList TableSchema::primaryKeys() const { return m_primaryKeys; } bool TableSchema::hasPrimaryKeys() const { return !m_primaryKeys.isEmpty(); } */ //const QString& TableSchema::name() const //{ // return m_name; //} //void TableSchema::setName(const QString& name) //{ // m_name=name; /* ListIterator it( m_fields ); Field *field; for (; (field = it.current())!=0; ++it) { int fcnt=m_fields.count(); for (int i=0;idebugString()); + s.append(QLatin1String("\n") + lookupSchema->debugString(f->name())); } return s; } Connection* TableSchema::connection() const { return (Connection*)m_conn; } void TableSchema::setKexiDBSystem(bool set) { if (set) m_native = true; m_isKexiDBSystem = set; } void TableSchema::setNative(bool set) { if (m_isKexiDBSystem && !set) { KexiDBWarn << "TableSchema::setNative(): cannot set native off" " when KexiDB system flag is set on!"; return; } m_native = set; } QuerySchema* TableSchema::query() { if (m_query) return m_query; m_query = new QuerySchema(*this); //it's owned by me return m_query; } Field* TableSchema::anyNonPKField() { if (!d->anyNonPKField) { Field *f = 0; for (QListIterator it(m_fields); it.hasPrevious();) { f = it.previous(); if (!f->isPrimaryKey() && (!m_pkey || !m_pkey->hasField(f))) break; } d->anyNonPKField = f; } return d->anyNonPKField; } bool TableSchema::setLookupFieldSchema(const QString& fieldName, LookupFieldSchema *lookupFieldSchema) { Field *f = field(fieldName); if (!f) { KexiDBWarn << "TableSchema::setLookupFieldSchema(): no such field '" << fieldName << "' in table " << name(); return false; } - if (lookupFieldSchema) + delete d->lookupFields.take(f); + if (lookupFieldSchema) { d->lookupFields.insert(f, lookupFieldSchema); - else - delete d->lookupFields.take(f); + } d->lookupFieldsList.clear(); //this will force to rebuid the internal cache return true; } LookupFieldSchema *TableSchema::lookupFieldSchema(const Field& field) const { return d->lookupFields.value(&field); } LookupFieldSchema *TableSchema::lookupFieldSchema(const QString& fieldName) { Field *f = TableSchema::field(fieldName); if (!f) return 0; return lookupFieldSchema(*f); } -const QVector& TableSchema::lookupFieldsList() +QVector TableSchema::lookupFields() const { if (d->lookupFields.isEmpty()) - return d->lookupFieldsList; + return QVector(); if (!d->lookupFields.isEmpty() && !d->lookupFieldsList.isEmpty()) return d->lookupFieldsList; //already updated //update d->lookupFieldsList.clear(); d->lookupFieldsList.resize(d->lookupFields.count()); uint i = 0; foreach(Field* f, m_fields) { QHash::ConstIterator itMap = d->lookupFields.constFind(f); if (itMap != d->lookupFields.constEnd()) { d->lookupFieldsList.insert(i, itMap.value()); i++; } } return d->lookupFieldsList; } //-------------------------------------- InternalTableSchema::InternalTableSchema(const QString& name) : TableSchema(name) { } InternalTableSchema::InternalTableSchema(const TableSchema& ts) : TableSchema(ts, false) { } InternalTableSchema::~InternalTableSchema() { } diff --git a/libs/db/tableschema.h b/libs/db/tableschema.h index 3a1084a8ad4..aa54041cf2d 100644 --- a/libs/db/tableschema.h +++ b/libs/db/tableschema.h @@ -1,220 +1,220 @@ /* This file is part of the KDE project Copyright (C) 2003 Joseph Wenninger Copyright (C) 2003-2006 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KEXIDB_TABLE_H #define KEXIDB_TABLE_H #include #include #include #include #include "fieldlist.h" #include "schemadata.h" #include "indexschema.h" #include "relationship.h" namespace KexiDB { class Connection; class LookupFieldSchema; /*! KexiDB::TableSchema provides information about native database table that can be stored using KexiDB database engine. */ class CALLIGRADB_EXPORT TableSchema : public FieldList, public SchemaData { public: typedef QList List; //!< Type of tables list typedef QList::ConstIterator ListIterator; //!< Iterator for tables list TableSchema(const QString & name); TableSchema(const SchemaData& sdata); TableSchema(); /*! Copy constructor. if \a copyId is true, it's copied as well, otherwise the table id becomes -1, what is usable when we want to store the copy as an independent table. */ TableSchema(const TableSchema& ts, bool copyId = true); /*! Copy constructor like \ref TableSchema(const TableSchema&, bool). \a setId is set as the table identifier. This is rarely usable, e.g. in project and data migration routines when we need to need deal with unique identifiers; @see KexiMigrate::performImport(). */ TableSchema(const TableSchema& ts, int setId); virtual ~TableSchema(); /*! Inserts \a field into a specified position (\a index). 'order' property of \a field is set automatically. */ virtual FieldList& insertField(uint index, Field *field); /*! Reimplemented for internal reasons. */ virtual bool removeField(KexiDB::Field *field); /*! \return list of fields that are primary key of this table. This method never returns 0 value, if there is no primary key, empty IndexSchema object is returned. IndexSchema object is owned by the table schema. */ IndexSchema* primaryKey() const { return m_pkey; } /*! Sets table's primary key index to \a pkey. Pass pkey==0 if you want to unassign existing primary key ("primary" property of given IndexSchema object will be cleared then so this index becomes ordinary index, still existing on table indeices list). If this table already has primary key assigned, it is unassigned using setPrimaryKey(0) call. Before assigning as primary key, you should add the index to indices list with addIndex() (this is not done automatically!). */ void setPrimaryKey(IndexSchema *pkey); const IndexSchema::ListIterator indicesIterator() const { return IndexSchema::ListIterator(m_indices.constBegin()); } const IndexSchema::List* indices() const { return &m_indices; } /*! Removes all fields from the list, clears name and all other properties. \sa FieldList::clear() */ virtual void clear(); /*! \return String for debugging purposes, if \a includeTableName is true, table name, caption, etc. is prepended, else only debug string for the fields are returned. */ QString debugString(bool includeTableName); /*! \return String for debugging purposes. Equal to debugString(true). */ virtual QString debugString(); /*! \return connection object if table was created/retrieved using a connection, otherwise 0. */ Connection* connection() const; /*! \return true if this is KexiDB storage system's table (used internally by KexiDB). This helps in hiding such tables in applications (if desired) and will also enable lookup of system tables for schema export/import functionality. Any internal KexiDB system table's schema (kexi__*) has cleared its SchemaData part, e.g. id=-1 for such table, and no description, caption and so on. This is because it represents a native database table rather that extended Kexi table. isKexiDBSystem()==true implies isNative()==true. By default (after allocation), TableSchema object has this property set to false. */ bool isKexiDBSystem() const { return m_isKexiDBSystem; } /*! Sets KexiDBSystem flag to on or off. When on, native flag is forced to be on. When off, native flag is not affected. \sa isKexiDBSystem() */ void setKexiDBSystem(bool set); /*! \return true if this is schema of native database object, When this is kexiDBSystem table, native flag is forced to be on. */ virtual bool isNative() const { return m_native || m_isKexiDBSystem; } /* Sets native flag. Does not allow to set this off for system KexiDB table. */ virtual void setNative(bool set); /*! \return query schema object that is defined by "select * from " This query schema object is owned by the table schema object. It is convenient way to get such a query when it is not available otherwise. Always returns non-0. */ QuerySchema* query(); /*! \return any field not being a part of primary key of this table. If there is no such field, returns 0. */ Field* anyNonPKField(); /*! Sets lookup field schema \a lookupFieldSchema for \a fieldName. Passing null \a lookupFieldSchema will remove the previously set lookup field. \return true if \a lookupFieldSchema has been added, or false if there is no such field \a fieldName. */ bool setLookupFieldSchema(const QString& fieldName, LookupFieldSchema *lookupFieldSchema); /*! \return lookup field schema for \a field. 0 is returned if there is no such field in the table or this field has no lookup schema. Note that even id non-zero is returned here, you may want to check whether lookup field's rowSource().name() is empty (if so, the field should behave as there was no lookup field defined at all). */ LookupFieldSchema *lookupFieldSchema(const Field& field) const; /*! \overload LookupFieldSchema *TableSchema::lookupFieldSchema( Field& field ) const */ LookupFieldSchema *lookupFieldSchema(const QString& fieldName); /*! \return list of lookup field schemas for this table. The order is the same as the order of fields within the table. */ - const QVector& lookupFieldsList(); + QVector lookupFields() const; protected: /*! Automatically retrieves table schema via connection. */ TableSchema(Connection *conn, const QString & name = QString()); IndexSchema::List m_indices; QPointer m_conn; IndexSchema *m_pkey; QuerySchema *m_query; //!< cached query schema that is defined by "select * from " class Private; Private * const d; private: //! Used by some ctors. void init(); //! Used by some ctors. void init(const TableSchema& ts, bool copyId); bool m_isKexiDBSystem : 1; friend class Connection; }; /*! Internal table with a name \a name. Rarely used. Use Connection::createTable() to create a table using this schema. The table will not be visible as user table. For example, 'kexi__blobs' table is created this way by Kexi application. */ class CALLIGRADB_EXPORT InternalTableSchema : public TableSchema { public: InternalTableSchema(const QString& name); InternalTableSchema(const TableSchema& ts); virtual ~InternalTableSchema(); }; } //namespace KexiDB #endif diff --git a/libs/db/utils.cpp b/libs/db/utils.cpp index 32b7f452b1a..99bbc4c3988 100644 --- a/libs/db/utils.cpp +++ b/libs/db/utils.cpp @@ -1,1499 +1,1634 @@ /* This file is part of the KDE project Copyright (C) 2004-2012 Jarosław Staniek Contains code from KConfigGroupPrivate from kconfiggroup.cpp (kdelibs 4) Copyright (c) 2006 Thomas Braxton Copyright (c) 1999 Preston Brown Copyright (c) 1997-1999 Matthias Kalle Dalheimer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "utils.h" #include "cursor.h" #include "drivermanager.h" #include "lookupfieldschema.h" #include "calligradb_global.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils_p.h" using namespace KexiDB; //! Cache struct TypeCache { TypeCache() { for (uint t = 0; t <= Field::LastType; t++) { const Field::TypeGroup tg = Field::typeGroup(t); TypeGroupList list; QStringList name_list, str_list; if (tlist.contains(tg)) { list = tlist.value(tg); name_list = nlist.value(tg); str_list = slist.value(tg); } list += t; name_list += KexiDB::Field::typeName(t); str_list += KexiDB::Field::typeString(t); tlist[ tg ] = list; nlist[ tg ] = name_list; slist[ tg ] = str_list; } def_tlist[ Field::InvalidGroup ] = Field::InvalidType; def_tlist[ Field::TextGroup ] = Field::Text; def_tlist[ Field::IntegerGroup ] = Field::Integer; def_tlist[ Field::FloatGroup ] = Field::Double; def_tlist[ Field::BooleanGroup ] = Field::Boolean; def_tlist[ Field::DateTimeGroup ] = Field::Date; def_tlist[ Field::BLOBGroup ] = Field::BLOB; } QHash< Field::TypeGroup, TypeGroupList > tlist; QHash< Field::TypeGroup, QStringList > nlist; QHash< Field::TypeGroup, QStringList > slist; QHash< Field::TypeGroup, Field::Type > def_tlist; }; K_GLOBAL_STATIC(TypeCache, KexiDB_typeCache) const TypeGroupList KexiDB::typesForGroup(KexiDB::Field::TypeGroup typeGroup) { return KexiDB_typeCache->tlist.value(typeGroup); } QStringList KexiDB::typeNamesForGroup(KexiDB::Field::TypeGroup typeGroup) { return KexiDB_typeCache->nlist.value(typeGroup); } QStringList KexiDB::typeStringsForGroup(KexiDB::Field::TypeGroup typeGroup) { return KexiDB_typeCache->slist.value(typeGroup); } KexiDB::Field::Type KexiDB::defaultTypeForGroup(KexiDB::Field::TypeGroup typeGroup) { return (typeGroup <= Field::LastTypeGroup) ? KexiDB_typeCache->def_tlist.value(typeGroup) : Field::InvalidType; } void KexiDB::getHTMLErrorMesage(Object* obj, QString& msg, QString &details) { Connection *conn = 0; if (!obj || !obj->error()) { if (dynamic_cast(obj)) { conn = dynamic_cast(obj)->connection(); obj = conn; } else { return; } } // if (dynamic_cast(obj)) { // conn = dynamic_cast(obj); //} if (!obj || !obj->error()) return; //lower level message is added to the details, if there is alread message specified if (!obj->msgTitle().isEmpty()) msg += "

" + obj->msgTitle(); if (msg.isEmpty()) msg = "

" + obj->errorMsg(); else details += "

" + obj->errorMsg(); if (!obj->serverErrorMsg().isEmpty()) details += "

" + i18n("Message from server:") + " " + obj->serverErrorMsg(); if (!obj->recentSQLString().isEmpty()) details += "

" + i18n("SQL statement:") + QString(" %1").arg(obj->recentSQLString()); int serverResult; QString serverResultName; if (obj->serverResult() != 0) { serverResult = obj->serverResult(); serverResultName = obj->serverResultName(); } else { serverResult = obj->previousServerResult(); serverResultName = obj->previousServerResultName(); } if ( !details.isEmpty() && ( !obj->serverErrorMsg().isEmpty() || !obj->recentSQLString().isEmpty() || !serverResultName.isEmpty() || serverResult != 0) ) { details += (QString("

") + i18n("Server result:") + " " + QString::number(serverResult)); if (!serverResultName.isEmpty()) { details += QString(" (%1)").arg(serverResultName); } } else { if (!serverResultName.isEmpty()) { details += (QString("

") + i18n("Server result:") + " " + serverResultName); } } if (!details.isEmpty() && !details.startsWith("")) { if (details.startsWith("

")) details = QString::fromLatin1("") + details; else details = QString::fromLatin1("

") + details; } } void KexiDB::getHTMLErrorMesage(Object* obj, QString& msg) { getHTMLErrorMesage(obj, msg, msg); } void KexiDB::getHTMLErrorMesage(Object* obj, ResultInfo *result) { getHTMLErrorMesage(obj, result->msg, result->desc); } int KexiDB::idForObjectName(Connection &conn, const QString& objName, int objType) { RecordData data; if (true != conn.querySingleRecord( QString::fromLatin1("select o_id from kexi__objects where lower(o_name)='%1' and o_type=%2") .arg(objName.toLower()).arg(objType), data)) return 0; bool ok; int id = data[0].toInt(&ok); return ok ? id : 0; } //----------------------------------------- TableOrQuerySchema::TableOrQuerySchema(Connection *conn, const QByteArray& name) : m_name(name) { m_table = conn->tableSchema(QString(name)); m_query = m_table ? 0 : conn->querySchema(QString(name)); if (!m_table && !m_query) KexiDBWarn << "TableOrQuery(FieldList &tableOrQuery) : " " tableOrQuery is neither table nor query!"; } TableOrQuerySchema::TableOrQuerySchema(Connection *conn, const QByteArray& name, bool table) : m_name(name) , m_table(table ? conn->tableSchema(QString(name)) : 0) , m_query(table ? 0 : conn->querySchema(QString(name))) { if (table && !m_table) KexiDBWarn << "TableOrQuery(Connection *conn, const QByteArray& name, bool table) : " "no table specified!"; if (!table && !m_query) KexiDBWarn << "TableOrQuery(Connection *conn, const QByteArray& name, bool table) : " "no query specified!"; } TableOrQuerySchema::TableOrQuerySchema(FieldList &tableOrQuery) : m_table(dynamic_cast(&tableOrQuery)) , m_query(dynamic_cast(&tableOrQuery)) { if (!m_table && !m_query) KexiDBWarn << "TableOrQuery(FieldList &tableOrQuery) : " " tableOrQuery is nether table nor query!"; } TableOrQuerySchema::TableOrQuerySchema(Connection *conn, int id) { m_table = conn->tableSchema(id); m_query = m_table ? 0 : conn->querySchema(id); if (!m_table && !m_query) KexiDBWarn << "TableOrQuery(Connection *conn, int id) : no table or query found for id==" << id << "!"; } TableOrQuerySchema::TableOrQuerySchema(TableSchema* table) : m_table(table) , m_query(0) { if (!m_table) KexiDBWarn << "TableOrQuery(TableSchema* table) : no table specified!"; } TableOrQuerySchema::TableOrQuerySchema(QuerySchema* query) : m_table(0) , m_query(query) { if (!m_query) KexiDBWarn << "TableOrQuery(QuerySchema* query) : no query specified!"; } uint TableOrQuerySchema::fieldCount() const { if (m_table) return m_table->fieldCount(); if (m_query) return m_query->fieldsExpanded().size(); return 0; } const QueryColumnInfo::Vector TableOrQuerySchema::columns(bool unique) { if (m_table) return m_table->query()->fieldsExpanded(unique ? QuerySchema::Unique : QuerySchema::Default); if (m_query) return m_query->fieldsExpanded(unique ? QuerySchema::Unique : QuerySchema::Default); KexiDBWarn << "TableOrQuery::fields() : no query or table specified!"; return QueryColumnInfo::Vector(); } QByteArray TableOrQuerySchema::name() const { if (m_table) return m_table->name().toLatin1(); if (m_query) return m_query->name().toLatin1(); return m_name; } QString TableOrQuerySchema::captionOrName() const { SchemaData *sdata = m_table ? static_cast(m_table) : static_cast(m_query); if (!sdata) return m_name; return sdata->caption().isEmpty() ? sdata->name() : sdata->caption(); } Field* TableOrQuerySchema::field(const QString& name) { if (m_table) return m_table->field(name); if (m_query) return m_query->field(name); return 0; } QueryColumnInfo* TableOrQuerySchema::columnInfo(const QString& name) { if (m_table) return m_table->query()->columnInfo(name); if (m_query) return m_query->columnInfo(name); return 0; } -QString TableOrQuerySchema::debugString() +QString TableOrQuerySchema::debugString() const { if (m_table) return m_table->debugString(); else if (m_query) return m_query->debugString(); return QString(); } -void TableOrQuerySchema::debug() +void TableOrQuerySchema::debug() const { if (m_table) return m_table->debug(); else if (m_query) return m_query->debug(); } Connection* TableOrQuerySchema::connection() const { if (m_table) return m_table->connection(); else if (m_query) return m_query->connection(); return 0; } //------------------------------------------ ConnectionTestThread::ConnectionTestThread(ConnectionTestDialog* dlg, const KexiDB::ConnectionData& connData) : m_dlg(dlg), m_connData(connData) { connect(this, SIGNAL(error(const QString&,const QString&)), dlg, SLOT(error(const QString&,const QString&)), Qt::QueuedConnection); // try to load driver now because it's not supported in different thread KexiDB::DriverManager manager; m_driver = manager.driver(m_connData.driverName); if (manager.error()) { emitError(&manager); m_driver = 0; } } void ConnectionTestThread::emitError(KexiDB::Object* object) { QString msg; QString details; KexiDB::getHTMLErrorMesage(object, msg, details); emit error(msg, details); } void ConnectionTestThread::run() { if (!m_driver) { return; } std::auto_ptr conn(m_driver->createConnection(m_connData)); if (!conn.get() || m_driver->error()) { //kDebug() << "err 1"; emitError(m_driver); return; } if (!conn.get()->connect() || conn.get()->error()) { //kDebug() << "err 2"; emitError(conn.get()); return; } // SQL database backends like PostgreSQL require executing "USE database" // if we really want to know connection to the server succeeded. QString tmpDbName; if (!conn->useTemporaryDatabaseIfNeeded(tmpDbName)) { //kDebug() << "err 3"; emitError(conn.get()); return; } //kDebug() << "emitError(0)"; emitError(0); } ConnectionTestDialog::ConnectionTestDialog(QWidget* parent, const KexiDB::ConnectionData& data, KexiDB::MessageHandler& msgHandler) : KProgressDialog(parent, i18n("Test Connection"), i18n("Testing connection to %1 database server...", data.serverInfoString(true)) ) , m_thread(new ConnectionTestThread(this, data)) , m_connData(data) , m_msgHandler(&msgHandler) , m_elapsedTime(0) , m_error(false) , m_stopWaiting(false) { setModal(true); showCancelButton(true); progressBar()->setFormat(""); //hide % progressBar()->setRange(0, 0); //to show busy indicator connect(&m_timer, SIGNAL(timeout()), this, SLOT(slotTimeout())); adjustSize(); resize(250, height()); } ConnectionTestDialog::~ConnectionTestDialog() { m_wait.wakeAll(); m_thread->terminate(); delete m_thread; } int ConnectionTestDialog::exec() { //kDebug() << "tid:" << QThread::currentThread() << "this_thread:" << thread(); m_timer.start(20); m_thread->start(); const int res = KProgressDialog::exec(); m_thread->wait(); m_timer.stop(); return res; } void ConnectionTestDialog::slotTimeout() { //kDebug() << "tid:" << QThread::currentThread() << "this_thread:" << thread(); KexiDBDbg << m_error; bool notResponding = false; if (m_elapsedTime >= 1000*5) {//5 seconds m_stopWaiting = true; notResponding = true; } KexiDBDbg << m_elapsedTime << m_stopWaiting << notResponding; if (m_stopWaiting) { m_timer.disconnect(this); m_timer.stop(); reject(); KexiDBDbg << "after reject"; // close(); if (m_error) { KexiDBDbg << "show?"; m_msgHandler->showErrorMessage(m_msg, m_details); //m_msgHandler->showErrorMessage(m_errorObj); KexiDBDbg << "shown"; m_error = false; } else if (notResponding) { KMessageBox::sorry(0, i18n("Test connection to %1 database server failed. The server is not responding.", m_connData.serverInfoString(true)), i18n("Test Connection")); } else { KMessageBox::information(0, i18n("Test connection to %1 database server established successfully.", m_connData.serverInfoString(true)), i18n("Test Connection")); } // slotCancel(); // reject(); m_wait.wakeAll(); return; } m_elapsedTime += 20; progressBar()->setValue(m_elapsedTime); } void ConnectionTestDialog::error(const QString& msg, const QString& details) { //kDebug() << "tid:" << QThread::currentThread() << "this_thread:" << thread(); kDebug() << msg << details; m_stopWaiting = true; if (!msg.isEmpty() || !details.isEmpty()) { m_error = true; m_msg = msg; m_details = details; kDebug() << "ERR!"; // m_msgHandler->showErrorMessage(msg, details); } } void ConnectionTestDialog::reject() { // m_wait.wakeAll(); m_thread->terminate(); m_timer.disconnect(this); m_timer.stop(); KProgressDialog::reject(); } void KexiDB::connectionTestDialog(QWidget* parent, const KexiDB::ConnectionData& data, KexiDB::MessageHandler& msgHandler) { ConnectionTestDialog dlg(parent, data, msgHandler); dlg.exec(); } int KexiDB::rowCount(Connection &conn, const QString& sql) { int count = -1; //will be changed only on success of querySingleNumber() QString selectSql(QString::fromLatin1("SELECT COUNT() FROM (") + sql + ") AS kexidb__subquery"); conn.querySingleNumber(selectSql, count); return count; } int KexiDB::rowCount(const KexiDB::TableSchema& tableSchema) { //! @todo does not work with non-SQL data sources if (!tableSchema.connection()) { KexiDBWarn << "KexiDB::rowsCount(const KexiDB::TableSchema&): no tableSchema.connection() !"; return -1; } int count = -1; //will be changed only on success of querySingleNumber() tableSchema.connection()->querySingleNumber( QString::fromLatin1("SELECT COUNT(*) FROM ") + tableSchema.connection()->driver()->escapeIdentifier(tableSchema.name()), count ); return count; } int KexiDB::rowCount(KexiDB::QuerySchema& querySchema) { //! @todo does not work with non-SQL data sources if (!querySchema.connection()) { KexiDBWarn << "KexiDB::rowsCount(const KexiDB::QuerySchema&): no querySchema.connection() !"; return -1; } int count = -1; //will be changed only on success of querySingleNumber() querySchema.connection()->querySingleNumber( QString::fromLatin1("SELECT COUNT(*) FROM (") + querySchema.connection()->selectStatement(querySchema) + ") AS kexidb__subquery", count ); return count; } int KexiDB::rowCount(KexiDB::TableOrQuerySchema& tableOrQuery) { if (tableOrQuery.table()) return rowCount(*tableOrQuery.table()); if (tableOrQuery.query()) return rowCount(*tableOrQuery.query()); return -1; } int KexiDB::fieldCount(KexiDB::TableOrQuerySchema& tableOrQuery) { if (tableOrQuery.table()) return tableOrQuery.table()->fieldCount(); if (tableOrQuery.query()) return tableOrQuery.query()->fieldsExpanded().count(); return -1; } QMap KexiDB::toMap(const ConnectionData& data) { QMap m; m["caption"] = data.caption; m["description"] = data.description; m["driverName"] = data.driverName; m["hostName"] = data.hostName; m["port"] = QString::number(data.port); m["useLocalSocketFile"] = QString::number((int)data.useLocalSocketFile); m["localSocketFileName"] = data.localSocketFileName; m["password"] = data.password; m["savePassword"] = QString::number((int)data.savePassword); m["userName"] = data.userName; m["fileName"] = data.fileName(); return m; } void KexiDB::fromMap(const QMap& map, ConnectionData& data) { data.caption = map["caption"]; data.description = map["description"]; data.driverName = map["driverName"]; data.hostName = map["hostName"]; data.port = map["port"].toInt(); data.useLocalSocketFile = map["useLocalSocketFile"].toInt() == 1; data.localSocketFileName = map["localSocketFileName"]; data.password = map["password"]; data.savePassword = map["savePassword"].toInt() == 1; data.userName = map["userName"]; data.setFileName(map["fileName"]); } bool KexiDB::splitToTableAndFieldParts(const QString& string, QString& tableName, QString& fieldName, SplitToTableAndFieldPartsOptions option) { const int id = string.indexOf('.'); if (option & SetFieldNameIfNoTableName && id == -1) { tableName.clear(); fieldName = string; return !fieldName.isEmpty(); } if (id <= 0 || id == int(string.length() - 1)) return false; tableName = string.left(id); fieldName = string.mid(id + 1); return !tableName.isEmpty() && !fieldName.isEmpty(); } bool KexiDB::supportsVisibleDecimalPlacesProperty(Field::Type type) { //! @todo add check for decimal type as well return Field::isFPNumericType(type); } QString KexiDB::formatNumberForVisibleDecimalPlaces(double value, int decimalPlaces) { //! @todo round? if (decimalPlaces < 0) { QString s(QString::number(value, 'f', 10 /*reasonable precision*/)); uint i = s.length() - 1; while (i > 0 && s[i] == '0') i--; if (s[i] == '.') //remove '.' i--; s = s.left(i + 1).replace('.', KGlobal::locale()->decimalSymbol()); return s; } if (decimalPlaces == 0) return QString::number((int)value); return KGlobal::locale()->formatNumber(value, decimalPlaces); } KexiDB::Field::Type KexiDB::intToFieldType(int type) { if (type < (int)KexiDB::Field::InvalidType || type > (int)KexiDB::Field::LastType) { KexiDBWarn << "KexiDB::intToFieldType(): invalid type " << type; return KexiDB::Field::InvalidType; } return (KexiDB::Field::Type)type; } static bool setIntToFieldType(Field& field, const QVariant& value) { bool ok; const int intType = value.toInt(&ok); if (!ok || KexiDB::Field::InvalidType == intToFieldType(intType)) {//for sanity KexiDBWarn << "KexiDB::setFieldProperties(): invalid type"; return false; } field.setType((KexiDB::Field::Type)intType); return true; } //! @internal for KexiDB::isBuiltinTableFieldProperty() struct KexiDB_BuiltinFieldProperties { KexiDB_BuiltinFieldProperties() { #define ADD(name) set.insert(name) ADD("type"); ADD("primaryKey"); ADD("indexed"); ADD("autoIncrement"); ADD("unique"); ADD("notNull"); ADD("allowEmpty"); ADD("unsigned"); ADD("name"); ADD("caption"); ADD("description"); ADD("maxLength"); ADD("maxLengthIsDefault"); ADD("precision"); ADD("defaultValue"); - ADD("width"); + ADD("defaultWidth"); ADD("visibleDecimalPlaces"); //! @todo always update this when new builtins appear! #undef ADD } QSet set; }; //! for KexiDB::isBuiltinTableFieldProperty() K_GLOBAL_STATIC(KexiDB_BuiltinFieldProperties, KexiDB_builtinFieldProperties) bool KexiDB::isBuiltinTableFieldProperty(const QByteArray& propertyName) { return KexiDB_builtinFieldProperties->set.contains(propertyName); } -bool KexiDB::setFieldProperties(Field& field, const QHash& values) +namespace KexiDB { +void getProperties(const LookupFieldSchema *lookup, QMap *values) { - QHash::ConstIterator it; + Q_ASSERT(values); + LookupFieldSchema::RowSource rowSource; + if (lookup) { + rowSource = lookup->rowSource(); + } + values->insert("rowSource", lookup ? rowSource.name() : QVariant()); + values->insert("rowSourceType", lookup ? rowSource.typeName() : QVariant()); + values->insert("rowSourceValues", + (lookup && !rowSource.values().isEmpty()) ? rowSource.values() : QVariant()); + values->insert("boundColumn", lookup ? lookup->boundColumn() : QVariant()); + QList variantList; + if (!lookup || lookup->visibleColumns().count() == 1) { + values->insert("visibleColumn", lookup ? lookup->visibleColumns().first() : QVariant()); + } + else { + QList visibleColumns = lookup->visibleColumns(); + foreach(const QVariant& variant, visibleColumns) { + variantList.append(variant); + } + values->insert("visibleColumn", variantList); + } + QList columnWidths; + variantList.clear(); + if (lookup) { + columnWidths = lookup->columnWidths(); + foreach(const QVariant& variant, columnWidths) { + variantList.append(variant); + } + } + values->insert("columnWidths", lookup ? variantList : QVariant()); + values->insert("showColumnHeaders", lookup ? lookup->columnHeadersVisible() : QVariant()); + values->insert("listRows", lookup ? lookup->maximumListRows() : QVariant()); + values->insert("limitToList", lookup ? lookup->limitToList() : QVariant()); + values->insert("displayWidget", lookup ? uint(lookup->displayWidget()) : QVariant()); +} +} // namespace KexiDB + +void KexiDB::getFieldProperties(const Field &field, QMap *values) +{ + Q_ASSERT(values); + values->clear(); + // normal values + values->insert("type", field.type()); + const uint constraints = field.constraints(); + values->insert("primaryKey", constraints & KexiDB::Field::PrimaryKey); + values->insert("indexed", constraints & KexiDB::Field::Indexed); + values->insert("autoIncrement", KexiDB::Field::isAutoIncrementAllowed(field.type()) + && (constraints & KexiDB::Field::AutoInc)); + values->insert("unique", constraints & KexiDB::Field::Unique); + values->insert("notNull", constraints & KexiDB::Field::NotNull); + values->insert("allowEmpty", !(constraints & KexiDB::Field::NotEmpty)); + const uint options = field.options(); + values->insert("unsigned", options & KexiDB::Field::Unsigned); + values->insert("name", field.name()); + values->insert("caption", field.caption()); + values->insert("description", field.description()); + values->insert("maxLength", field.maxLength()); + values->insert("maxLengthIsDefault", field.maxLengthStrategy() & Field::DefaultMaxLength); + values->insert("precision", field.precision()); + values->insert("defaultValue", field.defaultValue()); +#warning TODO values->insert("defaultWidth", field.defaultWidth()); + if (KexiDB::supportsVisibleDecimalPlacesProperty(field.type())) { + values->insert("visibleDecimalPlaces", field.defaultValue()); + } + // insert lookup-related values + LookupFieldSchema *lookup = field.table()->lookupFieldSchema(field); + KexiDB::getProperties(lookup, values); +} + +static bool containsLookupFieldSchemaProperties(const QMap& values) +{ + for (QMap::ConstIterator it(values.constBegin()); + it != values.constEnd(); ++it) + { + if (KexiDB::isLookupFieldSchemaProperty(it.key())) { + return true; + } + } + return false; +} + +bool KexiDB::setFieldProperties(Field& field, const QMap& values) +{ + QMap::ConstIterator it; if ((it = values.find("type")) != values.constEnd()) { if (!setIntToFieldType(field, *it)) return false; } #define SET_BOOLEAN_FLAG(flag, value) { \ constraints |= KexiDB::Field::flag; \ if (!value) \ constraints ^= KexiDB::Field::flag; \ } uint constraints = field.constraints(); bool ok = true; if ((it = values.find("primaryKey")) != values.constEnd()) SET_BOOLEAN_FLAG(PrimaryKey, (*it).toBool()); if ((it = values.find("indexed")) != values.constEnd()) SET_BOOLEAN_FLAG(Indexed, (*it).toBool()); if ((it = values.find("autoIncrement")) != values.constEnd() && KexiDB::Field::isAutoIncrementAllowed(field.type())) SET_BOOLEAN_FLAG(AutoInc, (*it).toBool()); if ((it = values.find("unique")) != values.constEnd()) SET_BOOLEAN_FLAG(Unique, (*it).toBool()); if ((it = values.find("notNull")) != values.constEnd()) SET_BOOLEAN_FLAG(NotNull, (*it).toBool()); if ((it = values.find("allowEmpty")) != values.constEnd()) SET_BOOLEAN_FLAG(NotEmpty, !(*it).toBool()); field.setConstraints(constraints); uint options = 0; if ((it = values.find("unsigned")) != values.constEnd()) { options |= KexiDB::Field::Unsigned; if (!(*it).toBool()) options ^= KexiDB::Field::Unsigned; } field.setOptions(options); if ((it = values.find("name")) != values.constEnd()) field.setName((*it).toString()); if ((it = values.find("caption")) != values.constEnd()) field.setCaption((*it).toString()); if ((it = values.find("description")) != values.constEnd()) field.setDescription((*it).toString()); if ((it = values.find("maxLength")) != values.constEnd()) field.setMaxLength((*it).isNull() ? 0/*default*/ : (*it).toUInt(&ok)); if (!ok) return false; if ((it = values.find("maxLengthIsDefault")) != values.constEnd() && (*it).toBool()) { field.setMaxLengthStrategy(Field::DefaultMaxLength); } if ((it = values.find("precision")) != values.constEnd()) field.setPrecision((*it).isNull() ? 0/*default*/ : (*it).toUInt(&ok)); if (!ok) return false; if ((it = values.find("defaultValue")) != values.constEnd()) field.setDefaultValue(*it); +#warning TODO defaultWidth +#if 0 + if ((it = values.find("defaultWidth")) != values.constEnd()) + field.setDefaultWidth((*it).isNull() ? 0/*default*/ : (*it).toUInt(&ok)); + if (!ok) + return false; +#endif + + // -- extended properties if ((it = values.find("visibleDecimalPlaces")) != values.constEnd() && KexiDB::supportsVisibleDecimalPlacesProperty(field.type())) field.setVisibleDecimalPlaces((*it).isNull() ? -1/*default*/ : (*it).toInt(&ok)); if (!ok) return false; + if (field.table() && containsLookupFieldSchemaProperties(values)) { + LookupFieldSchema *lookup = field.table()->lookupFieldSchema(field); + QScopedPointer createdLookup; + if (!lookup) { // create lookup if needed + createdLookup.reset(lookup = new LookupFieldSchema()); + } + if (lookup->setProperties(values)) { + if (createdLookup) { + if (field.table()->setLookupFieldSchema(field.name(), lookup)) { + createdLookup.take(); // ownership passed + lookup = 0; + } + } + } + } + return true; #undef SET_BOOLEAN_FLAG } //! @internal for isExtendedTableProperty() struct KexiDB_ExtendedProperties { KexiDB_ExtendedProperties() { #define ADD(name) set.insert( name ) ADD("visibledecimalplaces"); ADD("rowsource"); ADD("rowsourcetype"); ADD("rowsourcevalues"); ADD("boundcolumn"); ADD("visiblecolumn"); ADD("columnwidths"); ADD("showcolumnheaders"); ADD("listrows"); ADD("limittolist"); ADD("displaywidget"); #undef ADD } QSet set; }; //! for isExtendedTableProperty() K_GLOBAL_STATIC(KexiDB_ExtendedProperties, KexiDB_extendedProperties) bool KexiDB::isExtendedTableFieldProperty(const QByteArray& propertyName) { return KexiDB_extendedProperties->set.contains(QByteArray(propertyName).toLower()); } +//! @internal for isLookupFieldSchemaProperty() +struct KexiDB_LookupFieldSchemaProperties { + KexiDB_LookupFieldSchemaProperties() { + QMap tmp; + KexiDB::getProperties(0, &tmp); + foreach (const QByteArray &p, tmp.keys()) { + set.insert(p.toLower()); + } + } + QSet set; +}; + +//! for isLookupFieldSchemaProperty() +K_GLOBAL_STATIC(KexiDB_LookupFieldSchemaProperties, KexiDB_lookupFieldSchemaProperties) + +bool KexiDB::isLookupFieldSchemaProperty(const QByteArray& propertyName) +{ + return KexiDB_lookupFieldSchemaProperties->set.contains(QByteArray(propertyName).toLower()); +} + bool KexiDB::setFieldProperty(Field& field, const QByteArray& propertyName, const QVariant& value) { #define SET_BOOLEAN_FLAG(flag, value) { \ constraints |= KexiDB::Field::flag; \ if (!value) \ constraints ^= KexiDB::Field::flag; \ field.setConstraints( constraints ); \ return true; \ } #define GET_INT(method) { \ const uint ival = value.toUInt(&ok); \ if (!ok) \ return false; \ field.method( ival ); \ return true; \ } if (propertyName.isEmpty()) return false; bool ok; if (KexiDB::isExtendedTableFieldProperty(propertyName)) { //a little speedup: identify extended property in O(1) if ("visibleDecimalPlaces" == propertyName && KexiDB::supportsVisibleDecimalPlacesProperty(field.type())) { GET_INT(setVisibleDecimalPlaces); } else { if (!field.table()) { KexiDBWarn << QString( "KexiDB::setFieldProperty() Cannot set \"%1\" property - no table assigned for field!") .arg(QString(propertyName)); - } else { + } + else if (KexiDB::isLookupFieldSchemaProperty(propertyName)) { LookupFieldSchema *lookup = field.table()->lookupFieldSchema(field); - const bool hasLookup = lookup != 0; - if (!hasLookup) + const bool createLookup = !lookup; + if (createLookup) // create lookup if needed lookup = new LookupFieldSchema(); - if (LookupFieldSchema::setProperty(*lookup, propertyName, value)) { - if (!hasLookup && lookup) + if (lookup->setProperty(propertyName, value)) { + if (createLookup) field.table()->setLookupFieldSchema(field.name(), lookup); return true; } - delete lookup; + if (createLookup) + delete lookup; // not set, delete } } } else {//non-extended if ("type" == propertyName) return setIntToFieldType(field, value); uint constraints = field.constraints(); if ("primaryKey" == propertyName) SET_BOOLEAN_FLAG(PrimaryKey, value.toBool()); if ("indexed" == propertyName) SET_BOOLEAN_FLAG(Indexed, value.toBool()); if ("autoIncrement" == propertyName && KexiDB::Field::isAutoIncrementAllowed(field.type())) SET_BOOLEAN_FLAG(AutoInc, value.toBool()); if ("unique" == propertyName) SET_BOOLEAN_FLAG(Unique, value.toBool()); if ("notNull" == propertyName) SET_BOOLEAN_FLAG(NotNull, value.toBool()); if ("allowEmpty" == propertyName) SET_BOOLEAN_FLAG(NotEmpty, !value.toBool()); uint options = 0; if ("unsigned" == propertyName) { options |= KexiDB::Field::Unsigned; if (!value.toBool()) options ^= KexiDB::Field::Unsigned; field.setOptions(options); return true; } if ("name" == propertyName) { if (value.toString().isEmpty()) return false; field.setName(value.toString()); return true; } if ("caption" == propertyName) { field.setCaption(value.toString()); return true; } if ("description" == propertyName) { field.setDescription(value.toString()); return true; } if ("maxLength" == propertyName) GET_INT(setMaxLength); if ("maxLengthIsDefault" == propertyName) { field.setMaxLengthStrategy(Field::DefaultMaxLength); } if ("precision" == propertyName) GET_INT(setPrecision); if ("defaultValue" == propertyName) { field.setDefaultValue(value); return true; } - +#warning TODO defaultWidth +#if 0 + if ("defaultWidth" == propertyName) + GET_INT(setDefaultWidth); +#endif // last chance that never fails: custom field property field.setCustomProperty(propertyName, value); } KexiDBWarn << "KexiDB::setFieldProperty() property \"" << propertyName << "\" not found!"; return false; #undef SET_BOOLEAN_FLAG #undef GET_INT } int KexiDB::loadIntPropertyValueFromDom(const QDomNode& node, bool* ok) { QByteArray valueType = node.nodeName().toLatin1(); if (valueType.isEmpty() || valueType != "number") { if (ok) *ok = false; return 0; } const QString text(QDomNode(node).toElement().text()); int val = text.toInt(ok); return val; } QString KexiDB::loadStringPropertyValueFromDom(const QDomNode& node, bool* ok) { QByteArray valueType = node.nodeName().toLatin1(); if (valueType != "string") { if (ok) *ok = false; return QString(); } if (ok) *ok = true; return QDomNode(node).toElement().text(); } QVariant KexiDB::loadPropertyValueFromDom(const QDomNode& node, bool* ok) { QByteArray valueType = node.nodeName().toLatin1(); if (valueType.isEmpty()) { if (ok) *ok = false; return QVariant(); } if (ok) *ok = true; const QString text(QDomNode(node).toElement().text()); bool _ok; if (valueType == "string") { return text; } else if (valueType == "cstring") { return text.toLatin1(); } else if (valueType == "number") { // integer or double if (text.indexOf('.') != -1) { double val = text.toDouble(&_ok); if (_ok) return val; } else { const int val = text.toInt(&_ok); if (_ok) return val; const qint64 valLong = text.toLongLong(&_ok); if (_ok) return valLong; } } else if (valueType == "bool") { return QVariant(text.toLower() == "true" || text == "1"); } else { //! @todo add more QVariant types KexiDBWarn << "loadPropertyValueFromDom(): unknown type '" << valueType << "'"; } if (ok) *ok = false; return QVariant(); } QDomElement KexiDB::saveNumberElementToDom(QDomDocument& doc, QDomElement& parentEl, const QString& elementName, int value) { QDomElement el(doc.createElement(elementName)); parentEl.appendChild(el); QDomElement numberEl(doc.createElement("number")); el.appendChild(numberEl); numberEl.appendChild(doc.createTextNode(QString::number(value))); return el; } QDomElement KexiDB::saveBooleanElementToDom(QDomDocument& doc, QDomElement& parentEl, const QString& elementName, bool value) { QDomElement el(doc.createElement(elementName)); parentEl.appendChild(el); QDomElement numberEl(doc.createElement("bool")); el.appendChild(numberEl); numberEl.appendChild(doc.createTextNode( value ? QString::fromLatin1("true") : QString::fromLatin1("false"))); return el; } //! @internal Used in KexiDB::emptyValueForType() struct KexiDB_EmptyValueForTypeCache { KexiDB_EmptyValueForTypeCache() : values(int(Field::LastType) + 1) { #define ADD(t, value) values.insert(t, value); ADD(Field::Byte, 0); ADD(Field::ShortInteger, 0); ADD(Field::Integer, 0); ADD(Field::BigInteger, 0); ADD(Field::Boolean, false); ADD(Field::Float, 0.0); ADD(Field::Double, 0.0); //! @todo ok? we have no better defaults ADD(Field::Text, QString(" ")); ADD(Field::LongText, QString(" ")); ADD(Field::BLOB, QByteArray()); #undef ADD } QVector values; }; //! Used in KexiDB::emptyValueForType() K_GLOBAL_STATIC(KexiDB_EmptyValueForTypeCache, KexiDB_emptyValueForTypeCache) QVariant KexiDB::emptyValueForType(KexiDB::Field::Type type) { const QVariant val(KexiDB_emptyValueForTypeCache->values.at( (type <= Field::LastType) ? type : Field::InvalidType)); if (!val.isNull()) return val; else { //special cases if (type == Field::Date) return QDate::currentDate(); if (type == Field::DateTime) return QDateTime::currentDateTime(); if (type == Field::Time) return QTime::currentTime(); } KexiDBWarn << "KexiDB::emptyValueForType() no value for type " << Field::typeName(type); return QVariant(); } //! @internal Used in KexiDB::notEmptyValueForType() struct KexiDB_NotEmptyValueForTypeCache { KexiDB_NotEmptyValueForTypeCache() : values(int(Field::LastType) + 1) { #define ADD(t, value) values.insert(t, value); // copy most of the values for (int i = int(Field::InvalidType) + 1; i <= Field::LastType; i++) { if (i == Field::Date || i == Field::DateTime || i == Field::Time) continue; //'current' value will be returned if (i == Field::Text || i == Field::LongText) { ADD(i, QVariant(QString(""))); continue; } if (i == Field::BLOB) { //! @todo blobs will contain other mime types too QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); QPixmap pm(koSmallIcon("document-new")); pm.save(&buffer, "PNG"/*! @todo default? */); ADD(i, ba); continue; } ADD(i, KexiDB::emptyValueForType((Field::Type)i)); } #undef ADD } QVector values; }; //! Used in KexiDB::notEmptyValueForType() K_GLOBAL_STATIC(KexiDB_NotEmptyValueForTypeCache, KexiDB_notEmptyValueForTypeCache) QVariant KexiDB::notEmptyValueForType(KexiDB::Field::Type type) { const QVariant val(KexiDB_notEmptyValueForTypeCache->values.at( (type <= Field::LastType) ? type : Field::InvalidType)); if (!val.isNull()) return val; else { //special cases if (type == Field::Date) return QDate::currentDate(); if (type == Field::DateTime) return QDateTime::currentDateTime(); if (type == Field::Time) return QTime::currentTime(); } KexiDBWarn << "KexiDB::notEmptyValueForType() no value for type " << Field::typeName(type); return QVariant(); } QString KexiDB::escapeBLOB(const QByteArray& array, BLOBEscapingType type) { const int size = array.size(); if (size == 0) return QString(); int escaped_length = size * 2; if (type == BLOBEscape0xHex || type == BLOBEscapeOctal) escaped_length += 2/*0x or X'*/; else if (type == BLOBEscapeXHex) escaped_length += 3; //X' + ' QString str; str.reserve(escaped_length); if (str.capacity() < escaped_length) { KexiDBWarn << "KexiDB::Driver::escapeBLOB(): no enough memory (cannot allocate " << escaped_length << " chars)"; return QString(); } if (type == BLOBEscapeXHex) str = QString::fromLatin1("X'"); else if (type == BLOBEscape0xHex) str = QString::fromLatin1("0x"); else if (type == BLOBEscapeOctal) str = QString::fromLatin1("'"); int new_length = str.length(); //after X' or 0x, etc. if (type == BLOBEscapeOctal) { // only escape nonprintable characters as in Table 8-7: // http://www.postgresql.org/docs/8.1/interactive/datatype-binary.html // i.e. escape for bytes: < 32, >= 127, 39 ('), 92(\). for (int i = 0; i < size; i++) { const unsigned char val = array[i]; if (val < 32 || val >= 127 || val == 39 || val == 92) { str[new_length++] = '\\'; str[new_length++] = '\\'; str[new_length++] = '0' + val / 64; str[new_length++] = '0' + (val % 64) / 8; str[new_length++] = '0' + val % 8; } else { str[new_length++] = val; } } } else { for (int i = 0; i < size; i++) { const unsigned char val = array[i]; str[new_length++] = (val / 16) < 10 ? ('0' + (val / 16)) : ('A' + (val / 16) - 10); str[new_length++] = (val % 16) < 10 ? ('0' + (val % 16)) : ('A' + (val % 16) - 10); } } if (type == BLOBEscapeXHex || type == BLOBEscapeOctal) str[new_length++] = '\''; return str; } QByteArray KexiDB::pgsqlByteaToByteArray(const char* data, int length) { QByteArray array; int output = 0; for (int pass = 0; pass < 2; pass++) {//2 passes to avoid allocating buffer twice: // 0: count #of chars; 1: copy data const char* s = data; const char* end = s + length; if (pass == 1) { KexiDBDbg << "processBinaryData(): real size == " << output; array.resize(output); output = 0; } for (int input = 0; s < end; ++output) { // KexiDBDbg<<(int)s[0]<<" "<<(int)s[1]<<" "<<(int)s[2]<<" "<<(int)s[3]<<" "<<(int)s[4]; if (s[0] == '\\' && (s + 1) < end) { //special cases as in http://www.postgresql.org/docs/8.1/interactive/datatype-binary.html if (s[1] == '\'') {// \' if (pass == 1) array[output] = '\''; s += 2; } else if (s[1] == '\\') { // 2 backslashes if (pass == 1) array[output] = '\\'; s += 2; } else if ((input + 3) < length) {// \\xyz where xyz are 3 octal digits if (pass == 1) array[output] = char((int(s[1] - '0') * 8 + int(s[2] - '0')) * 8 + int(s[3] - '0')); s += 4; } else { KexiDBDrvWarn << "processBinaryData(): no octal value after backslash"; s++; } } else { if (pass == 1) array[output] = s[0]; s++; } // KexiDBDbg< KexiDB::stringListToIntList(const QStringList &list, bool *ok) { QList result; foreach (const QString &item, list) { int val = item.toInt(ok); if (ok && !*ok) { return QList(); } result.append(val); } if (ok) { *ok = true; } return result; } // Based on KConfigGroupPrivate::serializeList() from kconfiggroup.cpp (kdelibs 4) QString KexiDB::serializeList(const QStringList &list) { QString value = ""; if (!list.isEmpty()) { QStringList::ConstIterator it = list.constBegin(); const QStringList::ConstIterator end = list.constEnd(); value = QString(*it).replace('\\', "\\\\").replace(',', "\\,"); while (++it != end) { // In the loop, so it is not done when there is only one element. // Doing it repeatedly is a pretty cheap operation. value.reserve(4096); value += ','; value += QString(*it).replace('\\', "\\\\").replace(',', "\\,"); } // To be able to distinguish an empty list from a list with one empty element. if (value.isEmpty()) value = "\\0"; } return value; } // Based on KConfigGroupPrivate::deserializeList() from kconfiggroup.cpp (kdelibs 4) QStringList KexiDB::deserializeList(const QString &data) { if (data.isEmpty()) return QStringList(); if (data == QLatin1String("\\0")) return QStringList(QString()); QStringList value; QString val; val.reserve(data.size()); bool quoted = false; for (int p = 0; p < data.length(); p++) { if (quoted) { val += data[p]; quoted = false; } else if (data[p].unicode() == '\\') { quoted = true; } else if (data[p].unicode() == ',') { val.squeeze(); // release any unused memory value.append(val); val.clear(); val.reserve(data.size() - p); } else { val += data[p]; } } value.append(val); return value; } QList KexiDB::deserializeIntList(const QString &data, bool *ok) { return KexiDB::stringListToIntList( KexiDB::deserializeList(data), ok); } QString KexiDB::variantToString(const QVariant& v) { if (v.type() == QVariant::ByteArray) { return KexiDB::escapeBLOB(v.toByteArray(), KexiDB::BLOBEscapeHex); } else if (v.type() == QVariant::StringList) { return serializeList(v.toStringList()); } return v.toString(); } QVariant KexiDB::stringToVariant(const QString& s, QVariant::Type type, bool &ok) { if (s.isNull()) { ok = true; return QVariant(); } if (QVariant::Invalid == type) { ok = false; return QVariant(); } if (type == QVariant::ByteArray) {//special case: hex string const uint len = s.length(); QByteArray ba; ba.resize(len / 2 + len % 2); for (uint i = 0; i < (len - 1); i += 2) { int c = s.mid(i, 2).toInt(&ok, 16); if (!ok) { KexiDBWarn << "KexiDB::stringToVariant(): Error in digit " << i; return QVariant(); } ba[i/2] = (char)c; } ok = true; return ba; } if (type == QVariant::StringList) { ok = true; return deserializeList(s); } QVariant result(s); if (!result.convert(type)) { ok = false; return QVariant(); } ok = true; return result; } bool KexiDB::isDefaultValueAllowed(KexiDB::Field* field) { return field && !field->isUniqueKey(); } void KexiDB::getLimitsForType(Field::Type type, int &minValue, int &maxValue) { switch (type) { case Field::Byte: //! @todo always ok? minValue = 0; maxValue = 255; break; case Field::ShortInteger: minValue = -32768; maxValue = 32767; break; case Field::Integer: case Field::BigInteger: //cannot return anything larger default: minValue = (int) - 0x07FFFFFFF; maxValue = (int)(0x080000000 - 1); } } Field::Type KexiDB::maximumForIntegerTypes(Field::Type t1, Field::Type t2) { if (!Field::isIntegerType(t1) || !Field::isIntegerType(t2)) return Field::InvalidType; if (t1 == t2) return t2; if (t1 == Field::ShortInteger && t2 != Field::Integer && t2 != Field::BigInteger) return t1; if (t1 == Field::Integer && t2 != Field::BigInteger) return t1; if (t1 == Field::BigInteger) return t1; return KexiDB::maximumForIntegerTypes(t2, t1); //swap } QString KexiDB::simplifiedTypeName(const Field& field) { if (field.isNumericType()) return i18n("Number"); //simplify else if (field.type() == Field::BLOB) //! @todo support names of other BLOB subtypes return i18n("Image"); //simplify return field.typeGroupName(); } QString KexiDB::defaultFileBasedDriverMimeType() { return QString::fromLatin1("application/x-kexiproject-sqlite3"); } QString KexiDB::defaultFileBasedDriverIconName() { KMimeType::Ptr mimeType(KMimeType::mimeType( KexiDB::defaultFileBasedDriverMimeType())); if (mimeType.isNull()) { KexiDBWarn << QString("'%1' mimetype not installed!") .arg(KexiDB::defaultFileBasedDriverMimeType()); return QString(); } return mimeType->iconName(); } QString KexiDB::defaultFileBasedDriverName() { DriverManager dm; return dm.lookupByMime(KexiDB::defaultFileBasedDriverMimeType()).toLower(); } //-------------------------------------------------------------------------------- //! @internal class StaticSetOfStrings::Private { public: Private() : array(0), set(0) {} ~Private() { delete set; } const char** array; QSet *set; }; StaticSetOfStrings::StaticSetOfStrings() : d(new Private) { } StaticSetOfStrings::StaticSetOfStrings(const char* array[]) : d(new Private) { setStrings(array); } StaticSetOfStrings::~StaticSetOfStrings() { delete d; } void StaticSetOfStrings::setStrings(const char* array[]) { delete d->set; d->set = 0; d->array = array; } bool StaticSetOfStrings::isEmpty() const { return d->array == 0; } bool StaticSetOfStrings::contains(const QByteArray& string) const { if (!d->set) { d->set = new QSet(); for (const char ** p = d->array;*p;p++) { d->set->insert(QByteArray::fromRawData(*p, qstrlen(*p))); } } return d->set->contains(string); } //-------------------------------------------------------------------------------- bool KexiDB::isIdentifier(const QString& s) { uint i; const uint sLength = s.length(); for (i = 0; i < sLength; i++) { QChar c = s.at(i).toLower(); if (!(c == '_' || (c >= 'a' && c <= 'z') || (i > 0 && c >= '0' && c <= '9'))) break; } return i > 0 && i == sLength; } QString KexiDB::string2FileName(const QString &s) { QString fn = s.simplified(); fn.replace(' ', "_"); fn.replace('$', "_"); fn.replace('\\', "-"); fn.replace('/', "-"); fn.replace(':', "-"); fn.replace('*', "-"); return fn; } //-------------------------------------------------------------------------------- #ifdef CALLIGRADB_DEBUG_GUI static DebugGUIHandler s_debugGUIHandler = 0; void KexiDB::setDebugGUIHandler(DebugGUIHandler handler) { s_debugGUIHandler = handler; } void KexiDB::debugGUI(const QString& text) { if (s_debugGUIHandler) s_debugGUIHandler(text); } static AlterTableActionDebugGUIHandler s_alterTableActionDebugHandler = 0; void KexiDB::setAlterTableActionDebugHandler(AlterTableActionDebugGUIHandler handler) { s_alterTableActionDebugHandler = handler; } void KexiDB::alterTableActionDebugGUI(const QString& text, int nestingLevel) { if (s_alterTableActionDebugHandler) s_alterTableActionDebugHandler(text, nestingLevel); } #endif // CALLIGRADB_DEBUG_GUI #include "utils_p.moc" diff --git a/libs/db/utils.h b/libs/db/utils.h index 3d8f09871fc..75f8b2aea12 100644 --- a/libs/db/utils.h +++ b/libs/db/utils.h @@ -1,566 +1,576 @@ /* This file is part of the KDE project Copyright (C) 2004-2012 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KEXIDB_UTILS_H #define KEXIDB_UTILS_H #include #include #include #include "connection.h" #include "driver.h" #include "calligradb_global.h" class QDomNode; class QDomElement; class QDomDocument; namespace KexiDB { //! for convenience inline CALLIGRADB_EXPORT bool deleteRow(Connection &conn, TableSchema *table, const QString &keyname, const QString &keyval) { return table != 0 && conn.executeSQL("DELETE FROM " + table->name() + " WHERE " + keyname + "=" + conn.driver()->valueToSQL(Field::Text, QVariant(keyval))); } inline CALLIGRADB_EXPORT bool deleteRow(Connection &conn, const QString &tableName, const QString &keyname, const QString &keyval) { return conn.executeSQL("DELETE FROM " + tableName + " WHERE " + keyname + "=" + conn.driver()->valueToSQL(Field::Text, QVariant(keyval))); } inline CALLIGRADB_EXPORT bool deleteRow(Connection &conn, TableSchema *table, const QString &keyname, int keyval) { return table != 0 && conn.executeSQL("DELETE FROM " + table->name() + " WHERE " + keyname + "=" + conn.driver()->valueToSQL(Field::Integer, QVariant(keyval))); } inline CALLIGRADB_EXPORT bool deleteRow(Connection &conn, const QString &tableName, const QString &keyname, int keyval) { return conn.executeSQL("DELETE FROM " + tableName + " WHERE " + keyname + "=" + conn.driver()->valueToSQL(Field::Integer, QVariant(keyval))); } /*! Delete record with two generic criterias. */ inline CALLIGRADB_EXPORT bool deleteRow(Connection &conn, const QString &tableName, const QString &keyname1, Field::Type keytype1, const QVariant& keyval1, const QString &keyname2, Field::Type keytype2, const QVariant& keyval2) { return conn.executeSQL("DELETE FROM " + tableName + " WHERE " + keyname1 + "=" + conn.driver()->valueToSQL(keytype1, keyval1) + " AND " + keyname2 + "=" + conn.driver()->valueToSQL(keytype2, keyval2)); } /*! Delete record with three generic criterias. */ inline CALLIGRADB_EXPORT bool deleteRow(Connection &conn, const QString &tableName, const QString &keyname1, Field::Type keytype1, const QVariant& keyval1, const QString &keyname2, Field::Type keytype2, const QVariant& keyval2, const QString &keyname3, Field::Type keytype3, const QVariant& keyval3) { return conn.executeSQL("DELETE FROM " + tableName + " WHERE " + keyname1 + "=" + conn.driver()->valueToSQL(keytype1, keyval1) + " AND " + keyname2 + "=" + conn.driver()->valueToSQL(keytype2, keyval2) + " AND " + keyname3 + "=" + conn.driver()->valueToSQL(keytype3, keyval3)); } inline CALLIGRADB_EXPORT bool replaceRow(Connection &conn, TableSchema *table, const QString &keyname, const QString &keyval, const QString &valname, QVariant val, int ftype) { if (!table || !KexiDB::deleteRow(conn, table, keyname, keyval)) return false; return conn.executeSQL("INSERT INTO " + table->name() + " (" + keyname + "," + valname + ") VALUES (" + conn.driver()->valueToSQL(Field::Text, QVariant(keyval)) + "," + conn.driver()->valueToSQL(ftype, val) + ")"); } typedef QList TypeGroupList; /*! \return list of types for type group \a typeGroup. */ CALLIGRADB_EXPORT const TypeGroupList typesForGroup(Field::TypeGroup typeGroup); /*! \return list of i18n'd type names for type group \a typeGroup. */ CALLIGRADB_EXPORT QStringList typeNamesForGroup(Field::TypeGroup typeGroup); /*! \return list of (not-i18n'd) type names for type group \a typeGroup. */ CALLIGRADB_EXPORT QStringList typeStringsForGroup(Field::TypeGroup typeGroup); /*! \return default field type for type group \a typeGroup, for example, Field::Integer for Field::IntegerGroup. It is used e.g. in KexiAlterTableDialog, to properly fill 'type' property when user selects type group for a field. */ CALLIGRADB_EXPORT Field::Type defaultTypeForGroup(Field::TypeGroup typeGroup); /*! \return a slightly simplified type name for \a field. For BLOB type it returns i18n'd "Image" string or other, depending on the mime type. For numbers (either floating-point or integer) it returns i18n'd "Number: string. For other types it the same string as Field::typeGroupName() is returned. */ //! @todo support names of other BLOB subtypes CALLIGRADB_EXPORT QString simplifiedTypeName(const Field& field); /*! \return true if \a v represents an empty (but not null) value. Values of some types (as for strings) can be both empty and not null. */ inline bool isEmptyValue(Field *f, const QVariant &v) { if (f->hasEmptyProperty() && v.toString().isEmpty() && !v.toString().isNull()) return true; return v.isNull(); } /*! Sets \a msg to an error message retrieved from object \a obj, and \a details to details of this error (server message and result number). Does nothing if \a obj is null or no error occurred. \a msg and \a details strings are not overwritten. If \a msg is not empty, \a obj's error message is appended to \a details. */ CALLIGRADB_EXPORT void getHTMLErrorMesage(Object* obj, QString& msg, QString &details); /*! This methods works like above, but appends both a message and a description to \a msg. */ CALLIGRADB_EXPORT void getHTMLErrorMesage(Object* obj, QString& msg); /*! This methods works like above, but works on \a result's members instead. */ CALLIGRADB_EXPORT void getHTMLErrorMesage(Object* obj, ResultInfo *result); /*! Function useful for building WHERE parts of sql statements. Constructs an sql string like "fielname = value" for specific \a drv driver, field type \a t, \a fieldName and \a value. If \a value is null, "fieldname is NULL" string is returned. */ inline CALLIGRADB_EXPORT QString sqlWhere(Driver *drv, Field::Type t, const QString &fieldName, const QVariant &value) { if (value.isNull()) return fieldName + " is NULL"; return fieldName + "=" + drv->valueToSQL(t, value); } /*! \return identifier for object \a objName of type \a objType or 0 if such object does not exist. */ CALLIGRADB_EXPORT int idForObjectName(Connection &conn, const QString& objName, int objType); /*! Variant class providing a pointer to table or query. */ class CALLIGRADB_EXPORT TableOrQuerySchema { public: /*! Creates a new TableOrQuerySchema variant object, retrieving table or query schema using \a conn connection and \a name. If both table and query exists for \a name, table has priority over query. You should check whether a query or table has been found by testing (query() || table()) expression. */ TableOrQuerySchema(Connection *conn, const QByteArray& name); /*! Creates a new TableOrQuerySchema variant object, retrieving table or query schema using \a conn connection and \a name. If \a table is true, \a name is assumed to be a table name, otherwise \a name is assumed to be a query name. You should check whether a query or table has been found by testing (query() || table()) expression. */ TableOrQuerySchema(Connection *conn, const QByteArray& name, bool table); /*! Creates a new TableOrQuerySchema variant object. \a tableOrQuery must be of class TableSchema or QuerySchema. You should check whether a query or table has been found by testing (query() || table()) expression. */ TableOrQuerySchema(FieldList &tableOrQuery); /*! Creates a new TableOrQuerySchema variant object, retrieving table or query schema using \a conn connection and \a id. You should check whether a query or table has been found by testing (query() || table()) expression. */ TableOrQuerySchema(Connection *conn, int id); /*! Creates a new TableOrQuerySchema variant object, keeping a pointer so \a table object. */ TableOrQuerySchema(TableSchema* table); /*! Creates a new TableOrQuerySchema variant object, keeping a pointer so \a query object. */ TableOrQuerySchema(QuerySchema* query); //! \return a pointer to the query if it's provided QuerySchema* query() const { return m_query; } //! \return a pointer to the table if it's provided TableSchema* table() const { return m_table; } //! \return name of a query or table QByteArray name() const; //! \return caption (if present) or name of the table/query QString captionOrName() const; //! \return number of fields uint fieldCount() const; //! \return all columns for the table or the query const QueryColumnInfo::Vector columns(bool unique = false); /*! \return a field of the table or the query schema for name \a name or 0 if there is no such field. */ Field* field(const QString& name); /*! Like Field* field(const QString& name); but returns all information associated with field/column \a name. */ QueryColumnInfo* columnInfo(const QString& name); /*! \return connection object, for table or query or 0 if there's no table or query defined. */ Connection* connection() const; /*! \return String for debugging purposes. */ - QString debugString(); + QString debugString() const; /*! Shows debug information about table or query. */ - void debug(); + void debug() const; protected: QByteArray m_name; //!< the name is kept here because m_table and m_table can be 0 //! and we still want name() and acptionOrName() work. TableSchema* m_table; QuerySchema* m_query; }; //! @todo perhaps use quint64 here? /*! \return number of rows that can be retrieved after executing \a sql statement within a connection \a conn. The statement should be of type SELECT. For SQL data sources it does not fetch any records, only "COUNT(*)" SQL aggregation is used at the backed. -1 is returned if error occurred. */ int rowCount(Connection &conn, const QString& sql); //! @todo perhaps use quint64 here? /*! \return number of rows that can be retrieved from \a tableSchema. The table must be created or retrieved using a Connection object, i.e. tableSchema.connection() must not return 0. For SQL data sources it does not fetch any records, only "COUNT(*)" SQL aggregation is used at the backed. -1 is returned if error occurred. */ CALLIGRADB_EXPORT int rowCount(const TableSchema& tableSchema); //! @todo perhaps use quint64 here? /*! Like above but operates on a query schema. */ CALLIGRADB_EXPORT int rowCount(QuerySchema& querySchema); //! @todo perhaps use quint64 here? /*! Like above but operates on a table or query schema variant. */ CALLIGRADB_EXPORT int rowCount(TableOrQuerySchema& tableOrQuery); /*! \return a number of columns that can be retrieved from table or query schema. In case of query, expanded fields are counted. Can return -1 if \a tableOrQuery has neither table or query assigned. */ CALLIGRADB_EXPORT int fieldCount(TableOrQuerySchema& tableOrQuery); /*! shows connection test dialog with a progress bar indicating connection testing (within a second thread). \a data is used to perform a (temporary) test connection. \a msgHandler is used to display errors. On successful connecting, a message is displayed. After testing, temporary connection is closed. */ CALLIGRADB_EXPORT void connectionTestDialog(QWidget* parent, const ConnectionData& data, MessageHandler& msgHandler); /*! Saves connection data \a data into \a map. */ CALLIGRADB_EXPORT QMap toMap(const ConnectionData& data); /*! Restores connection data \a data from \a map. */ CALLIGRADB_EXPORT void fromMap(const QMap& map, ConnectionData& data); //! Used in splitToTableAndFieldParts(). enum SplitToTableAndFieldPartsOptions { FailIfNoTableOrFieldName = 0, //!< default value for splitToTableAndFieldParts() SetFieldNameIfNoTableName = 1 //!< see splitToTableAndFieldParts() }; /*! Splits \a string like "table.field" into "table" and "field" parts. On success, a table name is passed to \a tableName and a field name is passed to \a fieldName. The function fails if either: - \a string is empty, or - \a string does not contain '.' character and \a option is FailIfNoTableOrFieldName (the default), or - '.' character is the first of last character of \a string (in this case table name or field name could become empty what is not allowed). If \a option is SetFieldNameIfNoTableName and \a string does not contain '.', \a string is passed to \a fieldName and \a tableName is set to QString() without failure. If function fails, \a tableName and \a fieldName remain unchanged. \return true on success. */ CALLIGRADB_EXPORT bool splitToTableAndFieldParts(const QString& string, QString& tableName, QString& fieldName, SplitToTableAndFieldPartsOptions option = FailIfNoTableOrFieldName); /*! \return true if \a type supports "visibleDecimalPlaces" property. */ CALLIGRADB_EXPORT bool supportsVisibleDecimalPlacesProperty(Field::Type type); /*! \return string constructed by converting \a value. * If \a decimalPlaces is < 0, all meaningful fractional digits are returned. * If \a automatically is 0, just integer part is returned. * If \a automatically is > 0, fractional part should take exactly N digits: if the fractional part is shorter than N, additional zeros are appended. For example, "12.345" becomes "12.345000" if N=6. No rounding is actually performed. KLocale::formatNumber() and KLocale::decimalSymbol() are used to get locale settings. @see KexiDB::Field::visibleDecimalPlaces() */ CALLIGRADB_EXPORT QString formatNumberForVisibleDecimalPlaces(double value, int decimalPlaces); //! \return true if \a propertyName is a builtin field property. CALLIGRADB_EXPORT bool isBuiltinTableFieldProperty(const QByteArray& propertyName); //! \return true if \a propertyName is an extended field property. CALLIGRADB_EXPORT bool isExtendedTableFieldProperty(const QByteArray& propertyName); +//! \return true if \a propertyName is belongs to lookup field's schema. +CALLIGRADB_EXPORT bool isLookupFieldSchemaProperty(const QByteArray& propertyName); + /*! \return type of field for integer value \a type. If \a type cannot be casted to KexiDB::Field::Type, KexiDB::Field::InvalidType is returned. This can be used when type information is deserialized from a string or QVariant. */ CALLIGRADB_EXPORT Field::Type intToFieldType(int type); +/*! Gets property values for \a field. + Properties from extended schema are included. \a values is cleared before filling. + The same number of properties in the same order is returned. + This function is used e.g. for altering table design. + */ +CALLIGRADB_EXPORT void getFieldProperties(const Field &field, QMap *values); + /*! Sets property values for \a field. \return true if all the values are valid and allowed. On failure contents of \a field is undefined. - Properties coming from extended schema are also supported. + Properties from extended schema are also supported. This function is used e.g. by AlterTableHandler when property information comes in form of text. */ -CALLIGRADB_EXPORT bool setFieldProperties(Field& field, const QHash& values); +CALLIGRADB_EXPORT bool setFieldProperties(Field& field, const QMap& values); /*! Sets property value for \a field. \return true if the property has been found and the value is valid for this property. On failure contents of \a field is undefined. - Properties coming from extended schema are also supported as well as + Properties from extended schema are also supported as well as QVariant customProperty(const QString& propertyName) const; This function is used e.g. by AlterTableHandler when property information comes in form of text. */ CALLIGRADB_EXPORT bool setFieldProperty(Field& field, const QByteArray& propertyName, - const QVariant& value); + const QVariant& value); /*! @return property value loaded from a DOM \a node, written in a QtDesigner-like notation: <number>int</number> or <bool>bool</bool>, etc. Supported types are "string", "cstring", "bool", "number". For invalid values null QVariant is returned. You can check the validity of the returned value using QVariant::type(). */ CALLIGRADB_EXPORT QVariant loadPropertyValueFromDom(const QDomNode& node, bool *ok); /*! Convenience version of loadPropertyValueFromDom(). \return int value. */ CALLIGRADB_EXPORT int loadIntPropertyValueFromDom(const QDomNode& node, bool* ok); /*! Convenience version of loadPropertyValueFromDom(). \return QString value. */ CALLIGRADB_EXPORT QString loadStringPropertyValueFromDom(const QDomNode& node, bool* ok); /*! Saves integer element for value \a value to \a doc document within parent element \a parentEl. The value will be enclosed in "number" element and "elementName" element. Example: saveNumberElementToDom(doc, parentEl, "height", 15) will create \code 15 \endcode \return the reference to element created with tag elementName. */ CALLIGRADB_EXPORT QDomElement saveNumberElementToDom(QDomDocument& doc, QDomElement& parentEl, const QString& elementName, int value); /*! Saves boolean element for value \a value to \a doc document within parent element \a parentEl. Like saveNumberElementToDom() but creates "bool" tags. True/false values will be saved as "true"/"false" strings. \return the reference to element created with tag elementName. */ CALLIGRADB_EXPORT QDomElement saveBooleanElementToDom(QDomDocument& doc, QDomElement& parentEl, const QString& elementName, bool value); /*! \return an empty value that can be set for a database field of type \a type having "null" property set. Empty string is returned for text type, 0 for integer or floating-point types, false for boolean type, empty null byte array for BLOB type. For date, time and date/time types current date, time, date+time is returned, respectively. Returns null QVariant for unsupported values like KexiDB::Field::InvalidType. This function is efficient (uses a cache) and is heavily used by the AlterTableHandler for filling new columns. */ CALLIGRADB_EXPORT QVariant emptyValueForType(Field::Type type); /*! \return a value that can be set for a database field of type \a type having "notEmpty" property set. It works in a similar way as @ref QVariant emptyValueForType( KexiDB::Field::Type type ) with the following differences: - " " string (a single space) is returned for Text and LongText types - a byte array with saved "filenew" PNG image (icon) for BLOB type Returns null QVariant for unsupported values like KexiDB::Field::InvalidType. This function is efficient (uses a cache) and is heavily used by the AlterTableHandler for filling new columns. */ CALLIGRADB_EXPORT QVariant notEmptyValueForType(Field::Type type); //! Escaping types used in escapeBLOB(). enum BLOBEscapingType { BLOBEscapeXHex = 1, //!< escaping like X'1FAD', used by sqlite (hex numbers) BLOBEscape0xHex, //!< escaping like 0x1FAD, used by mysql (hex numbers) BLOBEscapeHex, //!< escaping like 1FAD without quotes or prefixes BLOBEscapeOctal //!< escaping like 'zk\\000$x', used by pgsql //!< (only non-printable characters are escaped using octal numbers) //!< See http://www.postgresql.org/docs/8.1/interactive/datatype-binary.html }; //! @todo reverse function for BLOBEscapeOctal is available: processBinaryData() in pqxxcursor.cpp - move it here /*! \return a string containing escaped, printable representation of \a array. Escaping is controlled by \a type. For empty array, QString() is returned, so if you want to use this function in an SQL statement, empty arrays should be detected and "NULL" string should be put instead. This is helper, used in Driver::escapeBLOB() and KexiDB::variantToString(). */ CALLIGRADB_EXPORT QString escapeBLOB(const QByteArray& array, BLOBEscapingType type); /*! \return byte array converted from \a data of length \a length. \a data is escaped in format used by PostgreSQL's bytea datatype described at http://www.postgresql.org/docs/8.1/interactive/datatype-binary.html This function is used by PostgreSQL KexiDB and migration drivers. */ CALLIGRADB_EXPORT QByteArray pgsqlByteaToByteArray(const char* data, int length); /*! \return int list converted from string list. If \a ok is not 0, *ok is set to result of the conversion. */ CALLIGRADB_EXPORT QList stringListToIntList(const QStringList &list, bool *ok); /*! \return string converted from list \a list. Separators are ',' characters, "," and "\\" are escaped. @see deserializeList() */ CALLIGRADB_EXPORT QString serializeList(const QStringList &list); /*! \return string list converted from \a data which was built using serializeList(). Separators are ',' characters, escaping is assumed as "\\,". */ CALLIGRADB_EXPORT QStringList deserializeList(const QString &data); /*! \return int list converted from \a data which was built using serializeList(). Separators are ',' characters, escaping is assumed as "\\,". If \a ok is not 0, *ok is set to result of the conversion. @see KexiDB::stringListToIntList() */ CALLIGRADB_EXPORT QList deserializeIntList(const QString &data, bool *ok); /*! \return string value serialized from a variant value \a v. This functions works like QVariant::toString() except the case when \a v is of type ByteArray. In this case KexiDB::escapeBLOB(v.toByteArray(), KexiDB::BLOBEscapeHex) is used. This function is needed for handling values of random type, for example "defaultValue" property of table fields can contain value of any type. Note: the returned string is an unescaped string. */ CALLIGRADB_EXPORT QString variantToString(const QVariant& v); /*! \return variant value of type \a type for a string \a s that was previously serialized using \ref variantToString( const QVariant& v ) function. \a ok is set to the result of the operation. */ CALLIGRADB_EXPORT QVariant stringToVariant(const QString& s, QVariant::Type type, bool &ok); /*! \return true if setting default value for \a field field is allowed. Fields with unique (and thus primary key) flags set do not accept default values. False is returned aslo if \a field is 0. */ CALLIGRADB_EXPORT bool isDefaultValueAllowed(Field* field); /*! Gets limits for values of type \a type. The result is put into \a minValue and \a maxValue. Supported types are Byte, ShortInteger, Integer and BigInteger Results for BigInteger or non-integer types are the same as for Integer due to limitation of int type. Signed integers are assumed. */ //! @todo add support for unsigned flag CALLIGRADB_EXPORT void getLimitsForType(Field::Type type, int &minValue, int &maxValue); /*! \return type that's maximum of two integer types \a t1 and \a t2, e.g. Integer for (Byte, Integer). If one of the types is not of the integer group, Field::InvalidType is returned. */ CALLIGRADB_EXPORT Field::Type maximumForIntegerTypes(Field::Type t1, Field::Type t2); /*! \return QVariant value converted from null-terminated \a data string. In case of BLOB type, \a data is not nul lterminated, so passing length is needed. */ inline QVariant cstringToVariant(const char* data, KexiDB::Field* f, int length = -1) { if (!data) return QVariant(); // from mo st to least frequently used types: if (!f || f->isTextType()) return QString::fromUtf8(data, length); if (f->isIntegerType()) { if (f->type() == KexiDB::Field::BigInteger) return QVariant(QString::fromLatin1(data, length).toLongLong()); return QVariant(QString::fromLatin1(data, length).toInt()); } if (f->isFPNumericType()) return QString::fromLatin1(data, length).toDouble(); if (f->type() == KexiDB::Field::BLOB) return QByteArray::fromRawData(data, length); // the default //! @todo date/time? QVariant result(QString::fromUtf8(data, length)); if (!result.convert(KexiDB::Field::variantType(f->type()))) return QVariant(); return result; } /*! \return default file-based driver mime type (typically something like "application/x-kexiproject-sqlite") */ CALLIGRADB_EXPORT QString defaultFileBasedDriverMimeType(); /*! \return icon name for default file-based driver (typically icon for something like "application/x-kexiproject-sqlite"). @see KexiDB::defaultFileBasedDriverMimeType() */ CALLIGRADB_EXPORT QString defaultFileBasedDriverIconName(); /*! \return default file-based driver name (currently, "sqlite3"). */ CALLIGRADB_EXPORT QString defaultFileBasedDriverName(); //! A set created from static (0-terminated) array of raw null-terminated strings. class CALLIGRADB_EXPORT StaticSetOfStrings { public: StaticSetOfStrings(); StaticSetOfStrings(const char* array[]); ~StaticSetOfStrings(); void setStrings(const char* array[]); bool isEmpty() const; bool contains(const QByteArray& string) const; private: class Private; Private * const d; }; /*! \return true if \a s is a valid identifier, ie. starts with a letter or '_' character and contains only letters, numbers and '_' character. */ CALLIGRADB_EXPORT bool isIdentifier(const QString& s); //! \return Valid filename based on \a s CALLIGRADB_EXPORT QString string2FileName(const QString &s); //! QDateTime - a hack needed because QVariant(QTime) has broken isNull() inline CALLIGRADB_EXPORT QDateTime stringToHackedQTime(const QString& s) { if (s.isEmpty()) return QDateTime(); // kDebug() << QDateTime( QDate(0,1,2), QTime::fromString( s, Qt::ISODate ) ).toString(Qt::ISODate); return QDateTime(QDate(0, 1, 2), QTime::fromString(s, Qt::ISODate)); } #ifdef CALLIGRADB_DEBUG_GUI typedef void(*DebugGUIHandler)(const QString&); CALLIGRADB_EXPORT void setDebugGUIHandler(DebugGUIHandler handler); CALLIGRADB_EXPORT void debugGUI(const QString& text); typedef void(*AlterTableActionDebugGUIHandler)(const QString&, int); CALLIGRADB_EXPORT void setAlterTableActionDebugHandler(AlterTableActionDebugGUIHandler handler); CALLIGRADB_EXPORT void alterTableActionDebugGUI(const QString& text, int nestingLevel = 0); #endif -} +} // namespace KexiDB #endif