Merge branch 'master' of git://scm.dev.nokia.troll.no/qt/qtdeclarative-staging
[profile/ivi/qtdeclarative.git] / src / declarative / qml / qdeclarativesqldatabase.cpp
1 /****************************************************************************
2 **
3 ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
4 ** All rights reserved.
5 ** Contact: Nokia Corporation (qt-info@nokia.com)
6 **
7 ** This file is part of the QtDeclarative module of the Qt Toolkit.
8 **
9 ** $QT_BEGIN_LICENSE:LGPL$
10 ** GNU Lesser General Public License Usage
11 ** This file may be used under the terms of the GNU Lesser General Public
12 ** License version 2.1 as published by the Free Software Foundation and
13 ** appearing in the file LICENSE.LGPL included in the packaging of this
14 ** file. Please review the following information to ensure the GNU Lesser
15 ** General Public License version 2.1 requirements will be met:
16 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
17 **
18 ** In addition, as a special exception, Nokia gives you certain additional
19 ** rights. These rights are described in the Nokia Qt LGPL Exception
20 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
21 **
22 ** GNU General Public License Usage
23 ** Alternatively, this file may be used under the terms of the GNU General
24 ** Public License version 3.0 as published by the Free Software Foundation
25 ** and appearing in the file LICENSE.GPL included in the packaging of this
26 ** file. Please review the following information to ensure the GNU General
27 ** Public License version 3.0 requirements will be met:
28 ** http://www.gnu.org/copyleft/gpl.html.
29 **
30 ** Other Usage
31 ** Alternatively, this file may be used in accordance with the terms and
32 ** conditions contained in a signed written agreement between you and Nokia.
33 **
34 **
35 **
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include "private/qdeclarativesqldatabase_p.h"
43
44 #include "qdeclarativeengine.h"
45 #include "private/qdeclarativeengine_p.h"
46 #include "private/qdeclarativerefcount_p.h"
47 #include "private/qdeclarativeengine_p.h"
48
49 #include <QtCore/qobject.h>
50 #include <QtScript/qscriptvalue.h>
51 #include <QtScript/qscriptvalueiterator.h>
52 #include <QtScript/qscriptcontext.h>
53 #include <QtScript/qscriptengine.h>
54 #include <QtScript/qscriptclasspropertyiterator.h>
55 #include <QtSql/qsqldatabase.h>
56 #include <QtSql/qsqlquery.h>
57 #include <QtSql/qsqlerror.h>
58 #include <QtSql/qsqlrecord.h>
59 #include <QtCore/qstack.h>
60 #include <QtCore/qcryptographichash.h>
61 #include <QtCore/qsettings.h>
62 #include <QtCore/qdir.h>
63 #include <QtCore/qdebug.h>
64
65 Q_DECLARE_METATYPE(QSqlDatabase)
66 Q_DECLARE_METATYPE(QSqlQuery)
67
68 QT_BEGIN_NAMESPACE
69
70 class QDeclarativeSqlQueryScriptClass: public QScriptClass {
71 public:
72     QDeclarativeSqlQueryScriptClass(QScriptEngine *engine) : QScriptClass(engine)
73     {
74         str_length = engine->toStringHandle(QLatin1String("length"));
75         str_forwardOnly = engine->toStringHandle(QLatin1String("forwardOnly")); // not in HTML5 (an optimization)
76     }
77
78     QueryFlags queryProperty(const QScriptValue &,
79                              const QScriptString &name,
80                              QueryFlags flags, uint *)
81     {
82         if (flags & HandlesReadAccess) {
83             if (name == str_length) {
84                 return HandlesReadAccess;
85             } else if (name == str_forwardOnly) {
86                 return flags;
87             }
88         }
89         if (flags & HandlesWriteAccess)
90             if (name == str_forwardOnly)
91                 return flags;
92         return 0;
93     }
94
95     QScriptValue property(const QScriptValue &object,
96                           const QScriptString &name, uint)
97     {
98         QSqlQuery query = qscriptvalue_cast<QSqlQuery>(object.data());
99         if (name == str_length) {
100             int s = query.size();
101             if (s<0) {
102                 // Inefficient.
103                 if (query.last()) {
104                     return query.at()+1;
105                 } else {
106                     return 0;
107                 }
108             } else {
109                 return s;
110             }
111         } else if (name == str_forwardOnly) {
112             return query.isForwardOnly();
113         }
114         return engine()->undefinedValue();
115     }
116
117     void setProperty(QScriptValue &object,
118                       const QScriptString &name, uint, const QScriptValue & value)
119     {
120         if (name == str_forwardOnly) {
121             QSqlQuery query = qscriptvalue_cast<QSqlQuery>(object.data());
122             query.setForwardOnly(value.toBool());
123         }
124     }
125
126     QScriptValue::PropertyFlags propertyFlags(const QScriptValue &/*object*/, const QScriptString &name, uint /*id*/)
127     {
128         if (name == str_length) {
129             return QScriptValue::Undeletable
130                 | QScriptValue::SkipInEnumeration;
131         }
132         return QScriptValue::Undeletable;
133     }
134
135 private:
136     QScriptString str_length;
137     QScriptString str_forwardOnly;
138 };
139
140 // If the spec changes to allow iteration, check git history...
141 // class QDeclarativeSqlQueryScriptClassPropertyIterator : public QScriptClassPropertyIterator
142
143
144
145 enum SqlException {
146     UNKNOWN_ERR,
147     DATABASE_ERR,
148     VERSION_ERR,
149     TOO_LARGE_ERR,
150     QUOTA_ERR,
151     SYNTAX_ERR,
152     CONSTRAINT_ERR,
153     TIMEOUT_ERR
154 };
155
156 static const char* sqlerror[] = {
157     "UNKNOWN_ERR",
158     "DATABASE_ERR",
159     "VERSION_ERR",
160     "TOO_LARGE_ERR",
161     "QUOTA_ERR",
162     "SYNTAX_ERR",
163     "CONSTRAINT_ERR",
164     "TIMEOUT_ERR",
165     0
166 };
167
168 #define THROW_SQL(error, desc) \
169 { \
170     QScriptValue errorValue = context->throwError(desc); \
171     errorValue.setProperty(QLatin1String("code"), error); \
172     return errorValue; \
173 }
174
175 static QString qmlsqldatabase_databasesPath(QScriptEngine *engine)
176 {
177     QDeclarativeScriptEngine *qmlengine = static_cast<QDeclarativeScriptEngine*>(engine);
178     return qmlengine->offlineStoragePath
179             + QDir::separator() + QLatin1String("Databases");
180 }
181
182 static void qmlsqldatabase_initDatabasesPath(QScriptEngine *engine)
183 {
184     QDir().mkpath(qmlsqldatabase_databasesPath(engine));
185 }
186
187 static QString qmlsqldatabase_databaseFile(const QString& connectionName, QScriptEngine *engine)
188 {
189     return qmlsqldatabase_databasesPath(engine) + QDir::separator()
190             + connectionName;
191 }
192
193
194 static QScriptValue qmlsqldatabase_item(QScriptContext *context, QScriptEngine *engine)
195 {
196     QSqlQuery query = qscriptvalue_cast<QSqlQuery>(context->thisObject().data());
197     int i = context->argument(0).toNumber();
198     if (query.at() == i || query.seek(i)) { // Qt 4.6 doesn't optimize seek(at())
199         QSqlRecord r = query.record();
200         QScriptValue row = engine->newObject();
201         for (int j=0; j<r.count(); ++j) {
202             row.setProperty(r.fieldName(j), QScriptValue(engine,r.value(j).toString()));
203         }
204         return row;
205     }
206     return engine->undefinedValue();
207 }
208
209 static QScriptValue qmlsqldatabase_executeSql_outsidetransaction(QScriptContext *context, QScriptEngine * /*engine*/)
210 {
211     THROW_SQL(DATABASE_ERR,QDeclarativeEngine::tr("executeSql called outside transaction()"));
212 }
213
214 static QScriptValue qmlsqldatabase_executeSql(QScriptContext *context, QScriptEngine *engine)
215 {
216     QSqlDatabase db = qscriptvalue_cast<QSqlDatabase>(context->thisObject());
217     QString sql = context->argument(0).toString();
218     QSqlQuery query(db);
219     bool err = false;
220
221     QScriptValue result;
222
223     if (query.prepare(sql)) {
224         if (context->argumentCount() > 1) {
225             QScriptValue values = context->argument(1);
226             if (values.isObject()) {
227                 if (values.isArray()) {
228                     int size = values.property(QLatin1String("length")).toInt32();
229                     for (int i = 0; i < size; ++i)
230                         query.bindValue(i, values.property(i).toVariant());
231                 } else {
232                     for (QScriptValueIterator it(values); it.hasNext();) {
233                         it.next();
234                         query.bindValue(it.name(),it.value().toVariant());
235                     }
236                 }
237             } else {
238                 query.bindValue(0,values.toVariant());
239             }
240         }
241         if (query.exec()) {
242             result = engine->newObject();
243             QDeclarativeScriptEngine *qmlengine = static_cast<QDeclarativeScriptEngine*>(engine);
244             if (!qmlengine->sqlQueryClass)
245                 qmlengine->sqlQueryClass = new QDeclarativeSqlQueryScriptClass(engine);
246             QScriptValue rows = engine->newObject(qmlengine->sqlQueryClass);
247             rows.setData(engine->newVariant(QVariant::fromValue(query)));
248             rows.setProperty(QLatin1String("item"), engine->newFunction(qmlsqldatabase_item,1), QScriptValue::SkipInEnumeration);
249             result.setProperty(QLatin1String("rows"),rows);
250             result.setProperty(QLatin1String("rowsAffected"),query.numRowsAffected());
251             result.setProperty(QLatin1String("insertId"),query.lastInsertId().toString());
252         } else {
253             err = true;
254         }
255     } else {
256         err = true;
257     }
258     if (err)
259         THROW_SQL(DATABASE_ERR,query.lastError().text());
260     return result;
261 }
262
263 static QScriptValue qmlsqldatabase_executeSql_readonly(QScriptContext *context, QScriptEngine *engine)
264 {
265     QString sql = context->argument(0).toString();
266     if (sql.startsWith(QLatin1String("SELECT"),Qt::CaseInsensitive)) {
267         return qmlsqldatabase_executeSql(context,engine);
268     } else {
269         THROW_SQL(SYNTAX_ERR,QDeclarativeEngine::tr("Read-only Transaction"))
270     }
271 }
272
273 static QScriptValue qmlsqldatabase_change_version(QScriptContext *context, QScriptEngine *engine)
274 {
275     if (context->argumentCount() < 2)
276         return engine->undefinedValue();
277
278     QSqlDatabase db = qscriptvalue_cast<QSqlDatabase>(context->thisObject());
279     QString from_version = context->argument(0).toString();
280     QString to_version = context->argument(1).toString();
281     QScriptValue callback = context->argument(2);
282
283     QScriptValue instance = engine->newObject();
284     instance.setProperty(QLatin1String("executeSql"), engine->newFunction(qmlsqldatabase_executeSql,1));
285     QScriptValue tx = engine->newVariant(instance,QVariant::fromValue(db));
286
287     QString foundvers = context->thisObject().property(QLatin1String("version")).toString();
288     if (from_version!=foundvers) {
289         THROW_SQL(VERSION_ERR,QDeclarativeEngine::tr("Version mismatch: expected %1, found %2").arg(from_version).arg(foundvers));
290         return engine->undefinedValue();
291     }
292
293     bool ok = true;
294     if (callback.isFunction()) {
295         ok = false;
296         db.transaction();
297         callback.call(QScriptValue(), QScriptValueList() << tx);
298         if (engine->hasUncaughtException()) {
299             db.rollback();
300         } else {
301             if (!db.commit()) {
302                 db.rollback();
303                 THROW_SQL(UNKNOWN_ERR,QDeclarativeEngine::tr("SQL transaction failed"));
304             } else {
305                 ok = true;
306             }
307         }
308     }
309
310     if (ok) {
311         context->thisObject().setProperty(QLatin1String("version"), to_version, QScriptValue::ReadOnly);
312 #ifndef QT_NO_SETTINGS
313         QSettings ini(qmlsqldatabase_databaseFile(db.connectionName(),engine) + QLatin1String(".ini"), QSettings::IniFormat);
314         ini.setValue(QLatin1String("Version"), to_version);
315 #endif
316     }
317
318     return engine->undefinedValue();
319 }
320
321 static QScriptValue qmlsqldatabase_transaction_shared(QScriptContext *context, QScriptEngine *engine, bool readOnly)
322 {
323     QSqlDatabase db = qscriptvalue_cast<QSqlDatabase>(context->thisObject());
324     QScriptValue callback = context->argument(0);
325     if (!callback.isFunction())
326         THROW_SQL(UNKNOWN_ERR,QDeclarativeEngine::tr("transaction: missing callback"));
327
328     QScriptValue instance = engine->newObject();
329     instance.setProperty(QLatin1String("executeSql"),
330         engine->newFunction(readOnly ? qmlsqldatabase_executeSql_readonly : qmlsqldatabase_executeSql,1));
331     QScriptValue tx = engine->newVariant(instance,QVariant::fromValue(db));
332
333     db.transaction();
334     callback.call(QScriptValue(), QScriptValueList() << tx);
335     instance.setProperty(QLatin1String("executeSql"),
336         engine->newFunction(qmlsqldatabase_executeSql_outsidetransaction));
337     if (engine->hasUncaughtException()) {
338         db.rollback();
339     } else {
340         if (!db.commit())
341             db.rollback();
342     }
343     return engine->undefinedValue();
344 }
345
346 static QScriptValue qmlsqldatabase_transaction(QScriptContext *context, QScriptEngine *engine)
347 {
348     return qmlsqldatabase_transaction_shared(context,engine,false);
349 }
350 static QScriptValue qmlsqldatabase_read_transaction(QScriptContext *context, QScriptEngine *engine)
351 {
352     return qmlsqldatabase_transaction_shared(context,engine,true);
353 }
354
355 /*
356     Currently documented in doc/src/declarative/globalobject.qdoc
357 */
358 static QScriptValue qmlsqldatabase_open_sync(QScriptContext *context, QScriptEngine *engine)
359 {
360 #ifndef QT_NO_SETTINGS
361     qmlsqldatabase_initDatabasesPath(engine);
362
363     QSqlDatabase database;
364
365     QString dbname = context->argument(0).toString();
366     QString dbversion = context->argument(1).toString();
367     QString dbdescription = context->argument(2).toString();
368     int dbestimatedsize = context->argument(3).toNumber();
369     QScriptValue dbcreationCallback = context->argument(4);
370
371     QCryptographicHash md5(QCryptographicHash::Md5);
372     md5.addData(dbname.toUtf8());
373     QString dbid(QLatin1String(md5.result().toHex()));
374
375     QString basename = qmlsqldatabase_databaseFile(dbid, engine);
376     bool created = false;
377     QString version = dbversion;
378
379     {
380         QSettings ini(basename+QLatin1String(".ini"),QSettings::IniFormat);
381
382         if (QSqlDatabase::connectionNames().contains(dbid)) {
383             database = QSqlDatabase::database(dbid);
384             version = ini.value(QLatin1String("Version")).toString();
385             if (version != dbversion && !dbversion.isEmpty() && !version.isEmpty())
386                 THROW_SQL(VERSION_ERR,QDeclarativeEngine::tr("SQL: database version mismatch"));
387         } else {
388             created = !QFile::exists(basename+QLatin1String(".sqlite"));
389             database = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), dbid);
390             if (created) {
391                 ini.setValue(QLatin1String("Name"), dbname);
392                 if (dbcreationCallback.isFunction())
393                     version = QString();
394                 ini.setValue(QLatin1String("Version"), version);
395                 ini.setValue(QLatin1String("Description"), dbdescription);
396                 ini.setValue(QLatin1String("EstimatedSize"), dbestimatedsize);
397                 ini.setValue(QLatin1String("Driver"), QLatin1String("QSQLITE"));
398             } else {
399                 if (!dbversion.isEmpty() && ini.value(QLatin1String("Version")) != dbversion) {
400                     // Incompatible
401                     THROW_SQL(VERSION_ERR,QDeclarativeEngine::tr("SQL: database version mismatch"));
402                 }
403                 version = ini.value(QLatin1String("Version")).toString();
404             }
405             database.setDatabaseName(basename+QLatin1String(".sqlite"));
406         }
407         if (!database.isOpen())
408             database.open();
409     }
410
411     QScriptValue instance = engine->newObject();
412     instance.setProperty(QLatin1String("transaction"), engine->newFunction(qmlsqldatabase_transaction,1));
413     instance.setProperty(QLatin1String("readTransaction"), engine->newFunction(qmlsqldatabase_read_transaction,1));
414     instance.setProperty(QLatin1String("version"), version, QScriptValue::ReadOnly);
415     instance.setProperty(QLatin1String("changeVersion"), engine->newFunction(qmlsqldatabase_change_version,3));
416
417     QScriptValue result = engine->newVariant(instance,QVariant::fromValue(database));
418
419     if (created && dbcreationCallback.isFunction()) {
420         dbcreationCallback.call(QScriptValue(), QScriptValueList() << result);
421     }
422
423     return result;
424 #else
425     return engine->undefinedValue();
426 #endif // QT_NO_SETTINGS
427 }
428
429 void qt_add_qmlsqldatabase(QScriptEngine *engine)
430 {
431     QScriptValue openDatabase = engine->newFunction(qmlsqldatabase_open_sync, 4);
432     engine->globalObject().setProperty(QLatin1String("openDatabaseSync"), openDatabase);
433
434     QScriptValue sqlExceptionPrototype = engine->newObject();
435     for (int i=0; sqlerror[i]; ++i)
436         sqlExceptionPrototype.setProperty(QLatin1String(sqlerror[i]),
437             i,QScriptValue::ReadOnly | QScriptValue::Undeletable | QScriptValue::SkipInEnumeration);
438
439     engine->globalObject().setProperty(QLatin1String("SQLException"), sqlExceptionPrototype);
440 }
441
442 /*
443 HTML5 "spec" says "rs.rows[n]", but WebKit only impelments "rs.rows.item(n)". We do both (and property iterator).
444 We add a "forwardOnly" property that stops Qt caching results (code promises to only go forward
445 through the data.
446 */
447
448 QT_END_NAMESPACE