first commit
This commit is contained in:
commit
44b8667f31
83
AUTOCOMPLETE_FEATURE.md
Normal file
83
AUTOCOMPLETE_FEATURE.md
Normal 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
62
COMPACT_UI_CHANGES.md
Normal 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
65
OFFLINE_RESOURCES.md
Normal 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
213
README.md
Normal 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
143
SELECT_FROM_AUTOCOMPLETE.md
Normal 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
59
app/__init__.py
Normal 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))
|
||||
BIN
app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/models.cpython-313.pyc
Normal file
BIN
app/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
73
app/models.py
Normal file
73
app/models.py
Normal 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}>'
|
||||
BIN
app/routes/__pycache__/api.cpython-313.pyc
Normal file
BIN
app/routes/__pycache__/api.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/auth.cpython-313.pyc
Normal file
BIN
app/routes/__pycache__/auth.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/main.cpython-313.pyc
Normal file
BIN
app/routes/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
187
app/routes/admin.py
Normal file
187
app/routes/admin.py
Normal 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
186
app/routes/api.py
Normal 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
56
app/routes/auth.py
Normal 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
182
app/routes/main.py
Normal 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
|
||||
BIN
app/services/__pycache__/database_manager.cpython-313.pyc
Normal file
BIN
app/services/__pycache__/database_manager.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/services/__pycache__/database_service.cpython-313.pyc
Normal file
BIN
app/services/__pycache__/database_service.cpython-313.pyc
Normal file
Binary file not shown.
76
app/services/database_manager.py
Normal file
76
app/services/database_manager.py
Normal 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)}
|
||||
268
app/services/database_service.py
Normal file
268
app/services/database_service.py
Normal 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
753
app/static/css/style.css
Normal 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;
|
||||
}
|
||||
9
app/static/css/vendor/all-local.min.css
vendored
Normal file
9
app/static/css/vendor/all-local.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
app/static/css/vendor/all.min.css
vendored
Normal file
9
app/static/css/vendor/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
app/static/css/vendor/bootstrap.min.css
vendored
Normal file
6
app/static/css/vendor/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
app/static/favicon.ico
Normal file
1
app/static/favicon.ico
Normal 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
1628
app/static/js/app.js
Normal file
File diff suppressed because it is too large
Load Diff
7
app/static/js/vendor/bootstrap.bundle.min.js
vendored
Normal file
7
app/static/js/vendor/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
app/static/webfonts/fa-brands-400.woff2
Normal file
BIN
app/static/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
app/static/webfonts/fa-regular-400.woff2
Normal file
BIN
app/static/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
app/static/webfonts/fa-solid-900.woff2
Normal file
BIN
app/static/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
47
app/templates/auth/login.html
Normal file
47
app/templates/auth/login.html
Normal 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 %}
|
||||
46
app/templates/auth/register.html
Normal file
46
app/templates/auth/register.html
Normal 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
61
app/templates/base.html
Normal 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>
|
||||
255
app/templates/dashboard.html
Normal file
255
app/templates/dashboard.html
Normal 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... Beispiel für Oracle: 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
BIN
demo_data.db
Normal file
Binary file not shown.
45
init_db.py
Normal file
45
init_db.py
Normal 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
BIN
instance/querybuilder.db
Normal file
Binary file not shown.
15
requirements.txt
Normal file
15
requirements.txt
Normal 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
6
run.py
Normal 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)
|
||||
78
test_autocomplete_debug.html
Normal file
78
test_autocomplete_debug.html
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user