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