first commit

This commit is contained in:
dgsoft 2025-10-14 21:27:41 +02:00
commit 44b8667f31
40 changed files with 4619 additions and 0 deletions

83
AUTOCOMPLETE_FEATURE.md Normal file
View File

@ -0,0 +1,83 @@
# SQL Autocomplete Funktionalität
## Übersicht
Die Query Builder Anwendung verfügt jetzt über eine intelligente Autocomplete-Funktion, die beim Schreiben von SQL-Queries automatisch Tabellenvorschläge anzeigt.
## Funktionsweise
### Aktivierung
- Autocomplete wird automatisch ausgelöst, wenn im Query-Feld `FROM` getippt wird
- Funktioniert auch bei partieller Eingabe nach `FROM` (z.B. `FROM US` zeigt alle Tabellen mit "US")
### Bedienung
#### Maus-Navigation
- **Klicken** auf einen Tabellennamen übernimmt diesen in die Query
- **Hover** über Items zeigt visuelles Feedback
#### Tastatur-Navigation
- **↓ / ↑** - Navigation durch die Vorschläge
- **Enter / Tab** - Auswahl des markierten Tabellennamens
- **Escape** - Schließt das Autocomplete-Dropdown
### Intelligente Filterung
- Zeigt maximal 10 relevante Tabellen
- Filtert basierend auf bereits getipptem Text nach `FROM`
- Case-insensitive Suche (Groß-/Kleinschreibung wird ignoriert)
## Technische Details
### UI-Komponenten
- **Dropdown-Container**: Positioniert relativ zum Query-Textfeld
- **Header**: "Verfügbare Tabellen" zur Orientierung
- **Items**: Tabellennamen mit Icon und Hover-Effekten
- **Keyboard-Navigation**: Visuelle Markierung des ausgewählten Items
### CSS-Klassen
```css
.autocomplete-dropdown /* Haupt-Container */
.autocomplete-header /* Header-Bereich */
.autocomplete-item /* Einzelne Tabellenvorschläge */
.autocomplete-item.selected /* Keyboard-markiertes Item */
```
### JavaScript-Events
- `input` - Erkennt Textänderungen und triggert Autocomplete
- `keydown` - Behandelt Keyboard-Navigation
- `click` - Schließt Dropdown bei Klick außerhalb
## Beispiel-Usage
### Basis-Verwendung
1. Tippe im Query-Feld: `SELECT * FROM `
2. Autocomplete-Dropdown erscheint automatisch
3. Wähle eine Tabelle aus der Liste
4. Tabelle wird automatisch eingefügt: `SELECT * FROM TABELLENAME `
### Mit Filterung
1. Tippe: `SELECT * FROM USER`
2. Zeigt nur Tabellen die "USER" enthalten
3. Auswahl übernimmt gefilterte Tabelle
### Keyboard-Shortcuts
- `FROM` + `↓` + `Enter` = Schnelle Tabellenauswahl
- `FROM TAB` + `Tab` + `Enter` = Erste gefilterte Tabelle auswählen
## Integration
- **Nahtlose Integration** in bestehende Drag & Drop Funktionalität
- **Keine Konflikte** mit anderen UI-Elementen
- **Responsive Design** passt sich an verschiedene Bildschirmgrößen an
- **Keyboard-freundlich** für Power-User
## Performance
- **Clientseitige Filterung** - Keine zusätzlichen Server-Requests
- **Begrenzte Anzeige** auf 10 Items für schnelle Performance
- **Lazy Loading** - Dropdown erscheint nur bei Bedarf
- **Auto-Hide** - Schließt automatisch bei Verlust des Fokus
## Erweiterbarkeit
Die Implementierung ist darauf vorbereitet für zukünftige Erweiterungen:
- Spalten-Autocomplete (`SELECT col1, col2`)
- Keyword-Autocomplete (`WHERE`, `ORDER BY`, etc.)
- Funktionen-Autocomplete (`COUNT`, `SUM`, etc.)
- Schema-übergreifende Suche

62
COMPACT_UI_CHANGES.md Normal file
View File

@ -0,0 +1,62 @@
# Kompaktere UI - Änderungen
## Übersicht
Die Benutzeroberfläche wurde kompakter gestaltet, um mehr Informationen auf weniger Platz anzuzeigen.
## Vorgenommene Änderungen
### 1. Kleinere Buttons (btn-sm)
- **Padding**: 0.25rem → 0.15rem (vertikal), 0.5rem → 0.4rem (horizontal)
- **Schriftgröße**: 0.875rem → 0.75rem
- **Zeilenhöhe**: Hinzugefügt 1.2 für kompaktere Darstellung
### 2. Extra kleine Buttons (btn-xs - neue Klasse)
- **Padding**: 0.1rem × 0.25rem
- **Schriftgröße**: 0.65rem
- **Zeilenhöhe**: 1.1
### 3. Tabellen-Items
- **Padding**: 8px 12px → 4px 8px
- **Margin**: 2px → 1px (vertikal)
- **Schriftgröße**: Standard → 0.85rem
- **Border-Radius**: 4px → 3px
### 4. Tabellen-Icons
- **Margin**: 8px → 6px (rechts)
- **Schriftgröße**: Hinzugefügt 0.8rem
### 5. Spalten-Container (.table-columns)
- **Margin-left**: 12px → 8px
- **Margin-top**: 4px → 2px
- **Padding**: 8px → 4px 6px
- **Border-Radius**: 4px → 3px
### 6. Spalten-Items (.column-item)
- **Padding**: 4px 8px → 2px 6px
- **Margin**: 1px → 0.5px (vertikal)
- **Schriftgröße**: 12px → 11px
- **Gap**: 6px → 4px
- **Border-Radius**: 3px → 2px
- **Zeilenhöhe**: Hinzugefügt 1.2
### 7. Spalten-Typ (.column-type)
- **Schriftgröße**: 10px → 9px
### 8. Toggle-Button
- **Margin**: 5px → 3px (rechts)
- **Padding**: Hinzugefügt 0.1rem 0.3rem
- **Schriftgröße**: Hinzugefügt 0.7rem
### 9. Container-Abstände
- **Table-Container Margin**: 8px → 4px (unten)
## Ergebnis
- ✅ **Kompaktere Darstellung** - Mehr Tabellen und Spalten sichtbar
- ✅ **Bessere Platzausnutzung** - Weniger Verschwendung von Bildschirmplatz
- ✅ **Erhaltene Funktionalität** - Drag & Drop und alle Interaktionen bleiben voll funktionsfähig
- ✅ **Verbesserte Lesbarkeit** - Trotz kleinerer Größe gut lesbar durch angepasste Zeilenhöhen
## Verwendung
- Bestehende Buttons verwenden automatisch die neuen kompakteren Größen
- Neue `.btn-xs` Klasse verfügbar für noch kleinere Buttons bei Bedarf
- Alle Spalten und Tabellen werden automatisch kompakter dargestellt

65
OFFLINE_RESOURCES.md Normal file
View File

@ -0,0 +1,65 @@
# Lokale Ressourcen für Query Builder
## Übersicht
Die Query Builder Anwendung ist nun vollständig für den Offline-Betrieb konfiguriert. Alle externen CSS- und JavaScript-Ressourcen wurden lokal gespeichert.
## Heruntergeladene Ressourcen
### Bootstrap 5.3.0
- **CSS**: `app/static/css/vendor/bootstrap.min.css` (228KB)
- **JavaScript**: `app/static/js/vendor/bootstrap.bundle.min.js` (79KB)
### Font Awesome 6.4.0
- **CSS**: `app/static/css/vendor/all-local.min.css` (100KB)
- **Webfonts**:
- `app/static/webfonts/fa-solid-900.woff2` (147KB)
- `app/static/webfonts/fa-regular-400.woff2` (25KB)
- `app/static/webfonts/fa-brands-400.woff2` (106KB)
## Verzeichnisstruktur
```
app/static/
├── css/
│ ├── vendor/
│ │ ├── bootstrap.min.css
│ │ ├── all.min.css (Original)
│ │ └── all-local.min.css (Angepasste Pfade)
│ └── style.css
├── js/
│ ├── vendor/
│ │ └── bootstrap.bundle.min.js
│ └── app.js
└── webfonts/
├── fa-solid-900.woff2
├── fa-regular-400.woff2
└── fa-brands-400.woff2
```
## Anpassungen
### Template-Updates
- `app/templates/base.html` wurde aktualisiert, um lokale Ressourcen zu verwenden
- Entfernt: CDN-Links für Bootstrap und Font Awesome
- Hinzugefügt: Lokale Pfade zu den heruntergeladenen Dateien
### Font Awesome CSS-Anpassung
- Originale `all.min.css` enthielt relative Pfade: `../webfonts/`
- Neue `all-local.min.css` mit korrigierten Pfaden: `../../webfonts/`
## Funktionalität
✅ Bootstrap CSS und JavaScript funktionieren vollständig
✅ Font Awesome Icons werden korrekt angezeigt
✅ Drag & Drop Funktionalität bleibt erhalten
✅ Alle interaktiven Elemente funktionieren
✅ Keine Internet-Verbindung erforderlich
## Vorteile
- 🔒 **Sicherheit**: Keine externen Abhängigkeiten
- ⚡ **Performance**: Schnellere Ladezeiten (keine CDN-Anfragen)
- 🌐 **Offline-Fähig**: Funktioniert ohne Internetverbindung
- 🛡️ **Zuverlässigkeit**: Keine Ausfälle durch externe Services
## Wartung
- Ressourcen sind in Standard-Versionen gespeichert (Bootstrap 5.3.0, Font Awesome 6.4.0)
- Bei Updates müssen neue Versionen manuell heruntergeladen und ersetzt werden
- Pfad-Anpassungen in CSS-Dateien beachten bei Font Awesome Updates

