QGIS API Documentation 3.99.0-Master (2fe06baccd8)
Loading...
Searching...
No Matches
qgsqueryresultwidget.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsqueryresultwidget.cpp - QgsQueryResultWidget
3
4 ---------------------
5 begin : 14.1.2021
6 copyright : (C) 2021 by Alessandro Pasotti
7 email : elpaso at itopen dot it
8 ***************************************************************************
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 2 of the License, or *
13 * (at your option) any later version. *
14 * *
15 ***************************************************************************/
17
19#include "qgsapplication.h"
20#include "qgscodeeditorsql.h"
21#include "qgscodeeditorwidget.h"
23#include "qgsexpressionutils.h"
24#include "qgsfileutils.h"
25#include "qgsgui.h"
26#include "qgshelp.h"
27#include "qgshistoryentry.h"
29#include "qgshistorywidget.h"
30#include "qgsmessagelog.h"
31#include "qgsnewnamedialog.h"
32#include "qgsproject.h"
33#include "qgsprovidermetadata.h"
34#include "qgsproviderregistry.h"
35#include "qgsquerybuilder.h"
37#include "qgsvectorlayer.h"
38
39#include <QClipboard>
40#include <QDialogButtonBox>
41#include <QFileDialog>
42#include <QInputDialog>
43#include <QMessageBox>
44#include <QShortcut>
45
46#include "moc_qgsqueryresultwidget.cpp"
47
49const QgsSettingsEntryString *QgsQueryResultWidget::settingLastSourceFolder = new QgsSettingsEntryString( QStringLiteral( "last-source-folder" ), sTreeSqlQueries, QString(), QStringLiteral( "Last used folder for SQL source files" ) );
52
53
54//
55// QgsQueryResultPanelWidget
56//
57
58QgsQueryResultPanelWidget::QgsQueryResultPanelWidget( QWidget *parent, QgsAbstractDatabaseProviderConnection *connection )
59 : QgsPanelWidget( parent )
60{
61 setupUi( this );
62
63 // Unsure :/
64 // mSqlEditor->setLineNumbersVisible( true );
65
66 splitter->setCollapsible( 0, false );
67 splitter->setCollapsible( 1, false );
68 QgsSettings settings;
69 splitter->restoreState( settings.value( QStringLiteral( "Windows/QueryResult/SplitState" ) ).toByteArray() );
70
71 connect( splitter, &QSplitter::splitterMoved, this, [this] {
72 QgsSettings settings;
73 settings.setValue( QStringLiteral( "Windows/QueryResult/SplitState" ), splitter->saveState() );
74 } );
75
76 // explicitly needed for some reason (Qt 5.15)
77 mainLayout->setSpacing( 6 );
78 progressLayout->setSpacing( 6 );
79
80 mResultsContainer->hide();
81 mQueryResultsTableView->hide();
82 mQueryResultsTableView->setItemDelegate( new QgsQueryResultItemDelegate( mQueryResultsTableView ) );
83 mQueryResultsTableView->setContextMenuPolicy( Qt::CustomContextMenu );
84 connect( mQueryResultsTableView, &QTableView::customContextMenuRequested, this, &QgsQueryResultPanelWidget::showCellContextMenu );
85
86 mProgressBar->hide();
87
88 mSqlEditor = new QgsCodeEditorSQL();
89 mCodeEditorWidget = new QgsCodeEditorWidget( mSqlEditor, mMessageBar );
90 QVBoxLayout *vl = new QVBoxLayout();
91 vl->setContentsMargins( 0, 0, 0, 0 );
92 vl->addWidget( mCodeEditorWidget );
93 mSqlEditorContainer->setLayout( vl );
94
95 connect( mExecuteButton, &QPushButton::pressed, this, &QgsQueryResultPanelWidget::executeQuery );
96
97 connect( mLoadLayerPushButton, &QPushButton::pressed, this, [this] {
98 if ( mConnection )
99 {
100 const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions options = sqlVectorLayerOptions();
101
102 try
103 {
104 QString message;
105 const bool res = mConnection->validateSqlVectorLayer( options, message );
106 if ( !res )
107 {
108 mMessageBar->pushCritical( QString(), message );
109 }
110 else
111 {
112 emit createSqlVectorLayer( mConnection->providerKey(), mConnection->uri(), options );
113 }
114 }
116 {
117 mMessageBar->pushCritical( tr( "Error validating query" ), e.what() );
118 }
119 }
120 } );
121 connect( mSqlEditor, &QgsCodeEditorSQL::textChanged, this, &QgsQueryResultPanelWidget::updateButtons );
122
123 connect( mSqlEditor, &QgsCodeEditorSQL::selectionChanged, this, [this] {
124 mExecuteButton->setText( mSqlEditor->selectedText().isEmpty() ? tr( "Execute" ) : tr( "Execute Selection" ) );
125 } );
126 connect( mFilterToolButton, &QToolButton::pressed, this, [this] {
127 if ( mConnection )
128 {
129 try
130 {
131 std::unique_ptr<QgsVectorLayer> vlayer { mConnection->createSqlVectorLayer( sqlVectorLayerOptions() ) };
132 QgsQueryBuilder builder { vlayer.get() };
133 if ( builder.exec() == QDialog::Accepted )
134 {
135 mFilterLineEdit->setText( builder.sql() );
136 }
137 }
138 catch ( const QgsProviderConnectionException &ex )
139 {
140 mMessageBar->pushCritical( tr( "Error opening filter dialog" ), tr( "There was an error while preparing SQL filter dialog: %1." ).arg( ex.what() ) );
141 }
142 }
143 } );
144
145
146 mStatusLabel->hide();
147 mSqlErrorText->hide();
148
149 mLoadAsNewLayerGroupBox->setCollapsed( true );
150
151 connect( mLoadAsNewLayerGroupBox, &QgsCollapsibleGroupBox::collapsedStateChanged, this, [this, connection]( bool collapsed ) {
152 if ( !collapsed )
153 {
154 // Configure the load layer interface
155 const bool showPkConfig { connection && connection->sqlLayerDefinitionCapabilities().testFlag( Qgis::SqlLayerDefinitionCapability::PrimaryKeys ) };
156 mPkColumnsCheckBox->setVisible( showPkConfig );
157 mPkColumnsComboBox->setVisible( showPkConfig );
158
159 const bool showGeometryColumnConfig { connection && connection->sqlLayerDefinitionCapabilities().testFlag( Qgis::SqlLayerDefinitionCapability::GeometryColumn ) };
160 mGeometryColumnCheckBox->setVisible( showGeometryColumnConfig );
161 mGeometryColumnComboBox->setVisible( showGeometryColumnConfig );
162
163 const bool showFilterConfig { connection && connection->sqlLayerDefinitionCapabilities().testFlag( Qgis::SqlLayerDefinitionCapability::SubsetStringFilter ) };
164 mFilterLabel->setVisible( showFilterConfig );
165 mFilterToolButton->setVisible( showFilterConfig );
166 mFilterLineEdit->setVisible( showFilterConfig );
167
168 const bool showDisableSelectAtId { connection && connection->sqlLayerDefinitionCapabilities().testFlag( Qgis::SqlLayerDefinitionCapability::UnstableFeatureIds ) };
169 mAvoidSelectingAsFeatureIdCheckBox->setVisible( showDisableSelectAtId );
170 }
171 } );
172
173 QShortcut *copySelection = new QShortcut( QKeySequence::Copy, mQueryResultsTableView );
174 connect( copySelection, &QShortcut::activated, this, &QgsQueryResultPanelWidget::copySelection );
175
176 setConnection( connection );
177}
178
179QgsQueryResultPanelWidget::~QgsQueryResultPanelWidget()
180{
181 cancelApiFetcher();
182 cancelRunningQuery();
183}
184
185QgsCodeEditorSQL *QgsQueryResultPanelWidget::sqlEditor()
186{
187 return mSqlEditor;
188}
189
190QgsCodeEditorWidget *QgsQueryResultPanelWidget::codeEditorWidget()
191{
192 return mCodeEditorWidget;
193}
194
195void QgsQueryResultPanelWidget::setSqlVectorLayerOptions( const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options )
196{
197 mSqlVectorLayerOptions = options;
198 if ( !options.sql.isEmpty() )
199 {
200 setQuery( options.sql );
201 }
202 mAvoidSelectingAsFeatureIdCheckBox->setChecked( options.disableSelectAtId );
203 mPkColumnsCheckBox->setChecked( !options.primaryKeyColumns.isEmpty() );
204 mPkColumnsComboBox->setCheckedItems( {} );
205 if ( !options.primaryKeyColumns.isEmpty() )
206 {
207 mPkColumnsComboBox->setCheckedItems( options.primaryKeyColumns );
208 }
209 mGeometryColumnCheckBox->setChecked( !options.geometryColumn.isEmpty() );
210 mGeometryColumnComboBox->clear();
211 if ( !options.geometryColumn.isEmpty() )
212 {
213 mGeometryColumnComboBox->setCurrentText( options.geometryColumn );
214 }
215 mFilterLineEdit->setText( options.filter );
216 mLayerNameLineEdit->setText( options.layerName );
217}
218
219void QgsQueryResultPanelWidget::setWidgetMode( QgsQueryResultWidget::QueryWidgetMode widgetMode )
220{
221 mQueryWidgetMode = widgetMode;
222 switch ( widgetMode )
223 {
224 case QgsQueryResultWidget::QueryWidgetMode::SqlQueryMode:
225 mLoadAsNewLayerGroupBox->setTitle( tr( "Load as New Layer" ) );
226 mLoadLayerPushButton->setText( tr( "Load Layer" ) );
227 mLoadAsNewLayerGroupBox->setCollapsed( true );
228 break;
229 case QgsQueryResultWidget::QueryWidgetMode::QueryLayerUpdateMode:
230 mLoadAsNewLayerGroupBox->setTitle( tr( "Update Query Layer" ) );
231 mLoadLayerPushButton->setText( tr( "Update Layer" ) );
232 mLoadAsNewLayerGroupBox->setCollapsed( false );
233 break;
234 }
235}
236
237void QgsQueryResultPanelWidget::executeQuery()
238{
239 mQueryResultsTableView->hide();
240 mSqlErrorText->hide();
241 mResultsContainer->hide();
242 mFirstRowFetched = false;
243
244 cancelRunningQuery();
245 if ( mConnection )
246 {
247 const QString sql { mSqlEditor->selectedText().isEmpty() ? mSqlEditor->text() : mSqlEditor->selectedText() };
248
249 bool ok = false;
250 mCurrentHistoryEntryId = QgsGui::historyProviderRegistry()->addEntry( QStringLiteral( "dbquery" ), QVariantMap {
251 { QStringLiteral( "query" ), sql },
252 { QStringLiteral( "provider" ), mConnection->providerKey() },
253 { QStringLiteral( "connection" ), mConnection->uri() },
254 },
255 ok );
256
257 mWasCanceled = false;
258 mFeedback = std::make_unique<QgsFeedback>();
259 mStopButton->setEnabled( true );
260 mStatusLabel->show();
261 mStatusLabel->setText( tr( "Executing query…" ) );
262 mProgressBar->show();
263 mProgressBar->setRange( 0, 0 );
264 mSqlErrorMessage.clear();
265
266 connect( mStopButton, &QPushButton::pressed, mFeedback.get(), [this] {
267 mStatusLabel->setText( tr( "Stopped" ) );
268 mFeedback->cancel();
269 mProgressBar->hide();
270 mWasCanceled = true;
271 } );
272
273 // Create model when result is ready
274 connect( &mQueryResultWatcher, &QFutureWatcher<QgsAbstractDatabaseProviderConnection::QueryResult>::finished, this, &QgsQueryResultPanelWidget::startFetching, Qt::ConnectionType::UniqueConnection );
275
276 QFuture<QgsAbstractDatabaseProviderConnection::QueryResult> future = QtConcurrent::run( [this, sql]() -> QgsAbstractDatabaseProviderConnection::QueryResult {
277 try
278 {
279 return mConnection->execSql( sql, mFeedback.get() );
280 }
281 catch ( QgsProviderConnectionException &ex )
282 {
283 mSqlErrorMessage = ex.what();
284 return QgsAbstractDatabaseProviderConnection::QueryResult();
285 }
286 } );
287 mQueryResultWatcher.setFuture( future );
288 }
289 else
290 {
291 showError( tr( "Connection error" ), tr( "Cannot execute query: connection to the database is not available." ) );
292 }
293}
294
295void QgsQueryResultPanelWidget::updateButtons()
296{
297 mFilterLineEdit->setEnabled( mFirstRowFetched );
298 mFilterToolButton->setEnabled( mFirstRowFetched );
299 const bool isEmpty = mSqlEditor->text().isEmpty();
300 mExecuteButton->setEnabled( !isEmpty );
301 mLoadAsNewLayerGroupBox->setVisible( mConnection && mConnection->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::SqlLayers ) );
302 mLoadAsNewLayerGroupBox->setEnabled(
303 mSqlErrorMessage.isEmpty() && mFirstRowFetched
304 );
305}
306
307void QgsQueryResultPanelWidget::showCellContextMenu( QPoint point )
308{
309 const QModelIndex modelIndex = mQueryResultsTableView->indexAt( point );
310 if ( modelIndex.isValid() )
311 {
312 QMenu *menu = new QMenu();
313 menu->setAttribute( Qt::WA_DeleteOnClose );
314
315 menu->addAction( QgsApplication::getThemeIcon( "mActionEditCopy.svg" ), tr( "Copy" ), this, [this] { copySelection(); }, QKeySequence::Copy );
316
317 menu->exec( mQueryResultsTableView->viewport()->mapToGlobal( point ) );
318 }
319}
320
321void QgsQueryResultPanelWidget::copySelection()
322{
323 const QModelIndexList selection = mQueryResultsTableView->selectionModel()->selectedIndexes();
324 if ( selection.empty() )
325 return;
326
327 int minRow = -1;
328 int maxRow = -1;
329 int minCol = -1;
330 int maxCol = -1;
331 for ( const QModelIndex &index : selection )
332 {
333 if ( minRow == -1 || index.row() < minRow )
334 minRow = index.row();
335 if ( maxRow == -1 || index.row() > maxRow )
336 maxRow = index.row();
337 if ( minCol == -1 || index.column() < minCol )
338 minCol = index.column();
339 if ( maxCol == -1 || index.column() > maxCol )
340 maxCol = index.column();
341 }
342
343 if ( minRow == maxRow && minCol == maxCol )
344 {
345 // copy only one cell
346 const QString text = mModel->data( selection.at( 0 ), Qt::DisplayRole ).toString();
347 QApplication::clipboard()->setText( text );
348 }
349 else
350 {
351 copyResults( minRow, maxRow, minCol, maxCol );
352 }
353}
354
355void QgsQueryResultPanelWidget::updateSqlLayerColumns()
356{
357 // Precondition
358 Q_ASSERT( mModel );
359
360 mFilterToolButton->setEnabled( true );
361 mFilterLineEdit->setEnabled( true );
362 mPkColumnsComboBox->clear();
363 mGeometryColumnComboBox->clear();
364 const bool hasPkInformation { !mSqlVectorLayerOptions.primaryKeyColumns.isEmpty() };
365 const bool hasGeomColInformation { !mSqlVectorLayerOptions.geometryColumn.isEmpty() };
366 static const QStringList geomColCandidates { QStringLiteral( "geom" ), QStringLiteral( "geometry" ), QStringLiteral( "the_geom" ) };
367 const QStringList constCols { mModel->columns() };
368 for ( const QString &c : constCols )
369 {
370 const bool pkCheckedState = hasPkInformation ? mSqlVectorLayerOptions.primaryKeyColumns.contains( c ) : c.contains( QStringLiteral( "id" ), Qt::CaseSensitivity::CaseInsensitive );
371 // Only check first match
372 mPkColumnsComboBox->addItemWithCheckState( c, pkCheckedState && mPkColumnsComboBox->checkedItems().isEmpty() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked );
373 mGeometryColumnComboBox->addItem( c );
374 if ( !hasGeomColInformation && geomColCandidates.contains( c, Qt::CaseSensitivity::CaseInsensitive ) )
375 {
376 mGeometryColumnComboBox->setCurrentText( c );
377 }
378 }
379 mPkColumnsCheckBox->setChecked( hasPkInformation );
380 mGeometryColumnCheckBox->setChecked( hasGeomColInformation );
381 if ( hasGeomColInformation )
382 {
383 mGeometryColumnComboBox->setCurrentText( mSqlVectorLayerOptions.geometryColumn );
384 }
385}
386
387void QgsQueryResultPanelWidget::cancelRunningQuery()
388{
389 // Cancel other threads
390 if ( mFeedback )
391 {
392 mFeedback->cancel();
393 }
394
395 // ... and wait
396 if ( mQueryResultWatcher.isRunning() )
397 {
398 mQueryResultWatcher.waitForFinished();
399 }
400}
401
402void QgsQueryResultPanelWidget::cancelApiFetcher()
403{
404 if ( mApiFetcher )
405 {
406 mApiFetcher->stopFetching();
407 // apiFetcher and apiFetcherWorkerThread will be deleted when the thread fetchingFinished signal is emitted
408 }
409}
410
411void QgsQueryResultPanelWidget::startFetching()
412{
413 if ( !mWasCanceled )
414 {
415 if ( !mSqlErrorMessage.isEmpty() )
416 {
417 showError( tr( "SQL error" ), mSqlErrorMessage, true );
418 }
419 else
420 {
421 if ( mQueryResultWatcher.result().rowCount() != static_cast<long long>( Qgis::FeatureCountState::UnknownCount ) )
422 {
423 mStatusLabel->setText( QStringLiteral( "Query executed successfully (%1 rows, %2 ms)" )
424 .arg( QLocale().toString( mQueryResultWatcher.result().rowCount() ), QLocale().toString( mQueryResultWatcher.result().queryExecutionTime() ) ) );
425 }
426 else
427 {
428 mStatusLabel->setText( QStringLiteral( "Query executed successfully (%1 s)" ).arg( QLocale().toString( mQueryResultWatcher.result().queryExecutionTime() ) ) );
429 }
430 mProgressBar->hide();
431 mModel = std::make_unique<QgsQueryResultModel>( mQueryResultWatcher.result() );
432 connect( mFeedback.get(), &QgsFeedback::canceled, mModel.get(), [this] {
433 mModel->cancel();
434 mWasCanceled = true;
435 } );
436
437 connect( mModel.get(), &QgsQueryResultModel::fetchMoreRows, this, [this]( long long maxRows ) {
438 mFetchedRowsBatchCount = 0;
439 mProgressBar->setRange( 0, maxRows );
440 mProgressBar->show();
441 } );
442
443 connect( mModel.get(), &QgsQueryResultModel::rowsInserted, this, [this]( const QModelIndex &, int first, int last ) {
444 if ( !mFirstRowFetched )
445 {
446 emit firstResultBatchFetched();
447 mFirstRowFetched = true;
448 mQueryResultsTableView->show();
449 mResultsContainer->show();
450 updateButtons();
451 updateSqlLayerColumns();
452 mActualRowCount = mModel->queryResult().rowCount();
453 }
454 mStatusLabel->setText( tr( "Fetched rows: %1/%2 %3 %4 ms" )
455 .arg( QLocale().toString( mModel->rowCount( mModel->index( -1, -1 ) ) ), mActualRowCount != -1 ? QLocale().toString( mActualRowCount ) : tr( "unknown" ), mWasCanceled ? tr( "(stopped)" ) : QString(), QLocale().toString( mQueryResultWatcher.result().queryExecutionTime() ) ) );
456 mFetchedRowsBatchCount += last - first + 1;
457 mProgressBar->setValue( mFetchedRowsBatchCount );
458 } );
459
460 mQueryResultsTableView->setModel( mModel.get() );
461 mQueryResultsTableView->show();
462 mResultsContainer->show();
463
464 connect( mModel.get(), &QgsQueryResultModel::fetchingComplete, mStopButton, [this] {
465 bool ok = false;
466 const QgsHistoryEntry currentHistoryEntry = QgsGui::historyProviderRegistry()->entry( mCurrentHistoryEntryId, ok );
467 QVariantMap entryDetails = currentHistoryEntry.entry;
468 entryDetails.insert( QStringLiteral( "rows" ), mActualRowCount );
469 entryDetails.insert( QStringLiteral( "time" ), mQueryResultWatcher.result().queryExecutionTime() );
470
471 QgsGui::historyProviderRegistry()->updateEntry( mCurrentHistoryEntryId, entryDetails );
472 mProgressBar->hide();
473 mStopButton->setEnabled( false );
474 } );
475 }
476 }
477 else
478 {
479 mStatusLabel->setText( tr( "SQL command aborted" ) );
480 mProgressBar->hide();
481 }
482}
483
484void QgsQueryResultPanelWidget::showError( const QString &title, const QString &message, bool isSqlError )
485{
486 mStatusLabel->show();
487 mStatusLabel->setText( tr( "An error occurred while executing the query" ) );
488 mProgressBar->hide();
489 mQueryResultsTableView->hide();
490 if ( isSqlError )
491 {
492 mSqlErrorText->show();
493 mSqlErrorText->setText( message );
494 mResultsContainer->show();
495 }
496 else
497 {
498 mMessageBar->pushCritical( title, message );
499 mResultsContainer->hide();
500 }
501}
502
503void QgsQueryResultPanelWidget::tokensReady( const QStringList &tokens )
504{
505 mSqlEditor->setExtraKeywords( mSqlEditor->extraKeywords() + tokens );
506 mSqlErrorText->setExtraKeywords( mSqlErrorText->extraKeywords() + tokens );
507}
508
509void QgsQueryResultPanelWidget::copyResults()
510{
511 const int rowCount = mModel->rowCount( QModelIndex() );
512 const int columnCount = mModel->columnCount( QModelIndex() );
513 copyResults( 0, rowCount - 1, 0, columnCount - 1 );
514}
515
516void QgsQueryResultPanelWidget::copyResults( int fromRow, int toRow, int fromColumn, int toColumn )
517{
518 QStringList rowStrings;
519 QStringList columnStrings;
520
521 const int rowCount = mModel->rowCount( QModelIndex() );
522 const int columnCount = mModel->columnCount( QModelIndex() );
523
524 toRow = std::min( toRow, rowCount - 1 );
525 toColumn = std::min( toColumn, columnCount - 1 );
526
527 rowStrings.reserve( toRow - fromRow );
528
529 // add titles first
530 for ( int col = fromColumn; col <= toColumn; col++ )
531 {
532 columnStrings += mModel->headerData( col, Qt::Horizontal, Qt::DisplayRole ).toString();
533 }
534 rowStrings += columnStrings.join( QLatin1Char( '\t' ) );
535 columnStrings.clear();
536
537 for ( int row = fromRow; row <= toRow; row++ )
538 {
539 for ( int col = fromColumn; col <= toColumn; col++ )
540 {
541 columnStrings += mModel->data( mModel->index( row, col ), Qt::DisplayRole ).toString();
542 }
543 rowStrings += columnStrings.join( QLatin1Char( '\t' ) );
544 columnStrings.clear();
545 }
546
547 if ( !rowStrings.isEmpty() )
548 {
549 const QString text = rowStrings.join( QLatin1Char( '\n' ) );
550 QString html = QStringLiteral( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\"><html><head><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"/></head><body><table border=\"1\"><tr><td>%1</td></tr></table></body></html>" ).arg( text );
551 html.replace( QLatin1String( "\t" ), QLatin1String( "</td><td>" ) ).replace( QLatin1String( "\n" ), QLatin1String( "</td></tr><tr><td>" ) );
552
553 QMimeData *mdata = new QMimeData();
554 mdata->setData( QStringLiteral( "text/html" ), html.toUtf8() );
555 if ( !text.isEmpty() )
556 {
557 mdata->setText( text );
558 }
559 // Transfers ownership to the clipboard object
560#ifdef Q_OS_LINUX
561 QApplication::clipboard()->setMimeData( mdata, QClipboard::Selection );
562#endif
563 QApplication::clipboard()->setMimeData( mdata, QClipboard::Clipboard );
564 }
565}
566
567QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions QgsQueryResultPanelWidget::sqlVectorLayerOptions() const
568{
569 const thread_local QRegularExpression rx( QStringLiteral( ";\\s*$" ) );
570 mSqlVectorLayerOptions.sql = mSqlEditor->text().replace( rx, QString() );
571 mSqlVectorLayerOptions.filter = mFilterLineEdit->text();
572 mSqlVectorLayerOptions.primaryKeyColumns = mPkColumnsComboBox->checkedItems();
573 mSqlVectorLayerOptions.geometryColumn = mGeometryColumnComboBox->currentText();
574 mSqlVectorLayerOptions.layerName = mLayerNameLineEdit->text();
575 mSqlVectorLayerOptions.disableSelectAtId = mAvoidSelectingAsFeatureIdCheckBox->isChecked();
576 QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions options { mSqlVectorLayerOptions };
577 // Override if not used
578 if ( !mPkColumnsCheckBox->isChecked() )
579 {
580 options.primaryKeyColumns.clear();
581 }
582 if ( !mGeometryColumnCheckBox->isChecked() )
583 {
584 options.geometryColumn.clear();
585 }
586 return options;
587}
588
589void QgsQueryResultPanelWidget::setConnection( QgsAbstractDatabaseProviderConnection *connection )
590{
591 mConnection.reset( connection );
592
593 cancelApiFetcher();
594
595 if ( connection )
596 {
597 // Add provider specific APIs
598 const QMultiMap<Qgis::SqlKeywordCategory, QStringList> keywordsDict { connection->sqlDictionary() };
599 QStringList keywords;
600 for ( auto it = keywordsDict.constBegin(); it != keywordsDict.constEnd(); it++ )
601 {
602 keywords.append( it.value() );
603 }
604
605 // Add static keywords from provider
606 mSqlEditor->setExtraKeywords( keywords );
607 mSqlErrorText->setExtraKeywords( keywords );
608
609 // Add dynamic keywords in a separate thread
610 QThread *apiFetcherWorkerThread = new QThread();
611 QgsConnectionsApiFetcher *apiFetcher = new QgsConnectionsApiFetcher( mConnection->uri(), mConnection->providerKey() );
612 apiFetcher->moveToThread( apiFetcherWorkerThread );
613 connect( apiFetcherWorkerThread, &QThread::started, apiFetcher, &QgsConnectionsApiFetcher::fetchTokens );
614 connect( apiFetcher, &QgsConnectionsApiFetcher::tokensReady, this, &QgsQueryResultPanelWidget::tokensReady );
615 connect( apiFetcher, &QgsConnectionsApiFetcher::fetchingFinished, apiFetcherWorkerThread, [apiFetcher, apiFetcherWorkerThread] {
616 apiFetcherWorkerThread->quit();
617 apiFetcherWorkerThread->wait();
618 apiFetcherWorkerThread->deleteLater();
619 apiFetcher->deleteLater();
620 } );
621
622 mApiFetcher = apiFetcher;
623 apiFetcherWorkerThread->start();
624 }
625
626 updateButtons();
627}
628
629void QgsQueryResultPanelWidget::setQuery( const QString &sql )
630{
631 mSqlEditor->setText( sql );
632}
633
634void QgsQueryResultPanelWidget::notify( const QString &title, const QString &text, Qgis::MessageLevel level )
635{
636 mMessageBar->pushMessage( title, text, level );
637}
638
639void QgsQueryResultPanelWidget::storeCurrentQuery( Qgis::QueryStorageBackend backend )
640{
641 const QStringList existingQueryNames = QgsGui::storedQueryManager()->allQueryNames( backend );
642 QgsNewNameDialog dlg(
643 QString(),
644 QString(),
645 QStringList(),
646 existingQueryNames
647 );
648 dlg.setWindowTitle( tr( "Store Query" ) );
649 dlg.setHintString( tr( "Name for the stored query" ) );
650 dlg.setOverwriteEnabled( true );
651 dlg.setConflictingNameWarning( tr( "A stored query with this name already exists, it will be overwritten." ) );
652 dlg.setShowExistingNamesCompleter( true );
653 if ( dlg.exec() != QDialog::Accepted )
654 return;
655
656 const QString name = dlg.name();
657 if ( name.isEmpty() )
658 return;
659
660 QgsGui::storedQueryManager()->storeQuery( name, mSqlEditor->text(), backend );
662 {
664 }
665}
666
667//
668// QgsQueryResultWidget
669//
670
671QgsQueryResultWidget::QgsQueryResultWidget( QWidget *parent, QgsAbstractDatabaseProviderConnection *connection )
672 : QWidget( parent )
673{
674 setupUi( this );
675
676 mToolBar->setIconSize( QgsGuiUtils::iconSize( false ) );
677
678 mQueryWidget = new QgsQueryResultPanelWidget( nullptr, connection );
679 mPanelStack->setMainPanel( mQueryWidget );
680
681 mPresetQueryMenu = new QMenu( this );
682 connect( mPresetQueryMenu, &QMenu::aboutToShow, this, &QgsQueryResultWidget::populatePresetQueryMenu );
683
684 QToolButton *presetQueryButton = new QToolButton();
685 presetQueryButton->setMenu( mPresetQueryMenu );
686 presetQueryButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconStoredQueries.svg" ) ) );
687 presetQueryButton->setPopupMode( QToolButton::InstantPopup );
688 mToolBar->addWidget( presetQueryButton );
689
690 connect( mActionOpenQuery, &QAction::triggered, this, &QgsQueryResultWidget::openQuery );
691 connect( mActionSaveQuery, &QAction::triggered, this, [this] { saveQuery( false ); } );
692 connect( mActionSaveQueryAs, &QAction::triggered, this, [this] { saveQuery( true ); } );
693
694 connect( mActionCut, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::cut );
695 connect( mActionCopy, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::copy );
696 connect( mActionPaste, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::paste );
697 connect( mActionUndo, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::undo );
698 connect( mActionRedo, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::redo );
699 mActionUndo->setEnabled( false );
700 mActionRedo->setEnabled( false );
701
702 connect( mActionFindReplace, &QAction::toggled, mQueryWidget->codeEditorWidget(), &QgsCodeEditorWidget::setSearchBarVisible );
703 connect( mQueryWidget->codeEditorWidget(), &QgsCodeEditorWidget::searchBarToggled, mActionFindReplace, &QAction::setChecked );
704 connect( mQueryWidget->sqlEditor(), &QgsCodeEditor::modificationChanged, this, &QgsQueryResultWidget::setHasChanged );
705
706 connect( mActionShowHistory, &QAction::toggled, this, &QgsQueryResultWidget::showHistoryPanel );
707
708 connect( mActionClear, &QAction::triggered, this, [this] {
709 // Cannot use setText() because it resets the undo/redo buffer.
710 mQueryWidget->sqlEditor()->SendScintilla( QsciScintilla::SCI_SETTEXT, "" );
711 } );
712
713 connect( mQueryWidget->sqlEditor(), &QgsCodeEditorSQL::textChanged, this, &QgsQueryResultWidget::updateButtons );
714
715 connect( mQueryWidget->sqlEditor(), &QgsCodeEditorSQL::copyAvailable, mActionCut, &QAction::setEnabled );
716 connect( mQueryWidget->sqlEditor(), &QgsCodeEditorSQL::copyAvailable, mActionCopy, &QAction::setEnabled );
717
718 connect( mQueryWidget, &QgsQueryResultPanelWidget::createSqlVectorLayer, this, &QgsQueryResultWidget::createSqlVectorLayer );
719 connect( mQueryWidget, &QgsQueryResultPanelWidget::firstResultBatchFetched, this, &QgsQueryResultWidget::firstResultBatchFetched );
720
721 updateButtons();
722 setHasChanged( false );
723}
724
725QgsQueryResultWidget::~QgsQueryResultWidget()
726{
727 if ( mHistoryWidget )
728 {
729 mPanelStack->closePanel( mHistoryWidget );
730 mHistoryWidget->deleteLater();
731 }
732}
733
734void QgsQueryResultWidget::setSqlVectorLayerOptions( const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options )
735{
736 if ( !options.sql.isEmpty() )
737 {
738 setQuery( options.sql );
739 }
740 mQueryWidget->setSqlVectorLayerOptions( options );
741}
742
743void QgsQueryResultWidget::setWidgetMode( QueryWidgetMode widgetMode )
744{
745 mQueryWidget->setWidgetMode( widgetMode );
746}
747
748void QgsQueryResultWidget::executeQuery()
749{
750 mQueryWidget->executeQuery();
751}
752
753void QgsQueryResultWidget::updateButtons()
754{
755 mQueryWidget->updateButtons();
756
757 const bool isEmpty = mQueryWidget->sqlEditor()->text().isEmpty();
758 mActionClear->setEnabled( !isEmpty );
759 mActionUndo->setEnabled( mQueryWidget->sqlEditor()->isUndoAvailable() );
760 mActionRedo->setEnabled( mQueryWidget->sqlEditor()->isRedoAvailable() );
761}
762
763void QgsQueryResultWidget::showError( const QString &title, const QString &message, bool isSqlError )
764{
765 mQueryWidget->showError( title, message, isSqlError );
766}
767
768void QgsQueryResultWidget::tokensReady( const QStringList & )
769{
770}
771
772void QgsQueryResultWidget::copyResults()
773{
774 mQueryWidget->copyResults();
775}
776
777void QgsQueryResultWidget::copyResults( int fromRow, int toRow, int fromColumn, int toColumn )
778{
779 mQueryWidget->copyResults( fromRow, toRow, fromColumn, toColumn );
780}
781
782void QgsQueryResultWidget::openQuery()
783{
784 if ( !mQueryWidget->codeEditorWidget()->filePath().isEmpty() && mHasChangedFileContents )
785 {
786 if ( QMessageBox::warning( this, tr( "Unsaved Changes" ), tr( "There are unsaved changes in the query. Continue?" ), QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::No ) == QMessageBox::StandardButton::No )
787 return;
788 }
789
790 QString initialDir = settingLastSourceFolder->value();
791 if ( initialDir.isEmpty() )
792 initialDir = QDir::homePath();
793
794 const QString fileName = QFileDialog::getOpenFileName( this, tr( "Open Query" ), initialDir, tr( "SQL queries (*.sql *.SQL)" ) + QStringLiteral( ";;" ) + QObject::tr( "All files" ) + QStringLiteral( " (*.*)" ) );
795
796 if ( fileName.isEmpty() )
797 return;
798
799 QFileInfo fi( fileName );
800 settingLastSourceFolder->setValue( fi.path() );
801
802 QgsTemporaryCursorOverride cursor( Qt::CursorShape::WaitCursor );
803
804 mQueryWidget->codeEditorWidget()->loadFile( fileName );
805 setHasChanged( false );
806}
807
808void QgsQueryResultWidget::saveQuery( bool saveAs )
809{
810 if ( mQueryWidget->codeEditorWidget()->filePath().isEmpty() || saveAs )
811 {
812 QString selectedFilter;
813
814 QString initialDir = settingLastSourceFolder->value();
815 if ( initialDir.isEmpty() )
816 initialDir = QDir::homePath();
817
818 QString newPath = QFileDialog::getSaveFileName(
819 this,
820 tr( "Save Query" ),
821 initialDir,
822 tr( "SQL queries (*.sql *.SQL)" ) + QStringLiteral( ";;" ) + QObject::tr( "All files" ) + QStringLiteral( " (*.*)" ),
823 &selectedFilter
824 );
825
826 if ( !newPath.isEmpty() )
827 {
828 QFileInfo fi( newPath );
829 settingLastSourceFolder->setValue( fi.path() );
830
831 if ( !selectedFilter.contains( QStringLiteral( "*.*)" ) ) )
832 newPath = QgsFileUtils::ensureFileNameHasExtension( newPath, { QStringLiteral( "sql" ) } );
833 mQueryWidget->codeEditorWidget()->save( newPath );
834 setHasChanged( false );
835 }
836 }
837 else if ( !mQueryWidget->codeEditorWidget()->filePath().isEmpty() )
838 {
839 mQueryWidget->codeEditorWidget()->save();
840 setHasChanged( false );
841 }
842}
843
844void QgsQueryResultWidget::setConnection( QgsAbstractDatabaseProviderConnection *connection )
845{
846 mQueryWidget->setConnection( connection );
847 updateButtons();
848}
849
850void QgsQueryResultWidget::setQuery( const QString &sql )
851{
852 mQueryWidget->sqlEditor()->setText( sql );
853 // from the QScintilla docs, calling setText clears undo history!
854 mActionUndo->setEnabled( false );
855 mActionRedo->setEnabled( false );
856}
857
858bool QgsQueryResultWidget::promptUnsavedChanges()
859{
860 if ( !mQueryWidget->codeEditorWidget()->filePath().isEmpty() && mHasChangedFileContents )
861 {
862 const QMessageBox::StandardButton ret = QMessageBox::question(
863 this,
864 tr( "Save Query?" ),
865 tr(
866 "There are unsaved changes in this query. Do you want to save those?"
867 ),
868 QMessageBox::StandardButton::Save
869 | QMessageBox::StandardButton::Cancel
870 | QMessageBox::StandardButton::Discard,
871 QMessageBox::StandardButton::Cancel
872 );
873
874 if ( ret == QMessageBox::StandardButton::Save )
875 {
876 saveQuery( false );
877 return true;
878 }
879 else if ( ret == QMessageBox::StandardButton::Discard )
880 {
881 return true;
882 }
883 else
884 {
885 return false;
886 }
887 }
888 else
889 {
890 return true;
891 }
892}
893
894
895void QgsQueryResultWidget::notify( const QString &title, const QString &text, Qgis::MessageLevel level )
896{
897 mQueryWidget->notify( title, text, level );
898}
899
900
901void QgsQueryResultWidget::setHasChanged( bool hasChanged )
902{
903 mActionSaveQuery->setEnabled( hasChanged );
904 mHasChangedFileContents = hasChanged;
905 updateDialogTitle();
906}
907
908void QgsQueryResultWidget::updateDialogTitle()
909{
910 QString fileName;
911 if ( !mQueryWidget->codeEditorWidget()->filePath().isEmpty() )
912 {
913 const QFileInfo fi( mQueryWidget->codeEditorWidget()->filePath() );
914 fileName = fi.fileName();
915 if ( mHasChangedFileContents )
916 {
917 fileName.prepend( '*' );
918 }
919 }
920
921 emit requestDialogTitleUpdate( fileName );
922}
923
924void QgsQueryResultWidget::populatePresetQueryMenu()
925{
926 mPresetQueryMenu->clear();
927
928 QMenu *storeQueryMenu = new QMenu( tr( "Store Current Query" ), mPresetQueryMenu );
929 mPresetQueryMenu->addMenu( storeQueryMenu );
930 QAction *storeInProfileAction = new QAction( tr( "In User Profile…" ), storeQueryMenu );
931 storeQueryMenu->addAction( storeInProfileAction );
932 storeInProfileAction->setEnabled( !mQueryWidget->sqlEditor()->text().isEmpty() );
933 connect( storeInProfileAction, &QAction::triggered, this, [this] {
934 storeCurrentQuery( Qgis::QueryStorageBackend::LocalProfile );
935 } );
936 QAction *storeInProjectAction = new QAction( tr( "In Current Project…" ), storeQueryMenu );
937 storeQueryMenu->addAction( storeInProjectAction );
938 storeInProjectAction->setEnabled( !mQueryWidget->sqlEditor()->text().isEmpty() );
939 connect( storeInProjectAction, &QAction::triggered, this, [this] {
941 } );
942
943
944 const QList< QgsStoredQueryManager::QueryDetails > storedQueries = QgsGui::storedQueryManager()->allQueries();
945 if ( !storedQueries.isEmpty() )
946 {
947 QList< QgsStoredQueryManager::QueryDetails > userProfileQueries;
948 std::copy_if( storedQueries.begin(), storedQueries.end(), std::back_inserter( userProfileQueries ), []( const QgsStoredQueryManager::QueryDetails &details ) {
949 return details.backend == Qgis::QueryStorageBackend::LocalProfile;
950 } );
951
952 QList< QgsStoredQueryManager::QueryDetails > projectQueries;
953 std::copy_if( storedQueries.begin(), storedQueries.end(), std::back_inserter( projectQueries ), []( const QgsStoredQueryManager::QueryDetails &details ) {
954 return details.backend == Qgis::QueryStorageBackend::CurrentProject;
955 } );
956
957 mPresetQueryMenu->addSection( QgsApplication::getThemeIcon( QStringLiteral( "mIconStoredQueries.svg" ) ), tr( "User Profile" ) );
958 for ( const QgsStoredQueryManager::QueryDetails &query : std::as_const( userProfileQueries ) )
959 {
960 QAction *action = new QAction( query.name, mPresetQueryMenu );
961 mPresetQueryMenu->addAction( action );
962 connect( action, &QAction::triggered, this, [this, query] {
963 mQueryWidget->sqlEditor()->insertText( query.definition );
964 } );
965 }
966 if ( userProfileQueries.empty() )
967 {
968 QAction *action = new QAction( tr( "No Stored Queries Available" ), mPresetQueryMenu );
969 action->setEnabled( false );
970 mPresetQueryMenu->addAction( action );
971 }
972
973 mPresetQueryMenu->addSection( QgsApplication::getThemeIcon( QStringLiteral( "mIconStoredQueries.svg" ) ), tr( "Current Project" ) );
974 for ( const QgsStoredQueryManager::QueryDetails &query : std::as_const( projectQueries ) )
975 {
976 QAction *action = new QAction( query.name, mPresetQueryMenu );
977 mPresetQueryMenu->addAction( action );
978 connect( action, &QAction::triggered, this, [this, query] {
979 mQueryWidget->sqlEditor()->insertText( query.definition );
980 } );
981 }
982 if ( projectQueries.empty() )
983 {
984 QAction *action = new QAction( tr( "No Stored Queries Available" ), mPresetQueryMenu );
985 action->setEnabled( false );
986 mPresetQueryMenu->addAction( action );
987 }
988
989 mPresetQueryMenu->addSeparator();
990
991 QMenu *removeQueryMenu = new QMenu( tr( "Removed Stored Query" ), mPresetQueryMenu );
992 mPresetQueryMenu->addMenu( removeQueryMenu );
993
994 for ( const QgsStoredQueryManager::QueryDetails &query : storedQueries )
995 {
996 QAction *action = new QAction( tr( "%1…" ).arg( query.name ), mPresetQueryMenu );
997 removeQueryMenu->addAction( action );
998 connect( action, &QAction::triggered, this, [this, query] {
999 const QMessageBox::StandardButton res = QMessageBox::question( this, tr( "Remove Stored Query" ), tr( "Are you sure you want to remove the stored query “%1”?" ).arg( query.name ), QMessageBox::Yes | QMessageBox::No, QMessageBox::No );
1000 if ( res == QMessageBox::Yes )
1001 {
1002 QgsGui::storedQueryManager()->removeQuery( query.name, query.backend );
1003 if ( query.backend == Qgis::QueryStorageBackend::CurrentProject )
1004 {
1006 }
1007 }
1008 } );
1009 }
1010 }
1011}
1012
1013void QgsQueryResultWidget::storeCurrentQuery( Qgis::QueryStorageBackend backend )
1014{
1015 const QStringList existingQueryNames = QgsGui::storedQueryManager()->allQueryNames( backend );
1016 QgsNewNameDialog dlg(
1017 QString(),
1018 QString(),
1019 QStringList(),
1020 existingQueryNames
1021 );
1022 dlg.setWindowTitle( tr( "Store Query" ) );
1023 dlg.setHintString( tr( "Name for the stored query" ) );
1024 dlg.setOverwriteEnabled( true );
1025 dlg.setConflictingNameWarning( tr( "A stored query with this name already exists, it will be overwritten." ) );
1026 dlg.setShowExistingNamesCompleter( true );
1027 if ( dlg.exec() != QDialog::Accepted )
1028 return;
1029
1030 const QString name = dlg.name();
1031 if ( name.isEmpty() )
1032 return;
1033
1034 QgsGui::storedQueryManager()->storeQuery( name, mQueryWidget->sqlEditor()->text(), backend );
1036 {
1038 }
1039}
1040
1041
1042void QgsQueryResultWidget::showHistoryPanel( bool show )
1043{
1044 // the below code block trips up the clang analyser!
1045 // NOLINTBEGIN(bugprone-branch-clone)
1046 if ( show )
1047 {
1048 mHistoryWidget = new QgsDatabaseQueryHistoryWidget();
1049 mHistoryWidget->setPanelTitle( tr( "SQL History" ) );
1050 mPanelStack->showPanel( mHistoryWidget );
1051 connect( mHistoryWidget, &QgsPanelWidget::panelAccepted, this, [this] { whileBlocking( mActionShowHistory )->setChecked( false ); } );
1052 connect( mHistoryWidget, &QgsDatabaseQueryHistoryWidget::sqlTriggered, this, [this]( const QString &connectionUri, const QString &provider, const QString &sql ) {
1053 Q_UNUSED( connectionUri );
1054 Q_UNUSED( provider );
1055
1056 mQueryWidget->sqlEditor()->setText( sql );
1057 mActionUndo->setEnabled( false );
1058 mActionRedo->setEnabled( false );
1059 mHistoryWidget->acceptPanel();
1060 } );
1061 }
1062 else if ( mHistoryWidget )
1063 {
1064 mPanelStack->closePanel( mHistoryWidget );
1065 mHistoryWidget->deleteLater();
1066 }
1067 // NOLINTEND(bugprone-branch-clone)
1068}
1069
1070
1072
1073void QgsConnectionsApiFetcher::fetchTokens()
1074{
1075 if ( mStopFetching )
1076 {
1077 emit fetchingFinished();
1078 return;
1079 }
1080
1081
1083 if ( !md )
1084 {
1085 emit fetchingFinished();
1086 return;
1087 }
1088 std::unique_ptr<QgsAbstractDatabaseProviderConnection> connection( static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( mUri, {} ) ) );
1089 if ( !mStopFetching && connection )
1090 {
1091 mFeedback = std::make_unique<QgsFeedback>();
1092 QStringList schemas;
1094 {
1095 try
1096 {
1097 schemas = connection->schemas();
1098 emit tokensReady( schemas );
1099 }
1100 catch ( QgsProviderConnectionException &ex )
1101 {
1102 QgsMessageLog::logMessage( tr( "Error retrieving schemas: %1" ).arg( ex.what() ), QStringLiteral( "QGIS" ), Qgis::MessageLevel::Warning );
1103 }
1104 }
1105 else
1106 {
1107 schemas.push_back( QString() ); // Fake empty schema for DBs not supporting it
1108 }
1109
1110 for ( const auto &schema : std::as_const( schemas ) )
1111 {
1112 if ( mStopFetching )
1113 {
1114 connection.reset();
1115 emit fetchingFinished();
1116 return;
1117 }
1118
1119 QStringList tableNames;
1120 try
1121 {
1122 const QList<QgsAbstractDatabaseProviderConnection::TableProperty> tables = connection->tables( schema, QgsAbstractDatabaseProviderConnection::TableFlags(), mFeedback.get() );
1123 for ( const QgsAbstractDatabaseProviderConnection::TableProperty &table : std::as_const( tables ) )
1124 {
1125 if ( mStopFetching )
1126 {
1127 connection.reset();
1128 emit fetchingFinished();
1129 return;
1130 }
1131 tableNames.push_back( table.tableName() );
1132 }
1133 emit tokensReady( tableNames );
1134 }
1135 catch ( QgsProviderConnectionException &ex )
1136 {
1137 QgsMessageLog::logMessage( tr( "Error retrieving tables: %1" ).arg( ex.what() ), QStringLiteral( "QGIS" ), Qgis::MessageLevel::Warning );
1138 }
1139
1140 // Get fields
1141 for ( const auto &table : std::as_const( tableNames ) )
1142 {
1143 if ( mStopFetching )
1144 {
1145 connection.reset();
1146 emit fetchingFinished();
1147 return;
1148 }
1149
1150 QStringList fieldNames;
1151 try
1152 {
1153 const QgsFields fields( connection->fields( schema, table, mFeedback.get() ) );
1154 if ( mStopFetching )
1155 {
1156 connection.reset();
1157 emit fetchingFinished();
1158 return;
1159 }
1160
1161 for ( const auto &field : std::as_const( fields ) )
1162 {
1163 fieldNames.push_back( field.name() );
1164 if ( mStopFetching )
1165 {
1166 connection.reset();
1167 emit fetchingFinished();
1168 return;
1169 }
1170 }
1171 emit tokensReady( fieldNames );
1172 }
1173 catch ( QgsProviderConnectionException &ex )
1174 {
1175 QgsMessageLog::logMessage( tr( "Error retrieving fields for table %1: %2" ).arg( table, ex.what() ), QStringLiteral( "QGIS" ), Qgis::MessageLevel::Warning );
1176 }
1177 }
1178 }
1179 }
1180
1181 connection.reset();
1182 emit fetchingFinished();
1183}
1184
1185void QgsConnectionsApiFetcher::stopFetching()
1186{
1187 mStopFetching = 1;
1188 if ( mFeedback )
1189 mFeedback->cancel();
1190}
1191
1192QgsQueryResultItemDelegate::QgsQueryResultItemDelegate( QObject *parent )
1193 : QStyledItemDelegate( parent )
1194{
1195}
1196
1197QString QgsQueryResultItemDelegate::displayText( const QVariant &value, const QLocale &locale ) const
1198{
1199 Q_UNUSED( locale )
1200 QString result { QgsExpressionUtils::toLocalizedString( value ) };
1201 // Show no more than 255 characters
1202 if ( result.length() > 255 )
1203 {
1204 result.truncate( 255 );
1205 result.append( QStringLiteral( "…" ) );
1206 }
1207 return result;
1208}
1209
1211
1212//
1213// QgsQueryResultDialog
1214//
1215
1216QgsQueryResultDialog::QgsQueryResultDialog( QgsAbstractDatabaseProviderConnection *connection, QWidget *parent )
1217 : QDialog( parent )
1218{
1219 setObjectName( QStringLiteral( "QgsQueryResultDialog" ) );
1221
1222 mWidget = new QgsQueryResultWidget( this, connection );
1223 QVBoxLayout *l = new QVBoxLayout();
1224 l->setContentsMargins( 6, 6, 6, 6 );
1225
1226 QDialogButtonBox *mButtonBox = new QDialogButtonBox( QDialogButtonBox::StandardButton::Close | QDialogButtonBox::StandardButton::Help );
1227 connect( mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::close );
1228 connect( mButtonBox, &QDialogButtonBox::helpRequested, this, [] {
1229 QgsHelp::openHelp( QStringLiteral( "managing_data_source/create_layers.html#execute-sql" ) );
1230 } );
1231 l->addWidget( mWidget );
1232 l->addWidget( mButtonBox );
1233
1234 setLayout( l );
1235}
1236
1237void QgsQueryResultDialog::closeEvent( QCloseEvent *event )
1238{
1239 if ( !mWidget->promptUnsavedChanges() )
1240 {
1241 event->ignore();
1242 }
1243 else
1244 {
1245 event->accept();
1246 }
1247}
1248
1249//
1250// QgsQueryResultMainWindow
1251//
1252
1253QgsQueryResultMainWindow::QgsQueryResultMainWindow( QgsAbstractDatabaseProviderConnection *connection, const QString &identifierName )
1254 : mIdentifierName( identifierName )
1255{
1256 setObjectName( QStringLiteral( "SQLCommandsDialog" ) );
1257
1259
1260 mWidget = new QgsQueryResultWidget( nullptr, connection );
1261 setCentralWidget( mWidget );
1262 mWidget->layout()->setContentsMargins( 6, 6, 6, 6 );
1263
1264 connect( mWidget, &QgsQueryResultWidget::requestDialogTitleUpdate, this, &QgsQueryResultMainWindow::updateWindowTitle );
1265
1266 updateWindowTitle( QString() );
1267}
1268
1269void QgsQueryResultMainWindow::closeEvent( QCloseEvent *event )
1270{
1271 if ( !mWidget->promptUnsavedChanges() )
1272 {
1273 event->ignore();
1274 }
1275 else
1276 {
1277 event->accept();
1278 }
1279}
1280
1281void QgsQueryResultMainWindow::updateWindowTitle( const QString &fileName )
1282{
1283 if ( fileName.isEmpty() )
1284 {
1285 if ( !mIdentifierName.isEmpty() )
1286 setWindowTitle( tr( "%1 — Execute SQL" ).arg( mIdentifierName ) );
1287 else
1288 setWindowTitle( tr( "Execute SQL" ) );
1289 }
1290 else
1291 {
1292 if ( !mIdentifierName.isEmpty() )
1293 setWindowTitle( tr( "%1 — %2 — Execute SQL" ).arg( fileName, mIdentifierName ) );
1294 else
1295 setWindowTitle( tr( "%1 — Execute SQL" ).arg( fileName ) );
1296 }
1297}
MessageLevel
Level for messages This will be used both for message log and message bar in application.
Definition qgis.h:156
@ Warning
Warning message.
Definition qgis.h:158
QueryStorageBackend
Stored query storage backends.
Definition qgis.h:3517
@ CurrentProject
Current QGIS project.
Definition qgis.h:3519
@ LocalProfile
Local user profile.
Definition qgis.h:3518
@ UnstableFeatureIds
SQL layer definition supports disabling select at id.
Definition qgis.h:1095
@ SubsetStringFilter
SQL layer definition supports subset string filter.
Definition qgis.h:1092
@ PrimaryKeys
SQL layer definition supports primary keys.
Definition qgis.h:1094
@ GeometryColumn
SQL layer definition supports geometry column.
Definition qgis.h:1093
Provides common functionality for database based connections.
virtual QList< QgsAbstractDatabaseProviderConnection::TableProperty > tables(const QString &schema=QString(), const QgsAbstractDatabaseProviderConnection::TableFlags &flags=QgsAbstractDatabaseProviderConnection::TableFlags(), QgsFeedback *feedback=nullptr) const
Returns information on the tables in the given schema.
@ SqlLayers
Can create vector layers from SQL SELECT queries.
@ Schemas
Can list schemas (if not set, the connection does not support schemas).
virtual Qgis::SqlLayerDefinitionCapabilities sqlLayerDefinitionCapabilities()
Returns SQL layer definition capabilities (Filters, GeometryColumn, PrimaryKeys).
virtual QMultiMap< Qgis::SqlKeywordCategory, QStringList > sqlDictionary()
Returns a dictionary of SQL keywords supported by the provider.
virtual QStringList schemas() const
Returns information about the existing schemas.
Capabilities capabilities() const
Returns connection capabilities.
virtual QgsFields fields(const QString &schema, const QString &table, QgsFeedback *feedback=nullptr) const
Returns the fields of a table and schema.
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
A SQL editor based on QScintilla2.
A widget which wraps a QgsCodeEditor in additional functionality.
void searchBarToggled(bool visible)
Emitted when the visibility of the search bar is changed.
void setSearchBarVisible(bool visible)
Sets whether the search bar is visible.
void collapsedStateChanged(bool collapsed)
Signal emitted when groupbox collapsed/expanded state is changed, and when first shown.
void sqlTriggered(const QString &connectionUri, const QString &provider, const QString &sql)
Emitted when the user has triggered a previously executed SQL statement in the widget.
QString what() const
void canceled()
Internal routines can connect to this signal if they use event loop.
Container of fields for a vector layer.
Definition qgsfields.h:46
static QString ensureFileNameHasExtension(const QString &fileName, const QStringList &extensions)
Ensures that a fileName ends with an extension from the provided list of extensions.
static void enableAutoGeometryRestore(QWidget *widget, const QString &key=QString())
Register the widget to allow its position to be automatically saved and restored when open and closed...
Definition qgsgui.cpp:221
static QgsHistoryProviderRegistry * historyProviderRegistry()
Returns the global history provider registry, used for tracking history providers.
Definition qgsgui.cpp:211
static QgsStoredQueryManager * storedQueryManager()
Returns the global stored SQL query manager.
Definition qgsgui.cpp:240
static void openHelp(const QString &key)
Opens help topic for the given help key using default system web browser.
Definition qgshelp.cpp:38
long long addEntry(const QString &providerId, const QVariantMap &entry, bool &ok, QgsHistoryProviderRegistry::HistoryEntryOptions options=QgsHistoryProviderRegistry::HistoryEntryOptions())
Adds an entry to the history logs.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true, const char *file=__builtin_FILE(), const char *function=__builtin_FUNCTION(), int line=__builtin_LINE())
Adds a message to the log instance (and creates it if necessary).
Base class for any widget that can be shown as an inline panel.
void panelAccepted(QgsPanelWidget *panel)
Emitted when the panel is accepted by the user.
static QgsProject * instance()
Returns the QgsProject singleton instance.
void setDirty(bool b=true)
Flag the project as dirty (modified).
Custom exception class for provider connection related exceptions.
Holds data provider key, description, and associated shared library file or function pointer informat...
virtual QgsAbstractProviderConnection * createConnection(const QString &uri, const QVariantMap &configuration)
Creates a new connection from uri and configuration, the newly created connection is not automaticall...
static QgsProviderRegistry * instance(const QString &pluginPath=QString())
Means of accessing canonical single instance.
QgsProviderMetadata * providerMetadata(const QString &providerKey) const
Returns metadata of the provider or nullptr if not found.
Query Builder for layers.
QString sql() const
Returns the sql statement entered in the dialog.
A string settings entry.
Stores settings for use within QGIS.
Definition qgssettings.h:65
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
QList< QgsStoredQueryManager::QueryDetails > allQueries() const
Returns details of all queries stored in the manager.
QStringList allQueryNames(Qgis::QueryStorageBackend backend=Qgis::QueryStorageBackend::LocalProfile) const
Returns a list of the names of all stored queries for the specified backend.
void removeQuery(const QString &name, Qgis::QueryStorageBackend backend=Qgis::QueryStorageBackend::LocalProfile)
Removes the stored query with matching name.
void storeQuery(const QString &name, const QString &query, Qgis::QueryStorageBackend backend=Qgis::QueryStorageBackend::LocalProfile)
Saves a query to the manager.
@ UnknownCount
Provider returned an unknown feature count.
Definition qgis.h:547
QSize iconSize(bool dockableToolbar)
Returns the user-preferred size of a window's toolbar icons.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition qgis.h:6511
The SqlVectorLayerOptions stores all information required to create a SQL (query) layer.
QString sql
The SQL expression that defines the SQL (query) layer.
QString filter
Additional subset string (provider-side filter), not all data providers support this feature: check s...
bool disableSelectAtId
If SelectAtId is disabled (default is false), not all data providers support this feature: check supp...
The TableProperty class represents a database table or view.