QGIS API Documentation 4.1.0-Master (5bf3c20f3c9)
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] { mExecuteButton->setText( mSqlEditor->selectedText().isEmpty() ? tr( "Execute" ) : tr( "Execute Selection" ) ); } );
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(
254 u"dbquery"_s,
255 QVariantMap {
256 { u"query"_s, sql },
257 { u"provider"_s, mConnection->providerKey() },
258 { u"connection"_s, mConnection->uri() },
259 },
260 ok
261 );
262
263 mWasCanceled = false;
264 mFeedback = std::make_unique<QgsFeedback>();
265 mStopButton->setEnabled( true );
266 mStatusLabel->show();
267 mStatusLabel->setText( tr( "Executing query…" ) );
268 mProgressBar->show();
269 mProgressBar->setRange( 0, 0 );
270 mSqlErrorMessage.clear();
271
272 connect( mStopButton, &QPushButton::pressed, mFeedback.get(), [this] {
273 mStatusLabel->setText( tr( "Stopped" ) );
274 mFeedback->cancel();
275 mProgressBar->hide();
276 mWasCanceled = true;
277 } );
278
279 // Create model when result is ready
280 connect( &mQueryResultWatcher, &QFutureWatcher<QgsAbstractDatabaseProviderConnection::QueryResult>::finished, this, &QgsQueryResultPanelWidget::startFetching, Qt::ConnectionType::UniqueConnection );
281
282 QFuture<QgsAbstractDatabaseProviderConnection::QueryResult> future = QtConcurrent::run( [this, sql]() -> QgsAbstractDatabaseProviderConnection::QueryResult {
283 try
284 {
285 return mConnection->execSql( sql, mFeedback.get() );
286 }
287 catch ( QgsProviderConnectionException &ex )
288 {
289 mSqlErrorMessage = ex.what();
290 return QgsAbstractDatabaseProviderConnection::QueryResult();
291 }
292 } );
293 mQueryResultWatcher.setFuture( future );
294 }
295 else
296 {
297 showError( tr( "Connection error" ), tr( "Cannot execute query: connection to the database is not available." ) );
298 }
299}
300
301void QgsQueryResultPanelWidget::updateButtons()
302{
303 mFilterLineEdit->setEnabled( mFirstRowFetched );
304 mFilterToolButton->setEnabled( mFirstRowFetched );
305 const bool isEmpty = mSqlEditor->text().isEmpty();
306 mExecuteButton->setEnabled( !isEmpty );
307 mLoadAsNewLayerGroupBox->setVisible( mConnection && mConnection->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::SqlLayers ) );
308 mLoadAsNewLayerGroupBox->setEnabled( mSqlErrorMessage.isEmpty() && mFirstRowFetched );
309}
310
311void QgsQueryResultPanelWidget::showCellContextMenu( QPoint point )
312{
313 const QModelIndex modelIndex = mQueryResultsTableView->indexAt( point );
314 if ( modelIndex.isValid() )
315 {
316 QMenu *menu = new QMenu();
317 menu->setAttribute( Qt::WA_DeleteOnClose );
318
319 menu->addAction( QgsApplication::getThemeIcon( "mActionEditCopy.svg" ), tr( "Copy" ), this, [this] { copySelection(); }, QKeySequence::Copy );
320
321 menu->exec( mQueryResultsTableView->viewport()->mapToGlobal( point ) );
322 }
323}
324
325void QgsQueryResultPanelWidget::copySelection()
326{
327 const QModelIndexList selection = mQueryResultsTableView->selectionModel()->selectedIndexes();
328 if ( selection.empty() )
329 return;
330
331 int minRow = -1;
332 int maxRow = -1;
333 int minCol = -1;
334 int maxCol = -1;
335 for ( const QModelIndex &index : selection )
336 {
337 if ( minRow == -1 || index.row() < minRow )
338 minRow = index.row();
339 if ( maxRow == -1 || index.row() > maxRow )
340 maxRow = index.row();
341 if ( minCol == -1 || index.column() < minCol )
342 minCol = index.column();
343 if ( maxCol == -1 || index.column() > maxCol )
344 maxCol = index.column();
345 }
346
347 if ( minRow == maxRow && minCol == maxCol )
348 {
349 // copy only one cell
350 const QString text = mModel->data( selection.at( 0 ), Qt::DisplayRole ).toString();
351 QApplication::clipboard()->setText( text );
352 }
353 else
354 {
355 copyResults( minRow, maxRow, minCol, maxCol );
356 }
357}
358
359void QgsQueryResultPanelWidget::updateSqlLayerColumns()
360{
361 // Precondition
362 Q_ASSERT( mModel );
363
364 mFilterToolButton->setEnabled( true );
365 mFilterLineEdit->setEnabled( true );
366 mPkColumnsComboBox->clear();
367 mGeometryColumnComboBox->clear();
368 const bool hasPkInformation { !mSqlVectorLayerOptions.primaryKeyColumns.isEmpty() };
369 const bool hasGeomColInformation { !mSqlVectorLayerOptions.geometryColumn.isEmpty() };
370 static const QStringList geomColCandidates { u"geom"_s, u"geometry"_s, u"the_geom"_s };
371 const QStringList constCols { mModel->columns() };
372 for ( const QString &c : constCols )
373 {
374 const bool pkCheckedState = hasPkInformation ? mSqlVectorLayerOptions.primaryKeyColumns.contains( c ) : c.contains( u"id"_s, Qt::CaseSensitivity::CaseInsensitive );
375 // Only check first match
376 mPkColumnsComboBox->addItemWithCheckState( c, pkCheckedState && mPkColumnsComboBox->checkedItems().isEmpty() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked );
377 mGeometryColumnComboBox->addItem( c );
378 if ( !hasGeomColInformation && geomColCandidates.contains( c, Qt::CaseSensitivity::CaseInsensitive ) )
379 {
380 mGeometryColumnComboBox->setCurrentText( c );
381 }
382 }
383 mPkColumnsCheckBox->setChecked( hasPkInformation );
384 mGeometryColumnCheckBox->setChecked( hasGeomColInformation );
385 if ( hasGeomColInformation )
386 {
387 mGeometryColumnComboBox->setCurrentText( mSqlVectorLayerOptions.geometryColumn );
388 }
389}
390
391void QgsQueryResultPanelWidget::cancelRunningQuery()
392{
393 // Cancel other threads
394 if ( mFeedback )
395 {
396 mFeedback->cancel();
397 }
398
399 // ... and wait
400 if ( mQueryResultWatcher.isRunning() )
401 {
402 mQueryResultWatcher.waitForFinished();
403 }
404}
405
406void QgsQueryResultPanelWidget::cancelApiFetcher()
407{
408 if ( mApiFetcher )
409 {
410 mApiFetcher->stopFetching();
411 // apiFetcher and apiFetcherWorkerThread will be deleted when the thread fetchingFinished signal is emitted
412 }
413}
414
415void QgsQueryResultPanelWidget::startFetching()
416{
417 if ( !mWasCanceled )
418 {
419 if ( !mSqlErrorMessage.isEmpty() )
420 {
421 showError( tr( "SQL error" ), mSqlErrorMessage, true );
422 }
423 else
424 {
425 if ( mQueryResultWatcher.result().rowCount() != static_cast<long long>( Qgis::FeatureCountState::UnknownCount ) )
426 {
427 mStatusLabel->setText(
428 u"Query executed successfully (%1 rows, %2 ms)"_s.arg( QLocale().toString( mQueryResultWatcher.result().rowCount() ), QLocale().toString( mQueryResultWatcher.result().queryExecutionTime() ) )
429 );
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(
461 QLocale().toString( mModel->rowCount( mModel->index( -1, -1 ) ) ),
462 mActualRowCount != -1 ? QLocale().toString( mActualRowCount ) : tr( "unknown" ),
463 mWasCanceled ? tr( "(stopped)" ) : QString(),
464 QLocale().toString( mQueryResultWatcher.result().queryExecutionTime() )
465 ) );
466 mFetchedRowsBatchCount += last - first + 1;
467 mProgressBar->setValue( mFetchedRowsBatchCount );
468 } );
469
470 mQueryResultsTableView->setModel( mModel.get() );
471 mQueryResultsTableView->show();
472 mResultsContainer->show();
473
474 connect( mModel.get(), &QgsQueryResultModel::fetchingComplete, mStopButton, [this] {
475 bool ok = false;
476 const QgsHistoryEntry currentHistoryEntry = QgsGui::historyProviderRegistry()->entry( mCurrentHistoryEntryId, ok );
477 QVariantMap entryDetails = currentHistoryEntry.entry;
478 entryDetails.insert( u"rows"_s, mActualRowCount );
479 entryDetails.insert( u"time"_s, mQueryResultWatcher.result().queryExecutionTime() );
480
481 QgsGui::historyProviderRegistry()->updateEntry( mCurrentHistoryEntryId, entryDetails );
482 mProgressBar->hide();
483 mStopButton->setEnabled( false );
484 } );
485 }
486 }
487 else
488 {
489 mStatusLabel->setText( tr( "SQL command aborted" ) );
490 mProgressBar->hide();
491 }
492}
493
494void QgsQueryResultPanelWidget::showError( const QString &title, const QString &message, bool isSqlError )
495{
496 mStatusLabel->show();
497 mStatusLabel->setText( tr( "An error occurred while executing the query" ) );
498 mProgressBar->hide();
499 mQueryResultsTableView->hide();
500 if ( isSqlError )
501 {
502 mSqlErrorText->show();
503 mSqlErrorText->setText( message );
504 mResultsContainer->show();
505 }
506 else
507 {
508 mMessageBar->pushCritical( title, message );
509 mResultsContainer->hide();
510 }
511}
512
513void QgsQueryResultPanelWidget::tokensReady( const QStringList &tokens )
514{
515 mSqlEditor->setExtraKeywords( mSqlEditor->extraKeywords() + tokens );
516 mSqlErrorText->setExtraKeywords( mSqlErrorText->extraKeywords() + tokens );
517}
518
519void QgsQueryResultPanelWidget::copyResults()
520{
521 const int rowCount = mModel->rowCount( QModelIndex() );
522 const int columnCount = mModel->columnCount( QModelIndex() );
523 copyResults( 0, rowCount - 1, 0, columnCount - 1 );
524}
525
526void QgsQueryResultPanelWidget::copyResults( int fromRow, int toRow, int fromColumn, int toColumn )
527{
528 QStringList rowStrings;
529 QStringList columnStrings;
530
531 const int rowCount = mModel->rowCount( QModelIndex() );
532 const int columnCount = mModel->columnCount( QModelIndex() );
533
534 toRow = std::min( toRow, rowCount - 1 );
535 toColumn = std::min( toColumn, columnCount - 1 );
536
537 rowStrings.reserve( toRow - fromRow );
538
539 // add titles first
540 for ( int col = fromColumn; col <= toColumn; col++ )
541 {
542 columnStrings += mModel->headerData( col, Qt::Horizontal, Qt::DisplayRole ).toString();
543 }
544 rowStrings += columnStrings.join( QLatin1Char( '\t' ) );
545 columnStrings.clear();
546
547 for ( int row = fromRow; row <= toRow; row++ )
548 {
549 for ( int col = fromColumn; col <= toColumn; col++ )
550 {
551 columnStrings += mModel->data( mModel->index( row, col ), Qt::DisplayRole ).toString();
552 }
553 rowStrings += columnStrings.join( QLatin1Char( '\t' ) );
554 columnStrings.clear();
555 }
556
557 if ( !rowStrings.isEmpty() )
558 {
559 const QString text = rowStrings.join( QLatin1Char( '\n' ) );
560 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
561 .arg( text );
562 html.replace( "\t"_L1, "</td><td>"_L1 ).replace( "\n"_L1, "</td></tr><tr><td>"_L1 );
563
564 QMimeData *mdata = new QMimeData();
565 mdata->setData( u"text/html"_s, html.toUtf8() );
566 if ( !text.isEmpty() )
567 {
568 mdata->setText( text );
569 }
570 // Transfers ownership to the clipboard object
571#ifdef Q_OS_LINUX
572 QApplication::clipboard()->setMimeData( mdata, QClipboard::Selection );
573#endif
574 QApplication::clipboard()->setMimeData( mdata, QClipboard::Clipboard );
575 }
576}
577
578QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions QgsQueryResultPanelWidget::sqlVectorLayerOptions() const
579{
580 const thread_local QRegularExpression rx( u";\\s*$"_s );
581 mSqlVectorLayerOptions.sql = mSqlEditor->text().replace( rx, QString() );
582 mSqlVectorLayerOptions.filter = mFilterLineEdit->text();
583 mSqlVectorLayerOptions.primaryKeyColumns = mPkColumnsComboBox->checkedItems();
584 mSqlVectorLayerOptions.geometryColumn = mGeometryColumnComboBox->currentText();
585 mSqlVectorLayerOptions.layerName = mLayerNameLineEdit->text();
586 mSqlVectorLayerOptions.disableSelectAtId = mAvoidSelectingAsFeatureIdCheckBox->isChecked();
587 QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions options { mSqlVectorLayerOptions };
588 // Override if not used
589 if ( !mPkColumnsCheckBox->isChecked() )
590 {
591 options.primaryKeyColumns.clear();
592 }
593 if ( !mGeometryColumnCheckBox->isChecked() )
594 {
595 options.geometryColumn.clear();
596 }
597 return options;
598}
599
600void QgsQueryResultPanelWidget::setConnection( QgsAbstractDatabaseProviderConnection *connection )
601{
602 mConnection.reset( connection );
603
604 cancelApiFetcher();
605
606 if ( connection )
607 {
608 // Add provider specific APIs
609 const QMultiMap<Qgis::SqlKeywordCategory, QStringList> keywordsDict { connection->sqlDictionary() };
610 QStringList keywords;
611 for ( auto it = keywordsDict.constBegin(); it != keywordsDict.constEnd(); it++ )
612 {
613 keywords.append( it.value() );
614 }
615
616 // Add static keywords from provider
617 mSqlEditor->setExtraKeywords( keywords );
618 mSqlErrorText->setExtraKeywords( keywords );
619
620 // Add dynamic keywords in a separate thread
621 QThread *apiFetcherWorkerThread = new QThread();
622 QgsConnectionsApiFetcher *apiFetcher = new QgsConnectionsApiFetcher( mConnection->uri(), mConnection->providerKey() );
623 apiFetcher->moveToThread( apiFetcherWorkerThread );
624 connect( apiFetcherWorkerThread, &QThread::started, apiFetcher, &QgsConnectionsApiFetcher::fetchTokens );
625 connect( apiFetcher, &QgsConnectionsApiFetcher::tokensReady, this, &QgsQueryResultPanelWidget::tokensReady );
626 connect( apiFetcher, &QgsConnectionsApiFetcher::fetchingFinished, apiFetcherWorkerThread, [apiFetcher, apiFetcherWorkerThread] {
627 apiFetcherWorkerThread->quit();
628 apiFetcherWorkerThread->wait();
629 apiFetcherWorkerThread->deleteLater();
630 apiFetcher->deleteLater();
631 } );
632
633 mApiFetcher = apiFetcher;
634 apiFetcherWorkerThread->start();
635 }
636
637 updateButtons();
638}
639
640void QgsQueryResultPanelWidget::setQuery( const QString &sql )
641{
642 mSqlEditor->setText( sql );
643}
644
645void QgsQueryResultPanelWidget::notify( const QString &title, const QString &text, Qgis::MessageLevel level )
646{
647 mMessageBar->pushMessage( title, text, level );
648}
649
650void QgsQueryResultPanelWidget::storeCurrentQuery( Qgis::QueryStorageBackend backend )
651{
652 const QStringList existingQueryNames = QgsGui::storedQueryManager()->allQueryNames( backend );
653 QgsNewNameDialog dlg( QString(), QString(), QStringList(), existingQueryNames );
654 dlg.setWindowTitle( tr( "Store Query" ) );
655 dlg.setHintString( tr( "Name for the stored query" ) );
656 dlg.setOverwriteEnabled( true );
657 dlg.setConflictingNameWarning( tr( "A stored query with this name already exists, it will be overwritten." ) );
658 dlg.setShowExistingNamesCompleter( true );
659 if ( dlg.exec() != QDialog::Accepted )
660 return;
661
662 const QString name = dlg.name();
663 if ( name.isEmpty() )
664 return;
665
666 QgsGui::storedQueryManager()->storeQuery( name, mSqlEditor->text(), backend );
668 {
670 }
671}
672
673//
674// QgsQueryResultWidget
675//
676
677QgsQueryResultWidget::QgsQueryResultWidget( QWidget *parent, QgsAbstractDatabaseProviderConnection *connection )
678 : QWidget( parent )
679{
680 setupUi( this );
681
682 mToolBar->setIconSize( QgsGuiUtils::iconSize( false ) );
683
684 mQueryWidget = new QgsQueryResultPanelWidget( nullptr, connection );
685 mPanelStack->setMainPanel( mQueryWidget );
686
687 mPresetQueryMenu = new QMenu( this );
688 connect( mPresetQueryMenu, &QMenu::aboutToShow, this, &QgsQueryResultWidget::populatePresetQueryMenu );
689
690 QToolButton *presetQueryButton = new QToolButton();
691 presetQueryButton->setMenu( mPresetQueryMenu );
692 presetQueryButton->setIcon( QgsApplication::getThemeIcon( u"mIconStoredQueries.svg"_s ) );
693 presetQueryButton->setPopupMode( QToolButton::InstantPopup );
694 mToolBar->addWidget( presetQueryButton );
695
696 connect( mActionOpenQuery, &QAction::triggered, this, &QgsQueryResultWidget::openQuery );
697 connect( mActionSaveQuery, &QAction::triggered, this, [this] { saveQuery( false ); } );
698 connect( mActionSaveQueryAs, &QAction::triggered, this, [this] { saveQuery( true ); } );
699
700 connect( mActionCut, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::cut );
701 connect( mActionCopy, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::copy );
702 connect( mActionPaste, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::paste );
703 connect( mActionUndo, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::undo );
704 connect( mActionRedo, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::redo );
705 mActionUndo->setEnabled( false );
706 mActionRedo->setEnabled( false );
707
708 connect( mActionFindReplace, &QAction::toggled, mQueryWidget->codeEditorWidget(), &QgsCodeEditorWidget::setSearchBarVisible );
709 connect( mQueryWidget->codeEditorWidget(), &QgsCodeEditorWidget::searchBarToggled, mActionFindReplace, &QAction::setChecked );
710 connect( mQueryWidget->sqlEditor(), &QgsCodeEditor::modificationChanged, this, &QgsQueryResultWidget::setHasChanged );
711
712 connect( mActionShowHistory, &QAction::toggled, this, &QgsQueryResultWidget::showHistoryPanel );
713
714 connect( mActionClear, &QAction::triggered, this, [this] {
715 // Cannot use setText() because it resets the undo/redo buffer.
716 mQueryWidget->sqlEditor()->SendScintilla( QsciScintilla::SCI_SETTEXT, "" );
717 } );
718
719 connect( mQueryWidget->sqlEditor(), &QgsCodeEditorSQL::textChanged, this, &QgsQueryResultWidget::updateButtons );
720
721 connect( mQueryWidget->sqlEditor(), &QgsCodeEditorSQL::copyAvailable, mActionCut, &QAction::setEnabled );
722 connect( mQueryWidget->sqlEditor(), &QgsCodeEditorSQL::copyAvailable, mActionCopy, &QAction::setEnabled );
723
724 connect( mQueryWidget, &QgsQueryResultPanelWidget::createSqlVectorLayer, this, &QgsQueryResultWidget::createSqlVectorLayer );
725 connect( mQueryWidget, &QgsQueryResultPanelWidget::firstResultBatchFetched, this, &QgsQueryResultWidget::firstResultBatchFetched );
726
727 updateButtons();
728 setHasChanged( false );
729}
730
731QgsQueryResultWidget::~QgsQueryResultWidget()
732{
733 if ( mHistoryWidget )
734 {
735 mPanelStack->closePanel( mHistoryWidget );
736 mHistoryWidget->deleteLater();
737 }
738}
739
740void QgsQueryResultWidget::setSqlVectorLayerOptions( const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options )
741{
742 if ( !options.sql.isEmpty() )
743 {
744 setQuery( options.sql );
745 }
746 mQueryWidget->setSqlVectorLayerOptions( options );
747}
748
749void QgsQueryResultWidget::setWidgetMode( QueryWidgetMode widgetMode )
750{
751 mQueryWidget->setWidgetMode( widgetMode );
752}
753
754void QgsQueryResultWidget::executeQuery()
755{
756 mQueryWidget->executeQuery();
757}
758
759void QgsQueryResultWidget::updateButtons()
760{
761 mQueryWidget->updateButtons();
762
763 const bool isEmpty = mQueryWidget->sqlEditor()->text().isEmpty();
764 mActionClear->setEnabled( !isEmpty );
765 mActionUndo->setEnabled( mQueryWidget->sqlEditor()->isUndoAvailable() );
766 mActionRedo->setEnabled( mQueryWidget->sqlEditor()->isRedoAvailable() );
767}
768
769void QgsQueryResultWidget::showError( const QString &title, const QString &message, bool isSqlError )
770{
771 mQueryWidget->showError( title, message, isSqlError );
772}
773
774void QgsQueryResultWidget::tokensReady( const QStringList & )
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 )
792 == QMessageBox::StandardButton::No )
793 return;
794 }
795
796 QString initialDir = settingLastSourceFolder->value();
797 if ( initialDir.isEmpty() )
798 initialDir = QDir::homePath();
799
800 const QString fileName = QFileDialog::getOpenFileName( this, tr( "Open Query" ), initialDir, tr( "SQL queries (*.sql *.SQL)" ) + u";;"_s + QObject::tr( "All files" ) + u" (*.*)"_s );
801
802 if ( fileName.isEmpty() )
803 return;
804
805 QFileInfo fi( fileName );
806 settingLastSourceFolder->setValue( fi.path() );
807
808 QgsTemporaryCursorOverride cursor( Qt::CursorShape::WaitCursor );
809
810 mQueryWidget->codeEditorWidget()->loadFile( fileName );
811 setHasChanged( false );
812}
813
814void QgsQueryResultWidget::saveQuery( bool saveAs )
815{
816 if ( mQueryWidget->codeEditorWidget()->filePath().isEmpty() || saveAs )
817 {
818 QString selectedFilter;
819
820 QString initialDir = settingLastSourceFolder->value();
821 if ( initialDir.isEmpty() )
822 initialDir = QDir::homePath();
823
824 QString newPath = QFileDialog::getSaveFileName( this, tr( "Save Query" ), initialDir, tr( "SQL queries (*.sql *.SQL)" ) + u";;"_s + QObject::tr( "All files" ) + u" (*.*)"_s, &selectedFilter );
825
826 if ( !newPath.isEmpty() )
827 {
828 QFileInfo fi( newPath );
829 settingLastSourceFolder->setValue( fi.path() );
830
831 if ( !selectedFilter.contains( u"*.*)"_s ) )
832 newPath = QgsFileUtils::ensureFileNameHasExtension( newPath, { u"sql"_s } );
833 mQueryWidget->codeEditorWidget()->save( newPath );
834 setHasChanged( false );
835 }
836 }
837 else if ( !mQueryWidget->codeEditorWidget()->filePath().isEmpty() )
838 {
839 mQueryWidget->codeEditorWidget()->save();
840 setHasChanged( false );
841 }
842}
843
844void QgsQueryResultWidget::setConnection( QgsAbstractDatabaseProviderConnection *connection )
845{
846 mQueryWidget->setConnection( connection );
847 updateButtons();
848}
849
850void QgsQueryResultWidget::setQuery( const QString &sql )
851{
852 mQueryWidget->sqlEditor()->setText( sql );
853 // from the QScintilla docs, calling setText clears undo history!
854 mActionUndo->setEnabled( false );
855 mActionRedo->setEnabled( false );
856}
857
858bool QgsQueryResultWidget::promptUnsavedChanges()
859{
860 if ( !mQueryWidget->codeEditorWidget()->filePath().isEmpty() && mHasChangedFileContents )
861 {
862 const QMessageBox::StandardButton ret = QMessageBox::question(
863 this,
864 tr( "Save Query?" ),
865 tr( "There are unsaved changes in this query. Do you want to save those?" ),
866 QMessageBox::StandardButton::Save | QMessageBox::StandardButton::Cancel | QMessageBox::StandardButton::Discard,
867 QMessageBox::StandardButton::Cancel
868 );
869
870 if ( ret == QMessageBox::StandardButton::Save )
871 {
872 saveQuery( false );
873 return true;
874 }
875 else if ( ret == QMessageBox::StandardButton::Discard )
876 {
877 return true;
878 }
879 else
880 {
881 return false;
882 }
883 }
884 else
885 {
886 return true;
887 }
888}
889
890
891void QgsQueryResultWidget::notify( const QString &title, const QString &text, Qgis::MessageLevel level )
892{
893 mQueryWidget->notify( title, text, level );
894}
895
896
897void QgsQueryResultWidget::setHasChanged( bool hasChanged )
898{
899 mActionSaveQuery->setEnabled( hasChanged );
900 mHasChangedFileContents = hasChanged;
901 updateDialogTitle();
902}
903
904void QgsQueryResultWidget::updateDialogTitle()
905{
906 QString fileName;
907 if ( !mQueryWidget->codeEditorWidget()->filePath().isEmpty() )
908 {
909 const QFileInfo fi( mQueryWidget->codeEditorWidget()->filePath() );
910 fileName = fi.fileName();
911 if ( mHasChangedFileContents )
912 {
913 fileName.prepend( '*' );
914 }
915 }
916
917 emit requestDialogTitleUpdate( fileName );
918}
919
920void QgsQueryResultWidget::populatePresetQueryMenu()
921{
922 mPresetQueryMenu->clear();
923
924 QMenu *storeQueryMenu = new QMenu( tr( "Store Current Query" ), mPresetQueryMenu );
925 mPresetQueryMenu->addMenu( storeQueryMenu );
926 QAction *storeInProfileAction = new QAction( tr( "In User Profile…" ), storeQueryMenu );
927 storeQueryMenu->addAction( storeInProfileAction );
928 storeInProfileAction->setEnabled( !mQueryWidget->sqlEditor()->text().isEmpty() );
929 connect( storeInProfileAction, &QAction::triggered, this, [this] { storeCurrentQuery( Qgis::QueryStorageBackend::LocalProfile ); } );
930 QAction *storeInProjectAction = new QAction( tr( "In Current Project…" ), storeQueryMenu );
931 storeQueryMenu->addAction( storeInProjectAction );
932 storeInProjectAction->setEnabled( !mQueryWidget->sqlEditor()->text().isEmpty() );
933 connect( storeInProjectAction, &QAction::triggered, this, [this] { storeCurrentQuery( Qgis::QueryStorageBackend::CurrentProject ); } );
934
935
936 const QList< QgsStoredQueryManager::QueryDetails > storedQueries = QgsGui::storedQueryManager()->allQueries();
937 if ( !storedQueries.isEmpty() )
938 {
939 QList< QgsStoredQueryManager::QueryDetails > userProfileQueries;
940 std::copy_if( storedQueries.begin(), storedQueries.end(), std::back_inserter( userProfileQueries ), []( const QgsStoredQueryManager::QueryDetails &details ) {
941 return details.backend == Qgis::QueryStorageBackend::LocalProfile;
942 } );
943
944 QList< QgsStoredQueryManager::QueryDetails > projectQueries;
945 std::copy_if( storedQueries.begin(), storedQueries.end(), std::back_inserter( projectQueries ), []( const QgsStoredQueryManager::QueryDetails &details ) {
946 return details.backend == Qgis::QueryStorageBackend::CurrentProject;
947 } );
948
949 mPresetQueryMenu->addSection( QgsApplication::getThemeIcon( u"mIconStoredQueries.svg"_s ), tr( "User Profile" ) );
950 for ( const QgsStoredQueryManager::QueryDetails &query : std::as_const( userProfileQueries ) )
951 {
952 QAction *action = new QAction( query.name, mPresetQueryMenu );
953 mPresetQueryMenu->addAction( action );
954 connect( action, &QAction::triggered, this, [this, query] { mQueryWidget->sqlEditor()->insertText( query.definition ); } );
955 }
956 if ( userProfileQueries.empty() )
957 {
958 QAction *action = new QAction( tr( "No Stored Queries Available" ), mPresetQueryMenu );
959 action->setEnabled( false );
960 mPresetQueryMenu->addAction( action );
961 }
962
963 mPresetQueryMenu->addSection( QgsApplication::getThemeIcon( u"mIconStoredQueries.svg"_s ), tr( "Current Project" ) );
964 for ( const QgsStoredQueryManager::QueryDetails &query : std::as_const( projectQueries ) )
965 {
966 QAction *action = new QAction( query.name, mPresetQueryMenu );
967 mPresetQueryMenu->addAction( action );
968 connect( action, &QAction::triggered, this, [this, query] { mQueryWidget->sqlEditor()->insertText( query.definition ); } );
969 }
970 if ( projectQueries.empty() )
971 {
972 QAction *action = new QAction( tr( "No Stored Queries Available" ), mPresetQueryMenu );
973 action->setEnabled( false );
974 mPresetQueryMenu->addAction( action );
975 }
976
977 mPresetQueryMenu->addSeparator();
978
979 QMenu *removeQueryMenu = new QMenu( tr( "Removed Stored Query" ), mPresetQueryMenu );
980 mPresetQueryMenu->addMenu( removeQueryMenu );
981
982 for ( const QgsStoredQueryManager::QueryDetails &query : storedQueries )
983 {
984 QAction *action = new QAction( tr( "%1…" ).arg( query.name ), mPresetQueryMenu );
985 removeQueryMenu->addAction( action );
986 connect( action, &QAction::triggered, this, [this, query] {
987 const QMessageBox::StandardButton res
988 = 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 );
989 if ( res == QMessageBox::Yes )
990 {
991 QgsGui::storedQueryManager()->removeQuery( query.name, query.backend );
992 if ( query.backend == Qgis::QueryStorageBackend::CurrentProject )
993 {
995 }
996 }
997 } );
998 }
999 }
1000}
1001
1002void QgsQueryResultWidget::storeCurrentQuery( Qgis::QueryStorageBackend backend )
1003{
1004 const QStringList existingQueryNames = QgsGui::storedQueryManager()->allQueryNames( backend );
1005 QgsNewNameDialog dlg( QString(), QString(), QStringList(), existingQueryNames );
1006 dlg.setWindowTitle( tr( "Store Query" ) );
1007 dlg.setHintString( tr( "Name for the stored query" ) );
1008 dlg.setOverwriteEnabled( true );
1009 dlg.setConflictingNameWarning( tr( "A stored query with this name already exists, it will be overwritten." ) );
1010 dlg.setShowExistingNamesCompleter( true );
1011 if ( dlg.exec() != QDialog::Accepted )
1012 return;
1013
1014 const QString name = dlg.name();
1015 if ( name.isEmpty() )
1016 return;
1017
1018 QgsGui::storedQueryManager()->storeQuery( name, mQueryWidget->sqlEditor()->text(), backend );
1020 {
1022 }
1023}
1024
1025
1026void QgsQueryResultWidget::showHistoryPanel( bool show )
1027{
1028 if ( show )
1029 {
1030 mHistoryWidget = new QgsDatabaseQueryHistoryWidget();
1031 mHistoryWidget->setPanelTitle( tr( "SQL History" ) );
1032 mPanelStack->showPanel( mHistoryWidget );
1033 connect( mHistoryWidget, &QgsPanelWidget::panelAccepted, this, [this] { whileBlocking( mActionShowHistory )->setChecked( false ); } );
1034 connect( mHistoryWidget, &QgsDatabaseQueryHistoryWidget::sqlTriggered, this, [this]( const QString &connectionUri, const QString &provider, const QString &sql ) {
1035 Q_UNUSED( connectionUri );
1036 Q_UNUSED( provider );
1037
1038 mQueryWidget->sqlEditor()->setText( sql );
1039 mActionUndo->setEnabled( false );
1040 mActionRedo->setEnabled( false );
1041 mHistoryWidget->acceptPanel();
1042 } );
1043 }
1044 else if ( mHistoryWidget )
1045 {
1046 mPanelStack->closePanel( mHistoryWidget );
1047 mHistoryWidget->deleteLater();
1048 }
1049}
1050
1051
1053
1054void QgsConnectionsApiFetcher::fetchTokens()
1055{
1056 if ( mStopFetching )
1057 {
1058 emit fetchingFinished();
1059 return;
1060 }
1061
1062
1064 if ( !md )
1065 {
1066 emit fetchingFinished();
1067 return;
1068 }
1069 std::unique_ptr<QgsAbstractDatabaseProviderConnection> connection( static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( mUri, {} ) ) );
1070 if ( !mStopFetching && connection )
1071 {
1072 mFeedback = std::make_unique<QgsFeedback>();
1073 QStringList schemas;
1075 {
1076 try
1077 {
1078 schemas = connection->schemas();
1079 emit tokensReady( schemas );
1080 }
1081 catch ( QgsProviderConnectionException &ex )
1082 {
1083 QgsMessageLog::logMessage( tr( "Error retrieving schemas: %1" ).arg( ex.what() ), u"QGIS"_s, Qgis::MessageLevel::Warning );
1084 }
1085 }
1086 else
1087 {
1088 schemas.push_back( QString() ); // Fake empty schema for DBs not supporting it
1089 }
1090
1091 for ( const auto &schema : std::as_const( schemas ) )
1092 {
1093 if ( mStopFetching )
1094 {
1095 connection.reset();
1096 emit fetchingFinished();
1097 return;
1098 }
1099
1100 QStringList tableNames;
1101 try
1102 {
1103 const QList<QgsAbstractDatabaseProviderConnection::TableProperty> tables = connection->tables( schema, QgsAbstractDatabaseProviderConnection::TableFlags(), mFeedback.get() );
1104 for ( const QgsAbstractDatabaseProviderConnection::TableProperty &table : std::as_const( tables ) )
1105 {
1106 if ( mStopFetching )
1107 {
1108 connection.reset();
1109 emit fetchingFinished();
1110 return;
1111 }
1112 tableNames.push_back( table.tableName() );
1113 }
1114 emit tokensReady( tableNames );
1115 }
1116 catch ( QgsProviderConnectionException &ex )
1117 {
1118 QgsMessageLog::logMessage( tr( "Error retrieving tables: %1" ).arg( ex.what() ), u"QGIS"_s, Qgis::MessageLevel::Warning );
1119 }
1120
1121 // Get fields
1122 for ( const auto &table : std::as_const( tableNames ) )
1123 {
1124 if ( mStopFetching )
1125 {
1126 connection.reset();
1127 emit fetchingFinished();
1128 return;
1129 }
1130
1131 QStringList fieldNames;
1132 try
1133 {
1134 const QgsFields fields( connection->fields( schema, table, mFeedback.get() ) );
1135 if ( mStopFetching )
1136 {
1137 connection.reset();
1138 emit fetchingFinished();
1139 return;
1140 }
1141
1142 for ( const auto &field : std::as_const( fields ) )
1143 {
1144 fieldNames.push_back( field.name() );
1145 if ( mStopFetching )
1146 {
1147 connection.reset();
1148 emit fetchingFinished();
1149 return;
1150 }
1151 }
1152 emit tokensReady( fieldNames );
1153 }
1154 catch ( QgsProviderConnectionException &ex )
1155 {
1156 QgsMessageLog::logMessage( tr( "Error retrieving fields for table %1: %2" ).arg( table, ex.what() ), u"QGIS"_s, Qgis::MessageLevel::Warning );
1157 }
1158 }
1159 }
1160 }
1161
1162 connection.reset();
1163 emit fetchingFinished();
1164}
1165
1166void QgsConnectionsApiFetcher::stopFetching()
1167{
1168 mStopFetching = 1;
1169 if ( mFeedback )
1170 mFeedback->cancel();
1171}
1172
1173QgsQueryResultItemDelegate::QgsQueryResultItemDelegate( QObject *parent )
1174 : QStyledItemDelegate( parent )
1175{}
1176
1177QString QgsQueryResultItemDelegate::displayText( const QVariant &value, const QLocale &locale ) const
1178{
1179 Q_UNUSED( locale )
1180 QString result { QgsExpressionUtils::toLocalizedString( value ) };
1181 // Show no more than 255 characters
1182 if ( result.length() > 255 )
1183 {
1184 result.truncate( 255 );
1185 result.append( u"…"_s );
1186 }
1187 return result;
1188}
1189
1191
1192//
1193// QgsQueryResultDialog
1194//
1195
1196QgsQueryResultDialog::QgsQueryResultDialog( QgsAbstractDatabaseProviderConnection *connection, QWidget *parent )
1197 : QDialog( parent )
1198{
1199 setObjectName( u"QgsQueryResultDialog"_s );
1201
1202 mWidget = new QgsQueryResultWidget( this, connection );
1203 QVBoxLayout *l = new QVBoxLayout();
1204 l->setContentsMargins( 6, 6, 6, 6 );
1205
1206 QDialogButtonBox *mButtonBox = new QDialogButtonBox( QDialogButtonBox::StandardButton::Close | QDialogButtonBox::StandardButton::Help );
1207 connect( mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::close );
1208 connect( mButtonBox, &QDialogButtonBox::helpRequested, this, [] { QgsHelp::openHelp( u"managing_data_source/create_layers.html#execute-sql"_s ); } );
1209 l->addWidget( mWidget );
1210 l->addWidget( mButtonBox );
1211
1212 setLayout( l );
1213}
1214
1215void QgsQueryResultDialog::closeEvent( QCloseEvent *event )
1216{
1217 if ( !mWidget->promptUnsavedChanges() )
1218 {
1219 event->ignore();
1220 }
1221 else
1222 {
1223 event->accept();
1224 }
1225}
1226
1227//
1228// QgsQueryResultMainWindow
1229//
1230
1231QgsQueryResultMainWindow::QgsQueryResultMainWindow( QgsAbstractDatabaseProviderConnection *connection, const QString &identifierName )
1232 : mIdentifierName( identifierName )
1233{
1234 setObjectName( u"SQLCommandsDialog"_s );
1235
1237
1238 mWidget = new QgsQueryResultWidget( nullptr, connection );
1239 setCentralWidget( mWidget );
1240 mWidget->layout()->setContentsMargins( 6, 6, 6, 6 );
1241
1242 connect( mWidget, &QgsQueryResultWidget::requestDialogTitleUpdate, this, &QgsQueryResultMainWindow::updateWindowTitle );
1243
1244 updateWindowTitle( QString() );
1245}
1246
1247void QgsQueryResultMainWindow::closeEvent( QCloseEvent *event )
1248{
1249 if ( !mWidget->promptUnsavedChanges() )
1250 {
1251 event->ignore();
1252 }
1253 else
1254 {
1255 event->accept();
1256 }
1257}
1258
1259void QgsQueryResultMainWindow::updateWindowTitle( const QString &fileName )
1260{
1261 if ( fileName.isEmpty() )
1262 {
1263 if ( !mIdentifierName.isEmpty() )
1264 setWindowTitle( tr( "%1 — Execute SQL" ).arg( mIdentifierName ) );
1265 else
1266 setWindowTitle( tr( "Execute SQL" ) );
1267 }
1268 else
1269 {
1270 if ( !mIdentifierName.isEmpty() )
1271 setWindowTitle( tr( "%1 — %2 — Execute SQL" ).arg( fileName, mIdentifierName ) );
1272 else
1273 setWindowTitle( tr( "%1 — Execute SQL" ).arg( fileName ) );
1274 }
1275}
MessageLevel
Level for messages This will be used both for message log and message bar in application.
Definition qgis.h:160
@ Warning
Warning message.
Definition qgis.h:162
QueryStorageBackend
Stored query storage backends.
Definition qgis.h:3631
@ CurrentProject
Current QGIS project.
Definition qgis.h:3633
@ LocalProfile
Local user profile.
Definition qgis.h:3632
@ UnstableFeatureIds
SQL layer definition supports disabling select at id.
Definition qgis.h:1121
@ SubsetStringFilter
SQL layer definition supports subset string filter.
Definition qgis.h:1118
@ PrimaryKeys
SQL layer definition supports primary keys.
Definition qgis.h:1120
@ GeometryColumn
SQL layer definition supports geometry column.
Definition qgis.h:1119
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(), Qgis::StringFormat format=Qgis::StringFormat::PlainText)
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:573
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:6880
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.