213
README.md Normal file
View File

@ -0,0 +1,213 @@
# Query Builder - SQL Web Interface
Eine Python Flask-Webanwendung zum Ausführen und Verwalten von SQL-Queries mit einer benutzerfreundlichen Web-Oberfläche.
## Features
- **Authentifikation**: Benutzeranmeldung und -registrierung
- **Multi-Datenbank-Unterstützung**: Verbindung zu Oracle, PostgreSQL und SQLite
- **Query Builder**: Interaktive SQL-Query-Eingabe und -Ausführung
- **Tabellen-Explorer**: Anzeige aller verfügbaren Datenbanktabellen in der linken Sidebar
- **Datenbankauswahl**: Auswahl zwischen verschiedenen konfigurierten Datenbankverbindungen
- **Verbindungstest**: Test der Datenbankverbindung mit einem Klick
- **Gespeicherte Queries**: Speichern und Verwalten häufig verwendeter SQL-Queries
- **API-Zugriff**: RESTful API zum Abrufen gespeicherter Queries als JSON oder CSV
- **Export-Funktionen**: Download von Query-Ergebnissen als CSV
## Projektstruktur
```
QueryBuilder/
├── app/
│ ├── __init__.py # Flask App Initialisierung
│ ├── models.py # Datenbankmodelle (User, SavedQuery, etc.)
│ ├── routes/
│ │ ├── auth.py # Authentifikation (Login/Register)
│ │ ├── main.py # Hauptrouten (Dashboard, Query-Ausführung)
│ │ └── api.py # API-Endpunkte für Query-Export
│ ├── services/
│ │ ├── database_service.py # Datenbankverbindung und Query-Ausführung
│ │ └── database_manager.py # Multi-Datenbank-Manager
│ ├── templates/
│ │ ├── base.html # Basis-Template
│ │ ├── dashboard.html # Hauptinterface
│ │ └── auth/
│ │ ├── login.html # Login-Seite
│ │ └── register.html # Registrierungs-Seite
│ └── static/
│ ├── css/style.css # Custom CSS
│ └── js/app.js # Frontend JavaScript
├── requirements.txt # Python Dependencies
├── .env # Umgebungsvariablen
├── run.py # Anwendungsstart
└── README.md # Diese Datei
```
## Installation
1. **Repository klonen und Abhängigkeiten installieren:**
```bash
cd QueryBuilder
pip install -r requirements.txt
```
2. **Umgebungsvariablen konfigurieren:**
```bash
# .env Datei erstellen (basierend auf .env.example)
cp .env.example .env
# Dann .env anpassen mit Ihren Datenbankverbindungen:
# Flask Konfiguration
SECRET_KEY=your-secret-key-here
FLASK_ENV=development
# Oracle Datenbank (optional)
ORACLE_HOST=your-oracle-host.com
ORACLE_PORT=1521
ORACLE_SERVICE_NAME=ORCL
ORACLE_USERNAME=your_username
ORACLE_PASSWORD=your_password
# PostgreSQL Datenbank (optional)
POSTGRES_HOST=your-postgres-host.com
POSTGRES_PORT=5432
POSTGRES_DATABASE=your_database
POSTGRES_USERNAME=your_username
POSTGRES_PASSWORD=your_password
```
3. **Anwendung starten:**
```bash
python run.py
```
4. **Im Browser öffnen:**
```
http://localhost:5000
```
## Standard-Login
- **Benutzername:** admin
- **Passwort:** admin123
## API-Endpunkte
### Gespeicherte Queries abrufen
```bash
# Alle gespeicherten Queries
GET /api/queries
# Spezifische Query nach Namen
GET /api/queries/{query_name}
# Query ausführen und Ergebnisse als JSON erhalten
GET /api/queries/{query_name}/execute?connection=demo
# Query ausführen und als CSV downloaden
GET /api/queries/{query_name}/execute?format=csv&connection=oracle
# Direkte Export-Endpunkte
GET /api/queries/{query_name}/export/json
GET /api/queries/{query_name}/export/csv
```
## Verwendung
### Query Builder Interface
- **Linke Sidebar**: Zeigt alle verfügbaren Datenbanktabellen
- **Rechte Seite**: SQL-Query-Eingabefeld und Ergebnisanzeige
- **Query ausführen**: Ctrl+Enter oder "Query Ausführen" Button
- **Query speichern**: Ctrl+S oder "Query Speichern" Button
### Demo-Daten
Die Anwendung erstellt automatisch Demo-Tabellen mit Beispieldaten:
- `customers` - Kundendaten
- `orders` - Bestellungen
- `products` - Produkte
### Beispiel-Queries
```sql
-- Alle Kunden anzeigen
SELECT * FROM customers;
-- Bestellungen mit Kundeninformationen
SELECT c.name, o.product_name, o.quantity, o.price
FROM customers c
JOIN orders o ON c.id = o.customer_id;
-- Top-Städte nach Kundenanzahl
SELECT city, COUNT(*) as customer_count
FROM customers
GROUP BY city
ORDER BY customer_count DESC;
```
## Datenbankunterstützung
Die Anwendung unterstützt mehrere Datenbanktypen gleichzeitig:
### SQLite (Standard)
- Keine Konfiguration erforderlich
- Demo-Datenbank mit Beispieldaten
- Lokale `querybuilder.db` Datei
### Oracle Database
```bash
# Umgebungsvariablen in .env setzen
ORACLE_HOST=your-oracle-host.com
ORACLE_PORT=1521
ORACLE_SERVICE_NAME=ORCL
ORACLE_USERNAME=your_username
ORACLE_PASSWORD=your_password
```
### PostgreSQL
```bash
# Umgebungsvariablen in .env setzen
POSTGRES_HOST=your-postgres-host.com
POSTGRES_PORT=5432
POSTGRES_DATABASE=your_database
POSTGRES_USERNAME=your_username
POSTGRES_PASSWORD=your_password
```
### Verbindungstest
- Über das Dashboard können Sie Datenbankverbindungen testen
- Der "Verbindung testen" Button prüft die Konnektivität
- Fehlermeldungen helfen bei der Fehlerdiagnose
## Technische Details
### Backend
- **Flask**: Web-Framework
- **SQLAlchemy**: ORM für Datenbankoperationen
- **Flask-Login**: Session-Management
- **cx-Oracle/oracledb**: Oracle-Datenbanktreiber
- **psycopg2**: PostgreSQL-Datenbanktreiber
- **SQLite**: Standard-Datenbank (keine zusätzliche Installation)
### Frontend
- **Bootstrap 5**: UI-Framework
- **Font Awesome**: Icons
- **Vanilla JavaScript**: Interactive Funktionalität
### Sicherheit
- Passwort-Hashing mit Werkzeug
- CSRF-Schutz
- SQL-Injection-Schutz durch parametrisierte Queries
- Session-basierte Authentifikation
## Erweiterungsmöglichkeiten
- Mehrere Datenbankverbindungen
- Query-Syntax-Highlighting
- Autocomplete für SQL-Keywords
- Query-Performance-Analyse
- Benutzerrollen und Berechtigungen
- Query-Historie
- Dashboard mit Diagrammen
## Lizenz
MIT License

143
SELECT_FROM_AUTOCOMPLETE.md Normal file
View File

