Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qhelpdbreader.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5#include "qhelp_global.h"
6
7#include <QtCore/qfile.h>
8#include <QtCore/qmap.h>
9#include <QtCore/qvariant.h>
10#include <QtSql/qsqldatabase.h>
11#include <QtSql/qsqlerror.h>
12#include <QtSql/qsqlquery.h>
13
15
16using namespace Qt::StringLiterals;
17
18QHelpDBReader::QHelpDBReader(const QString &dbName)
19 : m_dbName(dbName)
20 , m_uniqueId(QHelpGlobal::uniquifyConnectionName("QHelpDBReader"_L1, this))
21{}
22
23QHelpDBReader::QHelpDBReader(const QString &dbName, const QString &uniqueId, QObject *parent)
24 : QObject(parent)
25 , m_dbName(dbName)
26 , m_uniqueId(uniqueId)
27{}
28
30{
31 if (m_initDone)
32 QSqlDatabase::removeDatabase(m_uniqueId);
33}
34
36{
37 if (m_initDone)
38 return true;
39
40 if (!QFile::exists(m_dbName))
41 return false;
42
43 if (!initDB()) {
44 QSqlDatabase::removeDatabase(m_uniqueId);
45 return false;
46 }
47
48 m_initDone = true;
49 m_query.reset(new QSqlQuery(QSqlDatabase::database(m_uniqueId)));
50 return true;
51}
52
53bool QHelpDBReader::initDB()
54{
55 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"_L1, m_uniqueId);
56 db.setConnectOptions("QSQLITE_OPEN_READONLY"_L1);
57 db.setDatabaseName(m_dbName);
58 if (!db.open()) {
59 /*: The placeholders are: %1 - The name of the database which cannot be opened
60 %2 - The unique id for the connection
61 %3 - The actual error string */
62 m_error = tr("Cannot open database \"%1\" \"%2\": %3").arg(m_dbName, m_uniqueId, db.lastError().text());
63 return false;
64 }
65 return true;
66}
67
69{
70 if (!m_namespace.isEmpty())
71 return m_namespace;
72 if (m_query) {
73 m_query->exec("SELECT Name FROM NamespaceTable"_L1);
74 if (m_query->next())
75 m_namespace = m_query->value(0).toString();
76 }
77 return m_namespace;
78}
79
81{
82 if (m_query) {
83 m_query->exec("SELECT Name FROM FolderTable WHERE Id=1"_L1);
84 if (m_query->next())
85 return m_query->value(0).toString();
86 }
87 return {};
88}
89
90QString QHelpDBReader::version() const
91{
92 const QString versionString = metaData("version"_L1).toString();
93 if (versionString.isEmpty())
94 return qtVersionHeuristic();
95 return versionString;
96}
97
98QString QHelpDBReader::qtVersionHeuristic() const
99{
100 const QString nameSpace = namespaceName();
101 if (!nameSpace.startsWith("org.qt-project."_L1))
102 return {};
103
104 // We take the namespace tail, starting from the last letter in namespace name.
105 // We drop any non digit characters.
106 const QChar dot(u'.');
107 QString tail;
108 for (int i = nameSpace.size(); i > 0; --i) {
109 const QChar c = nameSpace.at(i - 1);
110 if (c.isDigit() || c == dot)
111 tail.prepend(c);
112
113 if (c.isLetter())
114 break;
115 }
116
117 if (!tail.startsWith(dot) && tail.count(dot) == 1) {
118 // The org.qt-project.qtquickcontrols2.5120 case,
119 // tail = 2.5120 here. We need to cut "2." here.
120 const int dotIndex = tail.indexOf(dot);
121 if (dotIndex > 0)
122 tail = tail.mid(dotIndex);
123 }
124
125 // Drop beginning dots
126 while (tail.startsWith(dot))
127 tail = tail.mid(1);
128
129 // Drop ending dots
130 while (tail.endsWith(dot))
131 tail.chop(1);
132
133 if (tail.count(dot) == 0) {
134 if (tail.size() > 5)
135 return tail;
136
137 // When we have 3 digits, we split it like: ABC -> A.B.C
138 // When we have 4 digits, we split it like: ABCD -> A.BC.D
139 // When we have 5 digits, we split it like: ABCDE -> A.BC.DE
140 const int major = tail.left(1).toInt();
141 const int minor = tail.size() == 3
142 ? tail.mid(1, 1).toInt() : tail.mid(1, 2).toInt();
143 const int patch = tail.size() == 5
144 ? tail.right(2).toInt() : tail.right(1).toInt();
145
146 return QString::fromUtf8("%1.%2.%3").arg(major).arg(minor).arg(patch);
147 }
148 return tail;
149}
150
151static bool isAttributeUsed(QSqlQuery *query, const QString &tableName, int attributeId)
152{
153 query->prepare(QString::fromLatin1("SELECT FilterAttributeId "
154 "FROM %1 "
155 "WHERE FilterAttributeId = ? "
156 "LIMIT 1").arg(tableName));
157 query->bindValue(0, attributeId);
158 query->exec();
159 return query->next(); // if we got a result it means it was used
160}
161
162static int filterDataCount(QSqlQuery *query, const QString &tableName)
163{
164 query->exec(QString::fromLatin1("SELECT COUNT(*) FROM"
165 "(SELECT DISTINCT * FROM %1)").arg(tableName));
166 query->next();
167 return query->value(0).toInt();
168}
169
171{
172 IndexTable table;
173 if (!m_query)
174 return table;
175
176 QMap<int, QString> attributeIds;
177 m_query->exec("SELECT DISTINCT Id, Name FROM FilterAttributeTable ORDER BY Id"_L1);
178 while (m_query->next())
179 attributeIds.insert(m_query->value(0).toInt(), m_query->value(1).toString());
180
181 // Maybe some are unused and specified erroneously in the named filter only,
182 // like it was in case of qtlocation.qch <= qt 5.9
183 QList<int> usedAttributeIds;
184 for (auto it = attributeIds.cbegin(), end = attributeIds.cend(); it != end; ++it) {
185 const int attributeId = it.key();
186 if (isAttributeUsed(m_query.get(), "IndexFilterTable"_L1, attributeId)
187 || isAttributeUsed(m_query.get(), "ContentsFilterTable"_L1, attributeId)
188 || isAttributeUsed(m_query.get(), "FileFilterTable"_L1, attributeId)) {
189 usedAttributeIds.append(attributeId);
190 }
191 }
192
193 bool legacy = false;
194 m_query->exec("SELECT * FROM pragma_table_info('IndexTable') WHERE name='ContextName'"_L1);
195 if (m_query->next())
196 legacy = true;
197
198 const QString identifierColumnName = legacy ? "ContextName"_L1 : "Identifier"_L1;
199 const int usedAttributeCount = usedAttributeIds.size();
200
201 QMap<int, IndexItem> idToIndexItem;
202 m_query->exec(QString::fromLatin1("SELECT Name, %1, FileId, Anchor, Id "
203 "FROM IndexTable "
204 "ORDER BY Id").arg(identifierColumnName));
205 while (m_query->next()) {
206 IndexItem indexItem;
207 indexItem.name = m_query->value(0).toString();
208 indexItem.identifier = m_query->value(1).toString();
209 indexItem.fileId = m_query->value(2).toInt();
210 indexItem.anchor = m_query->value(3).toString();
211 const int indexId = m_query->value(4).toInt();
212
213 idToIndexItem.insert(indexId, indexItem);
214 }
215
216 QMap<int, FileItem> idToFileItem;
217 QMap<int, int> originalFileIdToNewFileId;
218
219 int filesCount = 0;
220 m_query->exec(
221 "SELECT "
222 "FileNameTable.FileId, "
223 "FileNameTable.Name, "
224 "FileNameTable.Title "
225 "FROM FileNameTable, FolderTable "
226 "WHERE FileNameTable.FolderId = FolderTable.Id "
227 "ORDER BY FileId"_L1);
228 while (m_query->next()) {
229 const int fileId = m_query->value(0).toInt();
230 FileItem fileItem;
231 fileItem.name = m_query->value(1).toString();
232 fileItem.title = m_query->value(2).toString();
233
234 idToFileItem.insert(fileId, fileItem);
235 originalFileIdToNewFileId.insert(fileId, filesCount);
236 ++filesCount;
237 }
238
239 QMap<int, ContentsItem> idToContentsItem;
240
241 m_query->exec("SELECT Data, Id FROM ContentsTable ORDER BY Id"_L1);
242 while (m_query->next()) {
243 ContentsItem contentsItem;
244 contentsItem.data = m_query->value(0).toByteArray();
245 const int contentsId = m_query->value(1).toInt();
246
247 idToContentsItem.insert(contentsId, contentsItem);
248 }
249
250 bool optimized = true;
251
252 if (usedAttributeCount) {
253 // May optimize only when all usedAttributes are attached to every
254 // index and file. It means the number of rows in the
255 // IndexTable multiplied by number of used attributes
256 // must equal the number of rows inside IndexFilterTable
257 // (yes, we have a combinatorial explosion of data in IndexFilterTable,
258 // which we want to optimize). The same with FileNameTable and
259 // FileFilterTable.
260
261 const bool mayOptimizeIndexTable = filterDataCount(m_query.get(), "IndexFilterTable"_L1)
262 == idToIndexItem.size() * usedAttributeCount;
263 const bool mayOptimizeFileTable = filterDataCount(m_query.get(), "FileFilterTable"_L1)
264 == idToFileItem.size() * usedAttributeCount;
265 const bool mayOptimizeContentsTable =
266 filterDataCount(m_query.get(), "ContentsFilterTable"_L1)
267 == idToContentsItem.size() * usedAttributeCount;
268 optimized = mayOptimizeIndexTable && mayOptimizeFileTable && mayOptimizeContentsTable;
269
270 if (!optimized) {
271 m_query->exec(
272 "SELECT "
273 "IndexFilterTable.IndexId, "
274 "FilterAttributeTable.Name "
275 "FROM "
276 "IndexFilterTable, "
277 "FilterAttributeTable "
278 "WHERE "
279 "IndexFilterTable.FilterAttributeId = FilterAttributeTable.Id"_L1);
280 while (m_query->next()) {
281 const int indexId = m_query->value(0).toInt();
282 auto it = idToIndexItem.find(indexId);
283 if (it != idToIndexItem.end())
284 it.value().filterAttributes.append(m_query->value(1).toString());
285 }
286
287 m_query->exec(
288 "SELECT "
289 "FileFilterTable.FileId, "
290 "FilterAttributeTable.Name "
291 "FROM "
292 "FileFilterTable, "
293 "FilterAttributeTable "
294 "WHERE "
295 "FileFilterTable.FilterAttributeId = FilterAttributeTable.Id"_L1);
296 while (m_query->next()) {
297 const int fileId = m_query->value(0).toInt();
298 auto it = idToFileItem.find(fileId);
299 if (it != idToFileItem.end())
300 it.value().filterAttributes.append(m_query->value(1).toString());
301 }
302
303 m_query->exec(
304 "SELECT "
305 "ContentsFilterTable.ContentsId, "
306 "FilterAttributeTable.Name "
307 "FROM "
308 "ContentsFilterTable, "
309 "FilterAttributeTable "
310 "WHERE "
311 "ContentsFilterTable.FilterAttributeId = FilterAttributeTable.Id"_L1);
312 while (m_query->next()) {
313 const int contentsId = m_query->value(0).toInt();
314 auto it = idToContentsItem.find(contentsId);
315 if (it != idToContentsItem.end())
316 it.value().filterAttributes.append(m_query->value(1).toString());
317 }
318 }
319 }
320
321 // reindex fileId references
322 for (auto it = idToIndexItem.cbegin(), end = idToIndexItem.cend(); it != end; ++it) {
323 IndexItem item = it.value();
324 item.fileId = originalFileIdToNewFileId.value(item.fileId);
325 table.indexItems.append(item);
326 }
327
328 table.fileItems = idToFileItem.values();
329 table.contentsItems = idToContentsItem.values();
330
331 if (optimized) {
332 for (int attributeId : usedAttributeIds)
333 table.usedFilterAttributes.append(attributeIds.value(attributeId));
334 }
335 return table;
336}
337
339{
340 QList<QStringList> result;
341 if (m_query) {
342 m_query->exec(
343 "SELECT "
344 "FileAttributeSetTable.Id, "
345 "FilterAttributeTable.Name "
346 "FROM "
347 "FileAttributeSetTable, "
348 "FilterAttributeTable "
349 "WHERE FileAttributeSetTable.FilterAttributeId = FilterAttributeTable.Id "
350 "ORDER BY FileAttributeSetTable.Id"_L1);
351 int oldId = -1;
352 while (m_query->next()) {
353 const int id = m_query->value(0).toInt();
354 if (id != oldId) {
355 result.append(QStringList());
356 oldId = id;
357 }
358 result.last().append(m_query->value(1).toString());
359 }
360 }
361 return result;
362}
363
364QByteArray QHelpDBReader::fileData(const QString &virtualFolder,
365 const QString &filePath) const
366{
367 QByteArray ba;
368 if (virtualFolder.isEmpty() || filePath.isEmpty() || !m_query)
369 return ba;
370
371 namespaceName();
372 m_query->prepare(
373 "SELECT "
374 "FileDataTable.Data "
375 "FROM "
376 "FileDataTable, "
377 "FileNameTable, "
378 "FolderTable, "
379 "NamespaceTable "
380 "WHERE FileDataTable.Id = FileNameTable.FileId "
381 "AND (FileNameTable.Name = ? OR FileNameTable.Name = ?) "
382 "AND FileNameTable.FolderId = FolderTable.Id "
383 "AND FolderTable.Name = ? "
384 "AND FolderTable.NamespaceId = NamespaceTable.Id "
385 "AND NamespaceTable.Name = ?"_L1);
386 m_query->bindValue(0, filePath);
387 m_query->bindValue(1, QString("./"_L1 + filePath));
388 m_query->bindValue(2, virtualFolder);
389 m_query->bindValue(3, m_namespace);
390 m_query->exec();
391 if (m_query->next() && m_query->isValid())
392 ba = qUncompress(m_query->value(0).toByteArray());
393 return ba;
394}
395
397{
398 QStringList lst;
399 if (m_query) {
400 m_query->exec("SELECT Name FROM FilterNameTable"_L1);
401 while (m_query->next())
402 lst.append(m_query->value(0).toString());
403 }
404 return lst;
405}
406
407QStringList QHelpDBReader::filterAttributes(const QString &filterName) const
408{
409 QStringList lst;
410 if (m_query) {
411 if (filterName.isEmpty()) {
412 m_query->prepare("SELECT Name FROM FilterAttributeTable"_L1);
413 } else {
414 m_query->prepare(
415 "SELECT "
416 "FilterAttributeTable.Name "
417 "FROM "
418 "FilterAttributeTable, "
419 "FilterTable, "
420 "FilterNameTable "
421 "WHERE FilterNameTable.Name = ? "
422 "AND FilterNameTable.Id = FilterTable.NameId "
423 "AND FilterTable.FilterAttributeId = FilterAttributeTable.Id"_L1);
424 m_query->bindValue(0, filterName);
425 }
426 m_query->exec();
427 while (m_query->next())
428 lst.append(m_query->value(0).toString());
429 }
430 return lst;
431}
432
433QMultiMap<QString, QByteArray> QHelpDBReader::filesData(const QStringList &filterAttributes,
434 const QString &extensionFilter) const
435{
436 if (!m_query)
437 return {};
438
439 QString query;
440 QString extension;
441 if (!extensionFilter.isEmpty())
442 extension = "AND FileNameTable.Name LIKE \'%.%1\'"_L1.arg(extensionFilter);
443
444 if (filterAttributes.isEmpty()) {
445 query =
446 "SELECT "
447 "FileNameTable.Name, "
448 "FileDataTable.Data "
449 "FROM "
450 "FolderTable, "
451 "FileNameTable, "
452 "FileDataTable "
453 "WHERE FileDataTable.Id = FileNameTable.FileId "
454 "AND FileNameTable.FolderId = FolderTable.Id %1"_L1.arg(extension);
455 } else {
456 for (int i = 0; i < filterAttributes.size(); ++i) {
457 if (i > 0)
458 query.append(" INTERSECT "_L1);
459 query.append(
460 "SELECT "
461 "FileNameTable.Name, "
462 "FileDataTable.Data "
463 "FROM "
464 "FolderTable, "
465 "FileNameTable, "
466 "FileDataTable, "
467 "FileFilterTable, "
468 "FilterAttributeTable "
469 "WHERE FileDataTable.Id = FileNameTable.FileId "
470 "AND FileNameTable.FolderId = FolderTable.Id "
471 "AND FileNameTable.FileId = FileFilterTable.FileId "
472 "AND FileFilterTable.FilterAttributeId = FilterAttributeTable.Id "
473 "AND FilterAttributeTable.Name = \'%1\' %2"_L1
474 .arg(quote(filterAttributes.at(i)), extension));
475 }
476 }
477 m_query->exec(query);
478 QMultiMap<QString, QByteArray> result;
479 while (m_query->next())
480 result.insert(m_query->value(0).toString(), qUncompress(m_query->value(1).toByteArray()));
481 return result;
482}
483
484QVariant QHelpDBReader::metaData(const QString &name) const
485{
486 if (!m_query)
487 return {};
488
489 m_query->prepare("SELECT COUNT(Value), Value FROM MetaDataTable WHERE Name=?"_L1);
490 m_query->bindValue(0, name);
491 if (m_query->exec() && m_query->next() && m_query->value(0).toInt() == 1)
492 return m_query->value(1);
493 return {};
494}
495
496QString QHelpDBReader::quote(const QString &string) const
497{
498 QString s = string;
499 s.replace(u'\'', "\'\'"_L1);
500 return s;
501}
502
503QT_END_NAMESPACE
QString namespaceName() const
IndexTable indexTable() const
QString virtualFolder() const
QString version() const
QList< QStringList > filterAttributeSets() const
QStringList customFilters() const
Combined button and popup list for selecting options.
static bool isAttributeUsed(QSqlQuery *query, const QString &tableName, int attributeId)
static int filterDataCount(QSqlQuery *query, const QString &tableName)