Feature: Timeout-Einstellung und asynchrone DB-Abfrage mit Abbrechen-Dialog

DB-Abfragen laufen nun in einem Hintergrund-Thread mit QProgressDialog,
sodass die UI nicht mehr einfriert. connect_timeout wird als konfigurierbarer
Parameter (1-300s, Standard: 10) im Connection-String übergeben.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 21:31:40 +01:00
parent 66496c26d8
commit ec33a5b586
5 changed files with 287 additions and 187 deletions
+1
View File
@@ -89,6 +89,7 @@ class PostgreSqlDb(BaseModel):
username: str username: str
password: str password: str
ssl_mode: SSLMode = SSLMode.PREFER ssl_mode: SSLMode = SSLMode.PREFER
timeout: int = 10
class Project(BaseModel): class Project(BaseModel):
+9 -1
View File
@@ -19,7 +19,13 @@ class DatabaseTestThread(QThread):
def run(self): def run(self):
"""Führt den Datenbanktest in einem separaten Thread aus.""" """Führt den Datenbanktest in einem separaten Thread aus."""
try: try:
uri = f"postgresql://{self.connection_data['username']}:{self.connection_data['password']}@{self.connection_data['host']}:{self.connection_data['port']}/{self.connection_data['database']}" timeout = self.connection_data.get("timeout", 10)
uri = (
f"postgresql://{self.connection_data['username']}:{self.connection_data['password']}"
f"@{self.connection_data['host']}:{self.connection_data['port']}"
f"/{self.connection_data['database']}"
f"?connect_timeout={timeout}"
)
# Datenbankverbindung testen # Datenbankverbindung testen
r = pl.read_database_uri( r = pl.read_database_uri(
@@ -106,6 +112,7 @@ class PostgreSqlConfigDialog(QDialog):
self.ui.usernameEdit.setText(data.get("username", "")) self.ui.usernameEdit.setText(data.get("username", ""))
self.ui.passwordEdit.setText(data.get("password", "")) self.ui.passwordEdit.setText(data.get("password", ""))
self.ui.sslModeComboBox.setCurrentText(data.get("ssl_mode", "prefer")) self.ui.sslModeComboBox.setCurrentText(data.get("ssl_mode", "prefer"))
self.ui.timeoutSpinBox.setValue(data.get("timeout", 10))
def get_data(self): def get_data(self):
"""Gibt die eingegebenen Daten zurück.""" """Gibt die eingegebenen Daten zurück."""
@@ -126,4 +133,5 @@ class PostgreSqlConfigDialog(QDialog):
"username": self.ui.usernameEdit.text().strip(), "username": self.ui.usernameEdit.text().strip(),
"password": self.ui.passwordEdit.text(), # Passwort kann leer sein "password": self.ui.passwordEdit.text(), # Passwort kann leer sein
"ssl_mode": self.ui.sslModeComboBox.currentText(), "ssl_mode": self.ui.sslModeComboBox.currentText(),
"timeout": self.ui.timeoutSpinBox.value(),
} }
+20
View File
@@ -138,7 +138,27 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="7" column="0">
<widget class="QLabel" name="timeoutLabel">
<property name="text">
<string>Timeout (s):</string>
</property>
</widget>
</item>
<item row="7" column="1"> <item row="7" column="1">
<widget class="QSpinBox" name="timeoutSpinBox">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>300</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QPushButton" name="testConnectionButton"> <widget class="QPushButton" name="testConnectionButton">
<property name="text"> <property name="text">
<string>Verbindung testen</string> <string>Verbindung testen</string>
+16 -2
View File
@@ -3,7 +3,7 @@
################################################################################ ################################################################################
## Form generated from reading UI file 'PostgreSqlConfigDialog.ui' ## Form generated from reading UI file 'PostgreSqlConfigDialog.ui'
## ##
## Created by: Qt User Interface Compiler version 6.9.1 ## Created by: Qt User Interface Compiler version 6.10.1
## ##
## WARNING! All changes made in this file will be lost when recompiling UI file! ## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################ ################################################################################
@@ -110,10 +110,23 @@ class Ui_PostgreSqlConfigDialog(object):
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.sslModeComboBox) self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.sslModeComboBox)
self.timeoutLabel = QLabel(PostgreSqlConfigDialog)
self.timeoutLabel.setObjectName(u"timeoutLabel")
self.formLayout.setWidget(7, QFormLayout.ItemRole.LabelRole, self.timeoutLabel)
self.timeoutSpinBox = QSpinBox(PostgreSqlConfigDialog)
self.timeoutSpinBox.setObjectName(u"timeoutSpinBox")
self.timeoutSpinBox.setMinimum(1)
self.timeoutSpinBox.setMaximum(300)
self.timeoutSpinBox.setValue(10)
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.timeoutSpinBox)
self.testConnectionButton = QPushButton(PostgreSqlConfigDialog) self.testConnectionButton = QPushButton(PostgreSqlConfigDialog)
self.testConnectionButton.setObjectName(u"testConnectionButton") self.testConnectionButton.setObjectName(u"testConnectionButton")
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.testConnectionButton) self.formLayout.setWidget(8, QFormLayout.ItemRole.FieldRole, self.testConnectionButton)
self.verticalLayout.addLayout(self.formLayout) self.verticalLayout.addLayout(self.formLayout)
@@ -151,6 +164,7 @@ class Ui_PostgreSqlConfigDialog(object):
self.sslModeComboBox.setItemText(4, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-ca", None)) self.sslModeComboBox.setItemText(4, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-ca", None))
self.sslModeComboBox.setItemText(5, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-full", None)) self.sslModeComboBox.setItemText(5, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-full", None))
self.timeoutLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Timeout (s):", None))
self.testConnectionButton.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Verbindung testen", None)) self.testConnectionButton.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Verbindung testen", None))
# retranslateUi # retranslateUi
+86 -29
View File
@@ -10,13 +10,35 @@ import logging
from pathlib import Path from pathlib import Path
import polars as pl import polars as pl
from PySide6.QtWidgets import QMessageBox from PySide6.QtCore import QThread, Signal, Qt
from PySide6.QtWidgets import QMessageBox, QProgressDialog
from conf import app_settings, TreeNode, XslFile from conf import app_settings, TreeNode, XslFile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DatabaseQueryThread(QThread):
"""Thread für asynchrone Datenbankabfragen."""
query_completed = Signal(object) # pl.DataFrame
query_failed = Signal(str) # Fehlermeldung
def __init__(self, sql_query, connection_string):
super().__init__()
self.sql_query = sql_query
self.connection_string = connection_string
def run(self):
try:
df = pl.read_database_uri(self.sql_query, self.connection_string, engine="connectorx").sort(
["reporttyp_bez", "report_bez", "repfile_bez"]
)
self.query_completed.emit(df)
except Exception as e:
self.query_failed.emit(str(e))
class DatabaseMixin: class DatabaseMixin:
""" """
Mixin für Datenbank-Operationen. Mixin für Datenbank-Operationen.
@@ -31,7 +53,7 @@ class DatabaseMixin:
def on_load_from_fn2_clicked(self): def on_load_from_fn2_clicked(self):
""" """
Wird ausgeführt, wenn der Button "lade aus FN2" geklickt wird. Wird ausgeführt, wenn der Button "lade aus FN2" geklickt wird.
Führt SQL-Abfrage aus und aktualisiert die Projekt-Nodes. Startet SQL-Abfrage asynchron mit Fortschrittsdialog.
""" """
logger.debug("Button 'lade aus FN2' wurde geklickt!") logger.debug("Button 'lade aus FN2' wurde geklickt!")
@@ -52,29 +74,68 @@ class DatabaseMixin:
QMessageBox.warning(self, "Warnung", "PostgreSQL-Datenbank-Konfiguration nicht gefunden.") QMessageBox.warning(self, "Warnung", "PostgreSQL-Datenbank-Konfiguration nicht gefunden.")
return return
# Führe SQL-Abfrage aus # SQL-Abfrage und Connection-String vorbereiten
df = self._execute_sql_query(db_config) sql_query, connection_string = self._prepare_sql_query(db_config)
if df is None: if sql_query is None:
return # Fehler bereits angezeigt return # Fehler bereits angezeigt
# Verarbeite die Daten wie in readCsv.py # Fortschrittsdialog erstellen
new_nodes = self._process_sql_data(df) self._db_progress = QProgressDialog("Verbinde mit Datenbank...", "Abbrechen", 0, 0, self)
self._db_progress.setWindowTitle("Datenbank-Abfrage")
self._db_progress.setWindowModality(Qt.WindowModal)
self._db_progress.setMinimumDuration(0)
# Merge mit vorhandenen Nodes # Query-Thread erstellen und starten
self._merge_nodes_with_existing(new_nodes) self._db_query_thread = DatabaseQueryThread(sql_query, connection_string)
self._db_query_thread.query_completed.connect(self._on_db_query_completed)
# Speichere die aktualisierten Projekt-Einstellungen self._db_query_thread.query_failed.connect(self._on_db_query_failed)
self._save_project_settings() self._db_query_thread.finished.connect(self._cleanup_db_query)
self._db_progress.canceled.connect(self._on_db_query_canceled)
# Lade das Projekt neu self._db_query_thread.start()
self._load_nodes_to_tree()
# QMessageBox.information(self, "Erfolg", "Daten erfolgreich aus FN2 geladen und Projekt aktualisiert!")
except Exception as e: except Exception as e:
logger.error(f"Fehler beim Laden aus FN2: {e}") logger.error(f"Fehler beim Laden aus FN2: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden aus FN2:\n{str(e)}") QMessageBox.critical(self, "Fehler", f"Fehler beim Laden aus FN2:\n{str(e)}")
def _on_db_query_completed(self, df):
"""Wird aufgerufen, wenn die Datenbankabfrage erfolgreich war."""
# Ignoriere Ergebnis, falls der Benutzer abgebrochen hat
if hasattr(self, "_db_progress") and self._db_progress.wasCanceled():
return
try:
new_nodes = self._process_sql_data(df)
self._merge_nodes_with_existing(new_nodes)
self._save_project_settings()
self._load_nodes_to_tree()
except Exception as e:
logger.error(f"Fehler beim Verarbeiten der DB-Daten: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Verarbeiten der Daten:\n{str(e)}")
def _on_db_query_failed(self, error_msg):
"""Wird aufgerufen, wenn die Datenbankabfrage fehlgeschlagen ist."""
if hasattr(self, "_db_progress") and self._db_progress.wasCanceled():
return
error = f"Fehler beim Ausführen der SQL-Abfrage: {error_msg}"
logger.error(error)
QMessageBox.critical(self, "Fehler", error)
def _on_db_query_canceled(self):
"""Wird aufgerufen, wenn der Benutzer die Abfrage abbricht."""
logger.info("Datenbankabfrage vom Benutzer abgebrochen")
def _cleanup_db_query(self):
"""Räumt nach der Datenbankabfrage auf."""
if hasattr(self, "_db_progress"):
self._db_progress.canceled.disconnect(self._on_db_query_canceled)
self._db_progress.close()
self._db_progress.deleteLater()
del self._db_progress
if hasattr(self, "_db_query_thread"):
self._db_query_thread.deleteLater()
del self._db_query_thread
def _get_database_config(self, db_id): def _get_database_config(self, db_id):
""" """
Holt die Datenbank-Konfiguration anhand der ID. Holt die Datenbank-Konfiguration anhand der ID.
@@ -90,29 +151,27 @@ class DatabaseMixin:
return db return db
return None return None
def _execute_sql_query(self, db_config): def _prepare_sql_query(self, db_config):
""" """
Führt die SQL-Abfrage aus der data.sql Datei aus. Bereitet SQL-Abfrage und Connection-String vor.
Args: Args:
db_config: PostgreSQL-Datenbank-Konfiguration db_config: PostgreSQL-Datenbank-Konfiguration
Returns: Returns:
pl.DataFrame|None: Die Abfrageergebnisse oder None bei Fehler tuple[str, str]|tuple[None, None]: (sql_query, connection_string) oder (None, None) bei Fehler
""" """
try: try:
# Lade SQL-Abfrage aus Datei
sql_file_path = Path("src/res/data.sql") sql_file_path = Path("src/res/data.sql")
if not sql_file_path.exists(): if not sql_file_path.exists():
QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}") QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}")
return None return None, None
with open(sql_file_path, "r", encoding="utf-8") as f: with open(sql_file_path, "r", encoding="utf-8") as f:
sql_query = f.read() sql_query = f.read()
logger.debug(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen") logger.debug(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen")
# Verbindung zur PostgreSQL-Datenbank herstellen
connection_string = ( connection_string = (
"postgresql://" "postgresql://"
f"{db_config.username}:" f"{db_config.username}:"
@@ -121,19 +180,17 @@ class DatabaseMixin:
f"{db_config.port}/" f"{db_config.port}/"
f"{db_config.database}?" f"{db_config.database}?"
f"sslmode={db_config.ssl_mode.value}" f"sslmode={db_config.ssl_mode.value}"
f"&connect_timeout={db_config.timeout}"
) )
logger.info(f"Verbinde zu PostgreSQL: {db_config.host}:{db_config.port}/{db_config.database}") logger.info(f"Verbinde zu PostgreSQL: {db_config.host}:{db_config.port}/{db_config.database}")
df = pl.read_database_uri(sql_query, connection_string, engine="connectorx").sort( return sql_query, connection_string
["reporttyp_bez", "report_bez", "repfile_bez"]
)
return df
except Exception as e: except Exception as e:
error_msg = f"Fehler beim Ausführen der SQL-Abfrage: {str(e)}" error_msg = f"Fehler beim Vorbereiten der SQL-Abfrage: {str(e)}"
logger.error(error_msg) logger.error(error_msg)
QMessageBox.critical(self, "Fehler", error_msg) QMessageBox.critical(self, "Fehler", error_msg)
return None return None, None
def _process_sql_data(self, df): def _process_sql_data(self, df):
""" """