@ -0,0 +1,143 @@
# Erweiterte SQL Autocomplete - SELECT & FROM
## Übersicht
Die Autocomplete-Funktionalität wurde erweitert und unterstützt jetzt sowohl Spalten-Vorschläge nach `SELECT` als auch Tabellen-Vorschläge nach `FROM`.
## Neue Funktionen
### 1. Spalten-Autocomplete nach SELECT
**Aktivierung:**
- Automatisch nach `SELECT ` (mit Leerzeichen)
- Bei Komma-getrennten Spalten: `SELECT col1, `
- Bei partieller Eingabe: `SELECT USE` (zeigt Spalten mit "USE")
**Intelligente Erkennung:**
- Erkennt Tabelle aus FROM-Klausel automatisch
- Lädt entsprechende Spalten-Schema dynamisch
- Fallback zu allgemeinen Spalten wenn keine Tabelle erkannt
### 2. Kontext-bewusste Vorschläge
**Mit erkannter Tabelle:**
```sql
SELECT | FROM USERS
↑ Zeigt Spalten von USERS-Tabelle
```
**Ohne Tabelle (allgemeine Optionen):**
```sql
SELECT |
↑ Zeigt: *, COUNT(*), COUNT(1), ROWNUM, SYSDATE
```
## Funktionsweise
### Muster-Erkennung
1. **Nach SELECT**: `SELECT ` → Spalten-Autocomplete
2. **Spalten-Liste**: `SELECT name, ` → Weitere Spalten
3. **Partielle Eingabe**: `SELECT us` → Gefilterte Spalten
4. **Nach FROM**: `FROM ` → Tabellen-Autocomplete
### Schema-Integration
- **Dynamisches Laden**: Schema wird bei Bedarf vom Server abgerufen
- **Typ-Anzeige**: Spalten zeigen Datentyp an (z.B. `NAME (VARCHAR2)`)
- **Icons**: Unterschiedliche Icons für `*`, Spalten und Tabellen
### Keyboard-Navigation
- **↓/↑**: Navigation durch Vorschläge
- **Enter/Tab**: Auswahl übernehmen
- **Escape**: Dropdown schließen
- **Komma**: Automatische Trennung bei Mehrfach-Auswahl
## Beispiele
### Einfache Spalten-Auswahl
```sql
1. Tippe: SELECT
2. Wähle: * oder spezifische Spalte
3. Ergebnis: SELECT column_name
```
### Mehrere Spalten
```sql
1. Tippe: SELECT name,
2. Wähle: weitere Spalte aus Liste
3. Ergebnis: SELECT name, email
```
### Mit FROM-Kontext
```sql
1. Tippe: SELECT FROM USERS
2. Autocomplete lädt USERS-Spalten automatisch
3. Wähle aus USERS-spezifischen Spalten
```
### Gefilterte Suche
```sql
1. Tippe: SELECT user
2. Zeigt nur Spalten die "USER" enthalten
3. z.B.: USER_ID, USER_NAME, USERNAME
```
## Technische Details
### Neue JavaScript-Funktionen
```javascript
shouldShowColumnAutocomplete() // Erkennt SELECT-Kontext
showColumnAutocomplete() // Zeigt Spalten-Dropdown
extractTableNameFromQuery() // Findet Tabelle in FROM
loadTableSchemaForAutocomplete() // Lädt Schema dynamisch
selectColumnFromAutocomplete() // Fügt Spalte ein
```
### State-Management
```javascript
autocompleteState = {
isVisible: boolean,
selectedIndex: number,
items: array,
triggerPosition: number,
type: 'table' | 'column' // NEU: Unterscheidet Typ
}
```
### CSS-Verbesserungen
- **Icons**: `fa-columns` für Spalten, `fa-asterisk` für *
- **Typ-Info**: Grauer Text für Datentypen
- **Header**: Kontextuelle Überschriften ("Spalten von USERS")
## Intelligente Features
### 1. Kontext-Analyse
- Erkennt FROM-Klausel automatisch
- Lädt passende Spalten für die Tabelle
- Fallback bei fehlender/unklarer Tabelle
### 2. Smart-Insertion
- Erkennt Position in SELECT-Liste
- Ersetzt partielle Eingaben korrekt
- Respektiert Komma-Trennung
### 3. Performance-Optimierung
- **Caching**: Schema wird nicht doppelt geladen
- **Limitierung**: Max. 15 Spalten, 10 Tabellen
- **Lazy Loading**: Nur bei Bedarf laden
## Workflow-Integration
### Typischer Workflow
1. **Start**: `SELECT ` → Allgemeine Spalten oder *
2. **Tabelle**: `FROM tablename` → Spezifische Spalten verfügbar
3. **Erweitern**: Zurück zu SELECT → Tabellen-spezifische Vorschläge
4. **Verfeinern**: Komma + weitere Spalten auswählen
### Drag & Drop Integration
- Funktioniert weiterhin parallel zu Autocomplete
- Autocomplete für Tipper, Drag & Drop für Maus-User
- Beide Methoden ergänzen sich perfekt
## Erweiterbarkeit
Die Architektur unterstützt zukünftige Erweiterungen:
- **WHERE-Autocomplete**: Spalten + Operatoren
- **JOIN-Unterstützung**: Multi-Tabellen-Queries
- **Funktions-Vorschläge**: SQL-Funktionen
- **Alias-Erkennung**: Tabellen-Aliases berücksichtigen

59
app/__init__.py Normal file
View File

@ -0,0 +1,59 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_cors import CORS
import os
from dotenv import load_dotenv
# Lade Umgebungsvariablen
load_dotenv()
db = SQLAlchemy()
login_manager = LoginManager()
def create_app():
app = Flask(__name__)
# Konfiguration
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') or 'dev-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL') or 'sqlite:///querybuilder.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Initialisiere Extensions
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Bitte melden Sie sich an, um auf diese Seite zuzugreifen.'
CORS(app)
# Registriere Blueprints
from app.routes.auth import auth_bp
from app.routes.main import main_bp
from app.routes.api import api_bp
from app.routes.admin import admin_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(main_bp)
app.register_blueprint(api_bp, url_prefix='/api')
app.register_blueprint(admin_bp, url_prefix='/admin')
# Erstelle Datenbanktabellen
with app.app_context():
db.create_all()
# Erstelle Standard-Admin-User falls nicht vorhanden
from app.models import User
admin = User.query.filter_by(username='admin').first()
if not admin:
admin = User(username='admin', email='admin@example.com')
admin.set_password('admin123')
db.session.add(admin)
db.session.commit()
return app
@login_manager.user_loader
def load_user(user_id):
from app.models import User
return User.query.get(int(user_id))

Binary file not shown.

Binary file not shown.

73
app/models.py Normal file
View File

@ -0,0 +1,73 @@
from app import db
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
import json
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Beziehungen
saved_queries = db.relationship('SavedQuery', backref='user', lazy=True, cascade='all, delete-orphan')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
class SavedQuery(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
query_text = db.Column('query', db.Text, nullable=False) # Expliziter Spaltenname 'query', Attribut 'query_text'
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'query': self.query_text,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat()
}
def __repr__(self):
return f'<SavedQuery {self.name}>'
class DatabaseConnection(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
host = db.Column(db.String(200))
port = db.Column(db.Integer)
database = db.Column(db.String(100))
username = db.Column(db.String(100))
password = db.Column(db.String(200)) # In Produktion sollte dies verschlüsselt werden
db_type = db.Column(db.String(50)) # mysql, postgresql, sqlite, etc.
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'host': self.host,
'port': self.port,
'database': self.database,
'username': self.username,
'db_type': self.db_type
}
def __repr__(self):
return f'<DatabaseConnection {self.name}>'

Binary file not shown.

Binary file not shown.

Binary file not shown.

187
app/routes/admin.py Normal file
View File

@ -0,0 +1,187 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from functools import wraps
from app.models import User, db
from werkzeug.security import generate_password_hash
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
def admin_required(f):
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
if not current_user.is_admin:
flash('Zugriff verweigert. Administrator-Rechte erforderlich.', 'error')
return redirect(url_for('main.dashboard'))
return f(*args, **kwargs)
return decorated_function
@admin_bp.route('/')
@admin_required
def admin_dashboard():
"""Admin Dashboard mit Benutzerübersicht"""
users = User.query.order_by(User.created_at.desc()).all()
return render_template('admin/dashboard.html', users=users)
@admin_bp.route('/users')
@admin_required
def users():
"""Benutzerverwaltung"""
users = User.query.order_by(User.created_at.desc()).all()
return render_template('admin/users.html', users=users)
@admin_bp.route('/users/create', methods=['GET', 'POST'])
@admin_required
def create_user():
"""Neuen Benutzer erstellen"""
if request.method == 'POST':
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '').strip()
is_admin = 'is_admin' in request.form
# Validierung
if not username or not email or not password:
flash('Alle Felder sind erforderlich.', 'error')
return render_template('admin/create_user.html')
# Prüfe ob Benutzername bereits existiert
if User.query.filter_by(username=username).first():
flash('Benutzername bereits vergeben.', 'error')
return render_template('admin/create_user.html')
# Prüfe ob E-Mail bereits existiert
if User.query.filter_by(email=email).first():
flash('E-Mail-Adresse bereits vergeben.', 'error')
return render_template('admin/create_user.html')
try:
# Benutzer erstellen
user = User(
username=username,
email=email,
is_admin=is_admin
)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash(f'Benutzer "{username}" erfolgreich erstellt.', 'success')
return redirect(url_for('admin.users'))
except Exception as e:
db.session.rollback()
flash(f'Fehler beim Erstellen des Benutzers: {str(e)}', 'error')
return render_template('admin/create_user.html')
@admin_bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
@admin_required
def edit_user(user_id):
"""Benutzer bearbeiten"""
user = User.query.get_or_404(user_id)
if request.method == 'POST':
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '').strip()
is_admin = 'is_admin' in request.form
# Validierung
if not username or not email:
flash('Benutzername und E-Mail sind erforderlich.', 'error')
return render_template('admin/edit_user.html', user=user)
# Prüfe ob Benutzername bereits von anderem User verwendet wird
existing_user = User.query.filter_by(username=username).first()
if existing_user and existing_user.id != user_id:
flash('Benutzername bereits vergeben.', 'error')
return render_template('admin/edit_user.html', user=user)
# Prüfe ob E-Mail bereits von anderem User verwendet wird
existing_email = User.query.filter_by(email=email).first()
if existing_email and existing_email.id != user_id:
flash('E-Mail-Adresse bereits vergeben.', 'error')
return render_template('admin/edit_user.html', user=user)
try:
# Benutzer aktualisieren
user.username = username
user.email = email
user.is_admin = is_admin
# Passwort nur ändern wenn angegeben
if password:
user.set_password(password)
db.session.commit()
flash(f'Benutzer "{username}" erfolgreich aktualisiert.', 'success')
return redirect(url_for('admin.users'))
except Exception as e:
db.session.rollback()
flash(f'Fehler beim Aktualisieren des Benutzers: {str(e)}', 'error')
return render_template('admin/edit_user.html', user=user)
@admin_bp.route('/users/<int:user_id>/delete', methods=['POST'])
@admin_required
def delete_user(user_id):
"""Benutzer löschen"""
user = User.query.get_or_404(user_id)
# Verhindere dass der Admin sich selbst löscht
if user.id == current_user.id:
flash('Sie können sich nicht selbst löschen.', 'error')
return redirect(url_for('admin.users'))
# Verhindere das Löschen des letzten Admins
if user.is_admin:
admin_count = User.query.filter_by(is_admin=True).count()
if admin_count <= 1:
flash('Der letzte Administrator kann nicht gelöscht werden.', 'error')
return redirect(url_for('admin.users'))
try:
username = user.username
db.session.delete(user)
db.session.commit()
flash(f'Benutzer "{username}" erfolgreich gelöscht.', 'success')
except Exception as e:
db.session.rollback()
flash(f'Fehler beim Löschen des Benutzers: {str(e)}', 'error')
return redirect(url_for('admin.users'))
@admin_bp.route('/users/<int:user_id>/toggle_admin', methods=['POST'])
@admin_required
def toggle_admin(user_id):
"""Admin-Status umschalten"""
user = User.query.get_or_404(user_id)
# Verhindere dass der Admin sich selbst die Admin-Rechte entzieht
if user.id == current_user.id and user.is_admin:
return jsonify({'error': 'Sie können sich nicht selbst die Admin-Rechte entziehen.'}), 400
# Verhindere das Entziehen der Admin-Rechte des letzten Admins
if user.is_admin:
admin_count = User.query.filter_by(is_admin=True).count()
if admin_count <= 1:
return jsonify({'error': 'Der letzte Administrator kann nicht degradiert werden.'}), 400
try:
user.is_admin = not user.is_admin
db.session.commit()
status = 'Administrator' if user.is_admin else 'Benutzer'
return jsonify({
'success': True,
'message': f'{user.username} ist jetzt {status}.',
'is_admin': user.is_admin
})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500

