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