QGIS API Documentation 4.3.0-Master (18b5e825726)
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 if ( QApplication::clipboard()->supportsSelection() )
572 {
573 QApplication::clipboard()->setMimeData( mdata, QClipboard::Selection );
574 }
575 QApplication::clipboard()->setMimeData( mdata, QClipboard::Clipboard );
576 }
577}
578
579QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions QgsQueryResultPanelWidget::sqlVectorLayerOptions() const
580{
581 const thread_local QRegularExpression rx( u";\\s*$"_s );
582 mSqlVectorLayerOptions.sql = mSqlEditor->text().replace( rx, QString() );
583 mSqlVectorLayerOptions.filter = mFilterLineEdit->text();
584 mSqlVectorLayerOptions.primaryKeyColumns = mPkColumnsComboBox->checkedItems();
585 mSqlVectorLayerOptions.geometryColumn = mGeometryColumnComboBox->currentText();
586 mSqlVectorLayerOptions.layerName = mLayerNameLineEdit->text();
587 mSqlVectorLayerOptions.disableSelectAtId = mAvoidSelectingAsFeatureIdCheckBox->isChecked();
588 QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions options { mSqlVectorLayerOptions };
589 // Override if not used
590 if ( !mPkColumnsCheckBox->isChecked() )
591 {
592 options.primaryKeyColumns.clear();
593 }
594 if ( !mGeometryColumnCheckBox->isChecked() )
595 {
596 options.geometryColumn.clear();
597 }
598 return options;
599}
600
601void QgsQueryResultPanelWidget::setConnection( QgsAbstractDatabaseProviderConnection *connection )
602{
603 mConnection.reset( connection );
604
605 cancelApiFetcher();
606
607 if ( connection )
608 {
609 // Add provider specific APIs
610 const QMultiMap<Qgis::SqlKeywordCategory, QStringList> keywordsDict { connection->sqlDictionary() };
611 QStringList keywords;
612 for ( auto it = keywordsDict.constBegin(); it != keywordsDict.constEnd(); it++ )
613 {
614 keywords.append( it.value() );
615 }
616
617 // Add static keywords from provider
618 mSqlEditor->setExtraKeywords( keywords );
619 mSqlErrorText->setExtraKeywords( keywords );
620
621 // Add dynamic keywords in a separate thread
622 QThread *apiFetcherWorkerThread = new QThread();
623 QgsConnectionsApiFetcher *apiFetcher = new QgsConnectionsApiFetcher( mConnection->uri(), mConnection->providerKey() );
624 apiFetcher->moveToThread( apiFetcherWorkerThread );
625 connect( apiFetcherWorkerThread, &QThread::started, apiFetcher, &QgsConnectionsApiFetcher::fetchTokens );
626 connect( apiFetcher, &QgsConnectionsApiFetcher::tokensReady, this, &QgsQueryResultPanelWidget::tokensReady );
627 connect( apiFetcher, &QgsConnectionsApiFetcher::fetchingFinished, apiFetcherWorkerThread, [apiFetcher, apiFetcherWorkerThread] {
628 apiFetcherWorkerThread->quit();
629 apiFetcherWorkerThread->wait();
630 apiFetcherWorkerThread->deleteLater();
631 apiFetcher->deleteLater();
632 } );
633
634 mApiFetcher = apiFetcher;
635 apiFetcherWorkerThread->start();
636 }
637
638 updateButtons();
639}
640
641void QgsQueryResultPanelWidget::setQuery( const QString &sql )
642{
643 mSqlEditor->setText( sql );
644}
645
646void QgsQueryResultPanelWidget::notify( const QString &title, const QString &text, Qgis::MessageLevel level )
647{
648 mMessageBar->pushMessage( title, text, level );
649}
650
651void QgsQueryResultPanelWidget::storeCurrentQuery( Qgis::QueryStorageBackend backend )
652{
653 const QStringList existingQueryNames = QgsGui::storedQueryManager()->allQueryNames( backend );
654 QgsNewNameDialog dlg( QString(), QString(), QStringList(), existingQueryNames );
655 dlg.setWindowTitle( tr( "Store Query" ) );
656 dlg.setHintString( tr( "Name for the stored query" ) );
657 dlg.setOverwriteEnabled( true );
658 dlg.setConflictingNameWarning( tr( "A stored query with this name already exists, it will be overwritten." ) );
659 dlg.setShowExistingNamesCompleter( true );
660 if ( dlg.exec() != QDialog::Accepted )
661 return;
662
663 const QString name = dlg.name();
664 if ( name.isEmpty() )
665 return;
666
667 QgsGui::storedQueryManager()->storeQuery( name, mSqlEditor->text(), backend );
669 {
671 }
672}
673
674//
675// QgsQueryResultWidget
676//
677
678QgsQueryResultWidget::QgsQueryResultWidget( QWidget *parent, QgsAbstractDatabaseProviderConnection *connection )
679 : QWidget( parent )
680{
681 setupUi( this );
682
683 mToolBar->setIconSize( QgsGuiUtils::iconSize( false ) );
684
685 mQueryWidget = new QgsQueryResultPanelWidget( nullptr, connection );
686 mPanelStack->setMainPanel( mQueryWidget );
687
688 mPresetQueryMenu = new QMenu( this );
689 connect( mPresetQueryMenu, &QMenu::aboutToShow, this, &QgsQueryResultWidget::populatePresetQueryMenu );
690
691 QToolButton *presetQueryButton = new QToolButton();
692 presetQueryButton->setMenu( mPresetQueryMenu );
693 presetQueryButton->setIcon( QgsApplication::getThemeIcon( u"mIconStoredQueries.svg"_s ) );
694 presetQueryButton->setPopupMode( QToolButton::InstantPopup );
695 mToolBar->addWidget( presetQueryButton );
696
697 connect( mActionOpenQuery, &QAction::triggered, this, &QgsQueryResultWidget::openQuery );
698 connect( mActionSaveQuery, &QAction::triggered, this, [this] { saveQuery( false ); } );
699 connect( mActionSaveQueryAs, &QAction::triggered, this, [this] { saveQuery( true ); } );
700
701 connect( mActionCut, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::cut );
702 connect( mActionCopy, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::copy );
703 connect( mActionPaste, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::paste );
704 connect( mActionUndo, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::undo );
705 connect( mActionRedo, &QAction::triggered, mQueryWidget->sqlEditor(), &QgsCodeEditor::redo );
706 mActionUndo->setEnabled( false );
707 mActionRedo->setEnabled( false );
708
709 connect( mActionFindReplace, &QAction::toggled, mQueryWidget->codeEditorWidget(), &QgsCodeEditorWidget::setSearchBarVisible );
710 connect( mQueryWidget->codeEditorWidget(), &QgsCodeEditorWidget::searchBarToggled, mActionFindReplace, &QAction::setChecked );
711 connect( mQueryWidget->sqlEditor(), &QgsCodeEditor::modificationChanged, this, &QgsQueryResultWidget::setHasChanged );
712
713 connect( mActionShowHistory, &QAction::toggled, this, &QgsQueryResultWidget::showHistoryPanel );
714
715 connect( mActionClear, &QAction::triggered, this, [this] {
716 // Cannot use setText() because it resets the undo/redo buffer.
717 mQueryWidget->sqlEditor()->SendScintilla( QsciScintilla::SCI_SETTEXT, "" );
718 } );
719
720 connect( mQueryWidget->sqlEditor(), &QgsCodeEditorSQL::textChanged, this, &QgsQueryResultWidget::updateButtons );
721
722 connect( mQueryWidget->sqlEditor(), &QgsCodeEditorSQL::copyAvailable, mActionCut, &QAction::setEnabled );
723 connect( mQueryWidget->sqlEditor(), &QgsCodeEditorSQL::copyAvailable, mActionCopy, &QAction::setEnabled );
724
725 connect( mQueryWidget, &QgsQueryResultPanelWidget::createSqlVectorLayer, this, &QgsQueryResultWidget::createSqlVectorLayer );
726 connect( mQueryWidget, &QgsQueryResultPanelWidget::firstResultBatchFetched, this, &QgsQueryResultWidget::firstResultBatchFetched );
727
728 updateButtons();
729 setHasChanged( false );
730}
731
732QgsQueryResultWidget::~QgsQueryResultWidget()
733{
734 if ( mHistoryWidget )
735 {
736 mPanelStack->closePanel( mHistoryWidget );
737 mHistoryWidget->deleteLater();
738 }
739}
740
741void QgsQueryResultWidget::setSqlVectorLayerOptions( const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options )
742{
743 if ( !options.sql.isEmpty() )
744 {
745 setQuery( options.sql );
746 }
747 mQueryWidget->setSqlVectorLayerOptions( options );
748}
749
750void QgsQueryResultWidget::setWidgetMode( QueryWidgetMode widgetMode )
751{
752 mQueryWidget->setWidgetMode( widgetMode );
753}
754
755void QgsQueryResultWidget::executeQuery()
756{
757 mQueryWidget->executeQuery();
758}
759
760void QgsQueryResultWidget::updateButtons()
761{
762 mQueryWidget->updateButtons();
763
764 const bool isEmpty = mQueryWidget->sqlEditor()->text().isEmpty();
765 mActionClear->setEnabled( !isEmpty );
766 mActionUndo->setEnabled( mQueryWidget->sqlEditor()->isUndoAvailable() );
767 mActionRedo->setEnabled( mQueryWidget->sqlEditor()->isRedoAvailable() );
768}
769
770void QgsQueryResultWidget::showError( const QString &title, const QString &message, bool isSqlError )
771{
772 mQueryWidget->showError( title, message, isSqlError );
773}
774
775void QgsQueryResultWidget::tokensReady( const QStringList & )
776{}
777
778void QgsQueryResultWidget::copyResults()
779{
780 mQueryWidget->copyResults();
781}
782
783void QgsQueryResultWidget::copyResults( int fromRow, int toRow, int fromColumn, int toColumn )
784{
785 mQueryWidget->copyResults( fromRow, toRow, fromColumn, toColumn );
786}
787
788void QgsQueryResultWidget::openQuery()
789{
790 if ( !mQueryWidget->codeEditorWidget()->filePath().isEmpty() && mHasChangedFileContents )
791 {
792 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 )
793 == QMessageBox::StandardButton::No )
794 return;
795 }
796
797 QString initialDir = settingLastSourceFolder->value();
798 if ( initialDir.isEmpty() )
799 initialDir = QDir::homePath();
800
801 const QString fileName = QFileDialog::getOpenFileName( this, tr( "Open Query" ), initialDir, tr( "SQL queries (*.sql *.SQL)" ) + u";;"_s + QObject::tr( "All files" ) + u" (*.*)"_s );
802
803 if ( fileName.isEmpty() )
804 return;
805
806 QFileInfo fi( fileName );
807 settingLastSourceFolder->setValue( fi.path() );
808
809 QgsTemporaryCursorOverride cursor( Qt::CursorShape::WaitCursor );
810
811 mQueryWidget->codeEditorWidget()->loadFile( fileName );
812 setHasChanged( false );
813}
814
815void QgsQueryResultWidget::saveQuery( bool saveAs )
816{
817 if ( mQueryWidget->codeEditorWidget()->filePath().isEmpty() || saveAs )
818 {
819 QString selectedFilter;
820
821 QString initialDir = settingLastSourceFolder->value();
822 if ( initialDir.isEmpty() )
823 initialDir = QDir::homePath();
824
825 QString newPath = QFileDialog::getSaveFileName( this, tr( "Save Query" ), initialDir, tr( "SQL queries (*.sql *.SQL)" ) + u";;"_s + QObject::tr( "All files" ) + u" (*.*)"_s, &selectedFilter );
826
827 if ( !newPath.isEmpty() )
828 {
829 QFileInfo fi( newPath );
830 settingLastSourceFolder->setValue( fi.path() );
831
832 if ( !selectedFilter.contains( u"*.*)"_s ) )
833 newPath = QgsFileUtils::ensureFileNameHasExtension( newPath, { u"sql"_s } );
834 mQueryWidget->codeEditorWidget()->save( newPath );
835 setHasChanged( false );
836 }
837 }
838 else if ( !mQueryWidget->codeEditorWidget()->filePath().isEmpty() )
839 {
840 mQueryWidget->codeEditorWidget()->save();
841 setHasChanged( false );
842 }
843}
844
845void QgsQueryResultWidget::setConnection( QgsAbstractDatabaseProviderConnection *connection )
846{
847 mQueryWidget->setConnection( connection );
848 updateButtons();
849}
850
851void QgsQueryResultWidget::setQuery( const QString &sql )
852{
853 mQueryWidget->sqlEditor()->setText( sql );
854 // from the QScintilla docs, calling setText clears undo history!
855 mActionUndo->setEnabled( false );
856 mActionRedo->setEnabled( false );
857}
858
859bool QgsQueryResultWidget::promptUnsavedChanges()
860{
861 if ( !mQueryWidget->codeEditorWidget()->filePath().isEmpty() && mHasChangedFileContents )
862 {
863 const QMessageBox::StandardButton ret = QMessageBox::question(
864 this,
865 tr( "Save Query?" ),
866 tr( "There are unsaved changes in this query. Do you want to save those?" ),
867 QMessageBox::StandardButton::Save | QMessageBox::StandardButton::Cancel | QMessageBox::StandardButton::Discard,
868 QMessageBox::StandardButton::Cancel
869 );
870
871 if ( ret == QMessageBox::StandardButton::Save )
872 {
873 saveQuery( false );
874 return true;
875 }
876 else if ( ret == QMessageBox::StandardButton::Discard )
877 {
878 return true;
879 }
880 else
881 {
882 return false;
883 }
884 }
885 else
886 {
887 return true;
888 }
889}
890
891
892void QgsQueryResultWidget::notify( const QString &title, const QString &text, Qgis::MessageLevel level )
893{
894 mQueryWidget->notify( title, text, level );
895}
896
897
898void QgsQueryResultWidget::setHasChanged( bool hasChanged )
899{
900 mActionSaveQuery->setEnabled( hasChanged );
901 mHasChangedFileContents = hasChanged;
902 updateDialogTitle();
903}
904
905void QgsQueryResultWidget::updateDialogTitle()
906{
907 QString fileName;
908 if ( !mQueryWidget->codeEditorWidget()->filePath().isEmpty() )
909 {
910 const QFileInfo fi( mQueryWidget->codeEditorWidget()->filePath() );
911 fileName = fi.fileName();
912 if ( mHasChangedFileContents )
913 {
914 fileName.prepend( '*' );
915 }
916 }
917
918 emit requestDialogTitleUpdate( fileName );
919}
920
921void QgsQueryResultWidget::populatePresetQueryMenu()
922{
923 mPresetQueryMenu->clear();
924
925 QMenu *storeQueryMenu = new QMenu( tr( "Store Current Query" ), mPresetQueryMenu );
926 mPresetQueryMenu->addMenu( storeQueryMenu );
927 QAction *storeInProfileAction = new QAction( tr( "In User Profile…" ), storeQueryMenu );
928 storeQueryMenu->addAction( storeInProfileAction );
929 storeInProfileAction->setEnabled( !mQueryWidget->sqlEditor()->text().isEmpty() );
930 connect( storeInProfileAction, &QAction::triggered, this, [this] { storeCurrentQuery( Qgis::QueryStorageBackend::LocalProfile ); } );
931 QAction *storeInProjectAction = new QAction( tr( "In Current Project…" ), storeQueryMenu );
932 storeQueryMenu->addAction( storeInProjectAction );
933 storeInProjectAction->setEnabled( !mQueryWidget->sqlEditor()->text().isEmpty() );
934 connect( storeInProjectAction, &QAction::triggered, this, [this] { storeCurrentQuery( Qgis::QueryStorageBackend::CurrentProject ); } );
935
936
937 const QList< QgsStoredQueryManager::QueryDetails > storedQueries = QgsGui::storedQueryManager()->allQueries();
938 if ( !storedQueries.isEmpty() )
939 {
940 QList< QgsStoredQueryManager::QueryDetails > userProfileQueries;
941 std::copy_if( storedQueries.begin(), storedQueries.end(), std::back_inserter( userProfileQueries ), []( const QgsStoredQueryManager::QueryDetails &details ) {
942 return details.backend == Qgis::QueryStorageBackend::LocalProfile;
943 } );
944
945 QList< QgsStoredQueryManager::QueryDetails > projectQueries;
946 std::copy_if( storedQueries.begin(), storedQueries.end(), std::back_inserter( projectQueries ), []( const QgsStoredQueryManager::QueryDetails &details ) {
947 return details.backend == Qgis::QueryStorageBackend::CurrentProject;
948 } );
949
950 mPresetQueryMenu->addSection( QgsApplication::getThemeIcon( u"mIconStoredQueries.svg"_s ), tr( "User Profile" ) );
951 for ( const QgsStoredQueryManager::QueryDetails &query : std::as_const( userProfileQueries ) )
952 {
953 QAction *action = new QAction( query.name, mPresetQueryMenu );
954 mPresetQueryMenu->addAction( action );
955 connect( action, &QAction::triggered, this, [this, query] { mQueryWidget->sqlEditor()->insertText( query.definition ); } );
956 }
957 if ( userProfileQueries.empty() )
958 {
959 QAction *action = new QAction( tr( "No Stored Queries Available" ), mPresetQueryMenu );
960 action->setEnabled( false );
961 mPresetQueryMenu->addAction( action );
962 }
963
964 mPresetQueryMenu->addSection( QgsApplication::getThemeIcon( u"mIconStoredQueries.svg"_s ), tr( "Current Project" ) );
965 for ( const QgsStoredQueryManager::QueryDetails &query : std::as_const( projectQueries ) )
966 {
967 QAction *action = new QAction( query.name, mPresetQueryMenu );
968 mPresetQueryMenu->addAction( action );
969 connect( action, &QAction::triggered, this, [this, query] { mQueryWidget->sqlEditor()->insertText( query.definition ); } );
970 }
971 if ( projectQueries.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->addSeparator();
979
980 QMenu *removeQueryMenu = new QMenu( tr( "Removed Stored Query" ), mPresetQueryMenu );
981 mPresetQueryMenu->addMenu( removeQueryMenu );
982
983 for ( const QgsStoredQueryManager::QueryDetails &query : storedQueries )
984 {
985 QAction *action = new QAction( tr( "%1…" ).arg( query.name ), mPresetQueryMenu );
986 removeQueryMenu->addAction( action );
987 connect( action, &QAction::triggered, this, [this, query] {
988 const QMessageBox::StandardButton res
989 = 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 );
990 if ( res == QMessageBox::Yes )
991 {
992 QgsGui::storedQueryManager()->removeQuery( query.name, query.backend );
993 if ( query.backend == Qgis::QueryStorageBackend::CurrentProject )
994 {
996 }
997 }
998 } );
999 }
1000 }
1001}
1002
1003void QgsQueryResultWidget::storeCurrentQuery( Qgis::QueryStorageBackend backend )
1004{
1005 const QStringList existingQueryNames = QgsGui::storedQueryManager()->allQueryNames( backend );
1006 QgsNewNameDialog dlg( QString(), QString(), QStringList(), existingQueryNames );
1007 dlg.setWindowTitle( tr( "Store Query" ) );
1008 dlg.setHintString( tr( "Name for the stored query" ) );
1009 dlg.setOverwriteEnabled( true );
1010 dlg.setConflictingNameWarning( tr( "A stored query with this name already exists, it will be overwritten." ) );
1011 dlg.setShowExistingNamesCompleter( true );
1012 if ( dlg.exec() != QDialog::Accepted )
1013 return;
1014
1015 const QString name = dlg.name();
1016 if ( name.isEmpty() )
1017 return;
1018
1019 QgsGui::storedQueryManager()->storeQuery( name, mQueryWidget->sqlEditor()->text(), backend );
1021 {
1023 }
1024}
1025
1026
1027void QgsQueryResultWidget::showHistoryPanel( bool show )
1028{
1029 if ( show )
1030 {
1031 mHistoryWidget = new QgsDatabaseQueryHistoryWidget();
1032 mHistoryWidget->setPanelTitle( tr( "SQL History" ) );
1033 mPanelStack->showPanel( mHistoryWidget );
1034 connect( mHistoryWidget, &QgsPanelWidget::panelAccepted, this, [this] { whileBlocking( mActionShowHistory )->setChecked( false ); } );
1035 connect( mHistoryWidget, &QgsDatabaseQueryHistoryWidget::sqlTriggered, this, [this]( const QString &connectionUri, const QString &provider, const QString &sql ) {
1036 Q_UNUSED( connectionUri );
1037 Q_UNUSED( provider );
1038
1039 mQueryWidget->sqlEditor()->setText( sql );
1040 mActionUndo->setEnabled( false );
1041 mActionRedo->setEnabled( false );
1042 mHistoryWidget->acceptPanel();
1043 } );
1044 }
1045 else if ( mHistoryWidget )
1046 {
1047 mPanelStack->closePanel( mHistoryWidget );
1048 mHistoryWidget->deleteLater();
1049 }
1050}
1051
1052
1054
1055void QgsConnectionsApiFetcher::fetchTokens()
1056{
1057 if ( mStopFetching )
1058 {
1059 emit fetchingFinished();
1060 return;
1061 }
1062
1063
1065 if ( !md )
1066 {
1067 emit fetchingFinished();
1068 return;
1069 }
1070 std::unique_ptr<QgsAbstractDatabaseProviderConnection> connection( static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( mUri, {} ) ) );
1071 if ( !mStopFetching && connection )
1072 {
1073 mFeedback = std::make_unique<QgsFeedback>();
1074 QStringList schemas;
1076 {
1077 try
1078 {
1079 schemas = connection->schemas();
1080 emit tokensReady( schemas );
1081 }
1082 catch ( QgsProviderConnectionException &ex )
1083 {
1084 QgsMessageLog::logMessage( tr( "Error retrieving schemas: %1" ).arg( ex.what() ), u"QGIS"_s, Qgis::MessageLevel::Warning );
1085 }
1086 }
1087 else
1088 {
1089 schemas.push_back( QString() ); // Fake empty schema for DBs not supporting it
1090 }
1091
1092 for ( const auto &schema : std::as_const( schemas ) )
1093 {
1094 if ( mStopFetching )
1095 {
1096 connection.reset();
1097 emit fetchingFinished();
1098 return;
1099 }
1100
1101 QStringList tableNames;
1102 try
1103 {
1104 const QList<QgsAbstractDatabaseProviderConnection::TableProperty> tables = connection->tables( schema, QgsAbstractDatabaseProviderConnection::TableFlags(), mFeedback.get() );
1105 for ( const QgsAbstractDatabaseProviderConnection::TableProperty &table : std::as_const( tables ) )
1106 {
1107 if ( mStopFetching )
1108 {
1109 connection.reset();
1110 emit fetchingFinished();
1111 return;
1112 }
1113 tableNames.push_back( table.tableName() );
1114 }
1115 emit tokensReady( tableNames );
1116 }
1117 catch ( QgsProviderConnectionException &ex )
1118 {
1119 QgsMessageLog::logMessage( tr( "Error retrieving tables: %1" ).arg( ex.what() ), u"QGIS"_s, Qgis::MessageLevel::Warning );
1120 }
1121
1122 // Get fields
1123 for ( const auto &table : std::as_const( tableNames ) )
1124 {
1125 if ( mStopFetching )
1126 {
1127 connection.reset();
1128 emit fetchingFinished();
1129 return;
1130 }
1131
1132 QStringList fieldNames;
1133 try
1134 {
1135 const QgsFields fields( connection->fields( schema, table, mFeedback.get() ) );
1136 if ( mStopFetching )
1137 {
1138 connection.reset();
1139 emit fetchingFinished();
1140 return;
1141 }
1142
1143 for ( const auto &field : std::as_const( fields ) )
1144 {
1145 fieldNames.push_back( field.name() );
1146 if ( mStopFetching )
1147 {
1148 connection.reset();
1149 emit fetchingFinished();
1150 return;
1151 }
1152 }
1153 emit tokensReady( fieldNames );
1154 }
1155 catch ( QgsProviderConnectionException &ex )
1156 {
1157 QgsMessageLog::logMessage( tr( "Error retrieving fields for table %1: %2" ).arg( table, ex.what() ), u"QGIS"_s, Qgis::MessageLevel::Warning );
1158 }
1159 }
1160 }
1161 }
1162
1163 connection.reset();
1164 emit fetchingFinished();
1165}
1166
1167void QgsConnectionsApiFetcher::stopFetching()
1168{
1169 mStopFetching = 1;
1170 if ( mFeedback )
1171 mFeedback->cancel();
1172}
1173
1174QgsQueryResultItemDelegate::QgsQueryResultItemDelegate( QObject *parent )
1175 : QStyledItemDelegate( parent )
1176{}
1177
1178QString QgsQueryResultItemDelegate::displayText( const QVariant &value, const QLocale &locale ) const
1179{
1180 Q_UNUSED( locale )
1181 QString result { QgsExpressionUtils::toLocalizedString( value ) };
1182 // Show no more than 255 characters
1183 if ( result.length() > 255 )
1184 {
1185 result.truncate( 255 );
1186 result.append( u"…"_s );
1187 }
1188 return result;
1189}
1190
1192
1193//
1194// QgsQueryResultDialog
1195//
1196
1197QgsQueryResultDialog::QgsQueryResultDialog( QgsAbstractDatabaseProviderConnection *connection, QWidget *parent )
1198 : QDialog( parent )
1199{
1200 setObjectName( u"QgsQueryResultDialog"_s );
1202
1203 mWidget = new QgsQueryResultWidget( this, connection );
1204 QVBoxLayout *l = new QVBoxLayout();
1205 l->setContentsMargins( 6, 6, 6, 6 );
1206
1207 QDialogButtonBox *mButtonBox = new QDialogButtonBox( QDialogButtonBox::StandardButton::Close | QDialogButtonBox::StandardButton::Help );
1208 connect( mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::close );
1209 connect( mButtonBox, &QDialogButtonBox::helpRequested, this, [] { QgsHelp::openHelp( u"managing_data_source/create_layers.html#execute-sql"_s ); } );
1210 l->addWidget( mWidget );
1211 l->addWidget( mButtonBox );
1212
1213 setLayout( l );
1214}
1215
1216void QgsQueryResultDialog::closeEvent( QCloseEvent *event )
1217{
1218 if ( !mWidget->promptUnsavedChanges() )
1219 {
1220 event->ignore();
1221 }
1222 else
1223 {
1224 event->accept();
1225 }
1226}
1227
1228//
1229// QgsQueryResultMainWindow
1230//
1231
1232QgsQueryResultMainWindow::QgsQueryResultMainWindow( QgsAbstractDatabaseProviderConnection *connection, const QString &identifierName )
1233 : mIdentifierName( identifierName )
1234{
1235 setObjectName( u"SQLCommandsDialog"_s );
1236
1238
1239 mWidget = new QgsQueryResultWidget( nullptr, connection );
1240 setCentralWidget( mWidget );
1241 mWidget->layout()->setContentsMargins( 6, 6, 6, 6 );
1242
1243 connect( mWidget, &QgsQueryResultWidget::requestDialogTitleUpdate, this, &QgsQueryResultMainWindow::updateWindowTitle );
1244
1245 updateWindowTitle( QString() );
1246}
1247
1248void QgsQueryResultMainWindow::closeEvent( QCloseEvent *event )
1249{
1250 if ( !mWidget->promptUnsavedChanges() )
1251 {
1252 event->ignore();
1253 }
1254 else
1255 {
1256 event->accept();
1257 }
1258}
1259
1260void QgsQueryResultMainWindow::updateWindowTitle( const QString &fileName )
1261{
1262 if ( fileName.isEmpty() )
1263 {
1264 if ( !mIdentifierName.isEmpty() )
1265 setWindowTitle( tr( "%1 — Execute SQL" ).arg( mIdentifierName ) );
1266 else
1267 setWindowTitle( tr( "Execute SQL" ) );
1268 }
1269 else
1270 {
1271 if ( !mIdentifierName.isEmpty() )
1272 setWindowTitle( tr( "%1 — %2 — Execute SQL" ).arg( fileName, mIdentifierName ) );
1273 else
1274 setWindowTitle( tr( "%1 — Execute SQL" ).arg( fileName ) );
1275 }
1276}
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:3712
@ CurrentProject
Current QGIS project.
Definition qgis.h:3714
@ LocalProfile
Local user profile.
Definition qgis.h:3713
@ UnstableFeatureIds
SQL layer definition supports disabling select at id.
Definition qgis.h:1176
@ SubsetStringFilter
SQL layer definition supports subset string filter.
Definition qgis.h:1173
@ PrimaryKeys
SQL layer definition supports primary keys.
Definition qgis.h:1175
@ GeometryColumn
SQL layer definition supports geometry column.
Definition qgis.h:1174
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:588
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:7295
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.