186
app/routes/api.py Normal file
View File

@ -0,0 +1,186 @@
from flask import Blueprint, jsonify, request, make_response
from flask_login import login_required, current_user
from app.models import SavedQuery, db
from app.services.database_service import DatabaseService
from app.services.database_manager import DatabaseManager
import csv
import io
import json
api_bp = Blueprint('api', __name__)
@api_bp.route('/queries', methods=['GET'])
@login_required
def get_saved_queries():
"""API-Endpunkt um alle gespeicherten Queries zu erhalten"""
try:
queries = SavedQuery.query.filter_by(user_id=current_user.id).all()
return jsonify({
'success': True,
'queries': [query.to_dict() for query in queries]
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/queries/<query_name>', methods=['GET'])
@login_required
def get_query_by_name(query_name):
"""API-Endpunkt um eine gespeicherte Query nach Namen zu erhalten"""
try:
query = SavedQuery.query.filter_by(name=query_name, user_id=current_user.id).first()
if not query:
return jsonify({'error': 'Query nicht gefunden'}), 404
return jsonify({
'success': True,
'query': query.to_dict()
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/queries/<query_name>/execute', methods=['GET'])
@login_required
def execute_saved_query(query_name):
"""API-Endpunkt um eine gespeicherte Query auszuführen und Ergebnisse zu erhalten"""
try:
# Finde gespeicherte Query
saved_query = SavedQuery.query.filter_by(name=query_name, user_id=current_user.id).first()
if not saved_query:
return jsonify({'error': 'Query nicht gefunden'}), 404
# Datenbankverbindung bestimmen
connection = request.args.get('connection', 'oracle')
# Führe Query aus
db_manager = DatabaseManager()
db_service = db_manager.get_database_service(connection)
results = db_service.execute_query(saved_query.query_text)
# Format bestimmen (JSON oder CSV)
format_type = request.args.get('format', 'json').lower()
if format_type == 'csv':
return export_results_as_csv(results, query_name)
else:
return jsonify({
'success': True,
'query_name': query_name,
'results': results
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/queries/<query_name>/export/csv', methods=['GET'])
@login_required
def export_query_results_csv(query_name):
"""Exportiere Query-Ergebnisse als CSV"""
try:
# Finde gespeicherte Query
saved_query = SavedQuery.query.filter_by(name=query_name, user_id=current_user.id).first()
if not saved_query:
return jsonify({'error': 'Query nicht gefunden'}), 404
# Datenbankverbindung bestimmen
connection = request.args.get('connection', 'oracle')
# Führe Query aus
db_manager = DatabaseManager()
db_service = db_manager.get_database_service(connection)
results = db_service.execute_query(saved_query.query_text)
return export_results_as_csv(results, query_name)
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/queries/<query_name>/export/json', methods=['GET'])
@login_required
def export_query_results_json(query_name):
"""Exportiere Query-Ergebnisse als JSON"""
try:
# Finde gespeicherte Query
saved_query = SavedQuery.query.filter_by(name=query_name, user_id=current_user.id).first()
if not saved_query:
return jsonify({'error': 'Query nicht gefunden'}), 404
# Datenbankverbindung bestimmen
connection = request.args.get('connection', 'oracle')
# Führe Query aus
db_manager = DatabaseManager()
db_service = db_manager.get_database_service(connection)
results = db_service.execute_query(saved_query.query_text)
# Erstelle JSON-Response mit Download-Header
response = make_response(json.dumps({
'query_name': query_name,
'results': results
}, indent=2))
response.headers['Content-Type'] = 'application/json'
response.headers['Content-Disposition'] = f'attachment; filename="{query_name}_results.json"'
return response
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/update_query_name/<int:query_id>', methods=['PUT'])
@login_required
def update_query_name(query_id):
"""API-Endpunkt um den Namen einer gespeicherten Query zu aktualisieren"""
try:
data = request.get_json()
new_name = data.get('name', '').strip()
if not new_name:
return jsonify({'error': 'Name ist erforderlich'}), 400
# Finde Query
query = SavedQuery.query.filter_by(id=query_id, user_id=current_user.id).first()
if not query:
return jsonify({'error': 'Query nicht gefunden'}), 404
# Prüfe ob Name bereits existiert
existing_query = SavedQuery.query.filter_by(name=new_name, user_id=current_user.id).first()
if existing_query and existing_query.id != query_id:
return jsonify({'error': 'Eine Query mit diesem Namen existiert bereits'}), 400
# Update Name
query.name = new_name
db.session.commit()
return jsonify({
'success': True,
'message': 'Query-Name erfolgreich aktualisiert',
'query': query.to_dict()
})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
def export_results_as_csv(results, filename):
"""Hilfsfunktion um Ergebnisse als CSV zu exportieren"""
if not results or 'data' not in results or not results['data']:
return jsonify({'error': 'Keine Daten zum Exportieren'}), 400
# Erstelle CSV
output = io.StringIO()
# Headers schreiben
if results['columns']:
writer = csv.writer(output)
writer.writerow(results['columns'])
# Daten schreiben
for row in results['data']:
writer.writerow(row)
# Response erstellen
response = make_response(output.getvalue())
response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = f'attachment; filename="{filename}_results.csv"'
return response

56
app/routes/auth.py Normal file
View File

@ -0,0 +1,56 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required, current_user
from app.models import User
from app import db
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
login_user(user)
next_page = request.args.get('next')
return redirect(next_page) if next_page else redirect(url_for('main.dashboard'))
else:
flash('Ungültiger Benutzername oder Passwort')
return render_template('auth/login.html')
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
email = request.form['email']
password = request.form['password']
# Prüfe ob Benutzer bereits existiert
if User.query.filter_by(username=username).first():
flash('Benutzername bereits vergeben')
return render_template('auth/register.html')
if User.query.filter_by(email=email).first():
flash('E-Mail bereits vergeben')
return render_template('auth/register.html')
# Erstelle neuen Benutzer
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash('Registrierung erfolgreich')
return redirect(url_for('auth.login'))
return render_template('auth/register.html')
@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('auth.login'))

182
app/routes/main.py Normal file
View File

@ -0,0 +1,182 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from app.models import SavedQuery, DatabaseConnection
from app.services.database_service import DatabaseService
from app.services.database_manager import DatabaseManager
from app import db
import traceback
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
@login_required
def dashboard():
# Lade gespeicherte Queries des Benutzers
saved_queries = SavedQuery.query.filter_by(user_id=current_user.id).all()
# Lade verfügbare Datenbankverbindungen
db_manager = DatabaseManager()
available_connections = db_manager.get_available_connections()
return render_template('dashboard.html',
saved_queries=saved_queries,
available_connections=available_connections)
@main_bp.route('/execute_query', methods=['POST'])
@login_required
def execute_query():
data = request.get_json()
query = data.get('query', '').strip()
connection = data.get('connection', 'oracle') # Standard: Oracle
if not query:
return jsonify({'error': 'Query ist leer'}), 400
try:
# Verwende ausgewählte Datenbankverbindung
db_manager = DatabaseManager()
db_service = db_manager.get_database_service(connection)
results = db_service.execute_query(query)
return jsonify({
'success': True,
'results': results,
'connection': connection
})
except Exception as e:
return jsonify({
'error': f'Fehler beim Ausführen der Query: {str(e)}',
'traceback': traceback.format_exc()
}), 500
@main_bp.route('/get_tables')
@login_required
def get_tables():
connection = request.args.get('connection', 'oracle')
try:
db_manager = DatabaseManager()
db_service = db_manager.get_database_service(connection)
tables = db_service.get_tables()
return jsonify({
'success': True,
'tables': tables,
'connection': connection
})
except Exception as e:
return jsonify({
'error': f'Fehler beim Laden der Tabellen: {str(e)}'
}), 500
@main_bp.route('/get_table_schema/<table_name>')
@login_required
def get_table_schema(table_name):
connection = request.args.get('connection', 'oracle')
try:
db_manager = DatabaseManager()
db_service = db_manager.get_database_service(connection)
schema = db_service.get_table_schema(table_name)
return jsonify({
'success': True,
'schema': schema,
'connection': connection
})
except Exception as e:
return jsonify({
'error': f'Fehler beim Laden des Tabellenschemas: {str(e)}'
}), 500
@main_bp.route('/save_query', methods=['POST'])
@login_required
def save_query():
data = request.get_json()
name = data.get('name', '').strip()
description = data.get('description', '').strip()
query = data.get('query', '').strip()
if not name or not query:
return jsonify({'error': 'Name und Query sind erforderlich'}), 400
# Prüfe ob Query-Name bereits existiert
existing = SavedQuery.query.filter_by(name=name, user_id=current_user.id).first()
if existing:
return jsonify({'error': 'Query-Name bereits vergeben'}), 400
try:
saved_query = SavedQuery(
name=name,
description=description,
query_text=query,
user_id=current_user.id
)
db.session.add(saved_query)
db.session.commit()
return jsonify({
'success': True,
'message': 'Query erfolgreich gespeichert',
'query': saved_query.to_dict()
})
except Exception as e:
db.session.rollback()
return jsonify({'error': f'Fehler beim Speichern: {str(e)}'}), 500
@main_bp.route('/delete_query/<int:query_id>', methods=['DELETE'])
@login_required
def delete_query(query_id):
try:
query = SavedQuery.query.filter_by(id=query_id, user_id=current_user.id).first()
if not query:
return jsonify({'error': 'Query nicht gefunden'}), 404
db.session.delete(query)
db.session.commit()
return jsonify({
'success': True,
'message': 'Query erfolgreich gelöscht'
})
except Exception as e:
db.session.rollback()
return jsonify({'error': f'Fehler beim Löschen: {str(e)}'}), 500
@main_bp.route('/test_connection/<connection_name>')
@login_required
def test_connection(connection_name):
try:
db_manager = DatabaseManager()
result = db_manager.test_connection(connection_name)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'message': f'Fehler beim Testen der Verbindung: {str(e)}'
}), 500
@main_bp.route('/get_connections')
@login_required
def get_connections():
try:
db_manager = DatabaseManager()
connections = db_manager.get_available_connections()
return jsonify({
'success': True,
'connections': connections
})
except Exception as e:
return jsonify({
'error': f'Fehler beim Laden der Verbindungen: {str(e)}'
}), 500

View File

@ -0,0 +1,76 @@
from app.services.database_service import DatabaseService
import os
class DatabaseManager:
"""Manager für verschiedene Datenbankverbindungen"""
def __init__(self):
self.connections = {}
self._load_default_connections()
def _load_default_connections(self):
"""Lade Standard-Datenbankverbindungen"""
# Oracle-Datenbank (wenn konfiguriert)
oracle_host = os.environ.get('ORACLE_HOST')
if oracle_host:
self.connections['oracle'] = {
'name': 'Oracle Database',
'type': 'oracle',
'description': 'Oracle-Produktionsdatenbank',
'host': oracle_host,
'port': int(os.environ.get('ORACLE_PORT', 1521)),
'service_name': os.environ.get('ORACLE_SERVICE_NAME', 'ORCL'),
'username': os.environ.get('ORACLE_USERNAME'),
'password': os.environ.get('ORACLE_PASSWORD')
}
# PostgreSQL (wenn konfiguriert)
postgres_host = os.environ.get('POSTGRES_HOST')
if postgres_host:
self.connections['postgres'] = {
'name': 'PostgreSQL Database',
'type': 'postgresql',
'description': 'PostgreSQL-Datenbank',
'host': postgres_host,
'port': int(os.environ.get('POSTGRES_PORT', 5432)),
'database': os.environ.get('POSTGRES_DATABASE', 'postgres'),
'username': os.environ.get('POSTGRES_USERNAME'),
'password': os.environ.get('POSTGRES_PASSWORD')
}
def get_database_service(self, connection_name='demo'):
"""Erstelle DatabaseService für spezifische Verbindung"""
if connection_name not in self.connections:
raise ValueError(f"Datenbankverbindung '{connection_name}' nicht gefunden")
config = self.connections[connection_name]
# Nur Oracle-Verbindungen unterstützt
return DatabaseService(config)
def get_available_connections(self):
"""Hole alle verfügbaren Datenbankverbindungen"""
return [
{
'key': key,
'name': config['name'],
'type': config['type'],
'description': config['description']
}
for key, config in self.connections.items()
]
def test_connection(self, connection_name):
"""Teste eine Datenbankverbindung"""
try:
db_service = self.get_database_service(connection_name)
# Oracle Test-Query
test_query = "SELECT 1 FROM DUAL"
result = db_service.execute_query(test_query)
return {'success': True, 'message': 'Verbindung erfolgreich'}
except Exception as e:
return {'success': False, 'message': str(e)}

View File

@ -0,0 +1,268 @@
import os
from flask import current_app
import logging
# Oracle-Unterstützung
try:
import oracledb
ORACLE_AVAILABLE = True
except ImportError:
try:
import cx_Oracle
ORACLE_AVAILABLE = True
except ImportError:
ORACLE_AVAILABLE = False
logging.warning("Oracle-Treiber nicht verfügbar. Installiere cx-Oracle oder oracledb für Oracle-Unterstützung.")
# Weitere Datenbank-Treiber
try:
import psycopg2
POSTGRESQL_AVAILABLE = True
except ImportError:
POSTGRESQL_AVAILABLE = False
try:
import pymysql
MYSQL_AVAILABLE = True
except ImportError:
MYSQL_AVAILABLE = False
class DatabaseService:
def __init__(self, connection_config=None):
"""
Initialisiere Database Service - Nur Oracle-Unterstützung
connection_config: Dictionary mit Oracle-Verbindungsparametern
"""
if connection_config is None:
raise ValueError("Oracle-Verbindungskonfiguration erforderlich")
self.connection_config = connection_config
self.db_type = connection_config.get('type', 'oracle')
if self.db_type == 'oracle':
self._validate_oracle_config()
else:
raise ValueError(f"Datenbanktyp '{self.db_type}' wird nicht unterstützt. Nur Oracle ist verfügbar.")
def _validate_oracle_config(self):
"""Validiere Oracle-Konfiguration"""
if not ORACLE_AVAILABLE:
raise Exception("Oracle-Treiber nicht verfügbar. Installiere cx-Oracle oder oracledb.")
required_fields = ['host', 'username', 'password']
for field in required_fields:
if not self.connection_config.get(field):
raise Exception(f"Oracle-Konfiguration unvollständig: {field} fehlt")
def execute_query(self, query):
"""Führe Oracle SQL-Query aus und gebe Ergebnisse zurück"""
return self._execute_oracle_query(query)
def _execute_oracle_query(self, query):
"""Führe Oracle Query aus"""
if not ORACLE_AVAILABLE:
raise Exception("Oracle-Treiber nicht verfügbar")
# Erstelle Oracle-Verbindung
config = self.connection_config
dsn = f"{config['host']}:{config.get('port', 1521)}/{config.get('service_name', 'ORCL')}"
try:
# Verwende den neueren oracledb-Treiber wenn verfügbar, sonst cx_Oracle
try:
import oracledb
conn = oracledb.connect(
user=config['username'],
password=config['password'],
dsn=dsn
)
except ImportError:
import cx_Oracle
conn = cx_Oracle.connect(
user=config['username'],
password=config['password'],
dsn=dsn
)
cursor = conn.cursor()
try:
# Entferne Semikolon am Ende (Oracle mag das nicht bei einzelnen Statements)
cleaned_query = query.strip()
if cleaned_query.endswith(';'):
cleaned_query = cleaned_query[:-1]
cursor.execute(cleaned_query)
# Wenn es eine SELECT-Query ist, hole Ergebnisse
if query.strip().lower().startswith('select'):
columns = [desc[0] for desc in cursor.description]
data = cursor.fetchall()
# Konvertiere Oracle-spezifische Datentypen
processed_data = []
for row in data:
processed_row = []
for value in row:
# Konvertiere LOB, CLOB, BLOB zu String
if hasattr(value, 'read'):
processed_row.append(value.read())
# Konvertiere Oracle NUMBER zu Python-Typen
elif str(type(value)).find('NUMBER') != -1:
processed_row.append(float(value) if '.' in str(value) else int(value))
else:
processed_row.append(value)
processed_data.append(processed_row)
return {
'columns': columns,
'data': processed_data,
'row_count': len(processed_data)
}
else:
# Für INSERT, UPDATE, DELETE etc.
conn.commit()
return {
'message': f'Query erfolgreich ausgeführt. {cursor.rowcount} Zeilen betroffen.',
'affected_rows': cursor.rowcount
}
except Exception as e:
conn.rollback()
raise e
finally:
cursor.close()
except Exception as e:
raise Exception(f"Oracle-Datenbankfehler: {str(e)}")
finally:
if 'conn' in locals():
conn.close()
def get_tables(self):
"""Hole alle Oracle Tabellennamen"""
return self._get_oracle_tables()
def _get_oracle_tables(self):
"""Hole Oracle Tabellennamen"""
if not ORACLE_AVAILABLE:
raise Exception("Oracle-Treiber nicht verfügbar")
config = self.connection_config
dsn = f"{config['host']}:{config.get('port', 1521)}/{config.get('service_name', 'ORCL')}"
try:
try:
import oracledb
conn = oracledb.connect(
user=config['username'],
password=config['password'],
dsn=dsn
)
except ImportError:
import cx_Oracle
conn = cx_Oracle.connect(
user=config['username'],
password=config['password'],
dsn=dsn
)
cursor = conn.cursor()
# Hole alle Tabellen für den aktuellen Benutzer
cursor.execute("""
SELECT table_name
FROM user_tables
ORDER BY table_name
""")
tables = [row[0] for row in cursor.fetchall()]
cursor.close()
conn.close()
return tables
except Exception as e:
raise Exception(f"Fehler beim Laden der Oracle-Tabellen: {str(e)}")
def get_table_schema(self, table_name):
"""Hole Oracle Tabellenschema"""
return self._get_oracle_table_schema(table_name)
def _get_oracle_table_schema(self, table_name):
"""Hole Oracle Tabellenschema"""
if not ORACLE_AVAILABLE:
raise Exception("Oracle-Treiber nicht verfügbar")
config = self.connection_config
dsn = f"{config['host']}:{config.get('port', 1521)}/{config.get('service_name', 'ORCL')}"
try:
try:
import oracledb
conn = oracledb.connect(
user=config['username'],
password=config['password'],
dsn=dsn
)
except ImportError:
import cx_Oracle
conn = cx_Oracle.connect(
user=config['username'],
password=config['password'],
dsn=dsn
)
cursor = conn.cursor()
# Hole Spalten-Informationen
cursor.execute("""
SELECT
c.column_name,
c.data_type,
c.data_length,
c.data_precision,
c.data_scale,
c.nullable,
c.data_default,
CASE WHEN p.column_name IS NOT NULL THEN 'Y' ELSE 'N' END as primary_key
FROM user_tab_columns c
LEFT JOIN (
SELECT acc.column_name, acc.table_name
FROM user_constraints ac, user_cons_columns acc
WHERE ac.constraint_name = acc.constraint_name
AND ac.constraint_type = 'P'
AND ac.table_name = :table_name
) p ON c.column_name = p.column_name AND c.table_name = p.table_name
WHERE c.table_name = :table_name
ORDER BY c.column_id
""", {'table_name': table_name.upper()})
columns = []
for row in cursor.fetchall():
data_type = row[1]
if row[2]: # data_length
if row[3]: # data_precision
data_type += f"({row[3]}"
if row[4]: # data_scale
data_type += f",{row[4]}"
data_type += ")"
elif row[1] in ('VARCHAR2', 'CHAR', 'NVARCHAR2', 'NCHAR'):
data_type += f"({row[2]})"
columns.append({
'name': row[0],
'type': data_type,
'not_null': row[5] == 'N',
'default_value': row[6],
'primary_key': row[7] == 'Y'
})
cursor.close()
conn.close()
return columns
except Exception as e:
raise Exception(f"Fehler beim Laden des Oracle-Tabellenschemas: {str(e)}")

753
app/static/css/style.css Normal file
View File

@ -0,0 +1,753 @@
/* Hauptstyles für die Query Builder Anwendung */
/* Basis-Reset und Layout */
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
margin: 0;
padding: 0;
background-color: #f8f9fa;
color: #333;
line-height: 1.5;
}
/* Fallback für Bootstrap Klassen falls CDN nicht lädt */
.container-fluid {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
.row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.col-md-3 {
flex: 0 0 25%;
max-width: 25%;
padding-right: 15px;
padding-left: 15px;
}
.col-md-9 {
flex: 0 0 75%;
max-width: 75%;
padding-right: 15px;
padding-left: 15px;
}
.h-100 {
height: 100vh !important;
}
/* Navigation Bar */
.navbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
background-color: #343a40 !important;
color: white;
margin-bottom: 0;
}
.navbar-brand {
padding-top: 0.3125rem;
padding-bottom: 0.3125rem;
margin-right: 1rem;
font-size: 1.25rem;
color: white !important;
text-decoration: none;
font-weight: bold;
}
.navbar-nav {
display: flex;
flex-direction: row;
list-style: none;
margin: 0;
padding: 0;
}
.navbar-text {
color: rgba(255,255,255,.5) !important;
margin-right: 1rem;
}
.nav-link {
color: rgba(255,255,255,.5) !important;
text-decoration: none;
padding: 0.5rem 1rem;
}
.nav-link:hover {
color: rgba(255,255,255,.75) !important;
}
/* Buttons */
.btn {
display: inline-block;
font-weight: 400;
color: #212529;
text-align: center;
vertical-align: middle;
cursor: pointer;
background-color: transparent;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
text-decoration: none;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-primary:hover {
color: #fff;
background-color: #0056b3;
border-color: #004085;
}
.btn-success {
color: #fff;
background-color: #28a745;
border-color: #28a745;
}
.btn-success:hover {
color: #fff;
background-color: #1e7e34;
border-color: #1c7430;
}
.btn-secondary {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
.btn-secondary:hover {
color: #fff;
background-color: #545b62;
border-color: #4e555b;
}
.btn-outline-primary {
color: #007bff;
border-color: #007bff;
background-color: transparent;
}
.btn-outline-primary:hover {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-outline-success {
color: #28a745;
border-color: #28a745;
background-color: transparent;
}
.btn-outline-success:hover {
color: #fff;
background-color: #28a745;
border-color: #28a745;
}
.btn-outline-danger {
color: #dc3545;
border-color: #dc3545;
background-color: transparent;
}
.btn-outline-danger:hover {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
.btn-sm {
padding: 0.15rem 0.4rem;
font-size: 0.75rem;
border-radius: 0.15rem;
line-height: 1.2;
}
.btn-xs {
padding: 0.1rem 0.25rem;
font-size: 0.65rem;
border-radius: 0.1rem;
line-height: 1.1;
}
.btn-group {
position: relative;
display: inline-flex;
vertical-align: middle;
}
.btn-group > .btn {
position: relative;
flex: 1 1 auto;
margin-right: -1px;
}
.btn-group > .btn:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.btn-group > .btn:not(:first-child):not(:last-child) {
border-radius: 0;
}
.btn-group > .btn:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* Cards */
.card {
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border: 1px solid rgba(0,0,0,.125);
border-radius: 0.25rem;
margin-bottom: 1rem;
}
.card-body {
flex: 1 1 auto;
padding: 1.25rem;
}
.card-header {
padding: 0.75rem 1.25rem;
margin-bottom: 0;
background-color: rgba(0,0,0,.03);
border-bottom: 1px solid rgba(0,0,0,.125);
border-top-left-radius: calc(0.25rem - 1px);
border-top-right-radius: calc(0.25rem - 1px);
}
.card-footer {
padding: 0.75rem 1.25rem;
background-color: rgba(0,0,0,.03);
border-top: 1px solid rgba(0,0,0,.125);
}
.card-title {
margin-bottom: 0.75rem;
font-size: 1.25rem;
font-weight: 500;
}
.card-text {
margin-bottom: 1rem;
}
/* Forms */
.form-control {
display: block;
width: 100%;
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
color: #495057;
background-color: #fff;
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
.form-label {
margin-bottom: 0.5rem;
font-weight: 500;
}
/* Alerts */
.alert {
position: relative;
padding: 0.75rem 1.25rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: 0.25rem;
}
.alert-info {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb;
}
.alert-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
.alert-danger {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.alert-dismissible {
padding-right: 4rem;
}
/* Tables */
.table {
width: 100%;
margin-bottom: 1rem;
color: #212529;
border-collapse: collapse;
}
.table th,
.table td {
padding: 0.75rem;
vertical-align: top;
border-top: 1px solid #dee2e6;
}
.table thead th {
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
background-color: #f8f9fa;
font-weight: 600;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(0,0,0,.05);
}
.table-hover tbody tr:hover {
color: #212529;
background-color: rgba(0,0,0,.075);
}
/* Utilities */
.text-center { text-align: center !important; }
.text-muted { color: #6c757d !important; }
.text-danger { color: #dc3545 !important; }
.text-success { color: #28a745 !important; }
.text-primary { color: #007bff !important; }
.text-info { color: #17a2b8 !important; }
.text-warning { color: #ffc107 !important; }
.bg-light { background-color: #f8f9fa !important; }
.bg-dark { background-color: #343a40 !important; }
.border-end { border-right: 1px solid #dee2e6 !important; }
.mb-0 { margin-bottom: 0 !important; }
.mb-1 { margin-bottom: 0.25rem !important; }
.mb-2 { margin-bottom: 0.5rem !important; }
.mb-3 { margin-bottom: 1rem !important; }
.mb-4 { margin-bottom: 1.5rem !important; }
.mt-2 { margin-top: 0.5rem !important; }
.mt-3 { margin-top: 1rem !important; }
.mt-5 { margin-top: 3rem !important; }
.p-2 { padding: 0.5rem !important; }
.p-3 { padding: 1rem !important; }
.py-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; }
.d-flex { display: flex !important; }
.d-grid { display: grid !important; }
.justify-content-between { justify-content: space-between !important; }
.justify-content-center { justify-content: center !important; }
.align-items-center { align-items: center !important; }
.float-end { float: right !important; }
.ms-auto { margin-left: auto !important; }
.me-3 { margin-right: 1rem !important; }
/* Sidebar Styles */
.col-md-3.border-end {
height: calc(100vh - 56px);
overflow-y: auto;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
}
.col-md-9 {
height: calc(100vh - 56px);
overflow-y: auto;
}
/* Tabellen und Query Cards */
.saved-query-card {
transition: all 0.2s ease;
cursor: pointer;
}
.saved-query-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.table-container {
margin-bottom: 4px;
}
.table-item {
padding: 4px 8px;
margin: 1px 0;
background: white;
border: 1px solid #dee2e6;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.85rem;
}
.table-item:hover {
background: #f8f9fa;
border-color: #007bff;
}
.table-item.selected {
background: #e3f2fd;
border-color: #2196f3;
font-weight: 600;
}
.table-item i {
color: #6c757d;
margin-right: 6px;
font-size: 0.8rem;
}
/* Spalten-Anzeige unter Tabellen */
.table-columns {
margin-left: 8px;
margin-top: 2px;
padding: 4px 6px;
background: #f8f9fa;
border-left: 2px solid #007bff;
border-radius: 0 3px 3px 0;
}
.column-item {
padding: 2px 6px;
margin: 0.5px 0;
background: white;
border: 1px solid #e9ecef;
border-radius: 2px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 11px;
display: flex;
align-items: center;
gap: 4px;
line-height: 1.2;
}
.column-item:hover {
background: #e7f3ff;
border-color: #007bff;
}
.column-name {
font-weight: 500;
color: #495057;
}
.column-type {
font-size: 9px;
color: #6c757d;
margin-left: auto;
}
/* Toggle-Button für Spalten-Anzeige */
#toggle-columns-btn {
margin-right: 3px;
transition: all 0.3s ease;
padding: 0.1rem 0.3rem;
font-size: 0.7rem;
}
#toggle-columns-btn:hover {
transform: scale(1.05);
}
#toggle-columns-btn.btn-outline-secondary {
opacity: 0.7;
}
#toggle-columns-btn.btn-outline-primary {
opacity: 1;
}
/* Drag & Drop Styles */
.table-item, .column-item {
cursor: grab;
}
.table-item:active, .column-item:active {
cursor: grabbing;
}
.drag-icon {
opacity: 0;
transition: opacity 0.2s ease;
margin-left: auto;
color: #6c757d;
font-size: 10px;
}
.table-item:hover .drag-icon,
.column-item:hover .drag-icon {
opacity: 0.7;
}
/* Drop Zone Styles */
#query-input.drag-over {
border-color: #007bff !important;
box-shadow: 0 0 10px rgba(0, 123, 255, 0.3) !important;
background-color: #f8f9ff !important;
}
.table-item[draggable="true"]:hover,
.column-item[draggable="true"]:hover {
background-color: #e3f2fd;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Query Input Area */
#query-input {
font-family: 'Courier New', monospace;
font-size: 14px;
resize: vertical;
min-height: 150px;
}
/* Results Table */
.results-table {
max-height: 400px;
overflow: auto;
}
.results-table table {
font-size: 14px;
}
.results-table th {
position: sticky;
top: 0;
background: #f8f9fa;
border-bottom: 2px solid #dee2e6;
}
/* Loading Spinner */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Alert Styles */
.alert-custom {
margin: 0 0 15px 0;
padding: 10px 15px;
border-radius: 4px;
}
.alert-success-custom {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.alert-error-custom {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
/* Responsive Design */
@media (max-width: 768px) {
.col-md-3.border-end {
height: auto;
max-height: 300px;
}
.col-md-9 {
height: auto;
}
body {
overflow: auto;
}
.container-fluid {
height: auto;
overflow: auto;
}
.row.h-100 {
height: auto;
}
}
/* Button Improvements */
.btn-group-sm > .btn, .btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
/* Card Improvements */
.card {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
/* Navigation Improvements */
.navbar-brand {
font-weight: bold;
}
/* Modal Improvements */
.modal-content {
border-radius: 8px;
}
.modal-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
/* Table Schema Popup */
.schema-popup {
position: absolute;
background: white;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
z-index: 1000;
max-width: 300px;
font-size: 12px;
}
/* Autocomplete Dropdown */
.autocomplete-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #007bff;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1050;
max-height: 200px;
overflow-y: auto;
display: none;
}
.autocomplete-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s ease;
font-size: 0.9rem;
display: flex;
align-items: center;
}
.autocomplete-item:last-child {
border-bottom: none;
}
.autocomplete-item:hover {
background-color: #f8f9fa;
}
.autocomplete-item.selected {
background-color: #e3f2fd;
color: #1976d2;
}
.autocomplete-item i {
margin-right: 8px;
color: #6c757d;
font-size: 0.8rem;
}
.autocomplete-item small {
margin-left: auto;
font-size: 0.75rem;
color: #6c757d;
}
.autocomplete-item strong {
flex: 1;
}
.autocomplete-header {
padding: 6px 12px;
background-color: #f8f9fa;
font-size: 0.75rem;
font-weight: 600;
color: #495057;
border-bottom: 1px solid #dee2e6;
}
.schema-column {
padding: 2px 0;
border-bottom: 1px solid #f0f0f0;
}
.schema-column:last-child {
border-bottom: none;
}

File diff suppressed because one or more lines are too long

9
app/static/css/vendor/all.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
app/static/favicon.ico Normal file
View File

@ -0,0 +1 @@
# Placeholder für favicon - in Produktion sollte hier eine echte .ico Datei sein

1628
app/static/js/app.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}Login - Query Builder{% endblock %}
{% block content %}
<div class="row justify-content-center mt-5">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-header">
<h4 class="text-center mb-0">
<i class="fas fa-sign-in-alt"></i> Anmelden
</h4>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">Benutzername</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="fas fa-sign-in-alt"></i> Anmelden
</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<a href="{{ url_for('auth.register') }}" class="text-decoration-none">
Noch kein Konto? Hier registrieren
</a>
</div>
</div>
<div class="mt-3 p-3 bg-light rounded">
<h6>Demo-Zugang:</h6>
<p class="small mb-1"><strong>Benutzername:</strong> admin</p>
<p class="small mb-0"><strong>Passwort:</strong> admin123</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}Registrierung - Query Builder{% endblock %}
{% block content %}
<div class="row justify-content-center mt-5">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-header">
<h4 class="text-center mb-0">
<i class="fas fa-user-plus"></i> Registrierung
</h4>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">Benutzername</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">E-Mail</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success">
<i class="fas fa-user-plus"></i> Registrieren
</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<a href="{{ url_for('auth.login') }}" class="text-decoration-none">
Bereits ein Konto? Hier anmelden
</a>
</div>
</div>
</div>
</div>
{% endblock %}

61
app/templates/base.html Normal file
View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Query Builder{% endblock %}</title>
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<!-- Bootstrap CSS (lokal gehostet) -->
<link href="{{ url_for('static', filename='css/vendor/bootstrap.min.css') }}" rel="stylesheet">
<!-- Font Awesome Icons (lokal gehostet) -->
<link href="{{ url_for('static', filename='css/vendor/all-local.min.css') }}" rel="stylesheet">
<!-- Custom Styles (Fallback für Bootstrap + eigene Styles) -->
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('main.dashboard') }}">
<i class="fas fa-database"></i> Query Builder
</a>
{% if current_user.is_authenticated %}
<div class="navbar-nav ms-auto">
<span class="navbar-text me-3">
<i class="fas fa-user"></i> {{ current_user.username }}
</span>
<a class="nav-link" href="{{ url_for('auth.logout') }}">
<i class="fas fa-sign-out-alt"></i> Abmelden
</a>
</div>
{% endif %}
</div>
</nav>
<main class="container-fluid">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="row mt-3">
<div class="col-12">
{% for message in messages %}
<div class="alert alert-info alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<!-- Bootstrap JS (lokal gehostet) -->
<script src="{{ url_for('static', filename='js/vendor/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,255 @@
{% extends "base.html" %}
{% block title %}Dashboard - Query Builder{% endblock %}
{% block content %}
<div class="row h-100">
<!-- Linke Sidebar - Tabellen -->
<div class="col-md-2 border-end bg-light">
<div class="p-3">
<!-- Datenbankauswahl Section -->
<div class="mb-4">
<h6><i class="fas fa-database"></i> Datenbankverbindung</h6>
<select id="database-selector" class="form-select form-select-sm" onchange="onDatabaseChange()">
<option value="demo">Lade Verbindungen...</option>
</select>
<button class="btn btn-sm btn-outline-info mt-2 w-100" onclick="testConnection()">
<i class="fas fa-plug"></i> Verbindung testen
</button>
</div>
<!-- Tabellen Section -->
<div class="mb-4">
<h5>
<i class="fas fa-table"></i> Tabellen
<div class="float-end">
<button class="btn btn-sm btn-outline-secondary" id="toggle-columns-btn" onclick="toggleColumnsView()" title="Spalten ein/ausblenden">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-sm btn-outline-primary" onclick="loadTables()">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</h5>
<div id="tables-list">
<div class="text-muted">Lade Tabellen...</div>
</div>
</div>
</div>
</div>
<!-- Hauptbereich - Query Editor und Ergebnisse -->
<!-- Mittlere Spalte - Query Editor und Ergebnisse -->
<div class="col-md-8">
<div class="p-3">
<!-- Query Editor Section -->
<div class="mb-4">
<h5><i class="fas fa-code"></i> SQL Query Editor</h5>
<div class="card">
<div class="card-body">
<div class="position-relative">
<textarea id="query-input" class="form-control mb-3" rows="8"
placeholder="Geben Sie hier Ihre Oracle SQL-Query ein...&#10;&#10;Beispiel für Oracle:&#10;SELECT * FROM USERS WHERE ROWNUM <= 10"></textarea>
<div id="autocomplete-dropdown" class="autocomplete-dropdown"></div>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="executeQuery()">
<i class="fas fa-play"></i> Query Ausführen
</button>
<button type="button" class="btn btn-success" id="save-query-btn">
<i class="fas fa-save"></i> Query Speichern
</button>
<button class="btn btn-secondary" onclick="clearQuery()">
<i class="fas fa-trash"></i> Löschen
</button>
</div>
</div>
</div>
</div>
<!-- Ergebnisse Section -->
<div class="mb-3">
<h5><i class="fas fa-table"></i> Query-Ergebnisse</h5>
<div id="results-container" class="card">
<div class="card-body">
<div class="text-muted text-center py-4">
Führen Sie eine Query aus, um Ergebnisse anzuzeigen
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Rechte Sidebar - Gespeicherte Queries -->
<div class="col-md-2 border-start bg-light">
<div class="p-2 h-100">
<div class="mb-3">
<h6><i class="fas fa-bookmark"></i> Gespeicherte Queries</h6>
</div>
<!-- Query-Suche -->
<div class="mb-3">
<input type="text" id="query-search" class="form-control form-control-sm"
placeholder="Query suchen..." onkeyup="filterSavedQueries()">
</div>
<!-- Gespeicherte Queries Liste -->
<div id="saved-queries-list" class="overflow-auto" style="max-height: calc(100vh - 250px);">
{% for query in saved_queries %}
<div class="card mb-2 saved-query-card" data-query-id="{{ query.id }}">
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1" style="cursor: pointer;" onclick="loadSavedQuery({{ query.id }})">
<h6 class="card-title mb-1 text-primary">{{ query.name }}</h6>
<small class="text-muted d-block mb-1">{{ query.created_at.strftime('%d.%m.%Y %H:%M') }}</small>
<small class="text-muted query-preview">{{ query.query[:100] }}{% if query.query|length > 100 %}...{% endif %}</small>
</div>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="loadSavedQuery({{ query.id }})">
<i class="fas fa-play"></i> Laden
</a></li>
<li><a class="dropdown-item" href="#" onclick="editQueryName({{ query.id }})">
<i class="fas fa-edit"></i> Umbenennen
</a></li>
<li><a class="dropdown-item" href="#" onclick="duplicateQuery({{ query.id }})">
<i class="fas fa-copy"></i> Duplizieren
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" onclick="deleteSavedQuery({{ query.id }})">
<i class="fas fa-trash"></i> Löschen
</a></li>
</ul>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- Platzhalter wenn keine Queries vorhanden -->
<div id="no-queries-placeholder" class="text-center text-muted py-4" {% if saved_queries %}style="display: none;"{% endif %}>
<i class="fas fa-bookmark fa-2x mb-2"></i>
<p>Keine gespeicherten Queries vorhanden.</p>
<small>Speichern Sie Ihre erste Query mit dem <i class="fas fa-save"></i> Button!</small>
</div>
</div>
</div>
</div>
</div>
<!-- Save Query Modal -->
<div class="modal fade" id="saveQueryModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Query speichern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="query-name" class="form-label">Query-Name:</label>
<input type="text" class="form-control" id="query-name"
placeholder="z.B. Benutzer-Liste" required autofocus>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-success" onclick="saveQuery()">
<i class="fas fa-save"></i> Speichern
</button>
</div>
</div>
</div>
</div>
<!-- Edit Query Name Modal -->
<div class="modal fade" id="editQueryModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Query umbenennen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="edit-query-name" class="form-label">Neuer Name:</label>
<input type="text" class="form-control" id="edit-query-name" required autofocus>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" onclick="updateQueryName()">
<i class="fas fa-save"></i> Umbenennen
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Verbindungen werden automatisch in app.js geladen
// currentConnection ist jetzt in app.js definiert
// Die loadConnections und onDatabaseChange Funktionen sind jetzt in app.js
// Teste Datenbankverbindung
function testConnection() {
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Teste...';
button.disabled = true;
fetch(`/test_connection/${currentConnection}`)
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('success', `✓ ${data.message}`);
} else {
showAlert('danger', `✗ ${data.message}`);
}
})
.catch(error => {
showAlert('danger', `Fehler beim Testen der Verbindung: ${error}`);
})
.finally(() => {
button.innerHTML = originalText;
button.disabled = false;
});
}
// Hilfsfunktion für Alerts
function showAlert(type, message) {
// Entferne vorhandene Alerts
const existingAlert = document.querySelector('.alert');
if (existingAlert) {
existingAlert.remove();
}
// Erstelle neues Alert
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Füge Alert am Anfang des Contents ein
const content = document.querySelector('.container-fluid');
content.insertBefore(alertDiv, content.firstChild);
// Auto-hide nach 5 Sekunden
setTimeout(() => {
if (alertDiv && alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
</script>
{% endblock %}

BIN
demo_data.db Normal file

Binary file not shown.

45
init_db.py Normal file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""
Initialisierungs-Script für Query Builder Datenbank
Erstellt das neue Schema und löscht alte Datenbank-Dateien
"""
import os
import sys
import glob
# Lösche alle .db Dateien
print("Lösche alte Datenbankdateien...")
db_files = glob.glob("*.db")
for db_file in db_files:
try:
os.remove(db_file)
print(f"Gelöscht: {db_file}")
except Exception as e:
print(f"Fehler beim Löschen von {db_file}: {e}")
# Initialisiere neue Datenbank
print("Erstelle neue Datenbank mit korrektem Schema...")
try:
from app import create_app, db
from app.models import User, SavedQuery, DatabaseConnection
app = create_app()
with app.app_context():
# Erstelle alle Tabellen
db.create_all()
# Erstelle Standard-Admin-User falls nicht vorhanden
admin = User.query.filter_by(username='admin').first()
if not admin:
admin = User(username='admin', email='admin@example.com')
admin.set_password('admin123')
db.session.add(admin)
db.session.commit()
print("Admin-Benutzer erstellt: admin/admin123")
print("Datenbank erfolgreich initialisiert!")
except Exception as e:
print(f"Fehler bei der Datenbankinitialisierung: {e}")
sys.exit(1)

BIN
instance/querybuilder.db Normal file

Binary file not shown.

15
requirements.txt Normal file
View File

@ -0,0 +1,15 @@
Flask==2.3.3
Flask-Login==0.6.3
Flask-SQLAlchemy==3.0.5
Flask-WTF==1.1.1
WTForms==3.0.1
Werkzeug==2.3.7
SQLAlchemy==2.0.21
python-dotenv==1.0.0
psycopg2-binary==2.9.7
pymysql==1.1.0
cryptography==41.0.4
flask-cors==4.0.0
pandas==2.1.1
cx-Oracle==8.3.0
oracledb==1.4.2

6
run.py Normal file
View File

@ -0,0 +1,6 @@
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)

View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<title>Autocomplete Test</title>
<script>
function testColumnAutocomplete() {
// Teste die verschiedenen Funktionen
console.log('Testing extractTableNameFromQuery...');
// Test-Queries
const testQueries = [
'SELECT * FROM AUTH_CONFIG',
'SELECT NAME FROM AUTH_CONFIG WHERE',
'select id, name from AUTH_CONFIG',
'SELECT COUNT(*) FROM AUTH_CONFIG',
'SELECT * FROM AUTH_CONFIG ORDER BY'
];
testQueries.forEach(query => {
console.log(`Query: "${query}"`);
// Simuliere extractTableNameFromQuery
const upperText = query.toUpperCase();
const patterns = [
/\bFROM\s+([A-Z_][A-Z0-9_]*)/,
/\bFROM\s+([A-Z_][A-Z0-9_]*)\s+[A-Z_]/,
/\bFROM\s+([A-Z_][A-Z0-9_]*)\s*,/,
/\bFROM\s+([A-Z_][A-Z0-9_]*)\s*$/,
/\bFROM\s+([A-Z_][A-Z0-9_]*)\s+WHERE/,
/\bFROM\s+([A-Z_][A-Z0-9_]*)\s+ORDER/,
/\bFROM\s+([A-Z_][A-Z0-9_]*)\s+GROUP/
];
let found = false;
for (const pattern of patterns) {
const match = upperText.match(pattern);
if (match && match[1]) {
console.log(` Found table: ${match[1]} with pattern: ${pattern}`);
found = true;
break;
}
}
if (!found) {
console.log(' No table found');
}
// Teste shouldShowColumnAutocomplete
const textBeforeCursor = query.toUpperCase();
const hasFromClause = /\bFROM\s+[A-Z_][A-Z0-9_]*/.test(textBeforeCursor);
console.log(` Has FROM clause: ${hasFromClause}`);
const selectPatterns = [
/\bSELECT\s*$/,
/\bSELECT\s+((?:[A-Z_0-9*]+(?:\s*,\s*)?)*)\s*([A-Z_0-9]*)$/,
/\bSELECT\s+[^,]+(?:\s*,\s*[^,]*)*\s*,\s*([A-Z_0-9]*)$/
];
selectPatterns.forEach((pattern, index) => {
if (pattern.test(textBeforeCursor)) {
console.log(` SELECT pattern ${index} matched`);
}
});
console.log('---');
});
}
// Teste beim Laden der Seite
window.onload = testColumnAutocomplete;
</script>
</head>
<body>
<h1>Autocomplete Debug Test</h1>
<p>Öffne die Browser-Konsole um die Test-Ergebnisse zu sehen.</p>
<p>Teste dann in der echten Anwendung: "SELECT " gefolgt von "FROM AUTH_CONFIG"</p>
</body>
</html>