diff options
author | Denis Dzyubenko <denis.dzyubenko@nokia.com> | 2012-03-28 11:43:09 +0200 |
---|---|---|
committer | Denis Dzyubenko <denis.dzyubenko@nokia.com> | 2012-03-28 11:43:12 +0200 |
commit | 74c9cdcc15f4c323bd766b1f769bd2248a0b582d (patch) | |
tree | e62f38a035c379f5d8ce3df5577efccb4afdef87 | |
parent | 6db3585569eadd9aaaaa7871ca7a41b03e4d947d (diff) | |
parent | 897d1fad7c6ebb0c42e2c089dff89d980d9fa7df (diff) |
Merge remote-tracking branch 'gerrit/master' into hbtree
Change-Id: I3deb84bade6607fa99aebf7aff202ee3b2c2e8e6
58 files changed, 1774 insertions, 510 deletions
diff --git a/doc/doc.pri b/doc/doc.pri index 7f7eff6..939ef9d 100644 --- a/doc/doc.pri +++ b/doc/doc.pri @@ -1,6 +1,7 @@ OTHER_FILES += \ $$PWD/jsondb.qdocconf \ - $$PWD/jsondb-dita.qdocconf + $$PWD/jsondb-dita.qdocconf \ + $$PWD/src/*.qdoc docs_target.target = docs docs_target.commands = qdoc $$PWD/jsondb.qdocconf diff --git a/doc/src/index.qdoc b/doc/src/index.qdoc index 4026e8c..45de13f 100644 --- a/doc/src/index.qdoc +++ b/doc/src/index.qdoc @@ -26,11 +26,17 @@ ****************************************************************************/ /*! -\title Qt JsonDb Reference +\title Qt JSON DB Reference \page index.html \section1 Introduction +JSON DB is a no-SQL object store for Qt. It allows JSON objects to be stored, +modified and queried. It does not require schemas like SQL database, +which makes it particularly flexible. It has a number of useful features such +as the ability to mash-up data with database views, push notifications for +data changes in the database, and easy integration with QML-based UIs. + \section1 Accessing the Database. \section2 C++ API @@ -43,87 +49,73 @@ \section1 Using the Database -\section2 Schemas - -\section2 Query Language - -Central to the use of a large object store is the ability to query efficiently -for objects from within the object store. The JsonDB query language was -originally modeled off of the JSON Query language, see -\l {http://docs.persvr.org/documentation/jsonquery}. It has subsequently been -updated for faster performance and features have been added that make it more -suitable for our application. - -A simple query consists of square brackets surrounding an expression -(e.g., [EXPR]). A complex query is a string of simple queries concatenated -together (e.g., [EXPR1][EXPR2][EXPR3]). Each expression filters or applies an -action. - -\section3 Valid Expressions - -\table -\header -\li Expression -\li Description -\row -\li \c {[? filter]} -\li Query for matching objects. -\row -\li \c {[= fields]} -\li Restrict returned objects to particular fields. -\row -\li \c {[/ field]} -\li Sort in ascending order by field. -\row -\li \c {[\ field]} -\li Sort in descending order by field. -\row -\li \c {[count]} -\li Aggregation operation. -\row -\li \c {[*]} -\li Match all objects. -\endtable - -See also \l {Expression Examples} - -\section3 Stability of Sort in JsonDb - -The database as a set of objects with no natural ordering. Any time that we need -objects in a particular order, we use an index that orders the objects by a -particular field and comparison operator. It is unlikely that the sorting in one -index would be stable with respect to all other indexes on that object. +\section2 Use of JSON + +The database operates on objects in JSON format. Any JSON object can be stored +in the database, though JSON DB reserves properties beginning with an underscore +for its own use. + +See \l {Special JSON Properties} + +\section2 Creating, Updating and Removing Objects + +Objects are manipulated in the database from C++ using the +\l{QJsonDbWriteRequest} class and its associated convenience subclasses. + +In QML, objects are manipulated via the \l{Partition} item. + +\section2 Querying Objects + +Objects are queried from C++ using the \l{QJsonDbReadRequest} class and in QML +using \l{Query} item. + +See \l {Queries} + +\section2 Partitions + +The database is sub-divided into a number of smaller partitions, each with its +own unique name. All operations in JSON DB operate on exactly one partition at +a time. + +See \l {Partitions} \section2 Indexes +The index facility is provided to make queries perform better and to allow the +database to sort the returned data. Indexes are created in JSON DB by creating +objects with the type \c Index. + See \l {Object Indexes} -\section3 Query optimization +\section2 Views -In general, sorting should only be done on properties for which JSON DB has an -index. Any query with a sort order given for which there is no index, will -result in an error. For a query without explicitly provided sort order, full -object scan is performed, which should be avoided for all but the smallest data -sets. +Views allow objects in JSON DB to be transformed and mashed-up into new objects. +Views are defined via functions written in JavaScript and are automatically +kept up to date by the database. When the source object of a view is modified +in the database, JSON DB ensures the views that depend on that object are updated. +Views in JSON DB are based heavily on the Map/Reduce concept (see +\l {http://www.mapreduce.org}). -\section2 Views +Map functions are registered by creating an object of type \c Map in the database. -\list -\li \l {Map and Join Views} -\endlist +See \l {Map and Join Views} + +Reduce functions are registered by creating an object of type \c Reduce in the +database. + +See \l {Reduce Views} \section2 Notifications -Notifications are created, updated, and removed by using a -\l{QJsonDbConnection::addWatcher()}{dedicated api} which -takes a query expression and a list of actions as an argument. There are -actions available that allow watching for objects being created, updated or -removed from a database. +JSON DB clients can register to be informed about changes in the database via +notifications. Clients create notifications containing a query to monitor. +When an object matching the query is modified, JSON DB sends a notification +message to the client. -When a notification matches an action performed on the database, the -QJsonDbWatcher::notificationsAvailable() signal is emitted. +Notifications are registered via the \l {QJsonDbWatcher} class in C++ and +via the \l {Notification} item in QML. -See also \l{QJsonDbWatcher} +See \l {Notifications} \section1 Examples @@ -134,6 +126,7 @@ See also \l{QJsonDbWatcher} \li \l{declarative/desktop-inspector}{Desktop Database Inspector Example} \li \l{declarative/map-phone}{Creating a Map View} \li \l{declarative/joinview}{Creating a Join View} +\li \l{declarative/reduce-phone}{Creating a Reduce View} \li \l{client/chat}{Creating a chat client using the C++ API} \endlist */ diff --git a/doc/src/map-views.qdoc b/doc/src/map-views.qdoc index fcddae1..391c9c9 100644 --- a/doc/src/map-views.qdoc +++ b/doc/src/map-views.qdoc @@ -47,10 +47,32 @@ values of a single target type. The target type must extend \l View. \li targetType \li The output type of this mapping \row +\li targetKeyName +\li Name of the key for the emitted objects. Optional. \sa deterministic-map-uuids +\row \li map \li A dictionary whose keys are source type names and whose values are the functions to run on those source type. \endtable +\section2 Deterministic Map Uuids +\target{deterministic-map-uuids} + +By default, View objects created by Map are assigned a deterministic Uuid. If +the "map" or "join" functions assign _uuid to the object they +return, then that value is used as the uuid for the object. Otherwise, +the view engine constructs an identifier string as follows: +\code + sourceUuids.sort(); + if (targeKeyName.isEmpty()) { + var identifier = targetType + ":" + sourceUuids.join(":"); + object._uuid = jsondb.createUuidFromString(identifier); + } else { + var keyValue = object[targetKeyName]; + var identifier = targetType + ":" + sourceUuids.join(":") + ":" + keyValue; + object._uuid = jsondb.createUuidFromString(identifier); + } +\endcode + \section2 Map Proxy When the map functions run, they have access to a jsondb proxy object with one method: @@ -87,7 +109,7 @@ A join is defined by creating a \l Map object in the database with a "join" prop \li A dictionary whose keys are source type names and whose values are the functions to run on those source type. \endtable -\section2 JsonDb Join Proxy +\section2 JSON DB Join Proxy When the \l Join functions run, they have access to a jsondb proxy object with two methods: diff --git a/doc/src/notifications.qdoc b/doc/src/notifications.qdoc new file mode 100644 index 0000000..36a709f --- /dev/null +++ b/doc/src/notifications.qdoc @@ -0,0 +1,88 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** GNU Free Documentation License +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms +** and conditions contained in a signed written agreement between you +** and Nokia. +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! +\page notifications.html +\title Notifications + +JSON DB clients can register to be informed about changes in the database via +notifications. Clients create notifications containing a query to monitor. +When an object matching the query is modified, JSON DB sends a notification +message to the client. + +Notifications are registered via the \l {QJsonDbWatcher} class in C++ and +via the \l {Notification} item in QML. + +Notifications usually pertain to new events in the database, but by using the +\l {QJsonDbWatcher::initialStateNumber} property, it's possible to request +notifications based on past events in the database. This can be useful in +writing a program that would need to synchronize with the database. + +\section1 Actions + +Clients may register to receive notifications based on one or more types +of actions that may occur in the database. The available actions are: + +\table +\row +\row +\li Created +\li Notifications with this action generally represent entirely new objects +which match the specified query being created in the database. Another case is an +existing object which previously did not match the query being modified to now +match the query. + +\row +\li Updated +\li Notifications with this action represent an object that matches the +specified query being updated (while still matching the query). + +\row +\li Removed +\li Notifications with this action generally represent an existing object which +matches the specified query being removed from the database. Another case is an +existing object which previously matched the query being modified so that it no +longer matches the query. Whether the object has been removed or not can be +tested by checking if the object in the notification has a \c _deleted property +with the value of \c true. + +\row +\li StateChanged +\li Used for historical notifications only. When this action is received, it +means that the last notification for a particular state (transaction) has been +received. + +\endtable + +\section1 Lifecycle + +Under-the-hood, both \l {QJsonDbWatcher} and \l {Notification} function by +creating an object of type \c notification in the Ephemeral partition (see +\l {Partitions}). Once the object is created, notifications can begin being +sent. When a connection to the database drops, all of the notifications for +that connection are removed. +*/ diff --git a/doc/src/object-indexes.qdoc b/doc/src/object-indexes.qdoc index ecf92f7..80c3aff 100644 --- a/doc/src/object-indexes.qdoc +++ b/doc/src/object-indexes.qdoc @@ -52,6 +52,15 @@ specified, name defaults to propertyName. If propertyFunction is specified, then name must be specified. \row +\li objectType +\li An optional string or array of strings, naming the object types to +be indexed. If not specified, all objects in the main object table of +a partition will be indexed. + +If specified for non-view types, the index should have a name that is +distinct from its propertyName. + +\row \li propertyName \li A string or array of strings, naming the properties to be indexed. Mutually exclusive with propertyFunction. @@ -99,7 +108,14 @@ and "PreferLowerCase". \endtable -\section2 JsonDb Proxy +\section1 Stability of Sort in JSON DB + +The database is a set of objects with no natural ordering. Any time that we need +objects in a particular order, we use an index that orders the objects by a +particular field and comparison operator. It is unlikely that the sorting in one +index will be stable with respect to other indexes on that object. + +\section1 jsondb Proxy For indexes that use a JavaScript function, when the propertyFunction runs it has access to a client interface to the database. This proxy diff --git a/doc/src/partitions.qdoc b/doc/src/partitions.qdoc new file mode 100644 index 0000000..64911c2 --- /dev/null +++ b/doc/src/partitions.qdoc @@ -0,0 +1,58 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** GNU Free Documentation License +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms +** and conditions contained in a signed written agreement between you +** and Nokia. +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! +\page partitions.html +\title Partitions + +\section1 Introduction + +JSON DB may be configured as a number of partitions, each with its +own unique name. All operations in JSON DB operate on exactly one partition at +a time. + +\section1 Accessing Partitions + +The various partitions in a set are accessed via the \l {Partition} element in QML. In +C++, the partition is selected by \c setPartition() on a +\l {QJsonDbReadRequest}, \l {QJsonDbWriteRequest} or \l {QJsonDbWatcher}. + +\section1 The Ephemeral Partition + +JSON DB offers a partition called \c Ephemeral. Objects written to this +partition are not persistent, but rather stored entirely in memory. The +entire partition is cleared every time the database server is restarted. This +partition can be very useful for storing frequently updated data since it does not +cause any disk I/O. + +\code +QJsonDbReadRequest *request = new QJsonDbReadRequest(this); +request->setQuery("[?_type=\"Status\"]"); +request->setPartition("Ephemeral"); +\endcode + +*/ diff --git a/doc/src/expression-examples.qdoc b/doc/src/queries.qdoc index 15c8ba6..b34bdc0 100644 --- a/doc/src/expression-examples.qdoc +++ b/doc/src/queries.qdoc @@ -26,13 +26,28 @@ ****************************************************************************/ /*! -\page expression-examples.html -\title Expression Examples +\page queries.html +\title Queries -\previouspage {index.html} +\section1 Basics + +Central to the use of a large object store is the ability to query efficiently +for objects from within the object store. The JsonDB query language was +originally modeled on of the JSON Query language (see +\l {http://docs.persvr.org/documentation/jsonquery}). It has subsequently been +updated for faster performance and features have been added that make it more +suitable for our application. + +A simple query consists of square brackets surrounding an expression +(e.g., [EXPR]). A complex query is a string of simple queries concatenated +together (e.g., [EXPR1][EXPR2][EXPR3]). Each expression filters or applies an +action. + +\section1 Valid Expressions \table \row +\row \li \c {[?_type="Person"]} \li Return all objects that are Person types. \row @@ -42,6 +57,13 @@ \li \c {[?name exists][/_type]} \li All objects with a "name" field in the database, sorted by _type. \row +\li \c {[?name notExists][/_type]} +\li All objects not containing a "name" field in the database, sorted by _type. + +Note that this constraint precludes the use of an index on the "name" +field, because only objects containing a "name" property will appear +in the index on "name". +\row \li \c {[?name = %thename ][/_type]} \li All objects from the database where the "name" field is the same as the binding for thename in the bindings object provided to find, sorted by _type. @@ -122,5 +144,21 @@ insensitive. \li \c {[?_type="MESSAGE"][= { subject: Subject, sent: DateTimeSent, sender: Sender.Mailbox.EmailAddress } ]} \li List of new objects containing of subject, sent time, and sender email address from message objects. +\row +\li \c {[?_type="MESSAGE"][count]} +\li Count the number of objects of type "MESSAGE"] \endtable + +\section1 Query optimization + +In general, sorting can only be done on properties for which JSON DB has an +index. Any query with a sort order given for which there is no index, will +result in an error. For a query without explicitly provided sort order, a full +object scan is performed. This should be avoided for all but the smallest data +sets. + +See \l {Object Indexes} + */ + + diff --git a/doc/src/reduce-example.qdoc b/doc/src/reduce-example.qdoc new file mode 100644 index 0000000..a0a8b30 --- /dev/null +++ b/doc/src/reduce-example.qdoc @@ -0,0 +1,35 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** GNU Free Documentation License +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms +** and conditions contained in a signed written agreement between you +** and Nokia. +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + +\page creating-a-reduce.html +\title Creating a Reduce View + +\example declarative/reduce-phone + +*/ diff --git a/doc/src/reduce-views.qdoc b/doc/src/reduce-views.qdoc new file mode 100644 index 0000000..d16c5f9 --- /dev/null +++ b/doc/src/reduce-views.qdoc @@ -0,0 +1,136 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** GNU Free Documentation License +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms +** and conditions contained in a signed written agreement between you +** and Nokia. +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! +\page reduce-views.html +\title Reduce Views + +\target reduce + +\section1 Performing a Reduce + +A reduce is defined by creating a \l Reduce object in the database. A +Reduce operates on objects of a single source type, producing values +of a single target type. A Reduce incrementally combines source +objects with matching keys using JavaScript "add" and "subtract" +functions. + +\table +\row +\li _type +\li Reduce + +\row +\li targetType +\li The output type of this reduction. The target type must extend \l View. + +\row +\li targetKeyName +\li The name of the key property in the target objects. Defaults to "key" if unspecified. + +\row +\li targetValueName (should be "null") + +\li The name of the value property in the target objects. Defaults to +"value" if unspecified. If this is defined to be "null", then the +object returned from the "add" and "subtract" functions is used as the +whole target object. The old behavior is deprecated and will be +removed. + +\row +\li sourceType +\li The type of the source objects used in this reduction + +\row +\li sourceKeyName + +\li The name of the key property in the source objects. Defaults to +"key" if unspecified. Exactly one of sourceKeyName and +sourceKeyFunction must be specified. + +\row +\li sourceKeyFunction + +\li A string that represents a JavaScript function that takes an +object and returns the key value for that object. Exactly one of +sourceKeyName and sourceKeyFunction must be specified. + +\row +\li add +\li A string that evaluates to a Javascript function taking three arguments: keyValue, targetObject, sourceObject.. + +\row +\li subtract +\li A string that evaluates to a Javascript function taking three arguments: keyValue, targetObject, sourceObject. +\endtable + + +\section2 Deterministic Uuids + +View objects created by Reduce are assigned a deterministic Uuid. If +the "add" or "subtract" functions assign _uuid to the object they +return, then that value is used as the uuid for the object. Otherwise, +the view engine constructs an identifier string as follows: +\code + var identifier = targetType + ":" + reduceDefinitionUuid + ":" + keyValue; + var uuid = jsondb.createUuidFromString(identifier); +\endcode + +\section2 Theory of Operation + +When the view is updated, it operates on all of the source objects +that were changed since the last time the view was updated. + +If a source object is created, the key is extracted from the object +using either sourceKeyName or sourceKeyFunction. The "add" function is +applied to three arguments: the key value, the previous target object +for the key value or undefined, and the source object. + +If a source object is removed, the key is extracted from the object +that was removed using either sourceKeyName or sourceKeyFunction. The +"remove" function is applied to three arguments: the key value, the +previous target object for the key value or undefined, and the source +object. + +If a source object is updated, it is treated as a remove of the +previous value and a creation of the new value, so there will be a +call to the "subtract" function and then the "add" function. + +All of the changes are staged together during an update of the view to +minimize the number of changes of target objects visible via +notifications. + +\section2 Reduce Proxy + +When the map functions run, they have access to a jsondb proxy object with one method: + +\table +\row +\li \l {jsondb.createUuidFromString}{jsondb.createUuidFromString}(identifier) +\endtable + +*/ diff --git a/src/client/qjsondbconnection.cpp b/src/client/qjsondbconnection.cpp index c2253fb..f7f10b1 100644 --- a/src/client/qjsondbconnection.cpp +++ b/src/client/qjsondbconnection.cpp @@ -49,6 +49,7 @@ #include <qcoreevent.h> #include <qtimer.h> #include <qjsonarray.h> +#include <qthreadstorage.h> /*! \macro QT_USE_NAMESPACE_JSONDB @@ -100,8 +101,7 @@ QT_BEGIN_NAMESPACE_JSONDB -QJsonDbConnection *QJsonDbConnectionPrivate::defaultConnection = 0; -Q_GLOBAL_STATIC(QJsonDbConnection, _q_defaultConnection) +Q_GLOBAL_STATIC(QThreadStorage<QJsonDbConnection *>, _q_defaultConnection); /*! \class QJsonDbConnection @@ -702,31 +702,33 @@ bool QJsonDbConnection::removeWatcher(QJsonDbWatcher *watcher) /*! \nonreentrant - Sets the global default jsondb connection to \a connection. - - \warning In a multithreaded application, the default connection should be - set on application startup, before any threads that use jsondb are created. + Sets the default jsondb connection for the current thread to \a connection. + This transfers ownership of \a connection to QtJsonDb, it will be deleted + on thread exit, or on the next call to setDefaultConnection(). \sa QJsonDbConnection::defaultConnection() */ void QJsonDbConnection::setDefaultConnection(QJsonDbConnection *connection) { - QJsonDbConnectionPrivate::defaultConnection = connection; + _q_defaultConnection()->setLocalData(connection); } /*! - Returns the default jsondb connection object. - - \warning This function is thread-safe, however the QJsonDbConnection object - is not. + Returns the default jsondb connection object for the current thread. If no + connection has been set for the current thread with setDefaultConnection(), + a new connection is created. \sa QJsonDbConnection::setDefaultConnection() */ QJsonDbConnection *QJsonDbConnection::defaultConnection() { - if (QJsonDbConnectionPrivate::defaultConnection) - return QJsonDbConnectionPrivate::defaultConnection; - return _q_defaultConnection(); + QThreadStorage<QJsonDbConnection *> *storage = _q_defaultConnection(); + QJsonDbConnection *defaultConnection = storage->localData(); + if (!defaultConnection) { + defaultConnection = new QJsonDbConnection; + storage->setLocalData(defaultConnection); + } + return defaultConnection; } /*! diff --git a/src/client/qjsondbconnection_p.h b/src/client/qjsondbconnection_p.h index 9ed5349..bf65e6b 100644 --- a/src/client/qjsondbconnection_p.h +++ b/src/client/qjsondbconnection_p.h @@ -95,8 +95,6 @@ public: void removeWatcher(QJsonDbWatcher *); void reactivateAllWatchers(); - static QJsonDbConnection *defaultConnection; - QJsonDbConnection *q_ptr; QString socketName; QJsonDbConnection::Status status; diff --git a/src/client/qjsondbreadrequest.cpp b/src/client/qjsondbreadrequest.cpp index 181b4f8..dae0774 100644 --- a/src/client/qjsondbreadrequest.cpp +++ b/src/client/qjsondbreadrequest.cpp @@ -54,7 +54,7 @@ QT_BEGIN_NAMESPACE_JSONDB \brief The QJsonDbReadRequest class allows to query database. - See \l{Expression Examples} for documentation on the query string format. + See \l{Queries} for documentation on the query string format. \code QJsonDbReadRequest *request = new QJsonDbReadRequest; @@ -130,7 +130,7 @@ QJsonDbReadRequest::~QJsonDbReadRequest() \brief the query string Specifies the query string for the request in the format described in - \l{Expression Examples}. + \l{Queries}. \sa queryLimit, bindValue() */ diff --git a/src/client/qjsondbwatcher.h b/src/client/qjsondbwatcher.h index c2101ed..d647fdf 100644 --- a/src/client/qjsondbwatcher.h +++ b/src/client/qjsondbwatcher.h @@ -115,7 +115,7 @@ public: Q_SIGNALS: void notificationsAvailable(int count); void statusChanged(QtJsonDb::QJsonDbWatcher::Status newStatus); - void error(int code, const QString &message); + void error(QtJsonDb::QJsonDbWatcher::ErrorCode code, const QString &message); // signals for properties void lastStateNumberChanged(int stateNumber); diff --git a/src/common/jsondbcollator.cpp b/src/common/jsondbcollator.cpp index 19726fe..4fa45c4 100644 --- a/src/common/jsondbcollator.cpp +++ b/src/common/jsondbcollator.cpp @@ -52,6 +52,12 @@ QT_BEGIN_NAMESPACE_JSONDB +JsonDbCollatorPrivate::~JsonDbCollatorPrivate() +{ + if (collator) + ucol_close(collator); +} + static const int collationStringsCount = 13; static const char * const collationStrings[collationStringsCount] = { "default", @@ -87,19 +93,15 @@ JsonDbCollator::JsonDbCollator(const JsonDbCollator &other) JsonDbCollator::~JsonDbCollator() { - if (!d->ref.deref()) { - if (d->collator) - ucol_close(d->collator); - } + if (!d->ref.deref()) + delete d; } JsonDbCollator &JsonDbCollator::operator=(const JsonDbCollator &other) { if (this != &other) { - if (!d->ref.deref()) { - if (d->collator) - ucol_close(d->collator); - } + if (!d->ref.deref()) + delete d; *d = *other.d; d->ref.ref(); } @@ -146,7 +148,7 @@ void JsonDbCollator::init() Q_ASSERT((int)d->collation < collationStringsCount); const char *collationString = collationStrings[(int)d->collation]; UErrorCode status = U_ZERO_ERROR; - QByteArray name = (d->locale.bcp47Name().replace(QLatin1Char('-'), QLatin1Char('_')) + QString("@collation=") + QLatin1String(collationString)).toLatin1(); + QByteArray name = (d->locale.bcp47Name().replace(QLatin1Char('-'), QLatin1Char('_')) + QLatin1String("@collation=") + QLatin1String(collationString)).toLatin1(); d->collator = ucol_open(name.constData(), &status); if (U_FAILURE(status)) qWarning("Could not create collator: %d", status); @@ -211,10 +213,8 @@ void JsonDbCollator::detach() x->options = d->options; x->modified = true; x->collator = 0; - if (!d->ref.deref()) { - if (d->collator) - ucol_close(d->collator); - } + if (!d->ref.deref()) + delete d; d = x; } } diff --git a/src/common/jsondbcollator_p.h b/src/common/jsondbcollator_p.h index 3902369..5a76f29 100644 --- a/src/common/jsondbcollator_p.h +++ b/src/common/jsondbcollator_p.h @@ -73,6 +73,7 @@ public: options(0), collator(0) { ref.store(1); } + ~JsonDbCollatorPrivate(); int compare(ushort *s1, int len1, ushort *s2, int len2); }; diff --git a/src/daemon/dbserver.cpp b/src/daemon/dbserver.cpp index d7cc336..29b050c 100644 --- a/src/daemon/dbserver.cpp +++ b/src/daemon/dbserver.cpp @@ -103,6 +103,9 @@ DBServer::DBServer(const QString &filePath, const QString &baseName, QObject *pa mFilePath(filePath), mBaseName(baseName) { + // for queued connection handling + qRegisterMetaType<JsonDbPartition*>("JsonDbPartition*"); + qRegisterMetaType<QSet<QString> >("QSet<QString>"); QFileInfo info(filePath); @@ -229,18 +232,24 @@ bool DBServer::loadPartitions() this, SLOT(objectsUpdated(JsonDbUpdateList))); } + QHash<QString, JsonDbPartition*> oldPartitions = mPartitions; + oldPartitions.remove(mBaseName); if (!mDefaultPartition) { mDefaultPartition = new JsonDbPartition(QDir(mFilePath).absoluteFilePath(mBaseName + QLatin1String(".db")), mBaseName, mOwner, this); connect(mDefaultPartition, SIGNAL(objectsUpdated(JsonDbUpdateList)), this, SLOT(objectsUpdated(JsonDbUpdateList))); + connect(mDefaultPartition, SIGNAL(viewUpdated(QString)), + this, SLOT(viewUpdated(QString)), + Qt::QueuedConnection); if (!mDefaultPartition->open()) return false; + + mPartitions[mBaseName] = mDefaultPartition; } - QHash<QString, JsonDbPartition*> oldPartitions = mPartitions; JsonDbQueryResult partitions = mDefaultPartition->queryObjects(mOwner, JsonDbQuery::parse(QLatin1String("[?_type=\"Partition\"]"))); foreach (const JsonDbObject &partition, partitions.data) { @@ -254,6 +263,9 @@ bool DBServer::loadPartitions() JsonDbPartition *p = new JsonDbPartition(filename, name, mOwner, this); connect(p, SIGNAL(objectsUpdated(JsonDbUpdateList)), this, SLOT(objectsUpdated(JsonDbUpdateList))); + connect(p, SIGNAL(viewUpdated(QString)), + this, SLOT(viewUpdated(QString)), + Qt::QueuedConnection); if (!p->open()) return false; @@ -430,6 +442,8 @@ void DBServer::objectsUpdated(const QList<JsonDbUpdate> &objects) return; } + QSet<QString> eagerViewUpdates; + // FIXME: pretty good place to batch notifications foreach (const JsonDbUpdate &updated, objects) { @@ -452,14 +466,13 @@ void DBServer::objectsUpdated(const QList<JsonDbUpdate> &objects) QStringList notificationKeys; if (object.contains(JsonDbString::kTypeStr)) { notificationKeys << objectType; - if (mEagerViewSourceTypes.contains(objectType)) { - const QSet<QString> &targetTypes = mEagerViewSourceTypes[objectType]; - for (QSet<QString>::const_iterator it = targetTypes.begin(); it != targetTypes.end(); ++it) { - if (partition) - partition->updateView(*it); - } - } + + // eagerly update views if this object that was created isn't a view type itself + if (mEagerViewSourceTypes.contains(objectType) && partition + && !partition->findView(objectType)) + eagerViewUpdates.insert(objectType); } + if (object.contains(JsonDbString::kUuidStr)) notificationKeys << object.value(JsonDbString::kUuidStr).toString(); notificationKeys << "__generic_notification__"; @@ -477,6 +490,32 @@ void DBServer::objectsUpdated(const QList<JsonDbUpdate> &objects) } } } + + if (!eagerViewUpdates.isEmpty()) + QMetaObject::invokeMethod(this, "updateEagerViews", Qt::QueuedConnection, + Q_ARG(JsonDbPartition*, partition), + Q_ARG(QSet<QString>, eagerViewUpdates)); +} + +void DBServer::viewUpdated(const QString &type) +{ + JsonDbPartition *partition = qobject_cast<JsonDbPartition*>(sender()); + if (!partition) + return; + + if (mEagerViewSourceTypes.contains(type)) + updateEagerViews(partition, QSet<QString>() << type); +} + +void DBServer::updateEagerViews(JsonDbPartition *partition, const QSet<QString> &viewTypes) +{ + foreach (const QString &type, viewTypes) { + const QSet<QString> &targetTypes = mEagerViewSourceTypes[type]; + for (QSet<QString>::const_iterator it = targetTypes.begin(); it != targetTypes.end(); ++it) { + if (partition) + partition->updateView(*it); + } + } } void DBServer::objectUpdated(const QString &partitionName, quint32 stateNumber, JsonDbNotification *n, @@ -498,7 +537,8 @@ void DBServer::objectUpdated(const QString &partitionName, quint32 stateNumber, effectiveAction = JsonDbNotification::Create; } else if (oldMatches && (!newMatches || object.isDeleted())) { r = oldObject; - r.insert(JsonDbString::kDeletedStr, true); + if (object.isDeleted()) + r.insert(JsonDbString::kDeletedStr, true); effectiveAction = JsonDbNotification::Delete; } else if (oldMatches && newMatches) { effectiveAction = JsonDbNotification::Update; @@ -584,9 +624,7 @@ void DBServer::processRead(JsonStream *stream, JsonDbOwner *owner, const QJsonVa int limit = request.contains(JsonDbString::kLimitStr) ? request.value(JsonDbString::kLimitStr).toDouble() : -1; int offset = request.value(JsonDbString::kOffsetStr).toDouble(); - if (!parsedQuery->queryTerms.size() && !parsedQuery->orderTerms.size()) - response = createError(JsonDbError::MissingQuery, QString("Missing query: ") + parsedQuery->queryExplanation.join("\n")); - else if (limit < -1) + if (limit < -1) response = createError(JsonDbError::InvalidLimit, "Invalid limit"); else if (offset < 0) response = createError(JsonDbError::InvalidOffset, "Invalid offset"); @@ -760,7 +798,12 @@ void DBServer::createNotification(const JsonDbObject &object, JsonStream *stream QString query = object.value(JsonDbString::kQueryStr).toString(); QJsonObject bindings = object.value("bindings").toObject(); QString partition = object.value(JsonDbString::kPartitionStr).toString(); - quint32 stateNumber = object.value("initialStateNumber").toDouble(); + + bool ok; + quint32 stateNumber = object.value("initialStateNumber").toVariant().toInt(&ok); + if (!ok) + stateNumber = 0; + if (partition.isEmpty()) partition = mDefaultPartition->name(); @@ -851,6 +894,11 @@ void DBServer::notifyHistoricalChanges(JsonDbNotification *n) } foreach (const QString matchedType, matchedTypes) { JsonDbObjectTable *objectTable = partition->findObjectTable(matchedType); + + // views dont have a _type index + if (partition->findView(matchedType)) + indexName = JsonDbString::kUuidStr; + lastStateNumber = objectTable->stateNumber(); JsonDbIndexQuery *indexQuery = JsonDbIndexQuery::indexQuery(partition, objectTable, indexName, QString("string"), @@ -907,8 +955,7 @@ void DBServer::updateEagerViewTypes(const QString &objectType, JsonDbPartition * JsonDbPartition *DBServer::findPartition(const QString &partitionName) { JsonDbPartition *partition = mDefaultPartition; - // default partition is not in mPartitions - if (!partitionName.isEmpty() && partitionName != mDefaultPartition->name()) { + if (!partitionName.isEmpty()) { if (mPartitions.contains(partitionName)) partition = mPartitions[partitionName]; else diff --git a/src/daemon/dbserver.h b/src/daemon/dbserver.h index 3cadf38..d4df0cc 100644 --- a/src/daemon/dbserver.h +++ b/src/daemon/dbserver.h @@ -92,6 +92,8 @@ protected slots: void notified(const QString &id, quint32 stateNumber, const QJsonObject &object, const QString &action); void objectsUpdated(const JsonDbUpdateList &objects); + void viewUpdated(const QString &type); + void updateEagerViews(JsonDbPartition *partition, const QSet<QString> &viewTypes); private: void objectUpdated(const QString &partitionName, quint32 stateNumber, JsonDbNotification *n, JsonDbNotification::Action action, const JsonDbObject &oldObject, const JsonDbObject &object); diff --git a/src/daemon/jsondbephemeralpartition.cpp b/src/daemon/jsondbephemeralpartition.cpp index dee0983..989748f 100644 --- a/src/daemon/jsondbephemeralpartition.cpp +++ b/src/daemon/jsondbephemeralpartition.cpp @@ -129,6 +129,14 @@ JsonDbWriteResult JsonDbEphemeralPartition::updateObjects(const JsonDbOwner *own if (!mObjects.contains(object.uuid())) action = JsonDbNotification::Create; + // TODO: maybe be this somewhere else so that it doesn't pollute this code + if (object.type() == JsonDbString::kNotificationTypeStr && + object.value(JsonDbString::kQueryStr).toString().isEmpty()) { + result.code = JsonDbError::MissingQuery; + result.message = QLatin1String("Notification objects must contain a query paramater"); + return result; + } + object.insert(JsonDbString::kOwnerStr, owner->ownerId()); object.computeVersion(); mObjects[object.uuid()] = object; diff --git a/src/daemon/jsondbindex.cpp b/src/daemon/jsondbindex.cpp index 90ce8fa..356e3f8 100644 --- a/src/daemon/jsondbindex.cpp +++ b/src/daemon/jsondbindex.cpp @@ -100,10 +100,12 @@ JsonDbCollator::CasePreference _q_correctCasePreferenceString(const QString &s) #else int _q_correctCollationString(const QString &s) { + Q_UNUSED(s); return 0; } int _q_correctCasePreferenceString(const QString &s) { + Q_UNUSED(s); return 0; } #endif //NO_COLLATION_SUPPORT @@ -130,13 +132,15 @@ QString _q_bytesToHexString(const QByteArray &ba) } JsonDbIndex::JsonDbIndex(const QString &fileName, const QString &indexName, const QString &propertyName, - const QString &propertyType, const QString &locale, const QString &collation, + const QString &propertyType, const QStringList &objectType, const QString &locale, const QString &collation, const QString &casePreference, Qt::CaseSensitivity caseSensitivity, JsonDbObjectTable *objectTable) : QObject(objectTable) , mObjectTable(objectTable) + , mIndexName(indexName) , mPropertyName(propertyName) , mPath(propertyName.split('.')) , mPropertyType(propertyType) + , mObjectType(objectType) , mLocale(locale) , mCollation(collation) , mCasePreference(casePreference) @@ -239,7 +243,7 @@ bool JsonDbIndex::validateIndex(const JsonDbObject &newIndex, const JsonDbObject message = QString("Changing old index propertyType from '%1' to '%2' not supported") .arg(oldIndex.value(JsonDbString::kPropertyTypeStr).toString()) .arg(newIndex.value(JsonDbString::kPropertyTypeStr).toString()); - else if (oldIndex.value(JsonDbString::kObjectTypeStr).toString() != newIndex.value(JsonDbString::kObjectTypeStr).toString()) + else if (oldIndex.value(JsonDbString::kObjectTypeStr) != newIndex.value(JsonDbString::kObjectTypeStr)) message = QString("Changing old index objectType from '%1' to '%2' not supported") .arg(oldIndex.value(JsonDbString::kObjectTypeStr).toString()) .arg(newIndex.value(JsonDbString::kObjectTypeStr).toString()); @@ -330,6 +334,9 @@ void JsonDbIndex::indexObject(const ObjectKey &objectKey, JsonDbObject &object, if (mPropertyName == JsonDbString::kUuidStr) return; + if (!mObjectType.isEmpty() && !mObjectType.contains(object.value(JsonDbString::kTypeStr).toString())) + return; + Q_ASSERT(!object.contains(JsonDbString::kDeletedStr) && !object.value(JsonDbString::kDeletedStr).toBool()); QList<QJsonValue> fieldValues = indexValues(object); @@ -370,6 +377,8 @@ void JsonDbIndex::deindexObject(const ObjectKey &objectKey, JsonDbObject &object { if (mPropertyName == JsonDbString::kUuidStr) return; + if (!mObjectType.isEmpty() && !mObjectType.contains(object.value(JsonDbString::kTypeStr).toString())) + return; if (!mBdb) open(); QList<QJsonValue> fieldValues = indexValues(object); diff --git a/src/daemon/jsondbindex.h b/src/daemon/jsondbindex.h index 97520fa..dfbd084 100644 --- a/src/daemon/jsondbindex.h +++ b/src/daemon/jsondbindex.h @@ -71,7 +71,7 @@ class JsonDbIndex : public QObject Q_OBJECT public: JsonDbIndex(const QString &fileName, const QString &indexName, const QString &propertyName, - const QString &propertyType, const QString &locale, const QString &collation, + const QString &propertyType, const QStringList &objectType, const QString &locale, const QString &collation, const QString &casePreference, Qt::CaseSensitivity caseSensitivity, JsonDbObjectTable *objectTable); ~JsonDbIndex(); @@ -79,6 +79,7 @@ public: QString propertyName() const { return mPropertyName; } QStringList fieldPath() const { return mPath; } QString propertyType() const { return mPropertyType; } + QStringList objectType() const { return mObjectType; } QtAddOn::JsonDb::JsonDbBtree *bdb(); @@ -112,9 +113,11 @@ private slots: private: JsonDbObjectTable *mObjectTable; QString mFileName; + QString mIndexName; QString mPropertyName; QStringList mPath; QString mPropertyType; + QStringList mObjectType; QString mLocale; QString mCollation; QString mCasePreference; @@ -165,7 +168,7 @@ public: QString collation; QString casePreference; Qt::CaseSensitivity caseSensitivity; - QString objectType; + QStringList objectType; bool lazy; QPointer<JsonDbIndex> index; }; diff --git a/src/daemon/jsondbmapdefinition.cpp b/src/daemon/jsondbmapdefinition.cpp index a3fe0d1..23baa64 100644 --- a/src/daemon/jsondbmapdefinition.cpp +++ b/src/daemon/jsondbmapdefinition.cpp @@ -73,28 +73,22 @@ JsonDbMapDefinition::JsonDbMapDefinition(const JsonDbOwner *owner, JsonDbPartiti , mTargetType(definition.value("targetType").toString()) , mTargetTable(mPartition->findObjectTable(mTargetType)) { - if (!mDefinition.contains("sourceType")) { - QJsonObject sourceFunctions(mDefinition.contains("join") - ? mDefinition.value("join").toObject() - : mDefinition.value("map").toObject()); - mSourceTypes = sourceFunctions.keys(); - for (int i = 0; i < mSourceTypes.size(); i++) { - const QString &sourceType = mSourceTypes[i]; - mSourceTables[sourceType] = mPartition->findObjectTable(sourceType); - } - } else { - // TODO: remove this case - const QString sourceType = mDefinition.value("sourceType").toString(); + QJsonObject sourceFunctions(mDefinition.contains("join") + ? mDefinition.value("join").toObject() + : mDefinition.value("map").toObject()); + mSourceTypes = sourceFunctions.keys(); + for (int i = 0; i < mSourceTypes.size(); i++) { + const QString &sourceType = mSourceTypes[i]; mSourceTables[sourceType] = mPartition->findObjectTable(sourceType); - mSourceTypes.append(sourceType); } + if (mDefinition.contains("targetKeyName")) + mTargetKeyName = mDefinition.value("targetKeyName").toString(); } void JsonDbMapDefinition::definitionCreated() { - initScriptEngine(); - - mTargetTable->addIndexOnProperty(QLatin1String("_sourceUuids.*"), QLatin1String("string"), mTargetType); + initScriptEngine(); + initIndexes(); foreach (const QString &sourceType, mSourceTypes) { GetObjectsResult getObjectResponse = mPartition->getObjects(JsonDbString::kTypeStr, sourceType); @@ -135,54 +129,46 @@ void JsonDbMapDefinition::initScriptEngine() return; mScriptEngine = new QJSEngine(this); + QString message; + bool compiled = compileMapFunctions(mScriptEngine, mDefinition, mMapFunctions, message); + if (!compiled) + setError(message); QJSValue globalObject = mScriptEngine->globalObject(); globalObject.setProperty("console", mScriptEngine->newQObject(new Console())); - if (!mDefinition.contains("sourceType")) { - QJsonObject sourceFunctions(mDefinition.contains("join") - ? mDefinition.value("join").toObject() - : mDefinition.value("map").toObject()); - for (int i = 0; i < mSourceTypes.size(); i++) { - const QString &sourceType = mSourceTypes[i]; - const QString &script = sourceFunctions.value(sourceType).toString(); - QJSValue mapFunction = - mScriptEngine->evaluate(QString("var map_%1 = (%2); map_%1;").arg(QString(sourceType).replace(".", "_")).arg(script)); - if (mapFunction.isError() || !mapFunction.isCallable()) - setError( "Unable to parse map function: " + mapFunction.toString()); - mMapFunctions[sourceType] = mapFunction; - } - - mJoinProxy = new JsonDbJoinProxy(mOwner, mPartition, this); - connect(mJoinProxy, SIGNAL(lookupRequested(QJSValue,QJSValue)), - this, SLOT(lookupRequested(QJSValue,QJSValue))); - connect(mJoinProxy, SIGNAL(viewObjectEmitted(QJSValue)), - this, SLOT(viewObjectEmitted(QJSValue))); - globalObject.setProperty("_jsondb", mScriptEngine->newQObject(mJoinProxy)); - // we use this snippet of javascript so that we can bind "jsondb.emit" - // even though "emit" is a Qt keyword - if (mDefinition.contains("join")) - // only joins can use lookup() - mScriptEngine->evaluate("var jsondb = { emit: _jsondb.create, lookup: _jsondb.lookup, createUuidFromString: _jsondb.createUuidFromString};"); - else - mScriptEngine->evaluate("var jsondb = { emit: _jsondb.create, createUuidFromString: _jsondb.createUuidFromString };"); + mJoinProxy = new JsonDbJoinProxy(mOwner, mPartition, this); + connect(mJoinProxy, SIGNAL(lookupRequested(QJSValue,QJSValue)), + this, SLOT(lookupRequested(QJSValue,QJSValue))); + connect(mJoinProxy, SIGNAL(viewObjectEmitted(QJSValue)), + this, SLOT(viewObjectEmitted(QJSValue))); + globalObject.setProperty("_jsondb", mScriptEngine->newQObject(mJoinProxy)); + // we use this snippet of javascript so that we can bind "jsondb.emit" + // even though "emit" is a Qt keyword + if (mDefinition.contains("join")) + // only joins can use lookup() + mScriptEngine->evaluate("var jsondb = { emit: _jsondb.create, lookup: _jsondb.lookup, createUuidFromString: _jsondb.createUuidFromString};"); + else + mScriptEngine->evaluate("var jsondb = { emit: _jsondb.create, createUuidFromString: _jsondb.createUuidFromString };"); +} - } else { - const QString sourceType = mDefinition.value("sourceType").toString(); - const QString &script = mDefinition.value("map").toString(); +bool JsonDbMapDefinition::compileMapFunctions(QJSEngine *scriptEngine, QJsonObject definition, QMap<QString,QJSValue> &mapFunctions, QString &message) +{ + bool status = true; + QJsonObject sourceFunctions(definition.contains("join") + ? definition.value("join").toObject() + : definition.value("map").toObject()); + for (QJsonObject::const_iterator it = sourceFunctions.begin(); it != sourceFunctions.end(); ++it) { + const QString &sourceType = it.key(); + const QString &script = it.value().toString(); QJSValue mapFunction = - mScriptEngine->evaluate(QString("var map_%1 = (%2); map_%1;").arg(QString(sourceType).replace(".", "_")).arg(script)); - if (mapFunction.isError() || !mapFunction.isCallable()) - setError( "Unable to parse map function: " + mapFunction.toString()); - mMapFunctions[sourceType] = mapFunction; - - mMapProxy = new JsonDbMapProxy(mOwner, mPartition, this); - connect(mMapProxy, SIGNAL(lookupRequested(QJSValue,QJSValue)), - this, SLOT(lookupRequested(QJSValue,QJSValue))); - connect(mMapProxy, SIGNAL(viewObjectEmitted(QJSValue)), - this, SLOT(viewObjectEmitted(QJSValue))); - globalObject.setProperty("jsondb", mScriptEngine->newQObject(mMapProxy)); - qWarning() << "Old-style Map from sourceType" << sourceType << " to targetType" << mDefinition.value("targetType"); + scriptEngine->evaluate(QString("var map_%1 = (%2); map_%1;").arg(QString(sourceType).replace(".", "_")).arg(script)); + if (mapFunction.isError() || !mapFunction.isCallable()) { + message = QString( "Unable to parse map function: " + mapFunction.toString()); + status = false; + } + mapFunctions[sourceType] = mapFunction; } + return status; } void JsonDbMapDefinition::releaseScriptEngine() @@ -192,6 +178,12 @@ void JsonDbMapDefinition::releaseScriptEngine() mScriptEngine = 0; } + +void JsonDbMapDefinition::initIndexes() +{ + mTargetTable->addIndexOnProperty(QLatin1String("_sourceUuids.*"), QLatin1String("string"), mTargetType); +} + void JsonDbMapDefinition::updateObject(const JsonDbObject &beforeObject, const JsonDbObject &afterObject) { QHash<QString, JsonDbObject> unmappedObjects; @@ -338,7 +330,7 @@ void JsonDbMapDefinition::lookupRequested(const QJSValue &query, const QJSValue if (mapped.isError()) setError("Error executing map function during lookup: " + mapped.toString()); - mSourceUuids.removeLast(); + mSourceUuids.removeOne(uuid); } } @@ -346,13 +338,32 @@ void JsonDbMapDefinition::viewObjectEmitted(const QJSValue &value) { JsonDbObject newItem(JsonDbObject::fromJSValue(value).toObject()); newItem.insert(JsonDbString::kTypeStr, mTargetType); - QJsonArray sourceUuids; - foreach (const QString &str, mSourceUuids) - sourceUuids.append(str); - newItem.insert("_sourceUuids", sourceUuids); - - if (!newItem.contains(JsonDbString::kUuidStr)) - newItem.generateUuid(); + mSourceUuids.sort(); + QJsonArray sourceUuidArray; + foreach (const QString &sourceUuid, mSourceUuids) + sourceUuidArray.append(sourceUuid); + newItem.insert("_sourceUuids", sourceUuidArray); + + if (!newItem.contains(JsonDbString::kUuidStr)) { + if (newItem.contains(QLatin1String("_id"))) + newItem.generateUuid(); + else { + QString targetKeyString; + if (!mTargetKeyName.isEmpty()) + targetKeyString = JsonDbObject(newItem).propertyLookup(mTargetKeyName).toString(); + + // colon separated sorted source uuids + QString sourceUuidString = mSourceUuids.join(":"); + QString identifier = + QString("%1:%2%3%4") + .arg(mTargetType) + .arg(sourceUuidString) + .arg(targetKeyString.isEmpty() ? "" : ":") + .arg(targetKeyString); + newItem.insert(JsonDbString::kUuidStr, + JsonDbObject::createUuidFromString(identifier).toString()); + } + } QString uuid = newItem.value(JsonDbString::kUuidStr).toString(); mEmittedObjects.insert(uuid, newItem); @@ -381,14 +392,18 @@ bool JsonDbMapDefinition::validateDefinition(const JsonDbObject &map, JsonDbPart if (targetType.isEmpty()) { message = QLatin1Literal("targetType property for Map not specified"); + } else if (map.contains(QLatin1Literal("sourceType"))) { + message = QLatin1Literal("sourceType property for Map no longer supported"); } else if (!view) { message = QLatin1Literal("targetType must be of a type that extends View"); } else if (map.contains("join")) { QJsonObject sourceFunctions = map.value("join").toObject(); - sourceTypes = sourceFunctions.keys(); + if (sourceFunctions.isEmpty()) message = QLatin1Literal("sourceTypes and functions for Map with join not specified"); + sourceTypes = sourceFunctions.keys(); + foreach (const QString &sourceType, sourceTypes) { if (sourceFunctions.value(sourceType).toString().isEmpty()) message = QString("join function for source type '%1' not specified for Map").arg(sourceType); @@ -401,14 +416,41 @@ bool JsonDbMapDefinition::validateDefinition(const JsonDbObject &map, JsonDbPart if (map.contains("map")) message = QLatin1Literal("Map 'join' and 'map' options are mutually exclusive"); - else if (map.contains("sourceType")) - message = QLatin1Literal("Map 'join' and 'sourceType' options are mutually exclusive"); } else { QJsonValue mapValue = map.value("map"); - if (map.value("sourceType").toString().isEmpty() && !mapValue.isObject()) + if (!mapValue.isObject()) message = QLatin1String("sourceType property for Map not specified"); else if (!mapValue.isString() && !mapValue.isObject()) message = QLatin1String("map function for Map not specified"); + + if (mapValue.isObject()) { + + QJsonObject sourceFunctions = mapValue.toObject(); + + if (sourceFunctions.isEmpty()) + message = QLatin1Literal("sourceTypes and functions for Map with map not specified"); + + sourceTypes = sourceFunctions.keys(); + + foreach (const QString &sourceType, sourceTypes) { + if (sourceFunctions.value(sourceType).toString().isEmpty()) + message = QString("map function for source type '%1' not specified for Map").arg(sourceType); + if (view->mMapDefinitionsBySource.contains(sourceType) + && view->mMapDefinitionsBySource.value(sourceType)->uuid() != uuid) + message = + QString("duplicate Map definition on source %1 and target %2") + .arg(sourceType).arg(targetType); + } + } + } + + // check for parse errors + if (message.isEmpty()) { + // FIXME: This is static because otherwise we leak memory per QJSEngine instance + static QJSEngine *scriptEngine = new QJSEngine; + QMap<QString,QJSValue> mapFunctions; + compileMapFunctions(scriptEngine, map, mapFunctions, message); + scriptEngine->collectGarbage(); } return message.isEmpty(); diff --git a/src/daemon/jsondbmapdefinition.h b/src/daemon/jsondbmapdefinition.h index 6e78b21..61b19b3 100644 --- a/src/daemon/jsondbmapdefinition.h +++ b/src/daemon/jsondbmapdefinition.h @@ -83,9 +83,12 @@ public: void initScriptEngine(); void releaseScriptEngine(); + void initIndexes(); + void setError(const QString &errorMsg); void updateObject(const JsonDbObject &before, const JsonDbObject &after); static bool validateDefinition(const JsonDbObject &map, JsonDbPartition *partition, QString &message); + static bool compileMapFunctions(QJSEngine *scriptEngine, QJsonObject definition, QMap<QString,QJSValue> &mapFunctions, QString &message); public slots: void viewObjectEmitted(const QJSValue &value); @@ -99,6 +102,7 @@ private: JsonDbPartition *mPartition; const JsonDbOwner *mOwner; QJsonObject mDefinition; + QString mTargetKeyName; QJSEngine *mScriptEngine; JsonDbMapProxy *mMapProxy; // to be removed when old map/lookup converted to join/lookup JsonDbJoinProxy *mJoinProxy; @@ -108,7 +112,7 @@ private: QStringList mSourceTypes; JsonDbObjectTable *mTargetTable; QMap<QString,JsonDbObjectTable *> mSourceTables; - QList<QString> mSourceUuids; + QStringList mSourceUuids; // a set of uuids with sorted elements QHash<QString,JsonDbObject> mEmittedObjects; }; diff --git a/src/daemon/jsondbobject.cpp b/src/daemon/jsondbobject.cpp index 07cbabd..1a7467b 100644 --- a/src/daemon/jsondbobject.cpp +++ b/src/daemon/jsondbobject.cpp @@ -53,23 +53,6 @@ QT_ADDON_JSONDB_BEGIN_NAMESPACE -static QUuid generateUUIDv3(const QString &uri) -{ - QCryptographicHash hash(QCryptographicHash::Md5); - hash.addData(QUuid(0x6ba7b810, 0x9dad, 0x11d1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8).toRfc4122()); - hash.addData(uri.toUtf8()); - QByteArray hashResult = hash.result(); - - QUuid result = QUuid::fromRfc4122(hashResult); - - result.data3 &= 0x0FFF; - result.data3 |= (3 << 12); - result.data4[0] &= 0x3F; - result.data4[0] |= 0x80; - - return result; -} - JsonDbObject::JsonDbObject() { } @@ -118,12 +101,39 @@ void JsonDbObject::markDeleted() insert(JsonDbString::kDeletedStr, true); } +struct Uuid +{ + uint data1; + ushort data2; + ushort data3; + uchar data4[8]; +}; + +// copied from src/client/qjsondbobject.cpp: +static const Uuid JsonDbNamespace = {0x6ba7b810, 0x9dad, 0x11d1, { 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8} }; + +/*! + Returns deterministic uuid that can be used to identify given \a identifier. + + The uuid is generated using QtJsonDb UUID namespace on a value of the + given \a identifier. + + \sa QJsonDbObject::createUuidFromString(), QJsonDbObject::createUuid() +*/ +QUuid JsonDbObject::createUuidFromString(const QString &identifier) +{ + const QUuid ns(JsonDbNamespace.data1, JsonDbNamespace.data2, JsonDbNamespace.data3, + JsonDbNamespace.data4[0], JsonDbNamespace.data4[1], JsonDbNamespace.data4[2], + JsonDbNamespace.data4[3], JsonDbNamespace.data4[4], JsonDbNamespace.data4[5], + JsonDbNamespace.data4[6], JsonDbNamespace.data4[7]); + return QUuid::createUuidV3(ns, identifier); +} + void JsonDbObject::generateUuid() { QLatin1String idStr("_id"); if (contains(idStr)) { - QByteArray rfc4122 = generateUUIDv3(value(idStr).toString()).toRfc4122(); - QUuid uuid(QUuid::fromRfc4122((rfc4122))); + QUuid uuid(createUuidFromString(value(idStr).toString())); insert(JsonDbString::kUuidStr, uuid.toString()); } else { QUuid uuid(QUuid::createUuid()); diff --git a/src/daemon/jsondbobject.h b/src/daemon/jsondbobject.h index 29e704a..b648518 100644 --- a/src/daemon/jsondbobject.h +++ b/src/daemon/jsondbobject.h @@ -73,6 +73,7 @@ public: void markDeleted(); void generateUuid(); + static QUuid createUuidFromString(const QString &id); QString computeVersion(); bool updateVersionOptimistic(const JsonDbObject &other, QString *versionWritten); diff --git a/src/daemon/jsondbobjecttable.cpp b/src/daemon/jsondbobjecttable.cpp index 807afff..36daede 100644 --- a/src/daemon/jsondbobjecttable.cpp +++ b/src/daemon/jsondbobjecttable.cpp @@ -249,7 +249,7 @@ QHash<QString, IndexSpec> JsonDbObjectTable::indexSpecs() const } bool JsonDbObjectTable::addIndex(const QString &indexName, const QString &propertyName, - const QString &propertyType, const QString &objectType, const QString &propertyFunction, + const QString &propertyType, const QStringList &objectTypes, const QString &propertyFunction, const QString &locale, const QString &collation, const QString &casePreference, Qt::CaseSensitivity caseSensitivity) { @@ -273,9 +273,9 @@ bool JsonDbObjectTable::addIndex(const QString &indexName, const QString &proper indexSpec.collation = collation; indexSpec.casePreference = casePreference; indexSpec.caseSensitivity = caseSensitivity; - indexSpec.objectType = objectType; + indexSpec.objectType = objectTypes; indexSpec.lazy = false; //lazy; - indexSpec.index = new JsonDbIndex(mFilename, name, propertyName, propertyType, locale, collation, casePreference, caseSensitivity, this); + indexSpec.index = new JsonDbIndex(mFilename, name, propertyName, propertyType, objectTypes, locale, collation, casePreference, caseSensitivity, this); if (!propertyFunction.isEmpty() && propertyName.isEmpty()) // propertyName takes precedence indexSpec.index->setPropertyFunction(propertyFunction); indexSpec.index->setCacheSize(jsondbSettings->cacheSize()); @@ -291,7 +291,10 @@ bool JsonDbObjectTable::addIndex(const QString &indexName, const QString &proper indexObject.insert(JsonDbString::kCollationStr, collation); indexObject.insert(JsonDbString::kCaseSensitiveStr, (bool)caseSensitivity); indexObject.insert(JsonDbString::kCasePreferenceStr, casePreference); - indexObject.insert(JsonDbString::kObjectTypeStr, objectType); + QJsonArray objectTypeList; + foreach (const QString objectType, objectTypes) + objectTypeList.append(objectType); + indexObject.insert(JsonDbString::kObjectTypeStr, objectTypeList); indexObject.insert("lazy", false); indexObject.insert(JsonDbString::kPropertyFunctionStr, propertyFunction); Q_ASSERT(!name.isEmpty()); diff --git a/src/daemon/jsondbobjecttable.h b/src/daemon/jsondbobjecttable.h index e02194e..61d0fc0 100644 --- a/src/daemon/jsondbobjecttable.h +++ b/src/daemon/jsondbobjecttable.h @@ -140,7 +140,7 @@ public: bool addIndex(const QString &indexName, const QString &propertyName = QString(), const QString &propertyType = QString("string"), - const QString &objectType = QString(), + const QStringList &objectTypes = QStringList(), const QString &propertyFunction = QString(), const QString &locale = QString(), const QString &collation = QString(), @@ -149,7 +149,8 @@ public: bool addIndexOnProperty(const QString &propertyName, const QString &propertyType = QString("string"), const QString &objectType = QString()) - { return addIndex(propertyName, propertyName, propertyType, objectType); } + { return addIndex(propertyName, propertyName, propertyType, + objectType.isEmpty() ? QStringList() : (QStringList() << objectType)); } bool removeIndex(const QString &indexName); void reindexObjects(const QString &indexName, const QStringList &path, quint32 stateNumber); void indexObject(const ObjectKey &objectKey, JsonDbObject object, quint32 stateNumber); diff --git a/src/daemon/jsondbpartition.cpp b/src/daemon/jsondbpartition.cpp index 73c5cb2..75dab7b 100644 --- a/src/daemon/jsondbpartition.cpp +++ b/src/daemon/jsondbpartition.cpp @@ -459,6 +459,7 @@ JsonDbView *JsonDbPartition::addView(const QString &viewType) return view; view = new JsonDbView(this, viewType, this); + connect(view, SIGNAL(updated(QString)), this, SIGNAL(viewUpdated(QString))); view->open(); mViews.insert(viewType, view); return view; @@ -695,33 +696,52 @@ void JsonDbPartition::initIndexes() QString indexName = JsonDbIndex::determineName(indexObject); QString propertyName = indexObject.value(JsonDbString::kPropertyNameStr).toString(); QString propertyType = indexObject.value(JsonDbString::kPropertyTypeStr).toString(); - QString objectType = indexObject.value(JsonDbString::kObjectTypeStr).toString(); QString propertyFunction = indexObject.value(JsonDbString::kPropertyFunctionStr).toString(); QString locale = indexObject.value(JsonDbString::kLocaleStr).toString(); QString collation = indexObject.value(JsonDbString::kCollationStr).toString(); QString casePreference = indexObject.value(JsonDbString::kCasePreferenceStr).toString(); + QStringList objectTypes; + QJsonValue objectTypeValue = indexObject.value(JsonDbString::kObjectTypeStr); + if (objectTypeValue.isString()) { + objectTypes.append(objectTypeValue.toString()); + } else if (objectTypeValue.isArray()) { + foreach (const QJsonValue objectType, objectTypeValue.toArray()) + objectTypes.append(objectType.toString()); + } + Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive; if (indexObject.contains(JsonDbString::kCaseSensitiveStr)) caseSensitivity = (indexObject.value(JsonDbString::kCaseSensitiveStr).toBool() == true ? Qt::CaseSensitive : Qt::CaseInsensitive); - addIndex(indexName, propertyName, propertyType, objectType, propertyFunction, locale, collation, casePreference, caseSensitivity); + addIndex(indexName, propertyName, propertyType, objectTypes, propertyFunction, locale, collation, casePreference, caseSensitivity); } } } bool JsonDbPartition::addIndex(const QString &indexName, const QString &propertyName, - const QString &propertyType, const QString &objectType, const QString &propertyFunction, + const QString &propertyType, const QStringList &objectTypes, const QString &propertyFunction, const QString &locale, const QString &collation, const QString &casePreference, Qt::CaseSensitivity caseSensitivity) { Q_ASSERT(!indexName.isEmpty()); //qDebug() << "JsonDbBtreePartition::addIndex" << propertyName << objectType; - JsonDbObjectTable *table = findObjectTable(objectType); + JsonDbObjectTable *table = 0; + if (objectTypes.isEmpty()) + table = mainObjectTable(); + else + foreach (const QString &objectType, objectTypes) { + JsonDbObjectTable *t = findObjectTable(objectType); + if (table && (t != table)) { + qDebug() << "addIndex" << "index on multiple tables" << objectTypes; + return false; + } + table = t; + } const IndexSpec *indexSpec = table->indexSpec(indexName); if (indexSpec) return true; //if (gVerbose) qDebug() << "JsonDbBtreePartition::addIndex" << propertyName << objectType; - return table->addIndex(indexName, propertyName, propertyType, objectType, propertyFunction, locale, collation, casePreference, caseSensitivity); + return table->addIndex(indexName, propertyName, propertyType, objectTypes, propertyFunction, locale, collation, casePreference, caseSensitivity); } bool JsonDbPartition::removeIndex(const QString &indexName, const QString &objectType) @@ -853,7 +873,11 @@ JsonDbIndexQuery *JsonDbPartition::compileIndexQuery(const JsonDbOwner *owner, c int indexedQueryTermCount = 0; JsonDbObjectTable *table = mObjectTable; //TODO fix me JsonDbView *view = 0; + QList<QString> unindexablePropertyNames; // fields for which we cannot use an index if (orQueryTerms.size()) { + // first pass to find unindexable property names + for (int i = 0; i < orQueryTerms.size(); i++) + unindexablePropertyNames.append(orQueryTerms[i].findUnindexablePropertyNames()); for (int i = 0; i < orQueryTerms.size(); i++) { const OrQueryTerm orQueryTerm = orQueryTerms[i]; const QList<QString> &querypropertyNames = orQueryTerm.propertyNames(); @@ -872,7 +896,9 @@ JsonDbIndexQuery *JsonDbPartition::compileIndexQuery(const JsonDbOwner *owner, c if (table->indexSpec(propertyName)) indexedQueryTermCount++; - else if (indexCandidate.isEmpty() && (propertyName != JsonDbString::kTypeStr)) { + else if (indexCandidate.isEmpty() + && (propertyName != JsonDbString::kTypeStr) + && !unindexablePropertyNames.contains(propertyName)) { indexCandidate = propertyName; if (!queryTerm.joinField().isEmpty()) indexCandidate = queryTerm.joinPaths()[0].join("->"); @@ -938,19 +964,14 @@ JsonDbIndexQuery *JsonDbPartition::compileIndexQuery(const JsonDbOwner *owner, c if (!table->indexSpec(propertyName)) { if (jsondbSettings->verbose() || jsondbSettings->performanceLog()) qDebug() << "Unindexed sort term" << propertyName << orderTerm.ascending; - if (0) { - if (jsondbSettings->verbose()) - qDebug() << "adding index for sort term" << propertyName; - Q_ASSERT(table); - //TODO: remove this - table->addIndexOnProperty(propertyName); - Q_ASSERT(table->indexSpec(propertyName)); - if (jsondbSettings->verbose()) - qDebug() << "done adding index" << propertyName; - } else { - residualQuery->orderTerms.append(orderTerm); - continue; - } + residualQuery->orderTerms.append(orderTerm); + continue; + } + if (unindexablePropertyNames.contains(propertyName)) { + if (jsondbSettings->verbose() || jsondbSettings->performanceLog()) + qDebug() << "Unindexable sort term uses notExists" << propertyName << orderTerm.ascending; + residualQuery->orderTerms.append(orderTerm); + continue; } if (!indexQuery) { orderField = propertyName; @@ -992,7 +1013,10 @@ JsonDbIndexQuery *JsonDbPartition::compileIndexQuery(const JsonDbOwner *owner, c continue; } - if (!indexQuery && (propertyName != JsonDbString::kTypeStr) && table->indexSpec(propertyName)) { + if (!indexQuery + && (propertyName != JsonDbString::kTypeStr) + && table->indexSpec(propertyName) + && !unindexablePropertyNames.contains(propertyName)) { orderField = propertyName; const IndexSpec *indexSpec = table->indexSpec(propertyName); if (view) @@ -1103,6 +1127,14 @@ JsonDbQueryResult JsonDbPartition::queryObjects(const JsonDbOwner *owner, const JsonDbObjectList results; JsonDbObjectList joinedResults; + if (!(query->queryTerms.size() || query->orderTerms.size())) { + QJsonObject error; + error.insert(JsonDbString::kCodeStr, JsonDbError::MissingQuery); + error.insert(JsonDbString::kMessageStr, QString("Missing query: %1").arg(query->queryExplanation.join("\n"))); + result.error = error; + return result; + } + QElapsedTimer time; time.start(); JsonDbIndexQuery *indexQuery = compileIndexQuery(owner, query); @@ -1348,6 +1380,10 @@ JsonDbWriteResult JsonDbPartition::updateObjects(const JsonDbOwner *owner, const result.code = JsonDbError::InvalidSchemaOperation; result.message = errorMsg; return result; + } else if ((errorCode = checkBuiltInTypeAccessControl(forCreation, owner, master, oldMaster, errorMsg)) != JsonDbError::NoError) { + result.code = errorCode; + result.message = errorMsg; + return result; } } @@ -1551,6 +1587,70 @@ bool JsonDbPartition::checkNaturalObjectType(const JsonDbObject &object, QString return true; } +JsonDbError::ErrorCode JsonDbPartition::checkBuiltInTypeAccessControl(bool forCreation, const JsonDbOwner *owner, const JsonDbObject &object, const JsonDbObject &oldObject, QString &errorMsg) +{ + QString objectType = object.value(JsonDbString::kTypeStr).toString(); + errorMsg.clear(); + + // Access control checks + if (objectType == JsonDbString::kMapTypeStr || + objectType == JsonDbString::kReduceTypeStr) { + // Check that owner can write targetType + QJsonValue targetType = object.value(QLatin1String("targetType")); + JsonDbObject fake; // Just for access control + fake.insert (JsonDbString::kOwnerStr, object.value(JsonDbString::kOwnerStr)); + fake.insert (JsonDbString::kTypeStr, targetType); + if (!owner->isAllowed(fake, mPartitionName, "write")) { + errorMsg = QString::fromLatin1("Access denied %1").arg(targetType.toString()); + return JsonDbError::OperationNotPermitted; + } + bool forRemoval = object.isDeleted(); + + // For removal it is enough to be able to write to targetType + if (!forRemoval) { + if (!forCreation) { + // In update we want to check also the old targetType + QJsonValue oldTargetType = oldObject.value(QLatin1String("targetType")); + fake.insert (JsonDbString::kTypeStr, oldTargetType); + if (!owner->isAllowed(fake, mPartitionName, "write")) { + errorMsg = QString::fromLatin1("Access denied %1").arg(oldTargetType.toString()); + return JsonDbError::OperationNotPermitted; + } + } + // For create/update we need to check the read acces to sourceType(s) also + if (objectType == JsonDbString::kMapTypeStr) { + QScopedPointer<JsonDbMapDefinition> def(new JsonDbMapDefinition(owner, this, object)); + QStringList sourceTypes = def->sourceTypes(); + for (int i = 0; i < sourceTypes.size(); i++) { + fake.insert (JsonDbString::kTypeStr, sourceTypes[i]); + if (!owner->isAllowed(fake, mPartitionName, "read")) { + errorMsg = QString::fromLatin1("Access denied %1").arg(sourceTypes[i]); + return JsonDbError::OperationNotPermitted; + } + } + } else if (objectType == JsonDbString::kReduceTypeStr) { + QJsonValue sourceType = object.value(QLatin1String("sourceType")); + fake.insert (JsonDbString::kTypeStr, sourceType); + if (!owner->isAllowed(fake, mPartitionName, "read")) { + errorMsg = QString::fromLatin1("Access denied %1").arg(sourceType.toString()); + return JsonDbError::OperationNotPermitted; + } + } + } + } else if (objectType == JsonDbString::kSchemaTypeStr) { + // Check that owner can write name + QJsonValue name = object.value(QLatin1String("name")); + JsonDbObject fake; // Just for access control + fake.insert (JsonDbString::kOwnerStr, object.value(JsonDbString::kOwnerStr)); + fake.insert (JsonDbString::kTypeStr, name); + if (!owner->isAllowed(fake, mPartitionName, "write")) { + errorMsg = QString::fromLatin1("Access denied %1").arg(name.toString()); + return JsonDbError::OperationNotPermitted; + } + } + return JsonDbError::NoError; +} + JsonDbError::ErrorCode JsonDbPartition::checkBuiltInTypeValidity(const JsonDbObject &object, const JsonDbObject &oldObject, QString &errorMsg) { QString objectType = object.value(JsonDbString::kTypeStr).toString(); @@ -1585,10 +1685,20 @@ void JsonDbPartition::updateBuiltInTypes(const JsonDbObject &object, const JsonD if (object.contains(JsonDbString::kCaseSensitiveStr)) caseSensitivity = object.value(JsonDbString::kCaseSensitiveStr).toBool(); + QStringList objectTypes; + QJsonValue v = object.value(JsonDbString::kObjectTypeStr); + if (v.isString()) { + objectTypes = (QStringList() << v.toString()); + } else if (v.isArray()) { + QJsonArray array = v.toArray(); + foreach (const QJsonValue objectType, array) + objectTypes.append(objectType.toString()); + } + addIndex(indexName, object.value(JsonDbString::kPropertyNameStr).toString(), object.value(JsonDbString::kPropertyTypeStr).toString(), - object.value(JsonDbString::kObjectTypeStr).toString(), + objectTypes, object.value(JsonDbString::kPropertyFunctionStr).toString(), object.value(JsonDbString::kLocaleStr).toString(), object.value(JsonDbString::kCollationStr).toString(), @@ -1608,7 +1718,8 @@ void JsonDbPartition::updateBuiltInTypes(const JsonDbObject &object, const JsonD if (!oldObject.isEmpty()) JsonDbView::removeDefinition(this, oldObject); - if (!object.isDeleted()) + if (!(object.isDeleted() || + (object.contains(JsonDbString::kActiveStr) && !object.value(JsonDbString::kActiveStr).toBool()))) JsonDbView::createDefinition(this, object); } } @@ -1681,7 +1792,8 @@ void JsonDbPartition::updateSchemaIndexes(const QString &schemaName, QJsonObject QString propertyType = (propertyInfo.contains("type") ? propertyInfo.value("type").toString() : "string"); QStringList kpath = path; kpath << k; - addIndexOnProperty(kpath.join("."), propertyType, schemaName); + QString propertyName = kpath.join("."); + addIndex(propertyName, propertyName, propertyType); } if (propertyInfo.contains("properties")) updateSchemaIndexes(schemaName, propertyInfo, path + (QStringList() << k)); @@ -1781,6 +1893,7 @@ void JsonDbPartition::initSchemas() { JsonDbObject nameIndex; nameIndex.insert(JsonDbString::kTypeStr, JsonDbString::kIndexTypeStr); + nameIndex.insert(JsonDbString::kNameStr, QLatin1String("capabilityName")); nameIndex.insert(JsonDbString::kPropertyNameStr, QLatin1String("name")); nameIndex.insert(JsonDbString::kPropertyTypeStr, QLatin1String("string")); nameIndex.insert(JsonDbString::kObjectTypeStr, QLatin1String("Capability")); @@ -1797,7 +1910,7 @@ void JsonDbPartition::initSchemas() } JsonDbObject capability = QJsonObject::fromVariantMap(parser.result().toMap()); QString name = capability.value("name").toString(); - GetObjectsResult getObjectResponse = getObjects("name", name, "Capability"); + GetObjectsResult getObjectResponse = getObjects("capabilityName", name, "Capability"); int count = getObjectResponse.data.size(); if (!count) { if (jsondbSettings->verbose()) diff --git a/src/daemon/jsondbpartition.h b/src/daemon/jsondbpartition.h index 7d62249..f56eb40 100644 --- a/src/daemon/jsondbpartition.h +++ b/src/daemon/jsondbpartition.h @@ -116,16 +116,12 @@ public: bool addIndex(const QString &indexName, const QString &propertyName, const QString &propertyType = QString("string"), - const QString &objectType = QString(), + const QStringList &objectTypes = QStringList(), const QString &propertyFunction = QString(), const QString &locale = QString(), const QString &collation = QString(), const QString &casePreference = QString(), Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive); - bool addIndexOnProperty(const QString &propertyName, - const QString &propertyType = QString("string"), - const QString &objectType = QString()) - { return addIndex(propertyName, propertyName, propertyType, objectType); } bool removeIndex(const QString &indexName, const QString &objectType = QString()); bool checkQuota(const JsonDbOwner *owner, int size) const; @@ -173,6 +169,7 @@ public Q_SLOTS: void updateView(const QString &objectType, quint32 stateNumber=0); Q_SIGNALS: + void viewUpdated(const QString &objectType); void objectsUpdated(const JsonDbUpdateList &objects); protected: @@ -197,13 +194,14 @@ protected: bool checkNaturalObjectType(const JsonDbObject &object, QString &errorMsg); JsonDbError::ErrorCode checkBuiltInTypeValidity(const JsonDbObject &object, const JsonDbObject &oldObject, QString &errorMsg); + JsonDbError::ErrorCode checkBuiltInTypeAccessControl(bool forCreation, const JsonDbOwner *owner, const JsonDbObject &object, + const JsonDbObject &oldObject, QString &errorMsg); void updateBuiltInTypes(const JsonDbObject &object, const JsonDbObject &oldObject); void setSchema(const QString &schemaName, const QJsonObject &schema); void removeSchema(const QString &schemaName); void updateSchemaIndexes(const QString &schemaName, QJsonObject object, const QStringList &path=QStringList()); private: - JsonDbObjectTable *mObjectTable; QVector<JsonDbObjectTable *> mTableTransactions; diff --git a/src/daemon/jsondbquery.cpp b/src/daemon/jsondbquery.cpp index aa03fa9..be38e2c 100644 --- a/src/daemon/jsondbquery.cpp +++ b/src/daemon/jsondbquery.cpp @@ -41,6 +41,8 @@ #include <QDebug> #include <QFile> +#include <QJsonDocument> +#include <QStack> #include <QString> #include <QStringList> @@ -261,6 +263,9 @@ JsonDbQuery *JsonDbQuery::parse(const QString &query, const QJsonObject &binding JsonDbQuery *parsedQuery = new JsonDbQuery; parsedQuery->query = query; + if (!query.startsWith('[')) + return parsedQuery; + bool parseError = false; JsonDbQueryTokenizer tokenizer(query); QString token; @@ -338,6 +343,57 @@ JsonDbQuery *JsonDbQuery::parse(const QString &query, const QJsonObject &binding term.regExp().setCaseSensitivity(Qt::CaseInsensitive); //qDebug() << "pattern" << tvs.mid(2, eor-2); term.regExp().setPattern(tvs.mid(2, eor-2)); + } else if (op == "contains") { + bool ok = true;; + + QString value = tokenizer.pop(); + if (value == "[" || value == "{") { + QStack<QString> tokenStack; + tokenStack.push(value); + QString tkn = value; + + while (ok && !tkn.isEmpty()) { + tkn = tokenizer.pop(); + if (tkn == "]" && tokenStack.isEmpty()) { + tokenizer.push(tkn); + break; + } else { + value += tkn; + if (tkn == "]") { + if (tokenStack.pop() != "[") + ok = false; + } else if (tkn == "}") { + if (tokenStack.pop() != "{") + ok = false; + } + } + } + + if (ok) { + QJsonParseError parserError; + QJsonDocument parsedValue = QJsonDocument::fromJson(value.toUtf8(), &parserError); + if (parserError.error != QJsonParseError::NoError) { + ok = false; + } else { + if (parsedValue.isArray()) + term.setValue(parsedValue.array()); + else + term.setValue(parsedValue.object()); + } + } + } else { + parseJsonLiteral(value, &term, bindings, &ok); + } + + if (!ok) { + parsedQuery->queryExplanation.append(QString("Failed to parse query value '%1' in query '%2' %3 op %4") + .arg(value) + .arg(parsedQuery->query) + .arg(fieldSpec) + .arg(op)); + parseError = true; + break; + } } else if ((op != "exists") && (op != "notExists")) { QString value = tokenizer.pop(); bool ok = true;; @@ -472,7 +528,7 @@ JsonDbQuery *JsonDbQuery::parse(const QString &query, const QJsonObject &binding } } } - // TODO look at this again + if (!parsedQuery->queryTerms.size() && !parsedQuery->orderTerms.size()) { // match everything -- sort on type OrderTerm term; @@ -612,6 +668,18 @@ QList<QString> OrQueryTerm::propertyNames() const return propertyNames; } +QList<QString> OrQueryTerm::findUnindexablePropertyNames() const +{ + QList<QString> unindexablePropertyNames; + foreach (const QueryTerm &term, mTerms) { + const QString propertyName = term.propertyName(); + const QString op = term.op(); + if (op == QLatin1String("notExists") && !unindexablePropertyNames.contains(propertyName)) + unindexablePropertyNames.append(propertyName); + } + return unindexablePropertyNames; +} + OrderTerm::OrderTerm() { } diff --git a/src/daemon/jsondbquery.h b/src/daemon/jsondbquery.h index 6c57f62..30b7501 100644 --- a/src/daemon/jsondbquery.h +++ b/src/daemon/jsondbquery.h @@ -149,6 +149,7 @@ public: const QList<QueryTerm> &terms() const { return mTerms; } void addTerm(const QueryTerm &term) { mTerms.append(term); } QList<QString> propertyNames() const; + QList<QString> findUnindexablePropertyNames() const; private: QList<QueryTerm> mTerms; }; diff --git a/src/daemon/jsondbreducedefinition.cpp b/src/daemon/jsondbreducedefinition.cpp index 04d6a4b..1021413 100644 --- a/src/daemon/jsondbreducedefinition.cpp +++ b/src/daemon/jsondbreducedefinition.cpp @@ -73,22 +73,27 @@ JsonDbReduceDefinition::JsonDbReduceDefinition(const JsonDbOwner *owner, JsonDbP , mTargetType(mDefinition.value("targetType").toString()) , mTargetTable(mPartition->findObjectTable(mTargetType)) , mSourceType(mDefinition.value("sourceType").toString()) - , mTargetKeyName(mDefinition.contains("targetKeyName") ? mDefinition.value("targetKeyName").toString() : QString("key")) - , mTargetValueName(mDefinition.contains("targetValueName") ? mDefinition.value("targetValueName").toString() : QString("value")) - , mSourceKeyName(mDefinition.contains("sourceKeyName") ? mDefinition.value("sourceKeyName").toString() : QString("key")) - , mSourceKeyNameList(mSourceKeyName.split(".")) { + if (mDefinition.contains("targetKeyName")) + mTargetKeyName = mDefinition.value("targetKeyName").toString(); + else + mTargetKeyName = QLatin1String("key"); + if (mDefinition.contains("sourceKeyName")) + mSourceKeyName = mDefinition.value("sourceKeyName").toString(); + mSourceKeyNameList = mSourceKeyName.split("."); + if (mDefinition.contains("targetValueName")) { + if (mDefinition.value("targetValueName").isString()) + mTargetValueName = mDefinition.value("targetValueName").toString(); + } else + mTargetValueName = QLatin1String("value"); + } void JsonDbReduceDefinition::definitionCreated() { - // TODO: this index should not be automatic - mTargetTable->addIndexOnProperty(mSourceKeyName, QLatin1String("string"), mSourceType); - // TODO: this index should not be automatic - mTargetTable->addIndexOnProperty(mTargetKeyName, QLatin1String("string"), mTargetType); - mTargetTable->addIndexOnProperty(QLatin1String("_reduceUuid"), QLatin1String("string"), mTargetType); - initScriptEngine(); + initIndexes(); + GetObjectsResult getObjectResponse = mPartition->getObjects(JsonDbString::kTypeStr, mSourceType); if (!getObjectResponse.error.isNull()) { if (jsondbSettings->verbose()) @@ -118,46 +123,43 @@ void JsonDbReduceDefinition::initScriptEngine() return; mScriptEngine = new QJSEngine(this); + QString message; + bool status = compileFunctions(mScriptEngine, mDefinition, mFunctions, message); + if (!status) + setError(message); + Q_ASSERT(!mDefinition.value("add").toString().isEmpty()); Q_ASSERT(!mDefinition.value("subtract").toString().isEmpty()); QJSValue globalObject = mScriptEngine->globalObject(); globalObject.setProperty("console", mScriptEngine->newQObject(new Console())); - - QString script = mDefinition.value("add").toString(); - mAddFunction = mScriptEngine->evaluate(QString("var %1 = (%2); %1;").arg("add").arg(script)); - - if (mAddFunction.isError() || !mAddFunction.isCallable()) { - setError("Unable to parse add function: " + mAddFunction.toString()); - return; - } - - script = mDefinition.value("subtract").toString(); - mSubtractFunction = mScriptEngine->evaluate(QString("var %1 = (%2); %1;").arg("subtract").arg(script)); - - if (mSubtractFunction.isError() || !mSubtractFunction.isCallable()) - setError("Unable to parse subtract function: " + mSubtractFunction.toString()); } void JsonDbReduceDefinition::releaseScriptEngine() { - mAddFunction = QJSValue(); - mSubtractFunction = QJSValue(); + mFunctions.clear(); delete mScriptEngine; mScriptEngine = 0; } +void JsonDbReduceDefinition::initIndexes() +{ + // TODO: this index should not be automatic + if (!mSourceKeyName.isEmpty()) { + JsonDbObjectTable *sourceTable = mPartition->findObjectTable(mSourceType); + sourceTable->addIndexOnProperty(mSourceKeyName, QLatin1String("string")); + } + // TODO: this index should not be automatic + mTargetTable->addIndexOnProperty(mTargetKeyName, QLatin1String("string"), mTargetType); + mTargetTable->addIndexOnProperty(QLatin1String("_reduceUuid"), QLatin1String("string"), mTargetType); +} + void JsonDbReduceDefinition::updateObject(JsonDbObject before, JsonDbObject after) { initScriptEngine(); - Q_ASSERT(mAddFunction.isCallable()); - QJsonValue beforeKeyValue = mSourceKeyName.contains(".") - ? before.propertyLookup(mSourceKeyNameList) - : before.value(mSourceKeyName); - QJsonValue afterKeyValue = mSourceKeyName.contains(".") - ? after.propertyLookup(mSourceKeyNameList) - : after.value(mSourceKeyName); + QJsonValue beforeKeyValue = sourceKeyValue(before); + QJsonValue afterKeyValue = sourceKeyValue(after); if (!after.isEmpty() && !before.isEmpty() && (beforeKeyValue != afterKeyValue)) { // do a subtract only on the before key @@ -191,9 +193,9 @@ void JsonDbReduceDefinition::updateObject(JsonDbObject before, JsonDbObject afte QJsonValue value = previousValue; if (!before.isEmpty()) - value = subtractObject(keyValue, value, before); + value = addObject(JsonDbReduceDefinition::Subtract, keyValue, value, before); if (!after.isEmpty()) - value = addObject(keyValue, value, after); + value = addObject(JsonDbReduceDefinition::Add, keyValue, value, after); JsonDbWriteResult res; // if we had a previous object to reduce @@ -229,21 +231,28 @@ void JsonDbReduceDefinition::updateObject(JsonDbObject before, JsonDbObject afte setError(QString("Error executing add function: %1").arg(res.message)); } -QJsonValue JsonDbReduceDefinition::addObject(const QJsonValue &keyValue, const QJsonValue &previousValue, JsonDbObject object) +QJsonValue JsonDbReduceDefinition::addObject(JsonDbReduceDefinition::FunctionNumber functionNumber, + const QJsonValue &keyValue, QJsonValue previousValue, JsonDbObject object) { initScriptEngine(); QJSValue svKeyValue = JsonDbObject::toJSValue(keyValue, mScriptEngine); - QJSValue svPreviousValue = JsonDbObject::toJSValue(previousValue.toObject().value(mTargetValueName), mScriptEngine); + if (!mTargetValueName.isEmpty()) + previousValue = previousValue.toObject().value(mTargetValueName); + QJSValue svPreviousValue = JsonDbObject::toJSValue(previousValue, mScriptEngine); QJSValue svObject = JsonDbObject::toJSValue(object, mScriptEngine); QJSValueList reduceArgs; reduceArgs << svKeyValue << svPreviousValue << svObject; - QJSValue reduced = mAddFunction.call(reduceArgs); + QJSValue reduced = mFunctions[functionNumber].call(reduceArgs); if (!reduced.isUndefined() && !reduced.isError()) { - QJsonObject jsonReduced; - jsonReduced.insert(mTargetValueName, JsonDbObject::fromJSValue(reduced)); - return jsonReduced; + QJsonValue jsonReduced = JsonDbObject::fromJSValue(reduced); + QJsonObject jsonReducedObject; + if (!mTargetValueName.isEmpty()) + jsonReducedObject.insert(mTargetValueName, jsonReduced); + else + jsonReducedObject = jsonReduced.toObject(); + return jsonReducedObject; } else { if (reduced.isError()) @@ -253,31 +262,6 @@ QJsonValue JsonDbReduceDefinition::addObject(const QJsonValue &keyValue, const Q } } -QJsonValue JsonDbReduceDefinition::subtractObject(const QJsonValue &keyValue, const QJsonValue &previousValue, JsonDbObject object) -{ - initScriptEngine(); - Q_ASSERT(mSubtractFunction.isCallable()); - - QJSValue svKeyValue = JsonDbObject::toJSValue(keyValue, mScriptEngine); - QJSValue svPreviousValue = JsonDbObject::toJSValue(previousValue.toObject().value(mTargetValueName), - mScriptEngine); - QJSValue sv = JsonDbObject::toJSValue(object, mScriptEngine); - - QJSValueList reduceArgs; - reduceArgs << svKeyValue << svPreviousValue << sv; - QJSValue reduced = mSubtractFunction.call(reduceArgs); - - if (!reduced.isUndefined() && !reduced.isError()) { - QJsonObject jsonReduced; - jsonReduced.insert(mTargetValueName, JsonDbObject::fromJSValue(reduced)); - return jsonReduced; - } else { - if (reduced.isError()) - setError("Error executing subtract function: " + reduced.toString()); - return QJsonValue(QJsonValue::Undefined); - } -} - bool JsonDbReduceDefinition::isActive() const { return !mDefinition.contains(JsonDbString::kActiveStr) || mDefinition.value(JsonDbString::kActiveStr).toBool(); @@ -309,14 +293,67 @@ bool JsonDbReduceDefinition::validateDefinition(const JsonDbObject &reduce, Json && view->mReduceDefinitionsBySource.value(sourceType)->uuid() != uuid) message = QString("duplicate Reduce definition on source %1 and target %2") .arg(sourceType).arg(targetType); - else if (reduce.value("sourceKeyName").toString().isEmpty()) - message = QLatin1Literal("sourceKeyName property for Reduce not specified"); + else if (reduce.value("sourceKeyName").toString().isEmpty() && reduce.value("sourceKeyFunction").toString().isEmpty()) + message = QLatin1Literal("sourceKeyName or sourceKeyFunction must be provided for Reduce"); + else if (!reduce.value("sourceKeyName").toString().isEmpty() && !reduce.value("sourceKeyFunction").toString().isEmpty()) + message = QLatin1Literal("Only one of sourceKeyName and sourceKeyFunction may be provided for Reduce"); else if (reduce.value("add").toString().isEmpty()) message = QLatin1Literal("add function for Reduce not specified"); else if (reduce.value("subtract").toString().isEmpty()) message = QLatin1Literal("subtract function for Reduce not specified"); - + else if (reduce.contains("targetValueName") + && !(reduce.value("targetValueName").isString() || reduce.value("targetValueName").isNull())) + message = QLatin1Literal("targetValueName for Reduce must be a string or null"); + else { + // FIXME: This is static because otherwise we leak memory per QJSEngine instance + static QJSEngine *scriptEngine = new QJSEngine; + QVector<QJSValue> functions; + // check for script errors + compileFunctions(scriptEngine, reduce, functions, message); + scriptEngine->collectGarbage(); + } return message.isEmpty(); } +bool JsonDbReduceDefinition::compileFunctions(QJSEngine *scriptEngine, QJsonObject definition, + QVector<QJSValue> &functions, QString &message) +{ + bool status = true; + QStringList functionNames = (QStringList() + << QLatin1String("add") + << QLatin1String("subtract") + << QLatin1String("sourceKeyFunction")); + int i = 0; + functions.resize(3); + foreach (const QString &functionName, functionNames) { + int functionNumber = i++; + if (!definition.contains(functionName)) + continue; + QString script = definition.value(functionName).toString(); + QJSValue result = scriptEngine->evaluate(QString("(%1)").arg(script)); + + if (result.isError() || !result.isCallable()) { + message = QString("Unable to parse add function: %1").arg(result.toString()); + status = false; + continue; + } + functions[functionNumber] = result; + } + return status; +} + +QJsonValue JsonDbReduceDefinition::sourceKeyValue(const QJsonObject &object) +{ + if (object.isEmpty()) { + return QJsonValue(QJsonValue::Undefined); + } else if (mFunctions[JsonDbReduceDefinition::SourceKeyValue].isCallable()) { + QJSValueList args; + args << JsonDbObject::toJSValue(object, mScriptEngine); + QJsonValue keyValue = JsonDbObject::fromJSValue(mFunctions[JsonDbReduceDefinition::SourceKeyValue].call(args)); + return keyValue; + } else + return mSourceKeyName.contains(".") ? JsonDbObject(object).propertyLookup(mSourceKeyNameList) : object.value(mSourceKeyName); + +} + QT_END_NAMESPACE_JSONDB diff --git a/src/daemon/jsondbreducedefinition.h b/src/daemon/jsondbreducedefinition.h index e8f004f..214153e 100644 --- a/src/daemon/jsondbreducedefinition.h +++ b/src/daemon/jsondbreducedefinition.h @@ -77,8 +77,6 @@ public: QStringList sourceKeyNameList() const { return mSourceKeyNameList; } bool isActive() const; QJsonObject definition() const { return mDefinition; } - const QJSValue &addFunction() const { return mAddFunction; } - const QJSValue &subtractFunction() const { return mSubtractFunction; } const JsonDbOwner *owner() const { return mOwner; } static void definitionRemoved(JsonDbPartition *partition, JsonDbObjectTable *table, const QString targetType, const QString &definitionUuid); @@ -86,21 +84,30 @@ public: void initScriptEngine(); void releaseScriptEngine(); + void initIndexes(); + void updateObject(JsonDbObject before, JsonDbObject after); - QJsonValue addObject(const QJsonValue &keyValue, const QJsonValue &previousResult, JsonDbObject object); - QJsonValue subtractObject(const QJsonValue &keyValue, const QJsonValue &previousResult, JsonDbObject object); void setError(const QString &errorMsg); static bool validateDefinition(const JsonDbObject &reduce, JsonDbPartition *partition, QString &message); private: + enum FunctionNumber { + Add = 0, + Subtract = 1, + SourceKeyValue = 2 + }; + static bool compileFunctions(QJSEngine *scriptEngine, QJsonObject definition, QVector<QJSValue> &mFunctions, QString &message); + QJsonValue sourceKeyValue(const QJsonObject &object); + QJsonValue addObject(FunctionNumber fn, const QJsonValue &keyValue, QJsonValue previousResult, JsonDbObject object); + +private: const JsonDbOwner *mOwner; JsonDbPartition *mPartition; QJsonObject mDefinition; QJSEngine *mScriptEngine; - QJSValue mAddFunction; - QJSValue mSubtractFunction; + QVector<QJSValue> mFunctions; QString mUuid; QString mTargetType; JsonDbObjectTable *mTargetTable; diff --git a/src/daemon/jsondbview.cpp b/src/daemon/jsondbview.cpp index 4ca6e24..4adc5d6 100644 --- a/src/daemon/jsondbview.cpp +++ b/src/daemon/jsondbview.cpp @@ -159,18 +159,13 @@ void JsonDbView::createMapDefinition(QJsonObject mapDefinition) qDebug() << "createMapDefinition" << uuid << targetType << "{"; JsonDbOwner *owner = new JsonDbOwner(this); - owner->setOwnerCapabilities(mapDefinition.value(JsonDbString::kOwnerStr).toString(), mPartition); + owner->setAllowAll(true); JsonDbMapDefinition *def = new JsonDbMapDefinition(owner, mPartition, mapDefinition, this); + def->initIndexes(); + QStringList sourceTypes = def->sourceTypes(); for (int i = 0; i < sourceTypes.size(); i++) { const QString sourceType = sourceTypes[i]; - if (mMapDefinitionsBySource.contains(sourceType)) { - qWarning() << QString("Duplicate Map definition on source %1 and target %2") - .arg(sourceType).arg(targetType); - def->setError(QString("Duplicate Map definition on source %1 and target %2") - .arg(sourceType).arg(targetType)); - return; - } mMapDefinitionsBySource.insert(sourceType, def); } mMapDefinitions.insert(def->uuid(), def); @@ -192,7 +187,6 @@ void JsonDbView::removeMapDefinition(QJsonObject mapDefinition) const QStringList &sourceTypes = def->sourceTypes(); for (int i = 0; i < sourceTypes.size(); i++) mMapDefinitionsBySource.remove(sourceTypes[i]); - break; } } @@ -210,14 +204,9 @@ void JsonDbView::createReduceDefinition(QJsonObject reduceDefinition) qDebug() << "createReduceDefinition" << sourceType << targetType << sourceType << "{"; JsonDbOwner *owner = new JsonDbOwner(this); - owner->setOwnerCapabilities(reduceDefinition.value(JsonDbString::kOwnerStr).toString(), mPartition); + owner->setAllowAll(true); JsonDbReduceDefinition *def = new JsonDbReduceDefinition(owner, mPartition, reduceDefinition, this); - if (mReduceDefinitionsBySource.contains(sourceType)) { - def->setError(QString("Duplicate Reduce definition on source %1 and target %2") - .arg(sourceType).arg(targetType)); - return; - } - + def->initIndexes(); mReduceDefinitionsBySource.insert(sourceType, def); mReduceDefinitions.insert(def->uuid(), def); @@ -381,6 +370,8 @@ void JsonDbView::updateView(quint32 desiredStateNumber) if (jsondbSettings->performanceLog()) qDebug() << "updateView" << mViewType << timer.elapsed() << "ms"; mUpdating = false; + + emit updated(mViewType); } bool JsonDbView::processUpdatedDefinitions(const QString &viewType, quint32 targetStateNumber, @@ -431,10 +422,12 @@ bool JsonDbView::processUpdatedDefinitions(const QString &viewType, quint32 targ } definitionUuid = after.value(JsonDbString::kUuidStr).toString(); QString definitionType = after.value(JsonDbString::kTypeStr).toString(); - if (definitionType == JsonDbString::kMapTypeStr) - mMapDefinitions.value(definitionUuid)->definitionCreated(); - else - mReduceDefinitions.value(definitionUuid)->definitionCreated(); + if (!after.contains(JsonDbString::kActiveStr) || after.value(JsonDbString::kActiveStr).toBool()) { + if (definitionType == JsonDbString::kMapTypeStr) + mMapDefinitions.value(definitionUuid)->definitionCreated(); + else + mReduceDefinitions.value(definitionUuid)->definitionCreated(); + } } } if (!definitionUuid.isEmpty()) diff --git a/src/daemon/jsondbview.h b/src/daemon/jsondbview.h index 2f2efbc..a2d5675 100644 --- a/src/daemon/jsondbview.h +++ b/src/daemon/jsondbview.h @@ -80,6 +80,9 @@ public: bool isActive() const; +Q_SIGNALS: + void updated(const QString &type); + private: void createMapDefinition(QJsonObject mapDefinition); void removeMapDefinition(QJsonObject mapDefinition); diff --git a/src/daemon/schema/Index.json b/src/daemon/schema/Index.json index d98cd5b..7c60d15 100644 --- a/src/daemon/schema/Index.json +++ b/src/daemon/schema/Index.json @@ -18,7 +18,6 @@ "description": "Type of values stored in that property." }, "objectType": { - "type": "string", "description": "Object type to index. Optional." } } diff --git a/src/imports/jsondb-listmodel/jsondb-listmodel.pro b/src/imports/jsondb-listmodel/jsondb-listmodel.pro index b881c12..9e86677 100644 --- a/src/imports/jsondb-listmodel/jsondb-listmodel.pro +++ b/src/imports/jsondb-listmodel/jsondb-listmodel.pro @@ -13,6 +13,18 @@ qmldir.path += $$[QT_INSTALL_IMPORTS]/$$TARGETPATH INSTALLS += target qmldir +#rules for qmltypes +!cross_compile { + qtPrepareTool(QMLPLUGINDUMP, qmlplugindump) + QMLTYPESFILE = $$QT.jsondb.imports/$$TARGETPATH/plugin.qmltypes + mac: !exists($$QMLPLUGINDUMP): QMLPLUGINDUMP = "$${QMLPLUGINDUMP}.app/Contents/MacOS/qmlplugindump" + QMAKE_POST_LINK += LD_LIBRARY_PATH=$$QT.jsondb.libs $$QMLPLUGINDUMP QtAddOn.JsonDb 1.0 $$QT.jsondb.imports > $$QMLTYPESFILE + + qmltypes.files = $$QMLTYPESFILE + qmltypes.path = $$[QT_INSTALL_IMPORTS]/$$TARGETPATH + INSTALLS += qmltypes +} + VERSION = 1.0 include(../../common/common.pri) diff --git a/src/imports/jsondb/jsondb.pro b/src/imports/jsondb/jsondb.pro index adb7ce2..781e3d1 100644 --- a/src/imports/jsondb/jsondb.pro +++ b/src/imports/jsondb/jsondb.pro @@ -13,6 +13,18 @@ qmldir.path += $$[QT_INSTALL_IMPORTS]/$$TARGETPATH INSTALLS += target qmldir +#rules for qmltypes +!cross_compile { + qtPrepareTool(QMLPLUGINDUMP, qmlplugindump) + mac: !exists($$QMLPLUGINDUMP): QMLPLUGINDUMP = "$${QMLPLUGINDUMP}.app/Contents/MacOS/qmlplugindump" + QMLTYPESFILE = $$QT.jsondbcompat.imports/$$TARGETPATH/plugin.qmltypes + QMAKE_POST_LINK += LD_LIBRARY_PATH=$$QT.jsondbcompat.libs $$QMLPLUGINDUMP QtJsonDb 1.0 $$QT.jsondbcompat.imports > $$QMLTYPESFILE + + qmltypes.files = $$QMLTYPESFILE + qmltypes.path = $$[QT_INSTALL_IMPORTS]/$$TARGETPATH + INSTALLS += qmltypes +} + VERSION = 1.0 include(../../common/common.pri) diff --git a/src/imports/jsondb/jsondbqueryobject.cpp b/src/imports/jsondb/jsondbqueryobject.cpp index f9e2a46..b42daaa 100644 --- a/src/imports/jsondb/jsondbqueryobject.cpp +++ b/src/imports/jsondb/jsondbqueryobject.cpp @@ -124,10 +124,6 @@ void JsonDbQueryObject::setQuery(const QString &newQuery) JsonDbPartition* JsonDbQueryObject::partition() { - if (!partitionObject) { - defaultPartitionObject = new JsonDbPartition(); - setPartition(defaultPartitionObject); - } checkForReadyStatus(); return partitionObject; } @@ -384,8 +380,10 @@ void JsonDbQueryObject::checkForReadyStatus() JsonDbQueryObject::Status oldStatus = objectStatus; - if (!partitionObject) - partitionObject = qobject_cast<JsonDbPartition*>(parent()); + if (!partitionObject) { + defaultPartitionObject = new JsonDbPartition(); + setPartition(defaultPartitionObject); + } if (!parametersReady()) { objectStatus = JsonDbQueryObject::Null; if (objectStatus != oldStatus) diff --git a/tests/auto/accesscontrol/json/capabilities-view.json b/tests/auto/accesscontrol/json/capabilities-view.json new file mode 100644 index 0000000..be060de --- /dev/null +++ b/tests/auto/accesscontrol/json/capabilities-view.json @@ -0,0 +1,24 @@ +[ + { + "_type": "Capability", + "name": "views", + "partition": "all", + "accessRules": { + "rw": { + "read": ["[?_type startsWith \"Phone\"]", "[?_type = \"Contact\"]", "[?_type = \"_schemaType\"]", "[?_type = \"Map\"]", "[?_type = \"Reduce\"]"], + "write": ["[?_type startsWith \"Phone\"]", "[?_type = \"Contact\"]", "[?_type = \"_schemaType\"]", "[?_type = \"Map\"]", "[?_type = \"Reduce\"]"] + } + } + }, +{ + "_type": "Capability", + "name": "noviews", + "partition": "all", + "accessRules": { + "rw": { + "read": ["[?_type = \"Contact\"]", "[?_type = \"_schemaType\"]", "[?_type = \"Map\"]", "[?_type = \"Reduce\"]"], + "write": ["[?_type = \"Contact\"]", "[?_type = \"_schemaType\"]", "[?_type = \"Map\"]", "[?_type = \"Reduce\"]", "[?_type = \"PhoneCount3\"]"] + } + } +} +] diff --git a/tests/auto/accesscontrol/json/view-test.json b/tests/auto/accesscontrol/json/view-test.json new file mode 100644 index 0000000..223d5a4 --- /dev/null +++ b/tests/auto/accesscontrol/json/view-test.json @@ -0,0 +1,54 @@ +[ +{ + "_type": "_schemaType", + "name": "Phone", + "schema": { + "type": "object", + "extends": "View" + } +}, +{ + "_type": "_schemaType", + "name": "PhoneCount", + "schema": { + "type": "object", + "extends": "View" + } +}, +{ + "_type": "_schemaType", + "name": "Phone3", + "schema": { + "type": "object", + "extends": "View" + } +}, +{ + "_type": "_schemaType", + "name": "PhoneCount3", + "schema": { + "type": "object", + "extends": "View" + } +}, +{ + "_type": "Map", + "targetType": "Phone", + "map": {"Contact": "function (c) {\ + for (var i in c.phoneNumbers) {\ + var phone = c.phoneNumbers[i];\ + var id = c.displayName + ':' + phone.number; \ + var uuid = jsondb.createUuidFromString(id); \ + jsondb.emit({_uuid: uuid, key: phone.number, displayName: c.displayName});\ + }\ + }"} +}, +{ + "_type": "Reduce", + "targetType": "PhoneCount", + "sourceType": "Phone", + "sourceKeyName": "key", + "add": "function add (k, z, c) { if (!z) {z = {count: 0}}; z.count += 1; return z;}", + "subtract": "function subtract (k, z, c) { if (!z) {z = {count: 0}}; z.count -= 1; if (z.count) return z;}" +} +] diff --git a/tests/auto/accesscontrol/json/view-test2.json b/tests/auto/accesscontrol/json/view-test2.json new file mode 100644 index 0000000..c3e92b3 --- /dev/null +++ b/tests/auto/accesscontrol/json/view-test2.json @@ -0,0 +1,38 @@ +[ +{ + "_type": "_schemaType", + "name": "Phone2", + "schema": { + "type": "object", + "extends": "View" + } +}, +{ + "_type": "_schemaType", + "name": "PhoneCount2", + "schema": { + "type": "object", + "extends": "View" + } +}, +{ + "_type": "Map", + "targetType": "Phone3", + "map": {"Contact": "function (c) {\ + for (var i in c.phoneNumbers) {\ + var phone = c.phoneNumbers[i];\ + var id = c.displayName + ':' + phone.number; \ + var uuid = jsondb.createUuidFromString(id); \ + jsondb.emit({_uuid: uuid, key: phone.number, displayName: c.displayName});\ + }\ + }"} +}, +{ + "_type": "Reduce", + "targetType": "PhoneCount3", + "sourceType": "Phone", + "sourceKeyName": "key", + "add": "function add (k, z, c) { if (!z) {z = {count: 0}}; z.count += 1; return z;}", + "subtract": "function subtract (k, z, c) { if (!z) {z = {count: 0}}; z.count -= 1; if (z.count) return z;}" +} +] diff --git a/tests/auto/accesscontrol/testjsondb.cpp b/tests/auto/accesscontrol/testjsondb.cpp index ef6238f..4339934 100644 --- a/tests/auto/accesscontrol/testjsondb.cpp +++ b/tests/auto/accesscontrol/testjsondb.cpp @@ -102,6 +102,7 @@ private slots: void testAccessControl(); void testFindAccessControl(); + void testViewAccessControl(); private: JsonDbQueryResult find(JsonDbOwner *owner, const QString &query, const QJsonObject bindings = QJsonObject()); @@ -434,6 +435,50 @@ void TestJsonDb::testFindAccessControl() jsondbSettings->setEnforceAccessControl(false); } +/* + * Create Map and Reduce objects and check access control + */ + +void TestJsonDb::testViewAccessControl() +{ + jsondbSettings->setEnforceAccessControl(false); + QJsonArray defs(readJsonFile(":/security/json/capabilities-view.json").toArray()); + for (int i = 0; i < defs.size(); ++i) { + JsonDbObject object(defs.at(i).toObject()); + JsonDbWriteResult result = create(mOwner, object); + verifyGoodResult(result); + } + + jsondbSettings->setEnforceAccessControl(true); + QJsonObject viewCapabilities; + QJsonArray value; + value.append (QLatin1String("rw")); + viewCapabilities.insert (QLatin1String("views"), value); + mOwner->setAllowAll(false); + mOwner->setCapabilities(viewCapabilities, mJsonDbPartition); + + defs = readJsonFile(":/security/json/view-test.json").toArray(); + for (int i = 0; i < defs.size(); ++i) { + JsonDbObject object(defs.at(i).toObject()); + JsonDbWriteResult result = create(mOwner, object); + verifyGoodResult(result); + } + + QJsonObject novwCapabilities; + QJsonArray novwValue; + novwValue.append (QLatin1String("rw")); + novwCapabilities.insert (QLatin1String("noviews"), novwValue); + mOwner->setAllowAll(false); + mOwner->setCapabilities(novwCapabilities, mJsonDbPartition); + + defs = readJsonFile(":/security/json/view-test2.json").toArray(); + for (int i = 0; i < defs.size(); ++i) { + JsonDbObject object(defs.at(i).toObject()); + JsonDbWriteResult result = create(mOwner, object); + verifyErrorResult(result); + } +} + QStringList strings = (QStringList() << "abc" << "def" @@ -456,7 +501,7 @@ QJsonValue TestJsonDb::readJsonFile(const QString& filename) JsonReader parser; bool ok = parser.parse(json); if (!ok) { - qDebug() << filepath << parser.errorString(); + qDebug() << filepath << parser.errorString() << json; } QVariant v = parser.result(); return QJsonValue::fromVariant(v); diff --git a/tests/auto/client/test-jsondb-client.cpp b/tests/auto/client/test-jsondb-client.cpp index 40946c4..81290c5 100644 --- a/tests/auto/client/test-jsondb-client.cpp +++ b/tests/auto/client/test-jsondb-client.cpp @@ -98,6 +98,7 @@ private slots: void update(); void find(); void index(); + void multiTypeIndex(); void registerNotification(); void notify_data(); @@ -267,7 +268,7 @@ void TestJsonDbClient::initTestCase() QLatin1String("[?_type = \"_schemaType\"]") << QLatin1String("[?_type = \"Index\"]") << QLatin1String("[?_type = \"Partition\"]") << - QLatin1String("[?_type = \"Phone\"]") << + QLatin1String("[?_type startsWith \"Phone\"]") << QLatin1String("[?_type = \"Contact\"]") << QLatin1String("[?_type = \"Reduce\"]") << QLatin1String("[?_type = \"Map\"]"))); @@ -395,21 +396,24 @@ void TestJsonDbClient::initTestCase() gid_t gid = nextFreeGid(1042); gid_t gid2 = nextFreeGid(gid+1); QString appName = QString("com.test.foo.%1").arg(getpid()); + QByteArray appNameBA = appName.toLocal8Bit(); // Tes app name for app without any supplementary groups QString app2Name = QString("com.test.bar.%1").arg(getpid()); + QByteArray app2NameBA = app2Name.toLocal8Bit(); if (!errno) { // Add primary groups struct group grp; - grp.gr_name = appName.toLocal8Bit().data(); + grp.gr_name = appNameBA.data(); grp.gr_passwd = NULL; grp.gr_gid = gid; grp.gr_mem = (char *[]){NULL}; struct group grp2; - grp2.gr_name = app2Name.toLocal8Bit().data(); + grp2.gr_name = app2NameBA.data(); grp2.gr_passwd = NULL; grp2.gr_gid = gid2; grp2.gr_mem = (char *[]){NULL}; - FILE *grfile = ::fopen (etcigr.toLocal8Bit().data(), "a"); + QByteArray etcigrBA = etcigr.toLocal8Bit(); + FILE *grfile = ::fopen (etcigrBA.data(), "a"); ::putgrent(&grp, grfile); ::putgrent(&grp2, grfile); ::fclose (grfile); @@ -418,7 +422,7 @@ void TestJsonDbClient::initTestCase() // Add the user struct passwd pwd; - pwd.pw_name = appName.toLocal8Bit().data(); + pwd.pw_name = appNameBA.data(); pwd.pw_passwd = NULL; pwd.pw_uid = uid; pwd.pw_gid = gid; @@ -426,14 +430,15 @@ void TestJsonDbClient::initTestCase() pwd.pw_dir = NULL; pwd.pw_shell = NULL; struct passwd pwd2; - pwd2.pw_name = app2Name.toLocal8Bit().data(); + pwd2.pw_name = app2NameBA.data(); pwd2.pw_passwd = NULL; pwd2.pw_uid = uid2; pwd2.pw_gid = gid2; pwd2.pw_gecos = NULL; pwd2.pw_dir = NULL; pwd2.pw_shell = NULL; - FILE *pwdfile = ::fopen (etcipwd.toLocal8Bit().data(), "a"); + QByteArray etcipwdBA = etcipwd.toLocal8Bit(); + FILE *pwdfile = ::fopen (etcipwdBA.data(), "a"); ::putpwent(&pwd, pwdfile); ::putpwent(&pwd2, pwdfile); ::fclose (pwdfile); @@ -444,8 +449,8 @@ void TestJsonDbClient::initTestCase() grp.gr_passwd = NULL; grp.gr_gid = gid; // Add only the first user to it - grp.gr_mem = (char *[]){appName.toLocal8Bit().data(), NULL}; - grfile = ::fopen (etcigr.toLocal8Bit(), "a"); + grp.gr_mem = (char *[]){appNameBA.data(), NULL}; + grfile = ::fopen (etcigrBA.data(), "a"); ::putgrent(&grp, grfile); ::fclose (grfile); gidsAdded.append(gid); @@ -692,6 +697,15 @@ void TestJsonDbClient::find() int id = 0; int count; + // create an index on the name property of com.test.NameIndex objects + QVariantMap index; + index.insert(JsonDbString::kTypeStr, JsonDbString::kIndexTypeStr); + index.insert(JsonDbString::kNameStr, QLatin1String("com.test.NameIndex")); + index.insert(JsonDbString::kPropertyNameStr, QLatin1String("name")); + index.insert(JsonDbString::kObjectTypeStr, QLatin1String("com.test.find-test")); + id = mClient->create(index); + waitForResponse1(id); + QStringList nameList; // Create a few items for (count = 0 ; names[count] ; count++ ) { @@ -724,7 +738,7 @@ void TestJsonDbClient::find() // Find one, sorted in reverse alphabetical order query = QVariantMap(); - query.insert("query", "[?_type=\"com.test.find-test\"][\\name]"); + query.insert("query", "[?_type=\"com.test.find-test\"][\\com.test.NameIndex]"); id = mClient->find(query); waitForResponse1(id); answer = mData.toMap().value("data").toList(); @@ -752,7 +766,7 @@ void TestJsonDbClient::find() // Read should fail query = QVariantMap(); - query.insert("query", "[?_type=\"com.test.find-test\"][\\name]"); + query.insert("query", "[?_type=\"com.test.find-test\"][\\com.test.NameIndex]"); id = mClient->find(query); waitForResponse1(id); answer = mData.toMap().value("data").toList(); @@ -823,6 +837,55 @@ void TestJsonDbClient::index() waitForResponse1(id); } +void TestJsonDbClient::multiTypeIndex() +{ + QVERIFY(mClient); + + QVariantMap query; + int id = 0; + int count; + + QStringList indexedTypes = (QStringList() + << QLatin1String("com.test.find-test1") + << QLatin1String("com.test.find-test2")); + // create an index on the name property of com.test.NameIndex objects + QVariantMap index; + index.insert(JsonDbString::kTypeStr, JsonDbString::kIndexTypeStr); + index.insert(JsonDbString::kNameStr, QLatin1String("com.test.MultiTypeIndex")); + index.insert(JsonDbString::kPropertyNameStr, QLatin1String("name")); + index.insert(JsonDbString::kObjectTypeStr, indexedTypes); + id = mClient->create(index); + waitForResponse1(id); + + QStringList objectTypes = (QStringList() + << QLatin1String("com.test.find-test1") + << QLatin1String("com.test.find-test2") + << QLatin1String("com.test.find-test3")); + QStringList nameList; + // Create a few items + for (count = 0 ; names[count] ; count++ ) { + nameList << names[count]; + foreach (const QString objectType, objectTypes) { + QVariantMap item; + item.insert("_type", objectType); + item.insert("name", names[count]); + id = mClient->create(item); + waitForResponse1(id); + } + } + + // Find all in the index + query = QVariantMap(); + query.insert("query", "[?name exists][/com.test.MultiTypeIndex]"); + id = mClient->find(query); + waitForResponse1(id); + QVariantList answer = mData.toMap().value("data").toList(); + //qDebug() << "answer" << mData.toMap(); + QCOMPARE(answer.size(), nameList.size() * 2); + foreach (QVariant v, answer) + QVERIFY(indexedTypes.contains(v.toMap().value(JsonDbString::kTypeStr).toString())); +} + void TestJsonDbClient::notify_data() { QTest::addColumn<QString>("partition"); @@ -908,6 +971,7 @@ void TestJsonDbClient::notifyUpdate() // Update the notify-test object // query no longer matches, so we should receive a "remove" notification even though it is an update + // this means it should not contain the _deleted property object.insert("_uuid",uuid); object.insert("_version", version); object.insert("filter","nomatch"); @@ -918,6 +982,7 @@ void TestJsonDbClient::notifyUpdate() n = mNotifications.takeFirst(); QCOMPARE(n.mNotifyUuid, notifyUuid); QCOMPARE(n.mAction, QLatin1String("remove")); + QVERIFY(!n.mObject.toMap().contains(JsonDbString::kDeletedStr)); // Remove the notify-test object id = mClient->remove(object); diff --git a/tests/auto/daemon/json/map-reduce-schema.json b/tests/auto/daemon/json/map-reduce-schema.json index e43e49c..726d0cc 100644 --- a/tests/auto/daemon/json/map-reduce-schema.json +++ b/tests/auto/daemon/json/map-reduce-schema.json @@ -72,6 +72,7 @@ { "_type": "Map", "targetType": "Phone", + "targetKeyName": "key", "brokenMap": {"Contact":"function map (c) { for (var i in c.phoneNumbers) { var phone = c.phoneNumbers[i]; jsondb.emit({key: phone.number, name: c.displayName }); }}"}, "map": {"Contact":"function map (c) { for (var i in c.phoneNumbers) { var phone = c.phoneNumbers[i]; jsondb.emit({key: phone.number, displayName: c.displayName}); }}"} }, diff --git a/tests/auto/daemon/json/map-reduce.json b/tests/auto/daemon/json/map-reduce.json index a04ef1c..2736a9c 100644 --- a/tests/auto/daemon/json/map-reduce.json +++ b/tests/auto/daemon/json/map-reduce.json @@ -46,7 +46,7 @@ "name": "Phone", "schema": { "type": "object", - "extends": "View" + "extends": {"$ref": "View"} } }, { @@ -54,7 +54,7 @@ "name": "PhoneCount", "schema": { "type": "object", - "extends": "View" + "extends": {"$ref": "View"} } }, { diff --git a/tests/auto/daemon/json/reduce.json b/tests/auto/daemon/json/reduce.json index f95d4f1..5939fb7 100644 --- a/tests/auto/daemon/json/reduce.json +++ b/tests/auto/daemon/json/reduce.json @@ -20,6 +20,8 @@ "targetKeyName": "firstName", "targetValueName": "count", "add": "function add (k, z, c) { if (!z) {z = 0}; z += 1; if (z) return z;}", - "subtract": "function subtract (k, z, c) { if (!z) {z = 0}; z -= 1; if (z) return z;}" + "subtract": "function subtract (k, z, c) { if (!z) {z = 0}; z -= 1; if (z) return z;}", + "addFlattened": "function add (k, z, c) { if (!z) {z = {count: 0}}; z.count += 1; if (z.count) return z;}", + "subtractFlattened": "function subtract (k, z, c) { if (!z) {z = {count: 0}}; z.count -= 1; if (z.count) return z;}" } ] diff --git a/tests/auto/daemon/testjsondb.cpp b/tests/auto/daemon/testjsondb.cpp index e65939d..ce2781e 100644 --- a/tests/auto/daemon/testjsondb.cpp +++ b/tests/auto/daemon/testjsondb.cpp @@ -173,8 +173,11 @@ private slots: void mapSelfJoinSourceUuids(); void mapMapFunctionError(); void mapSchemaViolation(); + void mapMultipleEmitNoTargetKeyName(); void mapArrayConversion(); void reduce(); + void reduceFlattened(); + void reduceSourceKeyFunction(); void reduceRemoval(); void reduceUpdate(); void reduceDuplicate(); @@ -1423,7 +1426,7 @@ void TestJsonDb::mapDefinitionInvalid() JsonDbWriteResult schemaRes = create(mOwner, schema); verifyGoodResult(schemaRes); - JsonDbObject mapDefinition; + JsonDbObject mapDefinition, map; mapDefinition.insert(JsonDbString::kTypeStr, JsonDbString::kMapTypeStr); mapDefinition.insert("sourceType", QLatin1String("Contact")); mapDefinition.insert("map", QLatin1String("function map (c) { }")); @@ -1448,8 +1451,8 @@ void TestJsonDb::mapDefinitionInvalid() mapDefinition = JsonDbObject(); mapDefinition.insert(JsonDbString::kTypeStr, JsonDbString::kMapTypeStr); mapDefinition.insert("targetType", QLatin1String("MyViewType2")); - mapDefinition.insert("sourceType", QLatin1String("Contact")); - mapDefinition.insert("map", QLatin1String("function map (c) { }")); + map.insert(QLatin1String("Contact"), QLatin1String("function map (c) { }")); + mapDefinition.insert("map", map); res = create(mOwner, mapDefinition); verifyErrorResult(res); QVERIFY(res.message.contains("View")); @@ -1555,6 +1558,17 @@ void TestJsonDb::reduceDefinitionInvalid() verifyErrorResult(res); QVERIFY(res.message.contains("View")); + // fail because targetValue name is not a string or null + reduceDefinition = JsonDbObject(); + reduceDefinition.insert(JsonDbString::kTypeStr, QLatin1String("Reduce")); + reduceDefinition.insert("targetType", QLatin1String("MyViewType")); + reduceDefinition.insert("sourceType", QLatin1String("Contact")); + reduceDefinition.insert("add", QLatin1String("function add (k, z, c) { }")); + reduceDefinition.insert("subtract", QLatin1String("function subtract (k, z, c) { }")); + reduceDefinition.insert("targetValueName", true); + res = create(mOwner, reduceDefinition); + verifyErrorResult(res); + //schemaRes.value("result").toObject() verifyGoodResult(remove(mOwner, schema)); } @@ -1579,20 +1593,9 @@ void TestJsonDb::mapInvalidMapFunc() mapDefinition.insert("map", sourceToMapFunctions); JsonDbWriteResult defRes = create(mOwner, mapDefinition); - verifyGoodResult(defRes); - QString uuid = defRes.objectsWritten[0].uuid().toString(); - - // force the view to be updated - mJsonDbPartition->updateView("InvalidMapViewType"); - - // now check for an error - GetObjectsResult res = mJsonDbPartition->getObjects("_uuid", uuid, JsonDbString::kMapTypeStr); - QVERIFY(res.data.size() > 0); - mapDefinition = res.data.at(0); - QVERIFY(mapDefinition.contains(JsonDbString::kActiveStr) && !mapDefinition.value(JsonDbString::kActiveStr).toBool()); - QVERIFY(!mapDefinition.value(JsonDbString::kErrorStr).toString().isEmpty()); + verifyErrorResult(defRes); + QCOMPARE(defRes.objectsWritten.size(), 0); - verifyGoodResult(remove(mOwner, mapDefinition)); verifyGoodResult(remove(mOwner, schema)); } @@ -1616,16 +1619,8 @@ void TestJsonDb::reduceInvalidAddSubtractFuncs() reduceDefinition.insert("add", QLatin1String("function add (k, z, c) { ;")); // non-parsable add function reduceDefinition.insert("subtract", QLatin1String("function subtract (k, z, c) { }")); JsonDbWriteResult res = create(mOwner, reduceDefinition); - verifyGoodResult(res); - - mJsonDbPartition->updateView("MyViewType"); - - GetObjectsResult getObjects = mJsonDbPartition->getObjects("_uuid", res.objectsWritten[0].uuid().toString()); - reduceDefinition = getObjects.data.at(0); - QVERIFY(reduceDefinition.contains(JsonDbString::kActiveStr) && !reduceDefinition.value(JsonDbString::kActiveStr).toBool()); - QVERIFY(!reduceDefinition.value(JsonDbString::kErrorStr).toString().isEmpty()); + verifyErrorResult(res); - verifyGoodResult(remove(mOwner, reduceDefinition)); verifyGoodResult(remove(mOwner, schema)); } @@ -2134,6 +2129,55 @@ void TestJsonDb::mapSchemaViolation() jsondbSettings->setValidateSchemas(false); } +// verify that only one target object per source object is allowed without targetKeyName +void TestJsonDb::mapMultipleEmitNoTargetKeyName() +{ + jsondbSettings->setValidateSchemas(true); + + GetObjectsResult contactsRes = mJsonDbPartition->getObjects(JsonDbString::kTypeStr, QLatin1String("Contact")); + if (contactsRes.data.size() > 0) { + foreach (const JsonDbObject &toRemove, contactsRes.data) + remove(mOwner, toRemove); + } + + QJsonArray objects(readJsonFile(":/daemon/json/map-reduce-schema.json").toArray()); + JsonDbObjectList toDelete; + JsonDbObject map; + + for (int ii = 0; ii < objects.size(); ii++) { + JsonDbObject object(objects.at(ii).toObject()); + if (object.value(JsonDbString::kTypeStr).toString() != JsonDbString::kReduceTypeStr) { + + if (object.value(JsonDbString::kTypeStr).toString() == JsonDbString::kMapTypeStr) + object.remove("targetKeyName"); + + JsonDbWriteResult result = create(mOwner, object); + if (object.value(JsonDbString::kTypeStr).toString() == JsonDbString::kMapTypeStr) + map = object; + + verifyGoodResult(result); + if (object.value(JsonDbString::kTypeStr).toString() != JsonDbString::kMapTypeStr) + toDelete.append(object); + } + } + + mJsonDbPartition->updateView(map.value("targetType").toString()); + + GetObjectsResult getObjects = mJsonDbPartition->getObjects("_uuid", map.value(JsonDbString::kUuidStr).toString()); + QJsonObject mapDefinition = getObjects.data.at(0); + QVERIFY(!mapDefinition.contains(JsonDbString::kActiveStr)|| mapDefinition.value(JsonDbString::kActiveStr).toBool()); + QVERIFY(!mapDefinition.contains(JsonDbString::kErrorStr) || mapDefinition.value(JsonDbString::kErrorStr).toString().isEmpty()); + + getObjects = mJsonDbPartition->getObjects(JsonDbString::kTypeStr, QLatin1String("Phone")); + QCOMPARE(getObjects.data.size(), 2); + + verifyGoodResult(remove(mOwner, map)); + for (int ii = 0; ii < toDelete.size(); ii++) + verifyGoodResult(remove(mOwner, toDelete.at(ii))); + + jsondbSettings->setValidateSchemas(false); +} + void TestJsonDb::mapArrayConversion() { QJsonArray objects(readJsonFile(":/daemon/json/map-array-conversion.json").toArray()); @@ -2200,6 +2244,104 @@ void TestJsonDb::reduce() mJsonDbPartition->removeIndex("MyContactCount"); } +void TestJsonDb::reduceFlattened() +{ + QJsonArray objects(readJsonFile(":/daemon/json/reduce-data.json").toArray()); + + JsonDbObjectList toDelete; + JsonDbObjectList reduces; + + QHash<QString, int> firstNameCount; + for (int ii = 0; ii < objects.size(); ii++) { + JsonDbObject object(objects.at(ii).toObject()); + JsonDbWriteResult result = create(mOwner, object); + verifyGoodResult(result); + firstNameCount[object.value("firstName").toString()]++; + toDelete.append(object); + } + + objects = readJsonFile(":/daemon/json/reduce.json").toArray(); + for (int ii = 0; ii < objects.size(); ii++) { + JsonDbObject object(objects.at(ii).toObject()); + if (object.value(JsonDbString::kTypeStr).toString() == JsonDbString::kReduceTypeStr) { + object.insert(QLatin1String("add"), object.value(QLatin1String("addFlattened"))); + object.insert(QLatin1String("subtract"), object.value(QLatin1String("subtractFlattened"))); + // transitional behavior: null targetValueName indicates whole object is value of the Reduce + object.insert(QLatin1String("targetValueName"), QJsonValue(QJsonValue::Null)); + } + JsonDbWriteResult result = create(mOwner, object); + verifyGoodResult(result); + if (object.value(JsonDbString::kTypeStr).toString() == "Reduce") + reduces.append(object); + else + toDelete.append(object); + } + + JsonDbQueryResult queryResult = find(mOwner, QLatin1String("[?_type=\"MyContactCount\"]")); + verifyGoodQueryResult(queryResult); + QCOMPARE(queryResult.data.size(), firstNameCount.keys().count()); + + JsonDbObjectList data = queryResult.data; + for (int ii = 0; ii < data.size(); ii++) { + QCOMPARE((int)data.at(ii).value("count").toDouble(), + firstNameCount[data.at(ii).value("firstName").toString()]); + } + for (int ii = 0; ii < reduces.size(); ii++) + verifyGoodResult(remove(mOwner, reduces.at(ii))); + for (int ii = 0; ii < toDelete.size(); ii++) + verifyGoodResult(remove(mOwner, toDelete.at(ii))); + mJsonDbPartition->removeIndex("MyContactCount"); +} + +void TestJsonDb::reduceSourceKeyFunction() +{ + QJsonArray objects(readJsonFile(":/daemon/json/reduce-data.json").toArray()); + + JsonDbObjectList toDelete; + JsonDbObjectList reduces; + + QHash<QString, int> firstNameCount; + for (int ii = 0; ii < objects.size(); ii++) { + JsonDbObject object(objects.at(ii).toObject()); + JsonDbWriteResult result = create(mOwner, object); + verifyGoodResult(result); + firstNameCount[object.value("firstName").toString()]++; + toDelete.append(object); + } + + objects = readJsonFile(":/daemon/json/reduce.json").toArray(); + for (int ii = 0; ii < objects.size(); ii++) { + JsonDbObject object(objects.at(ii).toObject()); + if (object.value(JsonDbString::kTypeStr).toString() == JsonDbString::kReduceTypeStr) { + QString sourceKeyName = object.value(QLatin1String("sourceKeyName")).toString(); + object.remove(QLatin1String("sourceKeyName")); + object.insert(QLatin1String("sourceKeyFunction"), + QString("function (o) { return o.%1; }").arg(sourceKeyName)); + } + JsonDbWriteResult result = create(mOwner, object); + verifyGoodResult(result); + if (object.value(JsonDbString::kTypeStr).toString() == "Reduce") + reduces.append(object); + else + toDelete.append(object); + } + + JsonDbQueryResult queryResult = find(mOwner, QLatin1String("[?_type=\"MyContactCount\"]")); + verifyGoodQueryResult(queryResult); + QCOMPARE(queryResult.data.size(), firstNameCount.keys().count()); + + JsonDbObjectList data = queryResult.data; + for (int ii = 0; ii < data.size(); ii++) { + QCOMPARE((int)data.at(ii).value("count").toDouble(), + firstNameCount[data.at(ii).value("firstName").toString()]); + } + for (int ii = 0; ii < reduces.size(); ii++) + verifyGoodResult(remove(mOwner, reduces.at(ii))); + for (int ii = 0; ii < toDelete.size(); ii++) + verifyGoodResult(remove(mOwner, toDelete.at(ii))); + mJsonDbPartition->removeIndex("MyContactCount"); +} + void TestJsonDb::reduceRemoval() { QJsonArray objects(readJsonFile(":/daemon/json/reduce-data.json").toArray()); @@ -2776,7 +2918,7 @@ void TestJsonDb::find1() item.insert("find1", QString("Foobar!")); create(mOwner, item); - JsonDbQueryResult queryResult= find(mOwner, QLatin1String(".*")); + JsonDbQueryResult queryResult= find(mOwner, QLatin1String("[*]")); verifyGoodQueryResult(queryResult); QVERIFY(queryResult.data.size() >= 1); diff --git a/tests/auto/jsondbqueryobject/testjsondbqueryobject.cpp b/tests/auto/jsondbqueryobject/testjsondbqueryobject.cpp index f606747..8205a18 100644 --- a/tests/auto/jsondbqueryobject/testjsondbqueryobject.cpp +++ b/tests/auto/jsondbqueryobject/testjsondbqueryobject.cpp @@ -67,6 +67,12 @@ const QString qmlProgramForPartition = QLatin1String( "name: \"com.nokia.shared\";" "}"); +const QString qmlProgramForQueryWithoutPartition = QLatin1String( + "import QtQuick 2.0 \n" + "import QtJsonDb 1.0 as JsonDb \n" + "JsonDb.Query { " + "id:queryObject;" + "}"); TestJsonDbQueryObject::TestJsonDbQueryObject() : mTimedOut(false) @@ -125,8 +131,11 @@ void TestJsonDbQueryObject::initTestCase() } -ComponentData *TestJsonDbQueryObject::createComponent() +ComponentData *TestJsonDbQueryObject::createComponent(const QString &qml) { + QString tmp = qml; + if (tmp.isEmpty()) + tmp = qmlProgram; ComponentData *componentData = new ComponentData(); componentData->engine = new QQmlEngine(); QString error; @@ -136,7 +145,7 @@ ComponentData *TestJsonDbQueryObject::createComponent() return 0; } componentData->component = new QQmlComponent(componentData->engine); - componentData->component->setData(qmlProgram.toLocal8Bit(), QUrl()); + componentData->component->setData(tmp.toLocal8Bit(), QUrl()); componentData->qmlElement = componentData->component->create(); if (componentData->component->isError()) qDebug() << componentData->component->errors(); @@ -300,6 +309,43 @@ void TestJsonDbQueryObject::createQuery() deleteComponent(partition); } +void TestJsonDbQueryObject::queryWithoutPartition() +{ + ComponentData *queryObject = createComponent(qmlProgramForQueryWithoutPartition); + if (!queryObject || !queryObject->qmlElement) return; + + // define index on pos property + QVariantMap index; + index.insert("_type", "Index"); + index.insert("propertyName", "pos"); + index.insert("propertyType", "number"); + mClient->create(index); + + const QString queryString = QString("[?_type = \""+QString( __FUNCTION__ )+"\"][/pos]"); + queryObject->qmlElement->setProperty("query", queryString); + currentQmlElement = queryObject->qmlElement; + + //Create objects + QVariantList items = createObjectList(__FUNCTION__, 10).toList(); + mClient->create(QVariant(items)); + qSort(items.begin(), items.end(), posLessThan); + const QString expression("start();"); + QQmlExpression *expr; + expr = new QQmlExpression(queryObject->engine->rootContext(), queryObject->qmlElement, expression); + expr->evaluate(); + waitForCallback2(); + QCOMPARE(callbackError, false); + QCOMPARE(cbData.size(), 10); + for (int i = 0; i<10; i++) { + QVariantMap item = items[i].toMap(); + QVariantMap obj = cbData[i].toMap(); + QCOMPARE(obj.value("alphabet"), item.value("alphabet")); + } + + delete expr; + deleteComponent(queryObject); +} + void TestJsonDbQueryObject::queryBinding() { ComponentData *queryObject = createComponent(); diff --git a/tests/auto/jsondbqueryobject/testjsondbqueryobject.h b/tests/auto/jsondbqueryobject/testjsondbqueryobject.h index 50497a2..c79745a 100644 --- a/tests/auto/jsondbqueryobject/testjsondbqueryobject.h +++ b/tests/auto/jsondbqueryobject/testjsondbqueryobject.h @@ -76,6 +76,7 @@ private slots: void singleObject(); void multipleObjects(); void createQuery(); + void queryWithoutPartition(); void queryBinding(); void queryError(); void queryLimit(); @@ -88,7 +89,7 @@ protected slots: void timeout(); private: - ComponentData *createComponent(); + ComponentData *createComponent(const QString &qml = QString()); ComponentData *createPartitionComponent(); void deleteComponent(ComponentData *componentData); diff --git a/tests/auto/qjsondbwatcher/testqjsondbwatcher.cpp b/tests/auto/qjsondbwatcher/testqjsondbwatcher.cpp index 80be7eb..61d4bc0 100644 --- a/tests/auto/qjsondbwatcher/testqjsondbwatcher.cpp +++ b/tests/auto/qjsondbwatcher/testqjsondbwatcher.cpp @@ -95,7 +95,7 @@ public slots: // from a watcher void onWatcherNotificationsAvailable(int); void onWatcherStatusChanged(QtJsonDb::QJsonDbWatcher::Status); - void onWatcherError(int,QString); + void onWatcherError(QtJsonDb::QJsonDbWatcher::ErrorCode,QString); private: void removeDbFiles(); @@ -125,6 +125,12 @@ static const char dbfileprefix[] = "test-jsondb-watcher"; mEventLoop.exec(QEventLoop::AllEvents); \ } +#define waitForWatcherStatus(_watcher, _status) \ + { \ + while (_watcher.status() != _status) \ + mEventLoop.processEvents(QEventLoop::AllEvents); \ + } + TestQJsonDbWatcher::TestQJsonDbWatcher() : mProcess(0) { @@ -183,7 +189,7 @@ void TestQJsonDbWatcher::onWatcherStatusChanged(QtJsonDb::QJsonDbWatcher::Status } // this should go into a new version of clientwrapper.h -void TestQJsonDbWatcher::onWatcherError(int code, QString message) +void TestQJsonDbWatcher::onWatcherError(QtJsonDb::QJsonDbWatcher::ErrorCode code, QString message) { qCritical() << "onWatcherError" << code << message; mEventLoop.quit(); @@ -256,7 +262,7 @@ void TestQJsonDbWatcher::createAndRemove() this, SLOT(onWatcherNotificationsAvailable(int))); connect(&watcher, SIGNAL(statusChanged(QtJsonDb::QJsonDbWatcher::Status)), this, SLOT(onWatcherStatusChanged(QtJsonDb::QJsonDbWatcher::Status))); - connect(&watcher, SIGNAL(error(int,QString)), this, SLOT(onWatcherError(int,QString))); + connect(&watcher, SIGNAL(error(QtJsonDb::QJsonDbWatcher::ErrorCode,QString)), this, SLOT(onWatcherError(QtJsonDb::QJsonDbWatcher::ErrorCode,QString))); mConnection->addWatcher(&watcher); QJsonObject item; @@ -354,7 +360,7 @@ void TestQJsonDbWatcher::history() this, SLOT(onWatcherNotificationsAvailable(int))); connect(&watcher, SIGNAL(statusChanged(QtJsonDb::QJsonDbWatcher::Status)), this, SLOT(onWatcherStatusChanged(QtJsonDb::QJsonDbWatcher::Status))); - connect(&watcher, SIGNAL(error(int,QString)), this, SLOT(onWatcherError(int,QString))); + connect(&watcher, SIGNAL(error(QtJsonDb::QJsonDbWatcher::ErrorCode,QString)), this, SLOT(onWatcherError(QtJsonDb::QJsonDbWatcher::ErrorCode,QString))); // set the starting state watcher.setInitialStateNumber(firstStateNumber-1); @@ -362,6 +368,7 @@ void TestQJsonDbWatcher::history() // expecting one notification per create and one state change waitForResponseAndNotification(0, objects.size()+1); + waitForWatcherStatus(watcher, QJsonDbWatcher::Active); QList<QJsonDbNotification> notifications = watcher.takeNotifications(); QCOMPARE(notifications.size(), objects.size()+1); @@ -372,6 +379,31 @@ void TestQJsonDbWatcher::history() QCOMPARE(notifications.at(objects.size()).action(), QJsonDbWatcher::StateChanged); mConnection->removeWatcher(&watcher); + + // create a new historical watcher that should retrieve all the changes + QJsonDbWatcher watcher2; + watcher2.setWatchedActions(QJsonDbWatcher::All); + watcher2.setQuery(QLatin1String("[?_type=\"com.test.qjsondbwatcher-test\"]")); + connect(&watcher2, SIGNAL(notificationsAvailable(int)), + this, SLOT(onWatcherNotificationsAvailable(int))); + connect(&watcher2, SIGNAL(statusChanged(QtJsonDb::QJsonDbWatcher::Status)), + this, SLOT(onWatcherStatusChanged(QtJsonDb::QJsonDbWatcher::Status))); + connect(&watcher2, SIGNAL(error(QtJsonDb::QJsonDbWatcher::ErrorCode,QString)), this, SLOT(onWatcherError(QtJsonDb::QJsonDbWatcher::ErrorCode,QString))); + watcher2.setInitialStateNumber(-1); + + mConnection->addWatcher(&watcher2); + waitForResponseAndNotification(0, objects.size() + 1); + waitForWatcherStatus(watcher2, QJsonDbWatcher::Active); + + QList<QJsonDbNotification> notifications2 = watcher2.takeNotifications(); + QCOMPARE(notifications2.size(), objects.size()+1); + // we received one Create notification per object + foreach (const QJsonDbNotification n, notifications2.mid(0, objects.size())) + QCOMPARE(n.action(), QJsonDbWatcher::Created); + // we received one StateChanged notification + QCOMPARE(notifications2.at(objects.size()).action(), QJsonDbWatcher::StateChanged); + + mConnection->removeWatcher(&watcher2); } void TestQJsonDbWatcher::currentState() @@ -383,7 +415,7 @@ void TestQJsonDbWatcher::currentState() this, SLOT(onWatcherNotificationsAvailable(int))); connect(&watcher, SIGNAL(statusChanged(QtJsonDb::QJsonDbWatcher::Status)), this, SLOT(onWatcherStatusChanged(QtJsonDb::QJsonDbWatcher::Status))); - connect(&watcher, SIGNAL(error(int,QString)), this, SLOT(onWatcherError(int,QString))); + connect(&watcher, SIGNAL(error(QtJsonDb::QJsonDbWatcher::ErrorCode,QString)), this, SLOT(onWatcherError(QtJsonDb::QJsonDbWatcher::ErrorCode,QString))); // set the starting state to -1 to get the current state watcher.setInitialStateNumber(static_cast<quint32>(-1)); diff --git a/tests/auto/queries/dataset.json b/tests/auto/queries/dataset.json index 8585e44..7697e29 100644 --- a/tests/auto/queries/dataset.json +++ b/tests/auto/queries/dataset.json @@ -18,8 +18,6 @@ "num-demon-bunnies":3, "num-russian-bunnies":2 }, - - { "_type" : "dragon", "name": "marcia d'arcy", @@ -151,5 +149,20 @@ "name": "thomas reim", "type":"russian", "age": 8 + }, + { + "_type" : "dog", + "name" : "rover", + "friends" : ["spike", "reggie"] + }, + { + "_type" : "dog", + "name" : "spike", + "friends" : [ { "name" : "rover", "dog" : true }, { "name" : "puffy", "dog" : false } ] + }, + { + "_type" : "dog", + "name" : "reggie", + "friends" : [ ["larrry", "curly", "moe"], ["spike", "rover"] ] } ] diff --git a/tests/auto/queries/testjsondbqueries.cpp b/tests/auto/queries/testjsondbqueries.cpp index fc2fc55..d5218b6 100644 --- a/tests/auto/queries/testjsondbqueries.cpp +++ b/tests/auto/queries/testjsondbqueries.cpp @@ -98,6 +98,8 @@ private slots: void queryNotEqual(); void queryQuotedProperties(); void querySortedByIndexName(); + void queryContains(); + void queryInvalid(); private: void removeDbFiles(); @@ -311,6 +313,34 @@ void TestJsonDbQueries::queryFieldNotExists() QCOMPARE(queryResult.data.size(), mDataStats["num-bunnies"].toInt()); QVERIFY(confirmEachObject(queryResult.data, CheckObjectFieldEqualTo<QString>("_type", "bunny"))); + + // verify can use notExists on an indexed field + JsonDbObject index; + index.insert("_type", QString("Index")); + index.insert("propertyName", QString("color")); + index.insert("propertyType", QString("string")); + verifyGoodWriteResult(mJsonDbPartition->updateObject(mOwner, index)); + + // notExists on an indexed field will choose a different sortKey + queryResult = find(mOwner, QLatin1String("[?color notExists][? _type = \"bunny\" | _type=\"dragon\"]")); + QVERIFY(!queryResult.sortKeys.toArray().contains(QJsonValue(QLatin1String("color")))); + QCOMPARE(queryResult.data.size(), + mDataStats["num-bunnies"].toInt()); + QVERIFY(confirmEachObject(queryResult.data, CheckObjectFieldEqualTo<QString>("_type", "bunny"))); + + // notExists on an indexed field will choose a different sortKey, even if we try to force it + queryResult = find(mOwner, QLatin1String("[?color notExists][? _type = \"bunny\" | _type=\"dragon\"][/color]")); + QVERIFY(!queryResult.sortKeys.toArray().contains(QJsonValue(QLatin1String("color")))); + QCOMPARE(queryResult.data.size(), + mDataStats["num-bunnies"].toInt()); + QVERIFY(confirmEachObject(queryResult.data, CheckObjectFieldEqualTo<QString>("_type", "bunny"))); + + // exists on an indexed field will choose the field as sortKey + queryResult = find(mOwner, QLatin1String("[?color exists][? _type = \"bunny\" | _type=\"dragon\"]")); + QVERIFY(queryResult.sortKeys.toArray().contains(QJsonValue(QLatin1String("color")))); + QCOMPARE(queryResult.data.size(), + mDataStats["num-dragons"].toInt()); + QVERIFY(confirmEachObject(queryResult.data, CheckObjectFieldEqualTo<QString>("_type", "dragon"))); } void TestJsonDbQueries::queryLessThan() @@ -398,5 +428,32 @@ void TestJsonDbQueries::querySortedByIndexName() QVERIFY(confirmEachObject(queryResult.data, CheckSortOrder<double>("age", QList<double>() << 8 << 8 << 6 << 6 << 4 << 4 << 2 << 2 << 0 << 0))); } +void TestJsonDbQueries::queryContains() +{ + JsonDbQueryResult queryResult = find(mOwner, QLatin1String("[?_type = \"dog\"][?friends contains \"spike\" ]")); + QVERIFY(queryResult.error.isNull()); + QCOMPARE(queryResult.data.count(), 1); + queryResult = find(mOwner, QLatin1String("[?_type = \"dog\"][?friends contains { \"name\" : \"puffy\", \"dog\" : false } ]")); + QVERIFY(queryResult.error.isNull()); + QCOMPARE(queryResult.data.count(), 1); + queryResult = find(mOwner, QLatin1String("[?_type = \"dog\"][?friends contains [\"spike\", \"rover\"] ]")); + QVERIFY(queryResult.error.isNull()); + QCOMPARE(queryResult.data.count(), 1); + + // invalid queries + queryResult = find(mOwner, QLatin1String("[?_type = \"dog\"][?friends contains { \"name\" : \"puffy\" \"dog\" : false } ]")); + QVERIFY(!queryResult.error.isNull()); + queryResult = find(mOwner, QLatin1String("[?_type = \"dog\"][?friends contains [\"spike\", \"rover\" ]")); + QVERIFY(!queryResult.error.isNull()); + queryResult = find(mOwner, QLatin1String("[?_type = \"dog\"][?friends contains \"spike\", \"rover\" ] ]")); + QVERIFY(!queryResult.error.isNull()); +} + +void TestJsonDbQueries::queryInvalid() +{ + JsonDbQueryResult queryResult = find(mOwner, QLatin1String("foo")); + QVERIFY(!queryResult.error.isNull()); +} + QTEST_MAIN(TestJsonDbQueries) #include "testjsondbqueries.moc" diff --git a/tests/shared/util.h b/tests/shared/util.h index 6301c36..90e6535 100644 --- a/tests/shared/util.h +++ b/tests/shared/util.h @@ -77,9 +77,11 @@ QJsonDocument readJsonFile(const QString &filename, QJsonParseError *error = 0) { QString filepath = filename; QFile jsonFile(filepath); - if (!jsonFile.exists() && error) { - error->error = QJsonParseError::EndOfString; - error->offset = 0; + if (!jsonFile.exists()) { + if (error) { + error->error = QJsonParseError::MissingObject; + error->offset = 0; + } return QJsonDocument(); } jsonFile.open(QIODevice::ReadOnly); diff --git a/tools/aodbread/aodbread.pro b/tools/aodbread/aodbread.pro index 743ed8e..c8e8563 100644 --- a/tools/aodbread/aodbread.pro +++ b/tools/aodbread/aodbread.pro @@ -1,22 +1,13 @@ TARGET = aodbread -QT = testlib jsondbqson-private +QT = testlib CONFIG -= app_bundle CONFIG += debug -INCLUDEPATH += $$PWD/../../src/daemon -LIBS += -L$$QT.jsondb.libs - DEFINES += SRCDIR=\\\"$$PWD/\\\" -#include($$PWD/../../src/daemon/daemon.pri) include($$PWD/../../src/3rdparty/btree/btree.pri) -include($$PWD/../../src/3rdparty/qjson/qjson.pri) - -HEADERS += \ - $$PWD/../../src/daemon/aodb.h SOURCES += \ - $$PWD/../../src/daemon/aodb.cpp \ main.cpp diff --git a/tools/aodbread/main.cpp b/tools/aodbread/main.cpp index 4b28c71..4c80bcc 100644 --- a/tools/aodbread/main.cpp +++ b/tools/aodbread/main.cpp @@ -41,26 +41,21 @@ #include <QtCore> -#include "aodb.h" #include "btree.h" -#include <QtJsonDbQson/private/qson_p.h> -#include <QtJsonDbQson/private/qsonparser_p.h> +#include "qbtree.h" +#include "qbtreetxn.h" +#include "qbtreecursor.h" -#include "json.h" - -QT_ADDON_JSONDB_USE_NAMESPACE +#include <stdint.h> QString printable(const QByteArray &ba) { QByteArray array = ba; - if (ba.startsWith("QSN")) { - QVariant obj = qsonToVariant(QsonParser::fromRawData(ba)); - JsonWriter writer; -// writer.setAutoFormatting(true); - return QString("QSN:") +writer.toString(obj); - } + QJsonDocument doc = QJsonDocument::fromBinaryData(ba); + if (!doc.isNull()) + return QString("QSN:") + doc.toJson(); // check if it is a number bool isNumber = true; @@ -71,9 +66,8 @@ QString printable(const QByteArray &ba) } } if (isNumber) { - for (int i = 0; i < array.size(); ++i) { + for (int i = 0; i < array.size(); ++i) array[i] = array.at(i)+'0'; - } return QString("N:") +QString::fromLatin1(array); } @@ -92,9 +86,8 @@ QString printable(const QByteArray &ba) } } } - if (utf16) { + if (utf16) return QString("U:") + QString::fromUtf16((const ushort *)array.constData(), array.size()/2); - } for (int i = 0; i < array.size(); ++i) { if (array.at(i) == 0) @@ -108,9 +101,8 @@ QString printable(const QByteArray &ba) break; } } - if (isprintable) { + if (isprintable) return QString("S:") + QString::fromLatin1(array); - } if (ba.size() == 5 && ba.at(4) == 'S') { // state change @@ -150,13 +142,12 @@ bool gDump = false; bool gWantCompact = false; bool gShowAll = false; bool gShowAllReversed = false; -quint32 gShowState = 0; qint64 gDumpPage = 0; void usage() { qDebug() << QCoreApplication::arguments().at(0) - << "[--dump-page num] [--stat] [--dump] [--compact] [--all|--all-reversed] [--state num] database_file"; + << "[--dump-page num] [--stat] [--dump] [--compact] [--all|--all-reversed] database_file"; } int main(int argc, char **argv) @@ -183,13 +174,6 @@ int main(int argc, char **argv) gShowAll = true; } else if (arg == QLatin1String("--all-reversed")) { gShowAllReversed = true; - } else if (arg == QLatin1String("--state")) { - bool ok = false; - gShowState = args.takeFirst().toInt(&ok); - if (!ok) { - usage(); - return 0; - } } else if (arg == QLatin1String("--dump-page")) { bool ok = false; gDumpPage = args.takeFirst().toInt(&ok); @@ -217,15 +201,17 @@ int main(int argc, char **argv) return 1; } - AoDb db; - if (!db.open(filename, AoDb::ReadOnly)) { + QBtree db; + db.setFileName(filename); + db.setFlags(QBtree::ReadOnly); + if (!db.open()) { qDebug() << "cannot open" << filename; usage(); return 0; } if (gStat) { - const struct btree_stat *bs = db.stat(); + const struct btree_stat *bs = btree_stat(db.handle()); qDebug() << "stat:"; qDebug() << "\thits:" << bs->hits; qDebug() << "\treads:" << bs->reads; @@ -241,48 +227,25 @@ int main(int argc, char **argv) qDebug() << "\ttag:" << bs->tag; qDebug() << ""; } + if (gDump) { db.dump(); } - if (gShowState != 0) { -#if 0 - AoDb statedb; - QFileInfo fi(filename); - if (!statedb.open(QString("%1/%2-States.db").arg(fi.dir().path()).arg(fi.baseName()), AoDb::ReadOnly)) { - qDebug() << "cannot open" << filename; - usage(); - return 0; - } - ObjectCursor cursor(&statedb, &db); - bool ok = cursor.first(gShowState); - if (!ok) { - qDebug() << "Could not seek to" << gShowState; - return 0; - } - QByteArray key, value; - for (int i = 0; ok && cursor.current(key, value); ok = cursor.next(), ++i) { - QString keyString, valueString; - makePrintable(key, keyString, value, valueString); - qDebug() << i << ":" << keyString << ":" << valueString; - } -#endif - } - if (gShowAllReversed) { - AoDbCursor cursor(&db); + QBtreeCursor cursor(&db); QByteArray key, value; int i = 0; - for (bool ok = cursor.last(); ok && cursor.current(key, value); ok = cursor.prev(), ++i) { + for (bool ok = cursor.last(); ok && cursor.current(&key, &value); ok = cursor.prev(), ++i) { QString keyString, valueString; makePrintable(key, keyString, value, valueString); qDebug() << i << ":" << keyString << ":" << valueString; } } else if (gShowAll) { - AoDbCursor cursor(&db); + QBtreeCursor cursor(&db); QByteArray key, value; int i = 0; - for (bool ok = cursor.first(); ok && cursor.current(key, value); ok = cursor.next(), ++i) { + for (bool ok = cursor.first(); ok && cursor.current(&key, &value); ok = cursor.next(), ++i) { QString keyString, valueString; makePrintable(key, keyString, value, valueString); qDebug() << i << ":" << keyString << ":" << valueString; @@ -291,7 +254,9 @@ int main(int argc, char **argv) if (gWantCompact) { db.close(); - db.open(filename); + db.setFileName(filename); + db.setFlags(QBtree::Default); + db.open(); if (db.compact()) { qDebug() << "compacted."; } else { diff --git a/tools/jsondb-client/client.cpp b/tools/jsondb-client/client.cpp index decc202..aba2bcb 100644 --- a/tools/jsondb-client/client.cpp +++ b/tools/jsondb-client/client.cpp @@ -53,8 +53,7 @@ QT_USE_NAMESPACE -const char* InputThread::commands[] = { "changesSince", - "create {\"", +const char* InputThread::commands[] = { "create {\"", "help", "load", "notify create [?", @@ -274,8 +273,11 @@ void Client::onNotificationsAvailable(int) case QtJsonDb::QJsonDbWatcher::All: break; } - QString message = "Received notification: type " % actionString - % " for " % watcher->query() % " object:\n" % QJsonDocument(n.object()).toJson(); + QString message = QString("Received %1 notification for %2 [state %3]\n%4\n") + .arg(actionString) + .arg(watcher->query()) + .arg(n.stateNumber()) + .arg(QString::fromUtf8(QJsonDocument(n.object()).toJson())); InputThread::print(message); } if (!mInputThread) @@ -333,14 +335,22 @@ void Client::error(QtJsonDb::QJsonDbConnection::ErrorCode error, const QString & void Client::onRequestFinished() { - QtJsonDb::QJsonDbRequest *request = qobject_cast<QtJsonDb::QJsonDbRequest *>(sender()); - Q_ASSERT(request != 0); - if (!request) - return; + quint32 stateNumber; + QtJsonDb::QJsonDbRequest *request = qobject_cast<QtJsonDb::QJsonDbReadRequest*>(sender()); + + if (request) { + stateNumber = qobject_cast<QtJsonDb::QJsonDbReadRequest*>(request)->stateNumber(); + } else { + request = qobject_cast<QtJsonDb::QJsonDbWriteRequest*>(sender()); + if (!request) + return; + stateNumber = qobject_cast<QtJsonDb::QJsonDbWriteRequest*>(request)->stateNumber(); + } QList<QJsonObject> objects = request->takeResults(); - QString message = QLatin1String("Received ") + QString::number(objects.size()) + QLatin1String(" object(s):\n"); + QString message = QString("Received %1 object(s) [state %2]\n").arg(objects.size()).arg(stateNumber); + if (!objects.isEmpty()) { message += QJsonDocument(objects.front()).toJson().trimmed(); for (int i = 1; i < objects.size(); ++i) @@ -351,6 +361,9 @@ void Client::onRequestFinished() void Client::aboutToRemove(void) { + // remove the query from the request list + mRequests.takeFirst()->deleteLater(); + QtJsonDb::QJsonDbRequest *queryRequest = qobject_cast<QtJsonDb::QJsonDbRequest *>(sender()); Q_ASSERT(queryRequest != 0); if (!queryRequest) @@ -404,8 +417,7 @@ void Client::usage() << " remove [partition:<name>] OBJECT" << std::endl << " remove [partition:<name>] STRING [limit]" << std::endl << " query [partition:<name>] STRING [limit]" << std::endl - << " notify [partition:<name>] ACTIONS QUERY" << std::endl - << " changesSince [partition:<name>] STATENUMBER [type1 type2 ...]" << std::endl + << " notify [partition:<name>] ACTIONS QUERY [starting-state]" << std::endl << " load FILE1 FILE2 ..." << std::endl << " help" << std::endl << " quit" << std::endl @@ -419,7 +431,9 @@ void Client::usage() << " query [?_type=\"duck\"]" << std::endl << " remove {\"_uuid\": \"{18c9d905-5860-464e-a6dd-951464e366de}\", \"_version\": \"1-134f23dbb2\"}" << std::endl << " remove [?_type=\"duck\"]" << std::endl - << " notify create,remove [?_type=\"duck\"]" << std::endl; + << " notify create,remove [?_type=\"duck\"]" << std::endl + << " notify create,remove [?_type=\"duck\"] 53" << std::endl; + QString usageInfo = QString::fromStdString(out.str()); InputThread::print(usageInfo); @@ -495,18 +509,33 @@ bool Client::processCommand(const QString &command) else if (s == QLatin1String("update")) action = QtJsonDb::QJsonDbWatcher::Updated; if (action == QtJsonDb::QJsonDbWatcher::Action(0)) { - InputThread::print("uknown notification type" % s); + InputThread::print("unknown notification type" % s); return false; } actions |= action; } + + int startingState = 0; QString query = rest.mid(s+1).trimmed(); + if (!query.endsWith(']')) { + bool ok; + int index = query.lastIndexOf(' '); + int state = query.right(query.length() - index).trimmed().toInt(&ok); + if (ok) + startingState = state; + query = query.left(index); + } + if (mDebug) qDebug() << "Creating notification:" << alist << ":" << query; + QtJsonDb::QJsonDbWatcher *watcher = new QtJsonDb::QJsonDbWatcher(this); watcher->setPartition(partition); watcher->setQuery(query); watcher->setWatchedActions(actions); + if (startingState != 0) + watcher->setInitialStateNumber(startingState); + connect(watcher, SIGNAL(notificationsAvailable(int)), this, SLOT(onNotificationsAvailable(int))); connect(watcher, SIGNAL(statusChanged(QtJsonDb::QJsonDbWatcher::Status)), this, SLOT(onNotificationStatusChanged(QtJsonDb::QJsonDbWatcher::Status))); @@ -558,8 +587,6 @@ bool Client::processCommand(const QString &command) filenames[i] = filename.mid(1, filename.size()-2); } loadFiles(filenames); - } else if (cmd == "changesSince") { - qWarning() << "Not yet supported"; } else if (cmd == "connect") { mConnection->connectToServer(); } else if (cmd == "disconnect") { @@ -613,12 +640,12 @@ void Client::loadNextFile() } QFileInfo info(mFilesToLoad.first()); - if (info.suffix() == QLatin1Literal("json")) { + if (info.suffix() == QLatin1String("json")) { loadJsonFile(info.filePath()); - } else if (info.suffix() == QLatin1Literal("qml")) { + } else if (info.suffix() == QLatin1String("qml")) { loadQmlFile(info.filePath()); #ifndef QTJSONDB_NO_DEPRECATED - } else if (info.suffix() == QLatin1Literal("js")) { + } else if (info.suffix() == QLatin1String("js")) { loadJavaScriptFile(info.filePath()); #endif } else { diff --git a/tools/jsondb-client/jsondbproxy.cpp b/tools/jsondb-client/jsondbproxy.cpp index 5311220..650f910 100644 --- a/tools/jsondb-client/jsondbproxy.cpp +++ b/tools/jsondb-client/jsondbproxy.cpp @@ -67,9 +67,9 @@ QVariantMap _waitForResponse(QtJsonDb::QJsonDbRequest *request) { res.insert(QLatin1Literal("result"), data); } else { QVariantMap error; - error.insert(QLatin1Literal("code"), 1); - error.insert(QLatin1Literal("message"), "Error processing request"); - res.insert(QLatin1Literal("error"), error); + error.insert(QLatin1String("code"), 1); + error.insert(QLatin1String("message"), "Error processing request"); + res.insert(QLatin1String("error"), error); } return res; } @@ -84,9 +84,9 @@ JsonDbProxy::JsonDbProxy(QtJsonDb::QJsonDbConnection *conn, QObject *parent) : QVariantMap JsonDbProxy::find(QVariantMap object) { QtJsonDb::QJsonDbReadRequest *request = new QtJsonDb::QJsonDbReadRequest(this); - request->setQuery(object.value(QLatin1Literal("query")).toString()); - if (object.contains(QLatin1Literal("limit"))) - request->setQueryLimit(object.value(QLatin1Literal("limit")).toInt()); + request->setQuery(object.value(QLatin1String("query")).toString()); + if (object.contains(QLatin1String("limit"))) + request->setQueryLimit(object.value(QLatin1String("limit")).toInt()); mConnection->send(request); return _waitForResponse(request); } @@ -95,8 +95,8 @@ QVariantMap JsonDbProxy::create(QVariantMap object) { // handle the to-be-deprecated _id property QtJsonDb::QJsonDbObject obj = QJsonObject::fromVariantMap(object); - if (obj.uuid().isNull() && obj.contains(QLatin1Literal("_id"))) { - obj.setUuid(QtJsonDb::QJsonDbObject::createUuidFromString(obj.value(QLatin1Literal("_id")).toString())); + if (obj.uuid().isNull() && obj.contains(QLatin1String("_id"))) { + obj.setUuid(QtJsonDb::QJsonDbObject::createUuidFromString(obj.value(QLatin1String("_id")).toString())); obj.remove(QLatin1String("_id")); } QtJsonDb::QJsonDbCreateRequest *request = new QtJsonDb::QJsonDbCreateRequest(QList<QJsonObject>() << obj, @@ -126,8 +126,8 @@ QVariantMap JsonDbProxy::createList(QVariantList list) QList<QJsonObject> objects; foreach (const QVariant &object, list) { QtJsonDb::QJsonDbObject obj = QJsonObject::fromVariantMap(object.toMap()); - if (!obj.uuid().isNull() && obj.contains(QLatin1Literal("_id"))) { - obj.setUuid(QtJsonDb::QJsonDbObject::createUuidFromString(obj.value(QLatin1Literal("_id")).toString())); + if (!obj.uuid().isNull() && obj.contains(QLatin1String("_id"))) { + obj.setUuid(QtJsonDb::QJsonDbObject::createUuidFromString(obj.value(QLatin1String("_id")).toString())); obj.remove(QLatin1String("_id")); } objects << obj; diff --git a/tools/jsondb-client/main.cpp b/tools/jsondb-client/main.cpp index 1a1867f..b389131 100644 --- a/tools/jsondb-client/main.cpp +++ b/tools/jsondb-client/main.cpp @@ -94,17 +94,17 @@ int main(int argc, char * argv[]) continue; } - if (arg == QLatin1Literal("-help")) { + if (arg == QLatin1String("-help")) { usage(progname); - } else if (arg == QLatin1Literal("-debug")) { + } else if (arg == QLatin1String("-debug")) { debug = true; - } else if (arg == QLatin1Literal("-load")) { + } else if (arg == QLatin1String("-load")) { if (args.isEmpty()) { cout << "Must specify a file to load" << endl; usage(progname, 1); } filesToLoad << args.takeFirst(); - } else if (arg == QLatin1Literal("-terminate")) { + } else if (arg == QLatin1String("-terminate")) { terminate = true; } else { cout << "Unknown argument " << qPrintable(arg) << endl; |