diff --git a/CODE_STYLE.md b/CODE_STYLE.md index b6190a5..03024e2 100644 --- a/CODE_STYLE.md +++ b/CODE_STYLE.md @@ -15,31 +15,18 @@ If a requested change conflicts with these rules, the change must **stop** and c --- -## Core Rules (Quick Reference) +## Mandatory Rules (Quick Reference) -The following rules are the most critical and must always be respected: +The following rules are strict and MUST NOT be violated. When generating or modifying code, tools and agents MUST consult this file before producing changes. 1. All code, comments, documentation, commit messages, and user-facing text MUST be written in English. 2. Python typing rules MUST be respected (PEP 585 generics; `Optional[T]`, not `T | None`). -3. Import ordering and grouping rules MUST be followed exactly. -4. Functions and methods MUST NOT exceed 50 lines. -5. Code changes MUST avoid modifying unrelated code. -6. Naming MUST remain explicit and descriptive (no aggressive abbreviations). -7. Code MUST remain mypy-friendly whenever possible. - -When generating or modifying code, tools and agents MUST consult this file before producing changes. - ---- - -## Mandatory Rules - -The following rules are strict and MUST NOT be violated: - -- English must always be used for code and documentation. -- `typing.Optional[T]` MUST be used instead of `T | None`. -- `from __future__ import annotations` MUST NOT be used. -- Import ordering rules MUST be respected exactly. -- Functions MUST NOT exceed the maximum size limit. +3. `from __future__ import annotations` MUST NOT be used. +4. Import ordering and grouping rules MUST be followed exactly. +5. Functions and methods MUST NOT exceed 80 lines. +6. Code changes MUST avoid modifying unrelated code. +7. Naming MUST remain explicit and descriptive (no aggressive abbreviations). +8. Code MUST remain mypy-friendly whenever possible. --- @@ -431,9 +418,10 @@ This rule applies only to builtin modules. import os import sys +import gettext as _ + import numpy as np import pandas as pd -import gettext as _ ``` #### Bad example @@ -448,7 +436,7 @@ import sys When importing multiple symbols from the same module: -- Parenthesized multiline `from ... import (...)` MUST NOT be used for functions or methods. +- Parenthesized multiline `from ... import (...)` MUST NOT be used. - Imports MUST NOT be split into one line per symbol. - Prefer a single `from ... import ...` line whenever possible. - If a `from ... import ...` statement would exceed the maximum line width: @@ -632,14 +620,14 @@ class Example: ## 9. Function and Method Size -- A function/method MUST be at most 50 lines. -- If it exceeds 50 lines, it MUST be split into smaller functions/methods with clear names. +- A function/method MUST be at most 80 lines. +- If it exceeds 80 lines, it MUST be split into smaller functions/methods with clear names. --- ## 10. Walrus Operator ( := ) -- The walrus operator MAY be used when it improves clarity and avoids redundant calls. +- The walrus operator MUST be used whenever it avoids redundant calls or repeated expressions. - It MUST NOT be used when it makes the control flow harder to read. #### Good examples diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 7c0e50c..339790a 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -1,44 +1,33 @@ # PeterSQL — Project Status -> **Last Updated:** 2026-03-10 -> **Validation Policy:** new engine features are marked **PARTIAL** until broader integration validation is complete. +> **Last Updated:** 2026-04-27 +> **Status Rule:** newly implemented features are tracked as **PARTIAL** until validated across supported versions. +> **Definition of DONE:** engine methods implemented, integration tests pass on target versions, UI workflow exists (if user-facing), no known regressions, documentation updated. --- -## 1. Executive Summary +## Priority Matrix -### ✅ Solid and Stable Areas +| Priority | Focus | Target | +|----------|-------|--------| +| 🔴 **P0 - Validation Now** | stabilize newly added engine features | 1-2 weeks | +| 🟡 **P1 - Engine Gaps** | close remaining CRUD parity gaps | 2-4 weeks | +| 🟢 **P2 - UI Completeness** | add missing editors for exposed objects | 1-2 months | +| 🔵 **P3 - Advanced Features** | schema/security/import-export roadmap | 2-3 months | + +--- + +## 1. Solid and Stable Areas | Area | Status | |------|--------| | **SQLite Engine** | Most mature path with complete day-to-day table/record workflows. | | **MySQL/MariaDB Core** | Strong parity for tables, columns, indexes, foreign keys, records, views, triggers, functions. | | **UI Core Editors** | Table, columns, indexes, foreign keys, records, and view editor are operational. | +| **Multi-tab Query Editor** | Multi-tab editor with per-tab dirty tracking, autosave, cancelable execution, and configurable shortcuts. | | **Explorer Navigation** | Databases, tables, views, triggers, procedures, functions, and events are visible in tree explorer. | | **SSH Tunnel Support** | Implemented for MySQL, MariaDB, and PostgreSQL. | -### 🟡 Partial / Under Validation - -| Area | Current State | -|------|---------------| -| **MySQL Procedure** | Class + CRUD methods exist, context introspection exists, integration tests added, broader validation still ongoing. | -| **MariaDB Procedure** | Class + CRUD methods exist, context introspection exists, integration tests added, broader validation still ongoing. | -| **PostgreSQL Function** | Class + CRUD methods exist, context introspection exists, integration tests now cover create/alter/drop across supported PG versions; broader validation still ongoing. | -| **PostgreSQL Procedure** | Class + CRUD methods exist, context introspection exists, integration tests now cover create/alter/drop across supported PG versions; broader validation still ongoing. | -| **Check Constraints (MySQL/MariaDB/PostgreSQL)** | Engine classes and introspection exist, cross-version validation still needed. | -| **Connection Reliability Features** | Persistent connection statistics, empty DB password support, and TLS auto-retry are implemented and need longer real-world validation. | -| **SQL Dump / Backup** | `SQLDatabase.dump()` produces object-driven `.sql` dumps (schema + records); restore/import workflow is still missing. | -| **Database Lifecycle (Create/Drop)** | Engine database objects expose lifecycle methods, but context/UI workflow parity is still incomplete. | - -### ❌ Missing / Not Implemented - -| Area | Notes | -|------|-------| -| **Function/Procedure UI Editors** | Explorer lists objects, but dedicated create/edit UI is still missing. | -| **Database Create/Drop UI** | No complete create/drop workflow across engines. | -| **Schema/Sequence Management** | PostgreSQL schema/sequence CRUD is not available. | -| **User/Role/Grants** | Not implemented for any engine. | - --- ## 2. Engine Capability Matrix @@ -112,68 +101,116 @@ | Table / Column / Index / Foreign Key | ✅ | ✅ | ✅ | ✅ | Main table editor workflow complete. | | Check Constraint | ✅ | 🟡 | 🟡 | 🟡 | `TableCheckController` exists; broader multi-engine UX validation pending. | | View | ✅ | ✅ | ✅ | ✅ | Dedicated view editor is available. | -| Trigger | ✅ | ❌ | ❌ | ❌ | Explorer only; no dedicated trigger editor panel yet. | +| Trigger | ✅ | ❌ | ❌ | ❌ | Explorer only; no dedicated editor yet. | | Function | ✅ | ❌ | ❌ | ❌ | Explorer only; no dedicated editor yet. | | Procedure | ✅ | ❌ | ❌ | ❌ | Explorer only; no dedicated editor yet. | | Event | ✅ | ❌ | ❌ | ❌ | Explorer only. | | Records | ✅ | ✅ | ✅ | ✅ | Insert/update/delete/duplicate in table records tab. | +| Query Editor | ✅ | ✅ | ✅ | ✅ | Multi-tab, cancelable execution, configurable shortcuts, autosave. | --- -## 4. Cross-Cutting Notes +## 4. Feature Backlog -### Recently Added +### 🔴 P0 - Validation Now -- Persistent connection statistics in connection model and dialog. -- Empty database password accepted in connection validation. -- Automatic TLS retry path for MySQL/MariaDB when server requires TLS. -- Unit reliability coverage for MySQL/MariaDB TLS auto-retry and SSH tunnel lifecycle contracts. -- SQL dump/backup pipeline is now object-driven via `SQLDatabase.dump()` + per-object `raw_create()`. -- CI workflow split into `test`, `update` (nightly), and `release` jobs. +- [x] **PostgreSQL Function engine implementation** (PARTIAL) + - **Files:** `structures/engines/postgresql/database.py`, `structures/engines/postgresql/context.py` + - **Next:** long-run/manual workflow validation + broader regression checks. -### Main Remaining Risks +- [x] **PostgreSQL Procedure engine implementation** (PARTIAL) + - **Files:** `structures/engines/postgresql/database.py`, `structures/engines/postgresql/context.py` + - **Next:** long-run/manual workflow validation + introspection consistency checks. -- PostgreSQL Function/Procedure now have integration coverage for create/alter/drop, but still need broader long-run/manual validation. -- Check constraints across MySQL/MariaDB/PostgreSQL need more cross-version coverage. -- SQL dump/backup still needs broader cross-engine manual restore validation. -- SSH tunnel integration validation with testcontainers remains blocked (existing SSH integration suites are still skipped). -- UI parity lags engine parity for Trigger/Function/Procedure editors. +- [x] **Check constraint implementations for MySQL/MariaDB/PostgreSQL** (PARTIAL) + - **Files:** `structures/engines/mysql/`, `structures/engines/mariadb/`, `structures/engines/postgresql/` + - **Next:** cross-version validation matrix. ---- +- [x] **Connection reliability updates** (PARTIAL) + - **Scope:** persistent connection statistics, empty DB password support, TLS auto-retry (MySQL/MariaDB). + - **Files:** `structures/connection.py`, `windows/dialogs/connections/` + - **Next:** SSH testcontainers integration validation (currently skipped) + long-run behavioral validation. -## 5. Actionable Backlog (High Signal) +- [x] **SQL dump/backup object-driven flow** (PARTIAL) + - **Scope:** `SQLDatabase.dump()` generates SQL dump files through domain objects (`raw_create()`). + - **Files:** `structures/engines/database.py`, `structures/engines/dump.py`, per-engine `database.py` + - **Next:** cross-engine manual restore/import verification from produced dumps. -### Priority A — Validate Newly Implemented Features +### 🟡 P1 - Engine Gaps -1. PostgreSQL Function/Procedure long-run validation (manual workflows + regression suites after integration coverage). -2. Check constraints validation matrix for MySQL, MariaDB, PostgreSQL. -3. Connection statistics + TLS auto-retry robustness checks. +- [x] **MySQL Procedure implementation** (PARTIAL) + - **Files:** `structures/engines/mysql/context.py`, `structures/engines/mysql/database.py` -### Priority B — Close Engine Gaps +- [x] **MariaDB Procedure implementation** (PARTIAL) + - **Files:** `structures/engines/mariadb/context.py`, `structures/engines/mariadb/database.py` -1. Complete context/UI wiring for database lifecycle (create/drop) across engines. +- [ ] **Database lifecycle parity (context + UI wiring)** + - **Current state:** engine database objects expose create/alter/drop, but context/UI workflow remains read/list oriented. + - **Files:** `structures/engines/*/context.py` -### Priority C — UI Completeness +### 🟢 P2 - UI Completeness -1. Trigger create/edit UI. -2. Function create/edit UI. -3. Procedure create/edit UI. +- [x] **View Create/Edit Dialog** — DONE. (`windows/main/tabs/view.py`, `helpers/sql.py`) +- [x] **Multi-tab query editor** — DONE. (`windows/main/controller.py`, `windows/main/tabs/query.py`) +- [x] **Cancelable query execution with richer result metadata** — DONE. (`windows/main/tabs/query.py`) +- [x] **Configurable keyboard shortcuts for query editor** — DONE. (`windows/main/controller.py`, `windows/dialogs/settings/`) +- [ ] **Trigger Create/Edit UI** — Explorer visibility exists, editor panel missing. +- [ ] **Function Create/Edit UI** — Explorer visibility exists, editor panel missing. +- [ ] **Procedure Create/Edit UI** — Explorer visibility exists, editor panel missing. +- [ ] **Database Create/Drop UI** — Depends on engine create/drop API parity. -### Priority D — Future Platform Features +### 🔵 P3 - Advanced Features -1. PostgreSQL schema CRUD. -2. PostgreSQL sequence CRUD. -3. User/role/grants management. -4. Restore and structured import/export workflows. +- [ ] PostgreSQL schema CRUD +- [ ] PostgreSQL sequence CRUD +- [ ] User/role management +- [ ] Privileges/grants management +- [ ] Restore + structured import/export workflows +- [ ] PostgreSQL advanced objects (materialized views, partitioning, extensions) --- -## 6. Definition of DONE +## 5. Progress Snapshot + +- **P0 implemented (partial):** 5/5 +- **P1 gaps closed:** 2/3 +- **P2 UI tasks complete:** 4/8 +- **P3 advanced tasks complete:** 0/6 -A capability is treated as **DONE** only when: +--- + +## 6. Recently Added + +- SQL autocomplete extended to INSERT / UPDATE / DELETE and string literals; parser improved with JSON and multi-table coverage. +- Table execution flow updated in the records UI. +- `row_format` and `convert_data` options added to the MySQL/MariaDB table editor. +- `windows/main/` modules restructured into subdirectories (`database/`, `table/`, `query/`) for better separation of concerns. +- Advanced cell editor replaced with a dedicated `ColumnContentDialog` for displaying and editing large cell content. +- Database options action buttons now update live when options change. +- Tree explorer preserves expanded state after a failed connection attempt. +- Multi-tab query editor with per-tab dirty tracking, autosave before execution, and close/save confirmation dialogs. +- Cancelable query execution with background thread, per-statement result rendering, and execution summary. +- Configurable keyboard shortcuts for all query editor actions (execute, stop, new tab, close tab, save, save-as). +- `save` toolbar tool is disabled when the query has no unsaved changes. +- Settings module moved to `helpers/settings.py` with restructured key schema. +- `skip_before_connect` / `skip_after_connect` support added to all engine contexts. +- Persistent connection statistics in connection model and dialog. +- Empty database password accepted in connection validation. +- Automatic TLS retry path for MySQL/MariaDB when server requires TLS. +- Unit reliability coverage for MySQL/MariaDB TLS auto-retry and SSH tunnel lifecycle contracts. +- SQL dump/backup pipeline is now object-driven via `SQLDatabase.dump()` + per-object `raw_create()`. +- CI workflow split into `test`, `update` (nightly), and `release` jobs. + +--- + +## 7. Main Remaining Risks + +- PostgreSQL Function/Procedure still need broader long-run/manual validation. +- Check constraints across MySQL/MariaDB/PostgreSQL need more cross-version coverage. +- SQL dump/backup still needs broader cross-engine manual restore validation. +- SSH tunnel integration validation with testcontainers remains blocked. +- UI parity lags engine parity for Trigger/Function/Procedure editors. + +--- -- Engine methods are implemented (`create/read/update/delete` where applicable). -- Integration tests pass on target engine versions. -- UI workflow exists (if feature is user-facing in explorer). -- No known regression in existing suites. -- Documentation is updated (`README`, `PROJECT_STATUS`, `ROADMAP`). +*This document is a living reference and should be updated whenever a PARTIAL item is validated or a new gap is identified.* \ No newline at end of file diff --git a/PeterSQL.fbp b/PeterSQL.fbp index ec7b972..924f4ae 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -50,7 +50,7 @@ -1,-1 ConnectionsDialog - 800,600 + 900,768 wxDEFAULT_DIALOG_STYLE|wxDIALOG_NO_PARENT|wxRESIZE_BORDER ; ; forward_declare Connection @@ -1439,27 +1439,17 @@ 5 wxEXPAND - 0 + 1 - bSizer116 + bSizer159 wxHORIZONTAL none 5 - wxEXPAND - 0 - - 0 - protected - 156 - - - - 5 - wxALL + wxALIGN_CENTER_VERTICAL|wxALL 0 - + 1 1 1 @@ -1473,7 +1463,6 @@ 1 0 - 0 1 1 @@ -1489,15 +1478,16 @@ 0 0 wxID_ANY - Use TLS + Connection timeout + 0 0 0 - + 150,-1 1 - use_tls_enabled + m_staticText84 1 @@ -1511,20 +1501,17 @@ ; ; forward_declare 0 - - wxFILTER_NONE - wxDefaultValidator - + -1 5 wxALL - 0 - + 1 + 1 1 1 @@ -1538,7 +1525,6 @@ 1 0 - 0 1 1 @@ -1554,15 +1540,17 @@ 0 0 wxID_ANY - Use SSH tunnel + 10 + 60 0 + 0 0 1 - ssh_tunnel_enabled + connection_timeout 1 @@ -1572,14 +1560,11 @@ Resizable 1 - + wxSP_ARROW_KEYS ; ; forward_declare 0 - - wxFILTER_NONE - wxDefaultValidator - + @@ -1587,143 +1572,30 @@ - - 5 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_staticline5 - 1 - - - protected - 1 - - Resizable - 1 - - wxLI_HORIZONTAL - ; ; forward_declare - 0 - - - - - - - - - - - 0 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 1 - wxID_ANY - - 0 - - - 0 - - 1 - panel_source - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer105 - wxVERTICAL - none 5 wxEXPAND - 1 + 0 - bSizer106 + bSizer116 wxHORIZONTAL none 5 - wxALIGN_CENTER|wxALL + wxEXPAND 0 - + + 0 + protected + 156 + + + + 5 + wxALL + 0 + 1 1 1 @@ -1737,6 +1609,7 @@ 1 0 + 0 1 1 @@ -1752,8 +1625,7 @@ 0 0 wxID_ANY - Filename - 0 + Use TLS 0 @@ -1761,7 +1633,7 @@ 0 1 - m_staticText50 + use_tls 1 @@ -1770,22 +1642,46 @@ Resizable 1 - 150,-1 + ; ; forward_declare 0 + + wxFILTER_NONE + wxDefaultValidator + - -1 + + + + + + 5 + wxEXPAND + 0 + + + bSizer163 + wxHORIZONTAL + none + + 5 + wxEXPAND + 0 + + 0 + protected + 156 5 - wxALIGN_CENTER|wxALL - 1 - + wxALL + 0 + 1 1 1 @@ -1799,6 +1695,7 @@ 1 0 + 0 1 1 @@ -1814,15 +1711,15 @@ 0 0 wxID_ANY + Use SSH tunnel 0 - Select a file 0 1 - filename + ssh_tunnel_enabled 1 @@ -1832,7 +1729,7 @@ Resizable 1 - wxFLP_CHANGE_DIR|wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST + ; ; forward_declare 0 @@ -1840,8 +1737,6 @@ wxFILTER_NONE wxDefaultValidator - - *.* @@ -1849,437 +1744,383 @@ - - - - - 5 - wxEXPAND - 0 - - - bSizer122111 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Comments - 0 - - 0 - - - 0 - -1,-1 - 1 - m_staticText22111 - 1 - - - protected - 1 - - Resizable - 1 - 150,-1 - - - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - comments - 1 - - - protected - 1 - - Resizable - 1 - -1,200 - wxTE_MULTILINE - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - + + 5 + wxEXPAND + 0 + + + bSizer164 + wxHORIZONTAL + none + + 5 + wxEXPAND + 0 + + 0 + protected + 156 + + + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Compressed client/server protocol + + 0 + + + 0 + + 1 + compressed_protocol + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + - - - - - - SSH Tunnel - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 0 - - 1 - - 0 - 1 - wxID_ANY - - 0 - - - 0 - - 1 - panel_ssh_tunnel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer102 - wxVERTICAL - none - 5 - wxEXPAND - 0 - - - bSizer1213 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - SSH executable - 0 - - 0 - - - 0 - -1,-1 - 1 - m_staticText213 - 1 - - - protected - 1 - - Resizable - 1 - 150,-1 - - - 0 - - - - - -1 - - - - 5 - wxALIGN_CENTER|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - ssh_tunnel_executable - 1 - - - protected - 1 - - Resizable - 1 - - - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - ssh - - - - - - - - - 5 - wxEXPAND + 0 + wxEXPAND | wxALL 0 - + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 1 + wxID_ANY + + 0 + + + 0 - bSizer12131 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - SSH host + port - 0 - - 0 - - - 0 - -1,-1 - 1 - m_staticText2131 - 1 - - - protected - 1 - - Resizable - 1 - 150,-1 - - - 0 - - - - - -1 - - - - 5 - wxALIGN_CENTER|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 + 1 + panel_source + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer105 + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + + bSizer106 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Filename + 0 + + 0 + + + 0 + + 1 + m_staticText50 + 1 + + + protected + 1 + + Resizable + 1 + 150,-1 + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALIGN_CENTER|wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + Select a file + + 0 + + 1 + filename + 1 + + + protected + 1 + + Resizable + 1 + + wxFLP_CHANGE_DIR|wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + *.* + + + + + + + + + + + + 5 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_staticline5 + 1 + + + protected + 1 + + Resizable + 1 + + wxLI_HORIZONTAL + ; ; forward_declare + 0 + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer122111 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 0 1 @@ -2296,15 +2137,16 @@ 0 0 wxID_ANY + Comments + 0 0 - 0 0 - + -1,-1 1 - ssh_tunnel_hostname + m_staticText22111 1 @@ -2313,26 +2155,22 @@ Resizable 1 - + 150,-1 0 - - wxFILTER_NONE - wxDefaultValidator - - + -1 5 - wxALL - 0 - + wxALL|wxEXPAND + 1 + 1 1 1 @@ -2361,17 +2199,15 @@ 0 0 wxID_ANY - 22 - 65536 0 - 0 + 0 0 1 - ssh_tunnel_port + comments 1 @@ -2380,85 +2216,94 @@ Resizable 1 - - wxSP_ARROW_KEYS - ; ; forward_declare + -1,200 + wxTE_MULTILINE + 0 + + wxFILTER_NONE + wxDefaultValidator + - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - Load From File; icons/16x16/information.png - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_bitmap11 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - SSH host + port (the SSH server that forwards traffic to the DB) - - - - - + + + + + + SSH Tunnel + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 0 + + 1 + + 0 + 1 + wxID_ANY + + 0 + + + 0 + + 1 + panel_ssh_tunnel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer102 + wxVERTICAL + none 5 wxEXPAND 0 - bSizer12132 + bSizer1213 wxHORIZONTAL none @@ -2494,7 +2339,7 @@ 0 0 wxID_ANY - SSH username + SSH executable 0 0 @@ -2503,7 +2348,7 @@ 0 -1,-1 1 - m_staticText2132 + m_staticText213 1 @@ -2564,7 +2409,7 @@ 0 1 - ssh_tunnel_username + ssh_tunnel_executable 1 @@ -2582,7 +2427,7 @@ wxFILTER_NONE wxDefaultValidator - + ssh @@ -2596,7 +2441,7 @@ 0 - bSizer121321 + bSizer12131 wxHORIZONTAL none @@ -2632,7 +2477,7 @@ 0 0 wxID_ANY - SSH password + SSH host + port 0 0 @@ -2641,7 +2486,7 @@ 0 -1,-1 1 - m_staticText21321 + m_staticText2131 1 @@ -2702,7 +2547,7 @@ 0 1 - ssh_tunnel_password + ssh_tunnel_hostname 1 @@ -2712,7 +2557,7 @@ Resizable 1 - wxTE_PASSWORD + 0 @@ -2726,22 +2571,11 @@ - - - - 5 - wxEXPAND - 0 - - - bSizer1213211 - wxHORIZONTAL - none 5 - wxALIGN_CENTER|wxALL + wxALL 0 - + 1 1 1 @@ -2770,16 +2604,17 @@ 0 0 wxID_ANY - Local port - 0 + 22 + 65536 0 + 0 0 - -1,-1 + 1 - m_staticText213211 + ssh_tunnel_port 1 @@ -2788,22 +2623,22 @@ Resizable 1 - 150,-1 - - + + wxSP_ARROW_KEYS + ; ; forward_declare 0 + - -1 5 - wxALL - 1 - + wxALL|wxEXPAND + 0 + 1 1 1 @@ -2814,6 +2649,7 @@ 0 + Load From File; icons/16x16/information.png 1 0 @@ -2832,17 +2668,14 @@ 0 0 wxID_ANY - 0 - 65536 0 - 0 0 1 - ssh_tunnel_local_port + m_bitmap11 1 @@ -2852,11 +2685,9 @@ Resizable 1 - wxSP_ARROW_KEYS ; ; forward_declare 0 - if the value is set to 0, the first available port will be used - + SSH host + port (the SSH server that forwards traffic to the DB) @@ -2870,7 +2701,7 @@ 0 - bSizer1213212 + bSizer12132 wxHORIZONTAL none @@ -2906,7 +2737,7 @@ 0 0 wxID_ANY - Identity file + SSH username 0 0 @@ -2915,7 +2746,7 @@ 0 -1,-1 1 - m_staticText213212 + m_staticText2132 1 @@ -2939,7 +2770,7 @@ 5 wxALIGN_CENTER|wxALL 1 - + 1 1 1 @@ -2971,12 +2802,12 @@ 0 - Select a file + 0 0 1 - identity_file + ssh_tunnel_username 1 @@ -2986,8 +2817,8 @@ Resizable 1 - wxFLP_CHANGE_DIR|wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST - ; ; forward_declare + + 0 @@ -2995,7 +2826,6 @@ wxDefaultValidator - *.* @@ -3005,70 +2835,11 @@ 5 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_staticline6 - 1 - - - protected - 1 - - Resizable - 1 - - wxLI_HORIZONTAL - ; ; forward_declare - 0 - - - - - - - - 5 - wxEXPAND + wxEXPAND 0 - bSizer121311 + bSizer121321 wxHORIZONTAL none @@ -3104,7 +2875,7 @@ 0 0 wxID_ANY - Remote host + port + SSH password 0 0 @@ -3113,7 +2884,7 @@ 0 -1,-1 1 - m_staticText21311 + m_staticText21321 1 @@ -3174,7 +2945,7 @@ 0 1 - remote_hostname + ssh_tunnel_password 1 @@ -3184,7 +2955,7 @@ Resizable 1 - + wxTE_PASSWORD 0 @@ -3198,11 +2969,22 @@ + + + + 5 + wxEXPAND + 0 + + + bSizer1213211 + wxHORIZONTAL + none 5 - wxALL + wxALIGN_CENTER|wxALL 0 - + 1 1 1 @@ -3231,17 +3013,16 @@ 0 0 wxID_ANY - 3306 - 65536 + Local port + 0 0 - 0 0 - + -1,-1 1 - remote_port + m_staticText213211 1 @@ -3250,22 +3031,22 @@ Resizable 1 - - wxSP_ARROW_KEYS - ; ; forward_declare + 150,-1 + + 0 - + -1 5 - wxALL|wxEXPAND - 0 - + wxALL + 1 + 1 1 1 @@ -3276,7 +3057,6 @@ 0 - Load From File; icons/16x16/information.png 1 0 @@ -3295,14 +3075,17 @@ 0 0 wxID_ANY + 0 + 65536 0 + 0 0 1 - m_bitmap1 + ssh_tunnel_local_port 1 @@ -3312,9 +3095,11 @@ Resizable 1 + wxSP_ARROW_KEYS ; ; forward_declare 0 - Remote host/port is the real DB target (defaults to DB Host/Port). + if the value is set to 0, the first available port will be used + @@ -3322,82 +3107,18 @@ - - - - - - Statistics - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - panel_statistics - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer361 - wxVERTICAL - none 5 wxEXPAND 0 - bSizer37 + bSizer1213212 wxHORIZONTAL none 5 - wxALL + wxALIGN_CENTER|wxALL 0 1 @@ -3428,16 +3149,16 @@ 0 0 wxID_ANY - Created at + Identity file 0 0 0 - 200,-1 + -1,-1 1 - m_staticText15 + m_staticText213212 1 @@ -3446,9 +3167,9 @@ Resizable 1 - + 150,-1 - ; ; forward_declare + 0 @@ -3459,9 +3180,9 @@ 5 - wxALL - 0 - + wxALIGN_CENTER|wxALL + 1 + 1 1 1 @@ -3490,16 +3211,15 @@ 0 0 wxID_ANY - - 0 0 + Select a file 0 1 - created_at + identity_file 1 @@ -3509,45 +3229,109 @@ Resizable 1 - + wxFLP_CHANGE_DIR|wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST ; ; forward_declare 0 + + wxFILTER_NONE + wxDefaultValidator + + + *.* - -1 5 - wxEXPAND + wxEXPAND | wxALL 0 - + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 - bSizer371 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 + 1 + m_staticline6 + 1 + + + protected + 1 + + Resizable + 1 + + wxLI_HORIZONTAL + ; ; forward_declare + 0 + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer121311 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 1 1 @@ -3563,16 +3347,16 @@ 0 0 wxID_ANY - Last connection + Remote host + port 0 0 0 - 200,-1 + -1,-1 1 - m_staticText151 + m_staticText21311 1 @@ -3581,9 +3365,9 @@ Resizable 1 - + 150,-1 - ; ; forward_declare + 0 @@ -3594,9 +3378,9 @@ 5 - wxALL - 0 - + wxALIGN_CENTER|wxALL + 1 + 1 1 1 @@ -3625,16 +3409,15 @@ 0 0 wxID_ANY - - 0 0 + 0 0 1 - last_connection_at + remote_hostname 1 @@ -3645,31 +3428,24 @@ 1 - ; ; forward_declare + 0 + + wxFILTER_NONE + wxDefaultValidator + + - -1 - - - - 5 - wxEXPAND - 0 - - - bSizer3711 - wxHORIZONTAL - none 5 wxALL 0 - + 1 1 1 @@ -3698,16 +3474,17 @@ 0 0 wxID_ANY - Successful connections - 0 + 3306 + 65536 0 + 0 0 - 200,-1 + 1 - m_staticText1511 + remote_port 1 @@ -3717,21 +3494,21 @@ Resizable 1 - + wxSP_ARROW_KEYS ; ; forward_declare 0 + - -1 5 - wxALL + wxALL|wxEXPAND 0 - + 1 1 1 @@ -3742,6 +3519,7 @@ 0 + Load From File; icons/16x16/information.png 1 0 @@ -3760,8 +3538,6 @@ 0 0 wxID_ANY - - 0 0 @@ -3769,7 +3545,7 @@ 0 1 - successful_connected + m_bitmap1 1 @@ -3779,14 +3555,12 @@ Resizable 1 - ; ; forward_declare 0 - + Remote host/port is the real DB target (defaults to DB Host/Port). - -1 @@ -3797,12 +3571,12 @@ 0 - bSizer371111 + bSizer121322 wxHORIZONTAL none 5 - wxALL + wxALIGN_CENTER|wxALL 0 1 @@ -3833,16 +3607,16 @@ 0 0 wxID_ANY - Last successful connection + SSH extra args 0 0 0 - 200,-1 + -1,-1 1 - m_staticText151111 + m_staticText21322 1 @@ -3851,9 +3625,9 @@ Resizable 1 - + 150,-1 - ; ; forward_declare + 0 @@ -3864,9 +3638,9 @@ 5 - wxALL + wxALIGN_CENTER|wxALL 1 - + 1 1 1 @@ -3895,16 +3669,15 @@ 0 0 wxID_ANY - - 0 0 + 0 0 1 - last_successful_connection + ssh_tunnel_extra_args 1 @@ -3915,24 +3688,92 @@ 1 - ; ; forward_declare + 0 + + wxFILTER_NONE + wxDefaultValidator + + - -1 + + + + + + Statistics + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + panel_statistics + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer361 + wxVERTICAL + none 5 wxEXPAND 0 - bSizer37111 + bSizer37 wxHORIZONTAL none @@ -3968,7 +3809,7 @@ 0 0 wxID_ANY - Unsuccessful connections + Created at 0 0 @@ -3977,7 +3818,7 @@ 0 200,-1 1 - m_staticText15111 + m_staticText15 1 @@ -4039,7 +3880,7 @@ 0 1 - unsuccessful_connections + created_at 1 @@ -4067,7 +3908,7 @@ 0 - bSizer371112 + bSizer371 wxHORIZONTAL none @@ -4103,7 +3944,7 @@ 0 0 wxID_ANY - Last failure reason + Last connection 0 0 @@ -4112,7 +3953,7 @@ 0 200,-1 1 - m_staticText151112 + m_staticText151 1 @@ -4135,7 +3976,7 @@ 5 wxALL - 1 + 0 1 1 @@ -4174,7 +4015,7 @@ 0 1 - last_failure_raison + last_connection_at 1 @@ -4202,7 +4043,7 @@ 0 - bSizer3711121 + bSizer3711 wxHORIZONTAL none @@ -4238,7 +4079,7 @@ 0 0 wxID_ANY - Total connection attempts + Successful connections 0 0 @@ -4247,7 +4088,7 @@ 0 200,-1 1 - m_staticText1511121 + m_staticText1511 1 @@ -4270,7 +4111,7 @@ 5 wxALL - 1 + 0 1 1 @@ -4309,7 +4150,7 @@ 0 1 - total_connection_attempts + successful_connected 1 @@ -4337,7 +4178,7 @@ 0 - bSizer37111211 + bSizer371111 wxHORIZONTAL none @@ -4373,7 +4214,7 @@ 0 0 wxID_ANY - Average connection time (ms) + Last successful connection 0 0 @@ -4382,7 +4223,7 @@ 0 200,-1 1 - m_staticText15111211 + m_staticText151111 1 @@ -4444,7 +4285,7 @@ 0 1 - average_connection_time + last_successful_connection 1 @@ -4472,7 +4313,7 @@ 0 - bSizer371112111 + bSizer37111 wxHORIZONTAL none @@ -4508,7 +4349,7 @@ 0 0 wxID_ANY - Most recent connection duration + Unsuccessful connections 0 0 @@ -4517,7 +4358,7 @@ 0 200,-1 1 - m_staticText151112111 + m_staticText15111 1 @@ -4540,7 +4381,7 @@ 5 wxALL - 1 + 0 1 1 @@ -4579,7 +4420,7 @@ 0 1 - most_recent_connection_duration + unsuccessful_connections 1 @@ -4601,17233 +4442,561 @@ - - - - - - - - - - - - 5 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_staticline4 - 1 - - - protected - 1 - - Resizable - 1 - - wxLI_HORIZONTAL - ; ; forward_declare - 0 - - - - - - - - 0 - wxEXPAND - 0 - - - bSizer28 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - - bSizer301 - wxHORIZONTAL - none - - 5 - wxALL|wxBOTTOM - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/add.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Create - - 0 - - 0 - - - 0 - - 1 - btn_create - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_create - - MenuCreate - m_menu12 - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Create connection - m_menuItem16 - none - - - - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Create directory - m_menuItem17 - none - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/folder.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - - - 0 - - 0 - - - 0 - - 1 - btn_create_directory - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBU_EXACTFIT|wxBU_NOTEXT - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_create_directory - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/delete.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Delete - - 0 - - 0 - - - 0 - - 1 - btn_delete - 1 - - - protected - 1 - - - - Resizable - 1 - - - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete - - - - - - 5 - wxEXPAND - 1 - - - bSizer110 - wxHORIZONTAL - none - - - - 5 - wxEXPAND - 0 - - - bSizer29 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 1 - wxID_ANY - Cancel - - 0 - - 0 - - - 0 - - 1 - btn_cancel - 1 - - - protected - 1 - - - - Resizable - 1 - - - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/disk.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Save - - 0 - - 0 - - - 0 - - 1 - btn_save - 1 - - - protected - 1 - - - - Resizable - 1 - - - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_save - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/world_go.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Test - - 0 - - 0 - - - 0 - - 1 - btn_test - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_test_session - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/server_go.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Connect - - 0 - - 0 - - - 0 - - 1 - btn_open - 1 - - - protected - 1 - - - - Resizable - 1 - - - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_connect - - - - - - - - - - 0 - wxAUI_MGR_DEFAULT - - wxBOTH - - 1 - 0 - 1 - impl_virtual - - - - 0 - wxID_ANY - - 800,600 - SettingsDialog - - 800,600 - wxDEFAULT_DIALOG_STYLE - ; ; forward_declare - Settings - - 0 - - - - - - bSizer63 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_notebook4 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - - - Locale - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - locales - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer65 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - - bSizer64 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Language - 0 - - 0 - - - 0 - - 1 - m_staticText27 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - "English" "Italian" "French" - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_choice5 - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - wxBORDER_NONE - - - - - - - - - - - - - 0 - wxAUI_MGR_DEFAULT - - wxBOTH - - 1 - 0 - 1 - impl_virtual - - - - 0 - wxID_ANY - - 640,480 - AdvancedCellEditorDialog - - 900,550 - wxDEFAULT_DIALOG_STYLE - ; ; forward_declare - Edit Value - - 0 - - - - - - bSizer111 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - - bSizer112 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - - bSizer113 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Syntax - 0 - - 0 - - - 0 - - 1 - m_staticText51 - 1 - - - protected - 1 - - Resizable - 1 - -1,-1 - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - syntax_choice - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_syntax_changed - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 1 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - 0 - - 0 - 0 - wxID_ANY - 1 - 1 - - 0 - - - 0 - - 1 - advanced_stc_editor - 1 - - - protected - 1 - - 0 - Resizable - 1 - - ; ; forward_declare - 1 - 4 - 0 - - 0 - 0 - 0 - - - - - - - 5 - wxEXPAND - 0 - - - bSizer114 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Cancel - - 0 - - 0 - - - 0 - - 1 - m_button49 - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Ok - - 0 - - 0 - - - 0 - - 1 - m_button48 - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - 0 - - - wxBOTH - - 1 - 0 - 1 - impl_virtual - - - - 0 - wxID_ANY - - 800,600 - MainFrameView - - 1280,1024 - wxDEFAULT_FRAME_STYLE|wxMAXIMIZE_BOX - - PeterSQL - - 0 - - - wxTAB_TRAVERSAL - 1 - do_close - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - m_menubar2 - protected - - - - - - - - - - File - m_menu2 - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Settings - m_menuItem22 - none - - - on_settings - - - - Help - m_menu4 - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - About - m_menuItem15 - none - - - on_menu_about - - - - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - - 0 - - - 0 - - 1 - m_toolBar1 - 1 - 1 - - - protected - 1 - - Resizable - 5 - 1 - - wxTB_HORIZONTAL - ; ; forward_declare - 0 - - - - - - Load From File; icons/16x16/server_connect.png - 0 - wxID_ANY - wxITEM_NORMAL - Open connection manager - m_tool5 - protected - - - do_open_connection_manager - - - Load From File; icons/16x16/disconnect.png - 0 - wxID_ANY - wxITEM_NORMAL - Disconnect from server - m_tool4 - protected - - - do_disconnect - - - protected - - - Load From File; icons/16x16/database_refresh.png - 0 - wxID_ANY - wxITEM_NORMAL - tool - database_refresh - protected - Refresh - Refresh - - - protected - - - Load From File; icons/16x16/database_add.png - 0 - wxID_ANY - wxITEM_NORMAL - Add - database_add - protected - - - - - Load From File; icons/16x16/database_delete.png - 0 - wxID_ANY - wxITEM_NORMAL - Add - database_delete - protected - - - - - - - bSizer19 - wxVERTICAL - none - - 0 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel13 - 1 - - - protected - 1 - - Resizable - 1 - - - 0 - - - - wxTAB_TRAVERSAL - - - bSizer21 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_splitter51 - 1 - - - protected - 1 - - Resizable - 1 - -150 - -1 - 1 - - wxSPLIT_HORIZONTAL - wxSP_3D|wxSP_LIVE_UPDATE - ; ; forward_declare - 0 - - - - - - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel22 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer72 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 100 - - 0 - - 1 - m_splitter4 - 1 - - - protected - 1 - - Resizable - 0.0 - 320 - -1 - 1 - - wxSPLIT_VERTICAL - wxSP_LIVE_UPDATE - - 0 - - - - - - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel14 - 1 - - - protected - 1 - - Resizable - 1 - - - 0 - - - - wxTAB_TRAVERSAL - - - bSizer24 - wxHORIZONTAL - none - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - - - - - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - - - - 0 - - - 0 - - 1 - tree_ctrl_explorer - 1 - - - protected - - - - 1 - - self.tree_ctrl_explorer = wx.lib.agw.hypertreelist.HyperTreeList( self.m_panel14, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, agwStyle=wx.TR_DEFAULT_STYLE|wx.TR_SINGLE|wx.TR_FULL_ROW_HIGHLIGHT|wx.TR_HIDE_ROOT|wx.TR_LINES_AT_ROOT ) - import wx.lib.agw.hypertreelist - - Resizable - 1 - - ; ; forward_declare - 0 - - - - - - - - - MyMenu - m_menu5 - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - MyMenuItem - m_menuItem4 - none - - - - - - MyMenu - m_menu1 - protected - - - 1 - 1 - - wxID_ANY - wxITEM_NORMAL - MyMenuItem - m_menuItem5 - none - - - - - - - - - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel15 - 0 - - - protected - 1 - - Resizable - 1 - - - 0 - - - - wxTAB_TRAVERSAL - - - bSizer25 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - 16,16 - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - MainFrameNotebook - 1 - - - protected - 1 - - Resizable - 1 - - wxNB_FIXEDWIDTH - - 0 - - - - - on_page_chaged - - Load From File; icons/16x16/server.png - System - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - panel_system - 0 - - - protected - 1 - - Resizable - 1 - - - 0 - - - - wxTAB_TRAVERSAL - - - bSizer272 - wxVERTICAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - MyLabel - 0 - - 0 - - - 0 - - 1 - m_staticText291 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - system_databases - protected - - - - ; ; forward_declare - - - - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Databases - wxDATAVIEW_CELL_INERT - m_dataViewListColumn1 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Size - wxDATAVIEW_CELL_INERT - m_dataViewListColumn2 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Elements - wxDATAVIEW_CELL_INERT - m_dataViewListColumn3 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Modified at - wxDATAVIEW_CELL_INERT - m_dataViewListColumn4 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Tables - wxDATAVIEW_CELL_INERT - m_dataViewListColumn5 - protected - Text - -1 - - - - - - - - Load From File; icons/16x16/database.png - Database - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - panel_database - 0 - - - protected - 1 - - Resizable - 1 - - - 0 - - - - wxTAB_TRAVERSAL - - - bSizer27 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_notebook6 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - - - Options - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel30 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer80 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_splitter7 - 1 - - - protected - 1 - - Resizable - 0.0 - -1 - -1 - 1 - - wxSPLIT_HORIZONTAL - wxSP_3D - ; ; forward_declare - 0 - - - - - - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel54 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer158 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - - bSizer159 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Name - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText90 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - database_name - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer1481111 - wxHORIZONTAL - none - - - - 5 - wxEXPAND - 0 - - - bSizer142 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_character_set_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer139 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Character set - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText70 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_character_set - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_collation_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer1392 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Collation - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText702 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_collation - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer13911 - wxHORIZONTAL - none - - 0 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_encryption_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer1391 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Encryption - - 0 - - - 0 - - 1 - database_encryption - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_read_only_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer148 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Read Only - - 0 - - - 0 - - 1 - database_read_only - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer92 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_tablespace_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer13912 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Tablespace - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText7012 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_tablespace - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_connection_limit_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer139111 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Connection limit - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText70111 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - 0 - 0 - - 0 - - 0 - - 0 - - 1 - database_connection_limit - 1 - - - protected - 1 - - Resizable - 1 - - wxSP_ARROW_KEYS - ; ; forward_declare - 0 - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer1481 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_password_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer139121 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Password - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText70121 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl36 - 1 - - - protected - 1 - - Resizable - 1 - - wxTE_PASSWORD - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - 5 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_profile_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer1391111 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Profile - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText701111 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_profile - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer96 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_default_tablespace_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer1391212 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Default tablespace - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText701212 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_default_tablespace - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_temporary_tablespace_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer13912121 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Temporary tablespace - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText7012121 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_temporary_tablespace - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer14811 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_quota_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer1391211 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Quota - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText701211 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - database_quota - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - 5 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_unlimited_quota_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer13911111 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Unlimited quota - - 0 - - - 0 - - 1 - database_unlimited_quota - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer148111 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_account_status_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer13912111 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Account status - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText7012111 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_account_status - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxALIGN_CENTER - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_password_expire_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer139111111 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Password expire - - 0 - - - 0 - - 1 - database_password_expire - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - - - - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel55 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer154 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - - bSizer531 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Table: - 0 - - 0 - - - 0 - -1,-1 - 1 - m_staticText391 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxEXPAND - 0 - - 0 - protected - 100 - - - - 2 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/add.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Insert - - 0 - - 0 - - - 0 - - 1 - btn_insert_table - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_insert_table - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/table_multiple.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Clone - - 0 - - 0 - - - 0 - - 1 - btn_clone_table - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_clone_table - - - - 2 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/delete.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Delete - - 0 - - 0 - - - 0 - - 1 - btn_delete_table1 - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_table - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - - - 5 - wxEXPAND - 1 - - - bSizer152 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - list_ctrl_database_tables - protected - - - - ; ; forward_declare - - - - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn12 - protected - Text - -1 - - - wxALIGN_RIGHT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Rows - wxDATAVIEW_CELL_INERT - 1 - m_dataViewColumn13 - protected - Text - -1 - - - wxALIGN_RIGHT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Size - wxDATAVIEW_CELL_INERT - 2 - m_dataViewColumn14 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Created at - wxDATAVIEW_CELL_INERT - 3 - m_dataViewColumn15 - protected - Date - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Updated at - wxDATAVIEW_CELL_INERT - 4 - m_dataViewColumn16 - protected - Date - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Engine - wxDATAVIEW_CELL_INERT - 5 - m_dataViewColumn17 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Collation - wxDATAVIEW_CELL_INERT - 6 - m_dataViewColumn19 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Comments - wxDATAVIEW_CELL_INERT - 7 - m_dataViewColumn18 - protected - Text - -1 - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer138 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Cancel - - 0 - - 0 - - - 0 - - 1 - btn_cancel_database - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_cancel_database - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Delete - - 0 - - 0 - - - 0 - - 1 - btn_delete_database - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_database - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Apply - - 0 - - 0 - - - 0 - - 1 - btn_apply_database - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_apply_database - - - - - - - - - - Diagram - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 1 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel31 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer82 - wxVERTICAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - MyLabel - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText7011 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - MyLabel - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText7011111 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - MyLabel - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText70111111 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - - - - - - - MyMenu - m_menu15 - protected - - - - - Load From File; icons/16x16/table.png - Table - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - panel_table - 1 - - - protected - 1 - - Resizable - 1 - - - 0 - - - - wxTAB_TRAVERSAL - - - bSizer251 - wxVERTICAL - none - - 0 - wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 200 - - 0 - - 1 - m_splitter41 - 1 - - - protected - 1 - - Resizable - 0.0 - 200 - -1 - 1 - - wxSPLIT_HORIZONTAL - wxSP_LIVE_UPDATE - ; ; forward_declare - 0 - - - - - - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel19 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer55 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - 16,16 - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_notebook3 - 1 - - - protected - 1 - - Resizable - 1 - - wxNB_FIXEDWIDTH - - 0 - - - - - - Load From Embedded File; icons/16x16/table.png - Base - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - PanelTableBase - 1 - - - protected - 1 - - Resizable - 1 - - - 0 - - - - wxTAB_TRAVERSAL - - - bSizer262 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - - bSizer271 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Name - 0 - - 0 - - - 0 - - 1 - m_staticText8 - 1 - - - protected - 1 - - Resizable - 1 - 150,-1 - - - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - table_name - 1 - - - protected - 1 - - Resizable - 1 - - - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer273 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Comments - 0 - - 0 - - - 0 - - 1 - m_staticText83 - 1 - - - protected - 1 - - Resizable - 1 - 150,-1 - - - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - table_comment - 1 - - - protected - 1 - - Resizable - 1 - - wxTE_MULTILINE - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - - Load From File; icons/16x16/wrench.png - Options - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - PanelTableOptions - 1 - - - protected - 1 - - Resizable - 1 - - - 0 - - - - wxTAB_TRAVERSAL - - - bSizer261 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 2 - 0 - - gSizer11 - none - 0 - 0 - - 5 - wxEXPAND - 1 - - - bSizer27111 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Auto Increment - 0 - - 0 - - - 0 - - 1 - m_staticText8111 - 1 - - - protected - 1 - - Resizable - 1 - 150,-1 - - - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - table_auto_increment - 1 - - - protected - 1 - - Resizable - 1 - - - - 0 - - - wxFILTER_EXCLUDE_CHAR_LIST|wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer2712 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Engine - 0 - - 0 - - - 0 - - 1 - m_staticText812 - 1 - - - protected - 1 - - Resizable - 1 - 150,-1 - - - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - "" - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - table_engine - 1 - - - protected - 1 - - Resizable - 1 - 1 - - - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer2721 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Default Collation - 0 - - 0 - - - 0 - - 1 - m_staticText821 - 1 - - - protected - 1 - - Resizable - 1 - 150,-1 - - - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - table_collation - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - - - Load From File; icons/16x16/lightning.png - Indexes - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - PanelTableIndex - 1 - - - protected - 1 - - Resizable - 1 - - - 0 - - - - wxTAB_TRAVERSAL - - - bSizer28 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER - 0 - - - bSizer791 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/delete.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Remove - - 0 - - 0 - - - 0 - - 1 - btn_delete_index - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_index - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/cross.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Clear - - 0 - - 0 - - - 0 - - 1 - btn_clear_index - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_clear_index - - - - - - 0 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - dv_table_indexes - protected - - - - TableIndexesDataViewCtrl; .components.dataview; forward_declare - - - - - - - - - - - Load From File; icons/16x16/table_relationship.png - Foreign Keys - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - PanelTableFK - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer77 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - - bSizer78 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER - 0 - - - bSizer79 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/add.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Insert - - 0 - - 0 - - - 0 - - 1 - btn_insert_foreign_key - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_insert_foreign_key - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/delete.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Remove - - 0 - - 0 - - - 0 - - 1 - btn_delete_foreign_key - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_foreign_key - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/cross.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Clear - - 0 - - 0 - - - 0 - - 1 - btn_clear_foreign_key - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_clear_foreign_key - - - - - - 0 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - dv_table_foreign_keys - protected - - - - TableForeignKeysDataViewCtrl; .components.dataview; forward_declare - - - - - - - - - - - - - Load From File; icons/16x16/tick.png - Checks - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - PanelTableCheck - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer771 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - - bSizer781 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER - 0 - - - bSizer792 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/add.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Insert - - 0 - - 0 - - - 0 - - 1 - btn_insert_check - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_insert_foreign_key - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/delete.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Remove - - 0 - - 0 - - - 0 - - 1 - btn_delete_check - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_foreign_key - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/cross.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Clear - - 0 - - 0 - - - 0 - - 1 - btn_clear_check - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_clear_foreign_key - - - - - - 0 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - dv_table_checks - protected - - - - TableCheckDataViewCtrl; .components.dataview; forward_declare - - - - - - - - - - - - - Load From File; icons/16x16/code-folding.png - Create - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - PanelTableCreate - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer109 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 1 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - 0 - - 0 - 0 - wxID_ANY - 1 - 1 - - 0 - -1,-1 - - 0 - - 1 - sql_create_table - 1 - - - protected - 1 - - 0 - Resizable - 1 - -1,200 - ; ; forward_declare - 1 - 4 - 0 - - 1 - 0 - 0 - - - - - - - - - - - - - - - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - wxSYS_COLOUR_WINDOW - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - panel_table_columns - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer54 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 0 - - - bSizer53 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Columns: - 0 - - 0 - - - 0 - -1,-1 - 1 - m_staticText39 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxEXPAND - 0 - - 0 - protected - 100 - - - - 2 - wxLEFT|wxRIGHT - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/add.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Insert - - 0 - - 0 - - - 0 - - 1 - btn_insert_column - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_insert_column - - - - 2 - wxLEFT|wxRIGHT - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/delete.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Delete - - 0 - - 0 - - - 0 - - 1 - btn_delete_column - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_column - - - - 2 - wxLEFT|wxRIGHT - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/arrow_up.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Up - - 0 - - 0 - - - 0 - - 1 - btn_move_up_column - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_move_up_column - - - - 2 - wxLEFT|wxRIGHT - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/arrow_down.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Down - - 0 - - 0 - - - 0 - - 1 - btn_move_down_column - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_move_down_column - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - list_ctrl_table_columns - protected - - - - TableColumnsDataViewCtrl; .components.dataview; forward_declare - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer52 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Delete - - 0 - - 0 - - - 0 - - 1 - btn_delete_table - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_table - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Cancel - - 0 - - 0 - - - 0 - - 1 - btn_cancel_table - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_cancel_table - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Apply - - 0 - - 0 - - - 0 - - 1 - btn_apply_table - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - do_apply_table - - - - - - - MyMenu - menu_table_columns - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Add Index - add_index - none - - - - - - MyMenu - m_menu21 - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Add PrimaryKey - m_menuItem8 - none - - - - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Add Index - m_menuItem9 - none - - - - - - - - - - - - - - Load From File; icons/16x16/view.png - Views - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - panel_views - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer84 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_notebook7 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - - - Options - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - pnl_view_editor_root - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer85 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 0 - - - bSizer87 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Name - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText40 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALIGN_CENTER|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - txt_view_name - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer89 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - - bSizer116 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - pnl_row_definer - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - szr_view_definer - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Definer - 0 - - 0 - - - 0 - 150,-1 - 1 - lbl_view_definer - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALIGN_CENTER|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - cmb_view_definer - 1 - - - protected - 1 - - Resizable - -1 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - 5 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - pnl_row_schema - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - szr_view_schema - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Schema - 0 - - 0 - - - 0 - 150,-1 - 1 - lbl_view_schema - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALIGN_CENTER|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - cho_view_schema - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer8711 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - pnl_row_sql_security - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - szr_view_sql_security - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - SQL security - 0 - - 0 - - - 0 - 150,-1 - 1 - lbl_view_sql_security - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALIGN_CENTER|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - "DEFINER" "INVOKER" - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - cho_view_sql_security - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - pnl_row_algorithm - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - Algorithm - - szr_view_algorithm - wxVERTICAL - 1 - none - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - UNDEFINED - - 0 - - - 0 - - 1 - rad_view_algorithm_undefined - 1 - - - protected - 1 - - Resizable - 1 - - wxRB_GROUP - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - MERGE - - 0 - - - 0 - - 1 - rad_view_algorithm_merge - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - TEMPTABLE - - 0 - - - 0 - - 1 - rad_view_algorithm_temptable - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - pnl_row_constraint - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - View constraint - - szr_view_constraint - wxVERTICAL - 1 - none - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - None - - 0 - - - 0 - - 1 - rad_view_constraint_none - 1 - - - protected - 1 - - Resizable - 1 - - wxRB_GROUP - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - LOCAL - - 0 - - - 0 - - 1 - rad_view_constraint_local - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - CASCADE - - 0 - - - 0 - - 1 - rad_view_constraint_cascaded - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - CHECK ONLY - - 0 - - - 0 - - 1 - rad_view_constraint_check_only - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - READ ONLY - - 0 - - - 0 - - 1 - rad_view_constraint_read_only - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - - - - 5 - wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - pnl_row_security_barrier - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer126 - wxVERTICAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Force - - 0 - - - 0 - - 1 - chk_view_force - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - pnl_row_force - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer127 - wxVERTICAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Security barrier - - 0 - - - 0 - - 1 - chk_view_security_barrier - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 1 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - 0 - - 0 - 0 - wxID_ANY - 1 - 1 - - 0 - -1,-1 - - 0 - -1,200 - 1 - stc_view_select - 1 - - - protected - 1 - - 0 - Resizable - 1 - -1,-1 - ; ; forward_declare - 1 - 4 - 0 - - 1 - 0 - 0 - - - - - - - 5 - wxEXPAND - 0 - - - bSizer91 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Delete - - 0 - - 0 - - - 0 - - 1 - btn_delete_view - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Cancel - - 0 - - 0 - - - 0 - - 1 - btn_cancel_view - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Save - - 0 - - 0 - - - 0 - - 1 - btn_save_view - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - Load From File; icons/16x16/cog.png - Triggers - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - panel_triggers - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - - Load From File; icons/16x16/text_columns.png - Data - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - panel_records - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer61 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - - bSizer94 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total - 0 - - 0 - - - 0 - - 1 - name_database_table - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - - - 5 - wxEXPAND - 0 - - - bSizer83 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/add.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Insert record - - 0 - - 0 - - - 0 - - 1 - btn_insert_record - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_insert_record - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/add.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Duplicate record - - 0 - - 0 - - - 0 - - 1 - btn_duplicate_record - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_duplicate_record - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/delete.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Delete record - - 0 - - 0 - - - 0 - - 1 - btn_delete_record - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_record - - - - 5 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_staticline3 - 1 - - - protected - 1 - - Resizable - 1 - - wxLI_VERTICAL - ; ; forward_declare - 0 - - - - - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - 1 - If enabled, table edits are applied immediately without pressing Apply or Cancel - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Apply changes automatically - - 0 - - - 0 - - 1 - chb_auto_apply - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - If enabled, table edits are applied immediately without pressing Apply or Cancel - - wxFILTER_NONE - wxDefaultValidator - - - - - on_auto_apply - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/cancel.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Cancel - - 0 - - 0 - - - 0 - - 1 - btn_cancel_record - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/disk.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Apply - - 0 - - 0 - - - 0 - - 1 - btn_apply_record - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/resultset_next.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Next - - 0 - - 0 - - - 0 - - 1 - m_button40 - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_next_records - - - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - 0 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Filters - - 0 - - - 0 - - 1 - m_collapsiblePane1 - 1 - - - protected - 1 - - Resizable - 1 - - wxCP_DEFAULT_STYLE|wxCP_NO_TLW_RESIZE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - wxFULL_REPAINT_ON_RESIZE - on_collapsible_pane_changed - - - bSizer831 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 1 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - 0 - - 0 - 0 - wxID_ANY - 1 - 0 - - 0 - -1,-1 - - 0 - - 1 - sql_query_filters - 1 - - - protected - 1 - - 0 - Resizable - 1 - -1,100 - ; ; forward_declare - 1 - 4 - 0 - - 1 - 0 - 0 - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/tick.png - - 1 - 0 - 1 - CTRL+ENTER - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Apply - - 0 - - 0 - - - 0 - - 1 - m_button41 - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_apply_filters - - - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - ,90,400,10,70,0 - 0 - wxID_ANY - - - list_ctrl_table_records - protected - - - wxDV_MULTIPLE - TableRecordsDataViewCtrl; .components.dataview; forward_declare - - - - - - - - - MyMenu - m_menu10 - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Insert row - m_menuItem13 - none - Ins - - - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - MyMenuItem - m_menuItem14 - none - - - - - - - - Load From File; icons/16x16/arrow_right.png - Query - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - panel_query - 1 - - - protected - 1 - - Resizable - 1 - - - 0 - - - - wxTAB_TRAVERSAL - - - bSizer26 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_splitter6 - 1 - - - protected - 1 - - Resizable - 0.0 - -300 - -1 - 1 - - wxSPLIT_HORIZONTAL - wxSP_3D - ; ; forward_declare - 0 - - - - - - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel52 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer125 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 1 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - 1 - - 0 - 0 - wxID_ANY - 1 - 1 - - 0 - - - 0 - - 1 - sql_query_editor - 1 - - - protected - 1 - - 0 - Resizable - 1 - - ; ; forward_declare - 1 - 4 - 0 - - 1 - 0 - 0 - - - - - - - 5 - wxALIGN_RIGHT|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - New - - 0 - - 0 - - - 0 - - 1 - m_button12 - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 1 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel53 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer1261 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - notebook_sql_results - 1 - - - protected - 1 - - Resizable - 1 - - - FlatNotebook; wx.lib.agw.flatnotebook; forward_declare - 0 - - - - - - - - - - - - - - - - - Query #2 - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 1 - wxID_ANY - - 0 - - - 0 - - 1 - QueryPanelTpl - 1 - - - protected - 1 - - Resizable - 1 - - - 0 - - - - wxTAB_TRAVERSAL - - - bSizer263 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl101 - 1 - - - protected - 1 - - Resizable - 1 - - wxTE_MULTILINE|wxTE_RICH|wxTE_RICH2 - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer49 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Close - - 0 - - 0 - - - 0 - - 1 - m_button17 - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - New - - 0 - - 0 - - - 0 - - 1 - m_button121 - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - - - - - - - - - - - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - -1,-1 - - 0 - - 1 - panel_sql_log - 1 - - - protected - 1 - - Resizable - 1 - -1,-1 - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - sizer_log_sql - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 1 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - 0 - - 0 - 0 - wxID_ANY - 1 - 1 - - 0 - -1,-1 - - 0 - - 1 - sql_query_logs - 1 - - - protected - 1 - - 0 - Resizable - 1 - -1,200 - ; ; forward_declare - 1 - 4 - 0 - - 1 - 0 - 0 - - - - - - - - - - - - - - - - - - 1 - 0 - 1 - - 4 - - 0 - wxID_ANY - - - status_bar - protected - - - wxSTB_SIZEGRIP - - - - - - - - - 0 - wxAUI_MGR_DEFAULT - - - 1 - 0 - 1 - impl_virtual - - - 0 - wxID_ANY - - - Trash - - 500,300 - ; ; forward_declare - - 0 - - - wxTAB_TRAVERSAL - - - bSizer90 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl221 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer93 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - tree_ctrl_explorer____ - 1 - - - protected - 1 - - Resizable - 1 - - wxTL_DEFAULT_STYLE - ; ; forward_declare - 0 - - - - - - wxALIGN_LEFT - wxCOL_RESIZABLE - Column5 - wxCOL_WIDTH_DEFAULT - - - - - 5 - wxEXPAND - 1 - - - bSizer129 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - UNDEFINED - - 0 - - - 0 - - 1 - m_radioBtn11 - 1 - - - protected - 1 - - Resizable - 1 - - wxRB_GROUP - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - MERGE - - 0 - - - 0 - - 1 - m_radioBtn21 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - MyMenu - m_menu13 - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Import - m_menuItem10 - none - - - on_import - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - TEMPTABLE - - 0 - - - 0 - - 1 - m_radioBtn31 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Algorithm - 0 - - 0 - - - 0 - - 1 - m_staticText4011 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 1 - - 2 - wxBOTH - - - 0 - - fgSizer1 - wxFLEX_GROWMODE_NONE - none - 3 - 0 - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Read only - - 0 - - - 0 - - 1 - m_checkBox7 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - "UNDEFINED" "MERGE" "TEMPTABLE" - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Algorithm - 1 - - 0 - - - 0 - - 1 - rad_view_algorithm - 1 - - - protected - 1 - - Resizable - 0 - 1 - - wxRA_SPECIFY_COLS - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - "None" "LOCAL" "CASCADED" "CHECK OPTION" "READ ONLY" - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - View constraint - 1 - - 0 - - - 0 - -1,-1 - 1 - rad_view_constraint - 1 - - - protected - 1 - - Resizable - 0 - 1 - - wxRA_SPECIFY_COLS - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - MyMenu - m_menu15 - protected - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl10 - 1 - - - protected - 1 - - Resizable - 1 - - wxTE_MULTILINE|wxTE_RICH|wxTE_RICH2 - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl361 - 1 - - - protected - 1 - - Resizable - 1 - - wxTE_PASSWORD - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - notebook_sql_results - 1 - - - protected - 1 - - Resizable - 1 - - wxAUI_NB_DEFAULT_STYLE|wxAUI_NB_MIDDLE_CLICK_CLOSE - ; ; forward_declare - -1 - 0 - - - - - - - - - 5 - wxALIGN_CENTER|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - ssh_tunnel_password1 - 1 - - - protected - 1 - - Resizable - 1 - - wxTE_PASSWORD - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_encryption_old - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - 0 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - collapsible - - 0 - - - 0 - - 1 - m_collapsiblePane2 - 1 - - - protected - 1 - - Resizable - 1 - - wxCP_DEFAULT_STYLE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - tree_ctrl_sessions - 1 - - - protected - 1 - - Resizable - 1 - - wxTR_DEFAULT_STYLE|wxTR_FULL_ROW_HIGHLIGHT|wxTR_HAS_BUTTONS|wxTR_HIDE_ROOT|wxTR_TWIST_BUTTONS - ; ; forward_declare - 0 - - - - - show_tree_ctrl_menu - - MyMenu - m_menu12 - protected - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_treeListCtrl3 - 1 - - - protected - 1 - - Resizable - 1 - - wxTL_DEFAULT_STYLE - ; ; forward_declare - 0 - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - tree_ctrl_sessions1 - 1 - - - protected - 1 - - Resizable - 1 - - wxTL_DEFAULT_STYLE - ; ; forward_declare - 0 - - - - - - wxALIGN_LEFT - wxCOL_RESIZABLE - Column3 - wxCOL_WIDTH_DEFAULT - - - wxALIGN_LEFT - wxCOL_RESIZABLE - Column4 - wxCOL_WIDTH_DEFAULT - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - table_collationdd - 1 - - - protected - 1 - - Resizable - 1 - - - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl21 - 1 - - - protected - 1 - - Resizable - 1 - - wxTE_MULTILINE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 0 - wxEXPAND - 0 - - - bSizer51 - wxVERTICAL - none - - 0 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - panel_credentials - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer48 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_notebook8 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - - - - - MyMenu - m_menu3 - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - MyMenuItem - m_menuItem3 - none - - - - - - - - 0 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 1 - wxID_ANY - - 0 - - - 0 - - 1 - panel_source - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer52 - wxVERTICAL - none - - 0 - wxEXPAND - 0 - - - bSizer1212 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Filename - 0 - - 0 - - - 0 - -1,-1 - 1 - m_staticText212 - 1 - - - protected - 1 - - Resizable - 1 - 150,-1 - - - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - Select a file - - 0 - - 1 - filename - 1 - - - protected - 1 - - Resizable - 1 - - wxFLP_CHANGE_DIR|wxFLP_USE_TEXTCTRL - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - Database (*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3 - - - - - - - - - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Port - 0 - - 0 - - - 0 - -1,-1 - 1 - m_staticText2211 - 1 - - - protected - 1 - - Resizable - 1 - 150,-1 - - - 0 - - - - - -1 - - - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - ssh_tunnel_port - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALIGN_CENTER|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - ssh_tunnel_local_port - 1 - - - protected - 1 - - Resizable - 1 - - - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 1 - wxID_ANY - - 0 - - - 0 - - 1 - tree_ctrl_sessions2 - 1 - - - protected - 1 - - Resizable - 1 - - wxTR_DEFAULT_STYLE - ; ; forward_declare - 0 - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 1 - wxID_ANY - - 0 - - - 0 - - 1 - tree_ctrl_sessions_bkp3 - 1 - - - protected - 1 - - Resizable - 1 - - wxTL_DEFAULT_STYLE|wxTL_SINGLE - ; ; forward_declare - 0 - - - - - - wxALIGN_LEFT - wxCOL_RESIZABLE - Name - wxCOL_WIDTH_DEFAULT - - - wxALIGN_LEFT - wxCOL_RESIZABLE - Usage - wxCOL_WIDTH_DEFAULT - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 1 - wxID_ANY - - - tree_ctrl_sessions_bkp - protected - - - wxDV_SINGLE - ; ; forward_declare - - - - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Database - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn1 - protected - IconText - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Size - wxDATAVIEW_CELL_INERT - 1 - m_dataViewColumn3 - protected - Progress - 50 - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - %(total_rows)s - 0 - - 0 - - - 0 - - 1 - rows_database_table - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - rows total - 0 - - 0 - - - 0 - - 1 - m_staticText44 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 0 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - ____list_ctrl_database_tables - protected - - - - ; ; forward_declare - - - - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn5 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn6 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn7 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn8 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn9 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn10 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn11 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn20 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn21 - protected - Text - -1 - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - ___list_ctrl_database_tables - protected - - - - - - - - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - m_dataViewListColumn6 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Lines - wxDATAVIEW_CELL_INERT - m_dataViewListColumn7 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Size - wxDATAVIEW_CELL_INERT - m_dataViewListColumn8 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Created at - wxDATAVIEW_CELL_INERT - m_dataViewListColumn9 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Updated at - wxDATAVIEW_CELL_INERT - m_dataViewListColumn10 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Engine - wxDATAVIEW_CELL_INERT - m_dataViewListColumn11 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Comments - wxDATAVIEW_CELL_INERT - m_dataViewListColumn12 - protected - Text - -1 - - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_gauge1 - 1 - - - protected - 1 - - 100 - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxEXPAND - 0 - - 0 - protected - 150 - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 1 - wxID_ANY - - - tree_ctrl_explorer__ - protected - - - - ; ; forward_declare - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_vlistBox1 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_listBox1 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer871 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Temporary - 0 - - 0 - - - 0 - - 1 - m_staticText401 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - - 0 - - - 0 - - 1 - m_checkBox5 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - 0 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Engine options - - 0 - - - 0 - - 1 - m_collapsiblePane3 - 1 - - - protected - 1 - - Resizable - 1 - - wxCP_DEFAULT_STYLE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - bSizer115 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel41 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel42 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel43 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl2211 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl2212 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_comboBox11 - 1 - - - protected - 1 - - Resizable - -1 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxEXPAND - 1 - - 2 - 0 - - gSizer3 - none - 0 - 0 - - 5 - wxEXPAND - 1 - - - bSizer8712 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Algorithm - 0 - - 0 - - - 0 - - 1 - m_staticText4012 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - UNDEFINED - - 0 - - - 0 - - 1 - m_radioBtn1 - 1 - - - protected - 1 - - Resizable - 1 - - wxRB_GROUP - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - MERGE - - 0 - - - 0 - - 1 - m_radioBtn2 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - TEMPTABLE - - 0 - - - 0 - - 1 - m_radioBtn3 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer12211 - wxHORIZONTAL - none - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - RadioBtn - - 0 - - - 0 - - 1 - m_radioBtn10 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxEXPAND - 0 - - - bSizer86 - wxHORIZONTAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel44 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - Select a file - - 0 - - 1 - filename1 - 1 - - - protected - 1 - - Resizable - 1 - - wxFLP_CHANGE_DIR|wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - *.* - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl351 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - + + 5 + wxEXPAND + 0 + + + bSizer371112 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Last failure reason + 0 + + 0 + + + 0 + 200,-1 + 1 + m_staticText151112 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + + 0 + + 1 + last_failure_raison + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + + + 5 + wxEXPAND + 0 + + + bSizer3711121 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Total connection attempts + 0 + + 0 + + + 0 + 200,-1 + 1 + m_staticText1511121 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + + 0 + + 1 + total_connection_attempts + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + + + 5 + wxEXPAND + 0 + + + bSizer37111211 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Average connection time (ms) + 0 + + 0 + + + 0 + 200,-1 + 1 + m_staticText15111211 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + + 0 + + 1 + average_connection_time + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + + + 5 + wxEXPAND + 0 + + + bSizer371112111 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Most recent connection duration + 0 + + 0 + + + 0 + 200,-1 + 1 + m_staticText151112111 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + + 0 + + 1 + most_recent_connection_duration + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + + + + + + + + + 5 - wxALIGN_CENTER|wxALL + wxEXPAND | wxALL 0 - + 1 1 1 @@ -21856,450 +5025,407 @@ 0 0 wxID_ANY - Encryption - 0 0 0 - 150,-1 - 1 - m_staticText701 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - - MyMenu - m_menu11 - protected - - - - 0 - wxAUI_MGR_DEFAULT - - - 1 - 0 - 1 - impl_virtual - - - 0 - wxID_ANY - - - MyPanel1 - - 500,300 - ; ; forward_declare - - 0 - - - wxTAB_TRAVERSAL - - - 0 - wxAUI_MGR_DEFAULT - - wxBOTH - - 1 - 0 - 1 - impl_virtual - - - - 0 - wxID_ANY - - - EditColumnView - - 600,600 - wxDEFAULT_DIALOG_STYLE|wxSTAY_ON_TOP - - Edit Column - - 1 - - - - - - bSizer98 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - bSizer52 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Name - 0 - - 0 - - - 0 - - 1 - m_staticText26 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - wxST_NO_AUTORESIZE - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - column_name - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - + 1 + m_staticline4 + 1 + + + protected + 1 + + Resizable + 1 + + wxLI_HORIZONTAL + ; ; forward_declare + 0 + + + + + + + + 0 + wxEXPAND + 0 + + + bSizer28 + wxHORIZONTAL + none 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Datatype - 0 - - 0 - - - 0 + wxEXPAND + 1 + - 1 - m_staticText261 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 + bSizer301 + wxHORIZONTAL + none + + 5 + wxALL|wxBOTTOM + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/add.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Create + + 0 + + 0 + + + 0 + + 1 + btn_create + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_create + + MenuCreate + m_menu12 + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Create connection + m_menuItem16 + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Create directory + m_menuItem17 + none + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/folder.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + + + 0 + + 0 + + + 0 + + 1 + btn_create_directory + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBU_EXACTFIT|wxBU_NOTEXT + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_create_directory + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/delete.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Delete + + 0 + + 0 + + + 0 + + 1 + btn_delete + 1 + + + protected + 1 + + + + Resizable + 1 + + + + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_delete + + 5 - wxALL + wxEXPAND 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - column_datatype - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer5211 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Length/Set - 0 - - 0 - - - 0 + - 1 - m_staticText2611 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 + bSizer110 + wxHORIZONTAL + none 5 wxEXPAND - 1 + 0 - bSizer60 + bSizer29 wxHORIZONTAL none 5 - wxALIGN_CENTER|wxALL - 1 - + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 1 + wxID_ANY + Cancel + + 0 + + 0 + + + 0 + + 1 + btn_cancel + 1 + + + protected + 1 + + + + Resizable + 1 + + + + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxALL + 0 + 1 1 1 @@ -22308,63 +5434,73 @@ 0 0 + 0 + Load From File; icons/16x16/disk.png 1 0 1 1 + + 0 0 + Dock 0 Left 0 - 1 + 0 1 + 0 0 wxID_ANY + Save + + 0 0 - 0 0 1 - column_set + btn_save 1 protected 1 + + Resizable 1 - ; ; forward_declare + 0 wxFILTER_NONE wxDefaultValidator - + on_save 5 wxALL - 1 - + 0 + 1 1 1 @@ -22373,61 +5509,73 @@ 0 0 + 0 + Load From File; icons/16x16/world_go.png 1 0 1 1 + + 0 0 + Dock 0 Left 0 - 1 + 0 1 + 0 0 wxID_ANY - 0 - 65536 + Test + + 0 0 - 0 0 1 - column_length + btn_test 1 protected 1 + + Resizable 1 - wxSP_ARROW_KEYS + ; ; forward_declare 0 - + + wxFILTER_NONE + wxDefaultValidator + + on_test_session 5 wxALL - 1 - + 0 + 1 1 1 @@ -22436,15 +5584,20 @@ 0 0 + 0 + Load From File; icons/16x16/server_go.png 1 0 1 1 + + 0 0 + Dock 0 Left @@ -22452,109 +5605,149 @@ 0 1 + 0 0 wxID_ANY - 0 - 65536 + Connect + + 0 0 - 0 0 1 - column_scale + btn_open 1 protected 1 + + Resizable 1 - wxSP_WRAP - ; ; forward_declare + + 0 - + + wxFILTER_NONE + wxDefaultValidator + + on_connect - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Collation - 0 - - 0 - - - 0 - - 1 - m_staticText261111112 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - + + + + + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 0 + 1 + impl_virtual + + + + 0 + wxID_ANY + + 800,600 + SettingsDialog + + 800,600 + wxDEFAULT_DIALOG_STYLE + ; ; forward_declare + Settings + + 0 + + + + + + bSizer63 + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_notebook4 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + + + Locale + 0 + 1 1 1 @@ -22568,7 +5761,6 @@ 1 0 - 1 1 @@ -22591,7 +5783,7 @@ 0 1 - column_collation + locales 1 @@ -22599,31 +5791,422 @@ 1 Resizable - 0 1 - ; ; forward_declare 0 - - wxFILTER_NONE - wxDefaultValidator - - + wxTAB_TRAVERSAL + + + bSizer65 + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + + bSizer64 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Language + 0 + + 0 + + + 0 + + 1 + m_staticText27 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + "English" "Italian" "French" + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_choice5 + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + wxBORDER_NONE + + + + + + + + + + + + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 0 + 1 + impl_virtual + + + + 0 + wxID_ANY + + 640,480 + ColumnContentDialog + + 900,550 + wxDEFAULT_DIALOG_STYLE + ; ; forward_declare + Column content + + 0 + + + + + + bSizer111 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + + bSizer112 + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + + bSizer113 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Syntax + 0 + + 0 + + + 0 + + 1 + m_staticText51 + 1 + + + protected + 1 + + Resizable + 1 + -1,-1 + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + syntax_choice + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_syntax_changed + + + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 1 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + 0 + + 0 + 0 + wxID_ANY + 1 + 1 + + 0 + + + 0 + + 1 + advanced_stc_editor + 1 + + + protected + 1 + + 0 + Resizable + 1 + + ; ; forward_declare + 1 + 4 + 0 + + 0 + 0 + 0 + + + + + 5 wxEXPAND 0 - bSizer52111 + bSizer114 wxHORIZONTAL none @@ -22639,83 +6222,8 @@ 5 wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Unsigned - - 0 - - - 0 - - 1 - column_unsigned - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 1 - + 0 + 1 1 1 @@ -22724,16 +6232,20 @@ 0 0 + 0 + 1 0 - 0 1 1 + + 0 0 + Dock 0 Left @@ -22741,11 +6253,14 @@ 1 1 + 0 0 wxID_ANY - Allow NULL + Cancel + + 0 0 @@ -22753,13 +6268,15 @@ 0 1 - column_allow_null + m_button49 1 protected 1 + + Resizable 1 @@ -22769,28 +6286,18 @@ wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 + wxDefaultValidator + + + + 5 wxALL - 1 - + 0 + 1 1 1 @@ -22799,16 +6306,20 @@ 0 0 + 0 + 1 0 - 0 1 1 + + 0 0 + Dock 0 Left @@ -22816,11 +6327,14 @@ 1 1 + 0 0 wxID_ANY - Zero Fill + Ok + + 0 0 @@ -22828,13 +6342,15 @@ 0 1 - column_zero_fill + m_button48 1 protected 1 + + Resizable 1 @@ -22851,508 +6367,12256 @@ - - 5 - wxEXPAND - 1 - - 0 - protected - 0 + + + + + + 0 + + + wxBOTH + + 1 + 0 + 1 + impl_virtual + + + + 0 + wxID_ANY + + 800,600 + MainFrameView + + 1280,1024 + wxDEFAULT_FRAME_STYLE|wxMAXIMIZE_BOX + + PeterSQL + + 0 + + + wxTAB_TRAVERSAL + 1 + do_close + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + m_menubar2 + protected + + + + + + + + + + File + m_menu2 + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Settings + m_menuItem22 + none + + + on_settings + + + + Help + m_menu4 + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + About + m_menuItem15 + none + + + on_menu_about + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + + 0 + + + 0 + + 1 + m_toolBar1 + 1 + 1 + + + protected + 1 + + Resizable + 5 + 1 + + wxTB_HORIZONTAL + ; ; forward_declare + 0 + + + + + + Load From File; icons/16x16/server_connect.png + 0 + wxID_ANY + wxITEM_NORMAL + Open connection manager + m_tool5 + protected + + + do_open_connection_manager + + + protected + + + Load From File; icons/16x16/disconnect.png + 0 + wxID_ANY + wxITEM_NORMAL + Disconnect from server + m_tool4 + protected + + + on_database_disconnect + + + Load From File; icons/16x16/database_refresh.png + 0 + wxID_ANY + wxITEM_NORMAL + tool + database_refresh + protected + Refresh + Refresh + on_database_refresh + + + protected + + + Load From File; icons/16x16/database_add.png + 0 + wxID_ANY + wxITEM_NORMAL + Add + database_add + protected + + + + + Load From File; icons/16x16/database_delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Add + database_delete + protected + + + + + + + bSizer19 + wxVERTICAL + none + + 0 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel13 + 1 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bSizer21 + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + 0 + + 1 + m_splitter51 + 1 + + + protected + 1 + + Resizable + 1 + -150 + -1 + 1 + + wxSPLIT_HORIZONTAL + wxSP_3D|wxSP_LIVE_UPDATE + ; ; forward_declare + 0 + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel22 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer72 + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 100 + + 0 + + 1 + m_splitter4 + 1 + + + protected + 1 + + Resizable + 0.0 + 320 + -1 + 1 + + wxSPLIT_VERTICAL + wxSP_LIVE_UPDATE + + 0 + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel14 + 1 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bSizer24 + wxHORIZONTAL + none + + 5 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + + + + + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + + + + 0 + + + 0 + + 1 + tree_ctrl_explorer + 1 + + + protected + + + + 1 + + self.tree_ctrl_explorer = wx.lib.agw.hypertreelist.HyperTreeList( self.m_panel14, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, agwStyle=wx.TR_DEFAULT_STYLE|wx.TR_SINGLE|wx.TR_FULL_ROW_HIGHLIGHT|wx.TR_HIDE_ROOT|wx.TR_LINES_AT_ROOT ) + import wx.lib.agw.hypertreelist + + Resizable + 1 + + ; ; forward_declare + 0 + + + + + + + + + MyMenu + m_menu5 + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + MyMenuItem + m_menuItem4 + none + + + + + + MyMenu + m_menu1 + protected + + + 1 + 1 + + wxID_ANY + wxITEM_NORMAL + MyMenuItem + m_menuItem5 + none + + + + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel15 + 0 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bSizer25 + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + 16,16 + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + MainFrameNotebook + 1 + + + protected + 1 + + Resizable + 1 + + wxNB_FIXEDWIDTH + + 0 + + + + + on_page_chaged + + Load From File; icons/16x16/server.png + System + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + panel_system + 0 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bSizer272 + wxVERTICAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + MyLabel + 0 + + 0 + + + 0 + + 1 + m_staticText291 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + system_databases + protected + + + + ; ; forward_declare + + + + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE + Databases + wxDATAVIEW_CELL_INERT + m_dataViewListColumn1 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE + Size + wxDATAVIEW_CELL_INERT + m_dataViewListColumn2 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE + Elements + wxDATAVIEW_CELL_INERT + m_dataViewListColumn3 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE + Modified at + wxDATAVIEW_CELL_INERT + m_dataViewListColumn4 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE + Tables + wxDATAVIEW_CELL_INERT + m_dataViewListColumn5 + protected + Text + -1 + + + + + + + + Load From File; icons/16x16/database.png + Database + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + panel_database + 0 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bSizer27 + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_notebook6 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + + + Options + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel30 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer80 + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 200 + + 0 + + 1 + m_splitter7 + 1 + + + protected + 1 + + Resizable + 0.0 + 200 + -1 + 1 + + wxSPLIT_HORIZONTAL + wxSP_3D + ; ; forward_declare + 0 + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel54 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer158 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + + bSizer159 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Name + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText90 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + 0 + + 1 + database_name + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxEXPAND + 1 + + + bSizer1481111 + wxHORIZONTAL + none + + + + 5 + wxEXPAND + 0 + + + bSizer142 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_collation_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer1392 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Collation + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText702 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_collation + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + + + 5 + wxEXPAND + 0 + + + bSizer13911 + wxHORIZONTAL + none + + 0 + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_encryption_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer1391 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Encryption + + 0 + + + 0 + + 1 + database_encryption + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_read_only_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer148 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Read Only + + 0 + + + 0 + + 1 + database_read_only + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer92 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_tablespace_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer13912 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Tablespace + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText7012 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_tablespace + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_connection_limit_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer139111 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Connection limit + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText70111 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 0 + 0 + + 0 + + 0 + + 0 + + 1 + database_connection_limit + 1 + + + protected + 1 + + Resizable + 1 + + wxSP_ARROW_KEYS + ; ; forward_declare + 0 + + + + + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer1481 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_password_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer139121 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Password + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText70121 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + 0 + + 1 + m_textCtrl36 + 1 + + + protected + 1 + + Resizable + 1 + + wxTE_PASSWORD + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + 5 + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_profile_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer1391111 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Profile + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText701111 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_profile + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer96 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_default_tablespace_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer1391212 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Default tablespace + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText701212 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_default_tablespace + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_temporary_tablespace_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer13912121 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Temporary tablespace + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText7012121 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_temporary_tablespace + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer14811 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_quota_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer1391211 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Quota + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText701211 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + 0 + + 1 + database_quota + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + 5 + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_unlimited_quota_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer13911111 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Unlimited quota + + 0 + + + 0 + + 1 + database_unlimited_quota + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer148111 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_account_status_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer13912111 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Account status + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText7012111 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_account_status + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_password_expire_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer139111111 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Password expire + + 0 + + + 0 + + 1 + database_password_expire + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel55 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer154 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + + bSizer531 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER_VERTICAL|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Table: + 0 + + 0 + + + 0 + -1,-1 + 1 + m_staticText391 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND + 0 + + 0 + protected + 100 + + + + 2 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/add.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Insert + + 0 + + 0 + + + 0 + + 1 + btn_insert_table + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_insert_table + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/table_multiple.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Clone + + 0 + + 0 + + + 0 + + 1 + btn_clone_table + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_clone_table + + + + 2 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/delete.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Delete + + 0 + + 0 + + + 0 + + 1 + btn_delete_table1 + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_delete_table + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + + + 5 + wxEXPAND + 1 + + + bSizer152 + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + list_ctrl_database_tables + protected + + + + ; ; forward_declare + + + + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Name + wxDATAVIEW_CELL_INERT + 0 + m_dataViewColumn12 + protected + Text + -1 + + + wxALIGN_RIGHT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Rows + wxDATAVIEW_CELL_INERT + 1 + m_dataViewColumn13 + protected + Text + -1 + + + wxALIGN_RIGHT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Size + wxDATAVIEW_CELL_INERT + 2 + m_dataViewColumn14 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Created at + wxDATAVIEW_CELL_INERT + 3 + m_dataViewColumn15 + protected + Date + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Updated at + wxDATAVIEW_CELL_INERT + 4 + m_dataViewColumn16 + protected + Date + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Engine + wxDATAVIEW_CELL_INERT + 5 + m_dataViewColumn17 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Collation + wxDATAVIEW_CELL_INERT + 6 + m_dataViewColumn19 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Comments + wxDATAVIEW_CELL_INERT + 7 + m_dataViewColumn18 + protected + Text + -1 + + + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer138 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Cancel + + 0 + + 0 + + + 0 + + 1 + btn_cancel_database + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_cancel_database + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Delete + + 0 + + 0 + + + 0 + + 1 + btn_delete_database + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_delete_database + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Apply + + 0 + + 0 + + + 0 + + 1 + btn_apply_database + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_apply_database + + + + + + + + + + Diagram + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 1 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel31 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer82 + wxVERTICAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + MyLabel + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText7011 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + MyLabel + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText7011111 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + MyLabel + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText70111111 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + + + + + + + MyMenu + m_menu15 + protected + + + + + Load From File; icons/16x16/table.png + Table + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + panel_table + 1 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bSizer251 + wxVERTICAL + none + + 0 + wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 200 + + 0 + + 1 + m_splitter41 + 1 + + + protected + 1 + + Resizable + 0.0 + 200 + -1 + 1 + + wxSPLIT_HORIZONTAL + wxSP_LIVE_UPDATE + ; ; forward_declare + 0 + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel19 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer55 + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + 16,16 + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_notebook3 + 1 + + + protected + 1 + + Resizable + 1 + + wxNB_FIXEDWIDTH + + 0 + + + + + + Load From Embedded File; icons/16x16/table.png + Base + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + PanelTableBase + 1 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bSizer262 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + + bSizer271 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Name + 0 + + 0 + + + 0 + + 1 + m_staticText8 + 1 + + + protected + 1 + + Resizable + 1 + 150,-1 + + + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + 0 + + 1 + table_name + 1 + + + protected + 1 + + Resizable + 1 + + + + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxEXPAND + 1 + + + bSizer273 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Comments + 0 + + 0 + + + 0 + + 1 + m_staticText83 + 1 + + + protected + 1 + + Resizable + 1 + 150,-1 + + + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + 0 + + 1 + table_comment + 1 + + + protected + 1 + + Resizable + 1 + + wxTE_MULTILINE + + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + + Load From File; icons/16x16/wrench.png + Options + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + PanelTableOptions + 1 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bSizer261 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + 2 + 0 + + gSizer11 + none + 0 + 0 + + 5 + wxEXPAND + 1 + + + bSizer27111 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Auto Increment + 0 + + 0 + + + 0 + + 1 + m_staticText8111 + 1 + + + protected + 1 + + Resizable + 1 + 150,-1 + + + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + 0 + + 1 + table_auto_increment + 1 + + + protected + 1 + + Resizable + 1 + + + + 0 + + + wxFILTER_EXCLUDE_CHAR_LIST|wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer2712 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Engine + 0 + + 0 + + + 0 + + 1 + m_staticText812 + 1 + + + protected + 1 + + Resizable + 1 + 150,-1 + + + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + table_engine + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer2721 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Default Collation + 0 + + 0 + + + 0 + + 1 + m_staticText821 + 1 + + + protected + 1 + + Resizable + 1 + 150,-1 + + + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + table_collation + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Convert data + + 0 + + + 0 + + 1 + convert_data_collation + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + 5 + wxEXPAND + 1 + + + bSizer145 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Row format + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText71 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + table_row_format + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + + + Load From File; icons/16x16/lightning.png + Indexes + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + PanelTableIndex + 1 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bSizer28 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER + 0 + + + bSizer791 + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/delete.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Remove + + 0 + + 0 + + + 0 + + 1 + btn_delete_index + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_delete_index + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/cross.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Clear + + 0 + + 0 + + + 0 + + 1 + btn_clear_index + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_clear_index + + + + + + 0 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + dv_table_indexes + protected + + + + TableIndexesDataViewCtrl; .components.dataview; forward_declare + + + + + + + + + + + Load From File; icons/16x16/table_relationship.png + Foreign Keys + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + PanelTableFK + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer77 + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + + bSizer78 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER + 0 + + + bSizer79 + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/add.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Insert + + 0 + + 0 + + + 0 + + 1 + btn_insert_foreign_key + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_insert_foreign_key + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/delete.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Remove + + 0 + + 0 + + + 0 + + 1 + btn_delete_foreign_key + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_delete_foreign_key + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/cross.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Clear + + 0 + + 0 + + + 0 + + 1 + btn_clear_foreign_key + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_clear_foreign_key + + + + + + 0 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + dv_table_foreign_keys + protected + + + + TableForeignKeysDataViewCtrl; .components.dataview; forward_declare + + + + + + + + + + + + + Load From File; icons/16x16/tick.png + Checks + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + PanelTableCheck + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer771 + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + + bSizer781 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER + 0 + + + bSizer792 + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/add.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Insert + + 0 + + 0 + + + 0 + + 1 + btn_insert_check + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_insert_foreign_key + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/delete.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Remove + + 0 + + 0 + + + 0 + + 1 + btn_delete_check + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_delete_foreign_key + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/cross.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Clear + + 0 + + 0 + + + 0 + + 1 + btn_clear_check + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_clear_foreign_key + + + + + + 0 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + dv_table_checks + protected + + + + TableCheckDataViewCtrl; .components.dataview; forward_declare + + + + + + + + + + + + + Load From File; icons/16x16/code-folding.png + Create + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + PanelTableCreate + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer109 + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 1 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + 0 + + 0 + 0 + wxID_ANY + 1 + 1 + + 0 + -1,-1 + + 0 + + 1 + sql_create_table + 1 + + + protected + 1 + + 0 + Resizable + 1 + -1,200 + ; ; forward_declare + 1 + 4 + 0 + + 1 + 0 + 0 + + + + + + + + + + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + wxSYS_COLOUR_WINDOW + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + panel_table_columns + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer54 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + + 0 + + + 0 + + 1 + toolbar_columns + 1 + 1 + + + protected + 1 + + Resizable + 5 + 1 + + wxTB_HORIZONTAL|wxTB_HORZ_TEXT + ; ; forward_declare + 0 + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Columns: + 0 + + 0 + + + 0 + -1,-1 + 1 + m_staticText39 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + Load From File; icons/16x16/add.png + 0 + wxID_ANY + wxITEM_NORMAL + Add + tool_add_column + protected + + + on_insert_column + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Remove + tool_remove_column + protected + + + on_delete_column + + + protected + + + Load From File; icons/16x16/arrow_up.png + 0 + wxID_ANY + wxITEM_NORMAL + Move Up + tool_move_up_column + protected + + + on_move_up_column + + + Load From File; icons/16x16/arrow_down.png + 0 + wxID_ANY + wxITEM_NORMAL + Move Down + tool_move_down_column + protected + + + on_move_down_column + + + + + 5 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + list_ctrl_table_columns + protected + + + + TableColumnsDataViewCtrl; .components.dataview; forward_declare + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer52 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Delete + + 0 + + 0 + + + 0 + + 1 + btn_delete_table + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_delete_table + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Cancel + + 0 + + 0 + + + 0 + + 1 + btn_cancel_table + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_cancel_table + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Apply + + 0 + + 0 + + + 0 + + 1 + btn_apply_table + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + do_apply_table + + + + + + + MyMenu + menu_table_columns + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Add Index + add_index + none + + + + + + MyMenu + m_menu21 + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Add PrimaryKey + m_menuItem8 + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Add Index + m_menuItem9 + none + + + + + + + + + + + + + + Load From File; icons/16x16/view.png + Views + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + panel_views + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer84 + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_notebook7 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + + + Options + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + pnl_view_editor_root + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer85 + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 0 + + + bSizer87 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Name + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText40 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALIGN_CENTER|wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + 0 + + 1 + txt_view_name + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer89 + wxHORIZONTAL + none + + 5 + wxEXPAND + 1 + + + bSizer116 + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + pnl_row_definer + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + szr_view_definer + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Definer + 0 + + 0 + + + 0 + 150,-1 + 1 + lbl_view_definer + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALIGN_CENTER|wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + cmb_view_definer + 1 + + + protected + 1 + + Resizable + -1 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + 5 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + pnl_row_schema + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + szr_view_schema + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Schema + 0 + + 0 + + + 0 + 150,-1 + 1 + lbl_view_schema + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALIGN_CENTER|wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + cho_view_schema + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + 5 + wxEXPAND + 1 + + + bSizer8711 + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + pnl_row_sql_security + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + szr_view_sql_security + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + SQL security + 0 + + 0 + + + 0 + 150,-1 + 1 + lbl_view_sql_security + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALIGN_CENTER|wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + "DEFINER" "INVOKER" + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + cho_view_sql_security + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + pnl_row_algorithm + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + Algorithm + + szr_view_algorithm + wxVERTICAL + 1 + none + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + UNDEFINED + + 0 + + + 0 + + 1 + rad_view_algorithm_undefined + 1 + + + protected + 1 + + Resizable + 1 + + wxRB_GROUP + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + MERGE + + 0 + + + 0 + + 1 + rad_view_algorithm_merge + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + TEMPTABLE + + 0 + + + 0 + + 1 + rad_view_algorithm_temptable + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + pnl_row_constraint + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + View constraint + + szr_view_constraint + wxVERTICAL + 1 + none + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + None + + 0 + + + 0 + + 1 + rad_view_constraint_none + 1 + + + protected + 1 + + Resizable + 1 + + wxRB_GROUP + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + LOCAL + + 0 + + + 0 + + 1 + rad_view_constraint_local + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + CASCADE + + 0 + + + 0 + + 1 + rad_view_constraint_cascaded + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + CHECK ONLY + + 0 + + + 0 + + 1 + rad_view_constraint_check_only + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + READ ONLY + + 0 + + + 0 + + 1 + rad_view_constraint_read_only + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + + + + 5 + wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + pnl_row_security_barrier + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer126 + wxVERTICAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Force + + 0 + + + 0 + + 1 + chk_view_force + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + pnl_row_force + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer127 + wxVERTICAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Security barrier + + 0 + + + 0 + + 1 + chk_view_security_barrier + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + + + + + + + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 1 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + 0 + + 0 + 0 + wxID_ANY + 1 + 1 + + 0 + -1,-1 + + 0 + -1,200 + 1 + stc_view_select + 1 + + + protected + 1 + + 0 + Resizable + 1 + -1,-1 + ; ; forward_declare + 1 + 4 + 0 + + 1 + 0 + 0 + + + + + + + 5 + wxEXPAND + 0 + + + bSizer91 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Delete + + 0 + + 0 + + + 0 + + 1 + btn_delete_view + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Cancel + + 0 + + 0 + + + 0 + + 1 + btn_cancel_view + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Save + + 0 + + 0 + + + 0 + + 1 + btn_save_view + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + Load From File; icons/16x16/cog.png + Triggers + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + panel_triggers + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + + Load From File; icons/16x16/text_columns.png + Data + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + panel_records + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer61 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + + 0 + + + 0 + + 1 + m_toolBar3 + 1 + 1 + + + protected + 1 + + Resizable + 5 + 1 + + wxTB_HORIZONTAL|wxTB_HORZ_TEXT + ; ; forward_declare + 0 + + + + + + Load From File; icons/16x16/arrow_refresh.png + 0 + wxID_ANY + wxITEM_NORMAL + Refrsh + tool_refresh_records + protected + + + on_refresh_records + + + protected + + + Load From File; icons/16x16/add.png + 0 + wxID_ANY + wxITEM_NORMAL + Add + tool_insert_record + protected + + + on_insert_record + + + Load From File; icons/16x16/page_copy_columns.png + 0 + wxID_ANY + wxITEM_NORMAL + Duplicate + tool_duplicate_record + protected + + + on_duplicate_record + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Remove + tool_delete_record + protected + + + on_delete_record + + + protected + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + 1 + If enabled, table edits are applied immediately without pressing Apply or Cancel + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Apply changes automatically + + 0 + + + 0 + + 1 + chb_auto_apply + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + If enabled, table edits are applied immediately without pressing Apply or Cancel + + wxFILTER_NONE + wxDefaultValidator + + + + + on_auto_apply + + + Load From File; icons/16x16/tick.png + 0 + wxID_ANY + wxITEM_NORMAL + Apply + tool_apply_record + protected + + + on_apply_record + + + Load From File; icons/16x16/cross.png + 0 + wxID_ANY + wxITEM_NORMAL + Cancel + tool_cancel_record + protected + + + on_cancel_record + + + + + 5 + wxEXPAND + 0 + + + bSizer94 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + {database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows} + 0 + + 0 + + + 0 + + 1 + name_database_table + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/resultset_first.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + First + + 0 + + 0 + + + 0 + + 1 + btn_first_records + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_first_records + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/arrow_left.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + + + 0 + + 0 + + + 0 + + 1 + btn_prev_records + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE|wxBU_EXACTFIT + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_prev_records + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 100 + 1000 + + 0 + + 0 + + 0 + + 1 + limit_records + 1 + + + protected + 1 + + Resizable + 1 + + wxSP_ARROW_KEYS + ; ; forward_declare + 0 + + + + + + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/resultset_next.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + + + 0 + + 0 + + + 0 + + 1 + btn_next_records + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE|wxBU_EXACTFIT|wxBU_NOTEXT + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_next_records + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/resultset_last.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Last + + 0 + + 0 + + + 0 + + 1 + btn_last_records + 1 + + + protected + 1 + + wxRIGHT + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_last_records + + + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Filters + + 0 + + + 0 + + 1 + m_collapsiblePane1 + 1 + + + protected + 1 + + Resizable + 1 + + wxCP_DEFAULT_STYLE|wxCP_NO_TLW_RESIZE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + wxFULL_REPAINT_ON_RESIZE + on_collapsible_pane_changed + + + bSizer831 + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 1 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + 0 + + 0 + 0 + wxID_ANY + 1 + 0 + + 0 + -1,-1 + + 0 + + 1 + sql_query_filters + 1 + + + protected + 1 + + 0 + Resizable + 1 + -1,100 + ; ; forward_declare + 1 + 4 + 0 + + 1 + 0 + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/tick.png + + 1 + 0 + 1 + CTRL+ENTER + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Apply + + 0 + + 0 + + + 0 + + 1 + m_button41 + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_apply_filters + + + + + + + 5 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + ,90,400,10,70,0 + 0 + wxID_ANY + + + list_ctrl_table_records + protected + + + wxDV_MULTIPLE + TableRecordsDataViewCtrl; .components.dataview; forward_declare + + + + + + + + + MyMenu + m_menu10 + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Insert row + m_menuItem13 + none + Ins + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + MyMenuItem + m_menuItem14 + none + + + + + + + + Load From File; icons/16x16/arrow_right.png + Query + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + panel_query + 1 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bSizer26 + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + 0 + + 1 + m_splitter6 + 1 + + + protected + 1 + + Resizable + 0.0 + -300 + -1 + 1 + + wxSPLIT_HORIZONTAL + wxSP_3D + ; ; forward_declare + 0 + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel52 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer125 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + + 0 + + + 0 + + 1 + m_toolBar2 + 1 + 1 + + + protected + 1 + + Resizable + 5 + 1 + + wxTB_HORIZONTAL|wxTB_HORZ_TEXT + ; ; forward_declare + 0 + + + + + + Load From File; icons/16x16/add.png + 0 + wxID_ANY + wxITEM_NORMAL + Add + new_query + protected + + New query + on_new_query + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Close + close_query + protected + + Close query + on_close_query + + + protected + + + Load From File; icons/16x16/arrow_right.png + 0 + wxID_ANY + wxITEM_NORMAL + Run + execute_statement + protected + + Execute + on_execute_statement + + + Load From File; icons/16x16/arrows_lefttoright.png + 0 + wxID_ANY + wxITEM_NORMAL + Run all + execute_all_statements + protected + + Execute all statements + on_execute_statements + + + Load From File; icons/16x16/cancel.png + 0 + wxID_ANY + wxITEM_NORMAL + Stop + stop_statements + protected + + Stop + on_stop_statements + + + protected + + + Load From File; icons/16x16/disk.png + 0 + wxID_ANY + wxITEM_NORMAL + Save + save + protected + + + on_save + + + + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + notebook_query_editor + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + + + a page + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel63 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer146 + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 1 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + 1 + + 0 + 0 + wxID_ANY + 1 + 1 + + 0 + + + 0 + + 1 + sql_query_editor + 1 + + + protected + 1 + + 0 + Resizable + 1 + + ; ; forward_declare + 1 + 4 + 0 + + 1 + 0 + 0 + + + + + + + + + + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 1 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel53 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer1261 + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + notebook_query_results + 1 + + + protected + 1 + + Resizable + 1 + + + FlatNotebook; wx.lib.agw.flatnotebook; forward_declare + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + -1,-1 + + 0 + + 1 + panel_sql_log + 1 + + + protected + 1 + + Resizable + 1 + -1,-1 + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + sizer_log_sql + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 1 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + 0 + + 0 + 0 + wxID_ANY + 1 + 1 + + 0 + -1,-1 + + 0 + + 1 + sql_query_logs + 1 + + + protected + 1 + + 0 + Resizable + 1 + -1,200 + ; ; forward_declare + 1 + 4 + 0 + + 1 + 0 + 0 + + + + + + + + + + + + + + 1 + 0 + 1 + + 4 + + 0 + wxID_ANY + + + status_bar + protected + + + wxSTB_SIZEGRIP + + + + + + + + + 0 + wxAUI_MGR_DEFAULT + + + 1 + 0 + 1 + impl_virtual + + + 0 + wxID_ANY + + + Trash + + 500,300 + ; ; forward_declare + + 0 + + + wxTAB_TRAVERSAL + + + bSizer144 + wxVERTICAL + none 5 - wxEXPAND - 0 - + wxALIGN_CENTER + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 - bSizer53 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Default - 0 - - 0 - - - 0 - - 1 - m_staticText26111111 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - column_default - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - + 1 + database_character_set_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer139 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Character set + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText70 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + database_character_set + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + 5 - wxEXPAND + wxALIGN_RIGHT|wxALL 0 - + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + New + + 0 + + 0 + + + 0 - bSizer531 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Comments - 0 - - 0 - - - 0 - - 1 - m_staticText261111111 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - column_comments - 1 - - - protected - 1 - - Resizable - 1 - -1,100 - wxTE_MULTILINE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - + 1 + m_button12 + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + - + 5 - wxEXPAND - 0 - + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 1 + wxID_ANY + + 0 + + + 0 - bSizer532 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Virtuality - 0 - - 0 - - - 0 - - 1 - m_staticText261111113 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 + 1 + QueryPanelTpl + 1 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bSizer263 + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + 0 + + 1 + m_textCtrl101 + 1 + + + protected + 1 + + Resizable + 1 + + wxTE_MULTILINE|wxTE_RICH|wxTE_RICH2 + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - column_virtuality - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - + + 5 + wxEXPAND + 0 + + + bSizer49 + wxHORIZONTAL + none + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Close + + 0 + + 0 + + + 0 + + 1 + m_button17 + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + New + + 0 + + 0 + + + 0 + + 1 + m_button121 + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + - + 5 wxEXPAND 0 - + - bSizer5311 + bSizer83 wxHORIZONTAL none 5 - wxALL + wxEXPAND | wxALL 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Expression - 0 - - 0 - - - 0 - - 1 - m_staticText2611111111 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - + 1 1 1 @@ -23384,12 +18648,11 @@ 0 - 0 0 1 - column_expression + m_staticline3 1 @@ -23398,16 +18661,11 @@ Resizable 1 - -1,100 - wxTE_MULTILINE + + wxLI_VERTICAL ; ; forward_declare 0 - - wxFILTER_NONE - wxDefaultValidator - - @@ -23415,21 +18673,236 @@ - + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/add.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Insert record + + 0 + + 0 + + + 0 + + 1 + btn_insert_record + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_insert_record + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/add.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Duplicate record + + 0 + + 0 + + + 0 + + 1 + btn_duplicate_record + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_duplicate_record + + + 5 - wxEXPAND - 1 - - 0 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/delete.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Delete record + + 0 + + 0 + + + 0 + + 1 + btn_delete_record + 1 + + protected - 0 + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_delete_record - + 5 - wxEXPAND | wxALL + wxALL 0 - + 1 1 1 @@ -23438,26 +18911,35 @@ 0 0 + 0 + Load From File; icons/16x16/cancel.png 1 0 1 1 + + 0 0 + Dock 0 Left 0 - 1 + 0 1 + 0 0 wxID_ANY + Cancel + + 0 0 @@ -23465,37 +18947,202 @@ 0 1 - m_staticline2 + btn_cancel_record 1 protected 1 + + Resizable 1 - wxLI_HORIZONTAL + wxBORDER_NONE ; ; forward_declare 0 + + wxFILTER_NONE + wxDefaultValidator + - + 5 - wxEXPAND + wxALL 0 - + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/disk.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Apply + + 0 + + 0 + + + 0 - bSizer64 + 1 + btn_apply_record + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxALL|wxEXPAND + 0 + + + bSizer53 wxHORIZONTAL none - 5 - wxALL + 5 + wxEXPAND + 0 + + 0 + protected + 100 + + + + 2 + wxLEFT|wxRIGHT + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/add.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Insert + + 0 + + 0 + + + 0 + + 1 + btn_insert_column + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_insert_column + + + + 2 + wxLEFT|wxRIGHT 0 1 @@ -23509,7 +19156,7 @@ 0 - Load From File; icons/16x16/cancel.png + Load From File; icons/16x16/delete.png 1 0 @@ -23517,14 +19164,14 @@ 1 - 1 + 0 0 Dock 0 Left 0 - 1 + 0 1 @@ -23532,7 +19179,7 @@ 0 0 wxID_ANY - Cancel + Delete 0 @@ -23542,7 +19189,7 @@ 0 1 - m_button16 + btn_delete_column 1 @@ -23554,7 +19201,7 @@ Resizable 1 - + wxBORDER_NONE ; ; forward_declare 0 @@ -23565,21 +19212,87 @@ + on_delete_column - 5 - wxEXPAND - 1 - - 0 + 2 + wxLEFT|wxRIGHT + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/arrow_up.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Up + + 0 + + 0 + + + 0 + + 1 + btn_move_up_column + 1 + + protected - 0 + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_move_up_column - 5 - wxALL + 2 + wxLEFT|wxRIGHT 0 1 @@ -23593,7 +19306,7 @@ 0 - Load From File; icons/16x16/disk.png + Load From File; icons/16x16/arrow_down.png 1 0 @@ -23608,7 +19321,7 @@ 0 Left 0 - 1 + 0 1 @@ -23616,7 +19329,7 @@ 0 0 wxID_ANY - Save + Down 0 @@ -23626,7 +19339,7 @@ 0 1 - m_button15 + btn_move_down_column 1 @@ -23638,7 +19351,7 @@ Resizable 1 - + wxBORDER_NONE ; ; forward_declare 0 @@ -23649,6 +19362,17 @@ + on_move_down_column + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 @@ -25535,5 +21259,445 @@ + + + + wxBOTH + + 1 + 0 + 1 + impl_virtual + + + + 0 + wxID_ANY + + + MyWizard1 + + + wxDEFAULT_DIALOG_STYLE + ; ; forward_declare + + + 0 + + + + + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 0 + 1 + impl_virtual + + + + 0 + wxID_ANY + + 320,200 + SaveStatments + + + wxDEFAULT_DIALOG_STYLE + ; ; forward_declare + Save Starments + + 0 + + + + + + bSizer163 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + + bSizer164 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Location + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText86 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + Select a file + + 0 + + 1 + m_filePicker5 + 1 + + + protected + 1 + + Resizable + 1 + + wxFLP_DEFAULT_STYLE|wxFLP_SAVE|wxFLP_SMALL|wxFLP_USE_TEXTCTRL + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + *.sql + + + + + + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 5 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_staticline7 + 1 + + + protected + 1 + + Resizable + 1 + + wxLI_HORIZONTAL + ; ; forward_declare + 0 + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer165 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Cancel + + 0 + + 0 + + + 0 + + 1 + m_button57 + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Save + + 0 + + 0 + + + 0 + + 1 + m_button58 + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + diff --git a/README.md b/README.md index b651e06..70f9067 100644 --- a/README.md +++ b/README.md @@ -35,15 +35,14 @@ Use at your own risk and **do not rely on this project in production environment For a detailed status snapshot, see: - [PROJECT_STATUS.md](PROJECT_STATUS.md) -- [ROADMAP.md](ROADMAP.md) ### Recent updates -- PostgreSQL engine now includes **Function** and **Procedure** classes with CRUD-style operations. -- Check constraint support was added for **MySQL**, **MariaDB**, and **PostgreSQL** engine layers. -- Connection manager now tracks **persistent connection statistics** (attempts, success/failure, timing). -- Empty database passwords are now accepted for local setups. -- MySQL/MariaDB connections can auto-retry by enabling TLS when required by the server. +- SQL autocomplete extended to INSERT / UPDATE / DELETE and string literals; parser improved with JSON and multi-table coverage. +- Table execution flow updated in the records UI. +- `row_format` and `convert_data` options added to the MySQL/MariaDB table editor. +- `windows/main/` modules restructured into subdirectories (`database/`, `table/`, `query/`). +- Advanced cell editor replaced with a dedicated `ColumnContentDialog` for large content. --- diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 9096d56..0000000 --- a/ROADMAP.md +++ /dev/null @@ -1,152 +0,0 @@ -# PeterSQL — Development Roadmap - -> **Last Updated:** 2026-03-10 -> **Status Rule:** newly implemented features are tracked as **PARTIAL** until validated across supported versions. - ---- - -## 🎯 Overview - -This roadmap reflects the current project state and separates: - -1. features already implemented but still under validation, -2. true implementation gaps, -3. UI parity work. - ---- - -## 📊 Priority Matrix - -| Priority | Focus | Target | -|----------|-------|--------| -| 🔴 **P0 - Validation Now** | stabilize newly added engine features | 1-2 weeks | -| 🟡 **P1 - Engine Gaps** | close remaining CRUD parity gaps | 2-4 weeks | -| 🟢 **P2 - UI Completeness** | add missing editors for exposed objects | 1-2 months | -| 🔵 **P3 - Advanced Features** | schema/security/import-export roadmap | 2-3 months | - ---- - -## 🔴 P0 - Validation Now - -### Implemented recently (still PARTIAL) - -- [x] **PostgreSQL Function engine implementation** (PARTIAL) - - **Files:** `structures/engines/postgresql/database.py`, `structures/engines/postgresql/context.py` - - **Status:** integration coverage for create/alter/drop is now in place across supported PostgreSQL variants. - - **Next:** long-run/manual workflow validation + broader regression checks. - -- [x] **PostgreSQL Procedure engine implementation** (PARTIAL) - - **Files:** `structures/engines/postgresql/database.py`, `structures/engines/postgresql/context.py` - - **Status:** integration coverage for create/alter/drop is now in place across supported PostgreSQL variants. - - **Next:** long-run/manual workflow validation + introspection consistency checks. - -- [x] **Check constraint implementations for MySQL/MariaDB/PostgreSQL** (PARTIAL) - - **Files:** - - `structures/engines/mysql/database.py`, `structures/engines/mysql/context.py` - - `structures/engines/mariadb/database.py`, `structures/engines/mariadb/context.py` - - `structures/engines/postgresql/database.py`, `structures/engines/postgresql/context.py` - - **Next:** cross-version validation matrix. - -- [x] **Connection reliability updates** (PARTIAL) - - **Scope:** persistent connection statistics, empty DB password support, TLS auto-retry (MySQL/MariaDB). - - **Files:** - - `structures/connection.py` - - `windows/dialogs/connections/model.py` - - `windows/dialogs/connections/view.py` - - **Validation status:** unit tests now cover connection statistics updates, MySQL/MariaDB TLS retry behavior, and SSH tunnel context lifecycle contracts. - - **Next:** unblock and run SSH testcontainers integration validation (currently skipped) + long-run behavioral validation. - -- [x] **SQL dump/backup object-driven flow** (PARTIAL) - - **Scope:** `SQLDatabase.dump()` now generates SQL dump files through domain objects (`raw_create()`), with ordered schema + records sections. - - **Files:** - - `structures/engines/database.py` - - `structures/engines/dump.py` - - `structures/engines/mysql/database.py` - - `structures/engines/mariadb/database.py` - - `structures/engines/postgresql/database.py` - - `structures/engines/sqlite/database.py` - - **Validation status:** unit suite is green in serial and xdist runs. - - **Next:** cross-engine manual restore/import verification from produced dumps. - ---- - -## 🟡 P1 - Engine Gaps - -- [x] **MySQL Procedure implementation** (PARTIAL) - - **Status:** Engine CRUD + introspection implemented, integration tests added. - - **Files:** `structures/engines/mysql/context.py`, `structures/engines/mysql/database.py`, `tests/engines/mysql/test_integration_suite.py`, `tests/engines/base_procedure_tests.py` - -- [x] **MariaDB Procedure implementation** (PARTIAL) - - **Status:** Engine CRUD + introspection implemented, integration tests added. - - **Files:** `structures/engines/mariadb/context.py`, `structures/engines/mariadb/database.py`, `tests/engines/mariadb/test_integration_suite.py`, `tests/engines/base_procedure_tests.py` - -- [ ] **Database lifecycle parity (context + UI wiring)** - - **Current state:** engine database objects expose create/alter/drop, but context/UI workflow remains read/list oriented. - - **Files:** `structures/engines/*/context.py` - ---- - -## 🟢 P2 - UI Completeness - -- [x] **View Create/Edit Dialog** - - **Status:** DONE - - **Files:** `windows/main/tabs/view.py`, `helpers/sql.py` - -- [ ] **Trigger Create/Edit UI** - - **Current state:** explorer visibility exists, editor panel missing. - -- [ ] **Function Create/Edit UI** - - **Current state:** explorer visibility exists, editor panel missing. - -- [ ] **Procedure Create/Edit UI** - - **Current state:** explorer visibility exists, editor panel missing. - -- [ ] **Database Create/Drop UI** - - **Dependency:** engine create/drop API parity. - ---- - -## 🔵 P3 - Advanced Features - -- [ ] PostgreSQL schema CRUD -- [ ] PostgreSQL sequence CRUD -- [ ] User/role management -- [ ] Privileges/grants management -- [ ] Restore + structured import/export workflows -- [ ] PostgreSQL advanced objects (materialized views, partitioning, extensions) - ---- - -## 🛠️ Validation and Completion Criteria - -Before moving a PARTIAL item to DONE: - -- [ ] Integration tests pass on supported versions. -- [ ] Behavior is stable in repeated manual workflows. -- [ ] No regressions in current engine/UI suites. -- [ ] Documentation is aligned (`README`, `PROJECT_STATUS`, `ROADMAP`). - ---- - -## 📈 Progress Snapshot - -### Current Status - -- **P0 implemented (partial):** 5/5 -- **P1 gaps closed:** 2/3 -- **P2 UI tasks complete:** 1/5 -- **P3 advanced tasks complete:** 0/6 - -### Recent Highlights - -- PostgreSQL Function and Procedure engine classes added. -- PostgreSQL Function/Procedure integration tests now include ALTER coverage (create/alter/drop). -- Check constraint support added for MySQL, MariaDB, PostgreSQL. -- Connection statistics and TLS auto-retry behavior added in connection manager. -- SQL dump/backup pipeline refactored to object-driven generation (`SQLDatabase.dump()` + `raw_create()`). -- SSH tunnel unit contract tests added for context lifecycle and process stop behavior. -- CI workflow split into test/nightly-update/release lanes. - ---- - -*This roadmap is a living document and should be updated whenever a PARTIAL item is validated or a new gap is identified.* diff --git a/helpers/dataview.py b/helpers/dataview.py index a59467c..5f2a271 100644 --- a/helpers/dataview.py +++ b/helpers/dataview.py @@ -26,16 +26,13 @@ def has_value(self, *args): return self.get_value(args[0]) is not None -class AbstractBaseDataModel(): +class BaseDataModel(): def __init__(self, column_count: Optional[int] = None): self._data: list[Any] = [] - self._observable: Union[ObservableList, ObservableLazyList] = None - self._column_count = column_count def load(self, data: list[Any]): - if data: - self._data = data.copy() + self._data = data def filter(self, data: list[Any]): if data: @@ -59,7 +56,7 @@ def replace(self, data: Any, index: int) -> int: return index - def move(self, data: Any, current: int, future: int) -> (int, int): + def move(self, data: Any, current: int, future: int) -> tuple[int, int]: self._data[current], self._data[future] = self._data[future], self._data[current] return current, future @@ -93,100 +90,201 @@ def get_item_by_name(self, name: str): def get_item_by_filters(self, **filters): return next((d for d in self._data if all(hasattr(d, k) and getattr(d, k) == v for k, v in filters.items())), None) - @abc.abstractmethod - def set_observable(self, observable: Union[ObservableList, ObservableLazyList]): - raise NotImplementedError - data = property(lambda self: self._data, lambda self, *args: None, lambda self: None) -class BaseDataViewTreeModel(AbstractBaseDataModel, wx.dataview.PyDataViewModel): +class _DataViewListValueMixin: + def _get_column_fields(self): + return getattr(self, "MAP_COLUMN_FIELDS", None) + + def get_data_by_item(self, item: wx.dataview.DataViewItem): + return self.get_data_by_row(self.GetRow(item)) + + def clear(self): + super().clear() + self.Reset(0) + self.Cleared() + + def GetColumnCount(self) -> int: + fields = self._get_column_fields() + if fields: + return len(fields) + + return self._column_count + + def GetValueByRow(self, row, col): + if not self.data or row >= len(self.data): + return None + + field = self.get_data_by_row(row) + + # if fields := self._get_column_fields(): + # return fields[col].get_value(self.get_data_by_row(row)) + + # return self.get_data_by_row(row)[col] + + if self._get_column_fields(): + return self._get_column_fields()[col].get_value(field) + + # def GetValueByRow(self, row, col): + # if not len(self.data): + # return None + # + # table: SQLTable = self.get_data_by_row(row) + # + # return self.MAP_COLUMN_FIELDS[col].get_value(table) + + def HasValue(self, item, col): + if not self.data: + return False + + if fields := self._get_column_fields(): + return fields[col].has_value(self.get_data_by_item(item)) + + # print(self.get_data_by_item(item), col) + # return getattr(self.get_data_by_item(item), col, None) is not None + return self.get_data_by_item(item) is not None + + +class BaseDataViewListModel(_DataViewListValueMixin, BaseDataModel, wx.dataview.DataViewIndexListModel): def __init__(self, column_count: Optional[int] = None): - AbstractBaseDataModel.__init__(self, column_count) - wx.dataview.PyDataViewModel.__init__(self) + BaseDataModel.__init__(self, column_count) + wx.dataview.DataViewIndexListModel.__init__(self) - def _load(self, data: list[Any]): + def load(self, data: list[Any]): self.clear() - AbstractBaseDataModel.load(self, data) - self.Cleared() - def _append(self, data: Any) -> wx.dataview.DataViewItem: - AbstractBaseDataModel.append(self, data) + BaseDataModel.load(self, data) - item = self.ObjectToItem(data) + self.Reset(len(self._data)) - if item.IsOk(): - self.ItemAdded(wx.dataview.NullDataViewItem, item) + def append(self, data: Any) -> wx.dataview.DataViewItem: + index = BaseDataModel.append(self, data) - self.Cleared() + self.RowAppended() - return item + return self.GetItem(index) - def _replace(self, data: Any, index: int) -> wx.dataview.DataViewItem: - AbstractBaseDataModel.replace(self, data, index) + def insert(self, data: Any, index: int) -> wx.dataview.DataViewItem: + index = BaseDataModel.insert(self, data, index) - item = self.ObjectToItem(data) + self.RowInserted(index) - if item.IsOk(): - self.ItemAdded(wx.dataview.NullDataViewItem, item) + return self.GetItem(index) - self.Cleared() + def remove(self, data: Any) -> bool: + index = BaseDataModel.remove(self, data) - return item + self.RowDeleted(index) - def _insert(self, data: Any, index: int) -> wx.dataview.DataViewItem: - AbstractBaseDataModel.insert(self, data, index) + self.Reset(len(self._data)) - item = self.ObjectToItem(data) + return True - if item.IsOk(): - self.ItemAdded(wx.dataview.NullDataViewItem, item) + def replace(self, data: Any, index: int) -> bool: + index = BaseDataModel.replace(self, data, index) - self.Cleared() + self.RowChanged(index) - return item + return True - def _remove(self, data: Any) -> wx.dataview.DataViewItem: - AbstractBaseDataModel.remove(self, data) + def move(self, data: Any, current: int, future: int) -> bool: + BaseDataModel.move(self, data, current, future) - item = self.ObjectToItem(data) + self.RowChanged(current) + self.RowChanged(future) - if item.IsOk(): - self.ItemDeleted(wx.dataview.NullDataViewItem, item) + return True - self.Cleared() - return item +class BaseObservableDataModel(BaseDataModel): + def __init__(self, column_count: Optional[int] = None): + super().__init__(column_count) + self._observable: Union[ObservableList, ObservableLazyList] - def _pop(self, data: Any): - AbstractBaseDataModel.pop(self, data) + def load(self, data: list[Any]): + super().load(data.copy()) - item = self.ObjectToItem(data) + @abc.abstractmethod + def set_observable(self, observable: Union[ObservableList, ObservableLazyList]): + raise NotImplementedError + def _set_observable_handlers( + self, + observable: Union[ObservableList, ObservableLazyList], + on_load: Callable, + handlers: dict[CallbackEvent, Callable], + ): + self._observable = observable + self._observable.subscribe(on_load) + + for event, handler in handlers.items(): + self._observable.subscribe(handler, callback_event=event) + + +class BaseObservableDataViewTreeModel(BaseObservableDataModel, wx.dataview.PyDataViewModel): + def __init__(self, column_count: Optional[int] = None): + BaseObservableDataModel.__init__(self, column_count) + wx.dataview.PyDataViewModel.__init__(self) + + def _load(self, data: list[Any]): + self.clear() + BaseObservableDataModel.load(self, data) + self.Cleared() + + def _apply_tree_update(self, data: Any, deleted: bool = False) -> wx.dataview.DataViewItem: + item = self.ObjectToItem(data) if item.IsOk(): - self.ItemDeleted(wx.dataview.NullDataViewItem, item) + if deleted: + self.ItemDeleted(wx.dataview.NullDataViewItem, item) + else: + self.ItemAdded(wx.dataview.NullDataViewItem, item) self.Cleared() return item + def _append(self, data: Any) -> wx.dataview.DataViewItem: + BaseObservableDataModel.append(self, data) + return self._apply_tree_update(data) + + def _replace(self, data: Any, index: int) -> wx.dataview.DataViewItem: + BaseObservableDataModel.replace(self, data, index) + return self._apply_tree_update(data) + + def _insert(self, data: Any, index: int) -> wx.dataview.DataViewItem: + BaseObservableDataModel.insert(self, data, index) + return self._apply_tree_update(data) + + def _remove(self, data: Any) -> wx.dataview.DataViewItem: + BaseObservableDataModel.remove(self, data) + return self._apply_tree_update(data, deleted=True) + + def _pop(self, data: Any): + BaseObservableDataModel.pop(self, data) + return self._apply_tree_update(data, deleted=True) + def _filter(self, data: Any): self.clear() - AbstractBaseDataModel.filter(self, data) + BaseObservableDataModel.filter(self, data) self.Cleared() def find(self, resolution: Callable[[Any], bool]) -> Optional[Any]: return next((v for v in self._data if resolution(v)), None) def set_observable(self, observable: Union[ObservableList, ObservableLazyList]): - self._observable = observable - self._observable.subscribe(self._load) - self._observable.subscribe(self._append, callback_event=CallbackEvent.ON_APPEND) - self._observable.subscribe(self._replace, callback_event=CallbackEvent.ON_REPLACE) - self._observable.subscribe(self._insert, callback_event=CallbackEvent.ON_INSERT) - self._observable.subscribe(self._remove, callback_event=CallbackEvent.ON_REMOVE) - self._observable.subscribe(self._pop, callback_event=CallbackEvent.ON_POP) - self._observable.subscribe(self._filter, callback_event=CallbackEvent.ON_FILTER) + self._set_observable_handlers( + observable, + self._load, + { + CallbackEvent.ON_APPEND: self._append, + CallbackEvent.ON_REPLACE: self._replace, + CallbackEvent.ON_INSERT: self._insert, + CallbackEvent.ON_REMOVE: self._remove, + CallbackEvent.ON_POP: self._pop, + CallbackEvent.ON_FILTER: self._filter, + }, + ) def clear(self): super().clear() @@ -202,33 +300,33 @@ def GetColumnType(self, col): return "string" -class BaseDataViewListModel(AbstractBaseDataModel, wx.dataview.DataViewIndexListModel): +class BaseObservableDataViewListModel(_DataViewListValueMixin, BaseObservableDataModel, wx.dataview.DataViewIndexListModel): def __init__(self, column_count: Optional[int] = None): - AbstractBaseDataModel.__init__(self, column_count) + BaseObservableDataModel.__init__(self, column_count) wx.dataview.DataViewIndexListModel.__init__(self) def _load(self, data: list[Any]): self.clear() - AbstractBaseDataModel.load(self, data) + BaseObservableDataModel.load(self, data) self.Reset(len(self._data)) def _append(self, data: Any) -> wx.dataview.DataViewItem: - index = AbstractBaseDataModel.append(self, data) + index = BaseObservableDataModel.append(self, data) self.RowAppended() return self.GetItem(index) def _insert(self, data: Any, index: int) -> wx.dataview.DataViewItem: - index = AbstractBaseDataModel.insert(self, data, index) + index = BaseObservableDataModel.insert(self, data, index) self.RowInserted(index) return self.GetItem(index) def _remove(self, data: Any) -> bool: - index = AbstractBaseDataModel.remove(self, data) + index = BaseObservableDataModel.remove(self, data) self.RowDeleted(index) @@ -237,14 +335,14 @@ def _remove(self, data: Any) -> bool: return True def _replace(self, data: Any, index: int) -> bool: - index = AbstractBaseDataModel.replace(self, data, index) + index = BaseObservableDataModel.replace(self, data, index) self.RowChanged(index) return True def _move(self, data: Any, current: int, future: int) -> bool: - AbstractBaseDataModel.move(self, data, current, future) + BaseObservableDataModel.move(self, data, current, future) self.RowChanged(current) self.RowChanged(future) @@ -252,46 +350,13 @@ def _move(self, data: Any, current: int, future: int) -> bool: return True def set_observable(self, observable: Union[ObservableList, ObservableLazyList]): - self._observable = observable - self._observable.subscribe(self._load) - self._observable.subscribe(self._append, callback_event=CallbackEvent.ON_APPEND) - self._observable.subscribe(self._insert, callback_event=CallbackEvent.ON_INSERT) - self._observable.subscribe(self._remove, callback_event=CallbackEvent.ON_REMOVE) - self._observable.subscribe(self._move, callback_event=CallbackEvent.ON_MOVE) - - def get_data_by_item(self, item: wx.dataview.DataViewItem): - row = self.GetRow(item) - - return self.get_data_by_row(row) - - def clear(self): - AbstractBaseDataModel.clear(self) - self.Reset(0) - self.Cleared() - - def GetColumnCount(self) -> int: - if hasattr(self, "MAP_COLUMN_FIELDS"): - return len(self.MAP_COLUMN_FIELDS.keys()) - - return self._column_count - - def GetValueByRow(self, row, col): - if not self.data: - return "" - - if row >= len(self.data): - return "" - - if not hasattr(self, "MAP_COLUMN_FIELDS"): - return "" - - return self.MAP_COLUMN_FIELDS[col].get_value(self.get_data_by_row(row)) - - def HasValue(self, item, col): - if not self.data: - return False - - if not hasattr(self, "MAP_COLUMN_FIELDS"): - return True - - return self.MAP_COLUMN_FIELDS[col].has_value(self.get_data_by_item(item)) + self._set_observable_handlers( + observable, + self._load, + { + CallbackEvent.ON_APPEND: self._append, + CallbackEvent.ON_INSERT: self._insert, + CallbackEvent.ON_REMOVE: self._remove, + CallbackEvent.ON_MOVE: self._move, + }, + ) diff --git a/helpers/loader.py b/helpers/loader.py index b25d725..b3e25cf 100644 --- a/helpers/loader.py +++ b/helpers/loader.py @@ -16,9 +16,8 @@ def _update_loading_state(cls): @contextmanager def cursor_wait(cls): """Context manager to show wait cursor during operations""" - token = object() # Unique token for this operation + token = object() - # Add token to queue current_queue = cls._queue() current_queue.append(token) cls._queue(current_queue) @@ -27,7 +26,6 @@ def cursor_wait(cls): try: yield finally: - # Remove token from queue current_queue = cls._queue() if token in current_queue: current_queue.remove(token) diff --git a/helpers/settings.py b/helpers/settings.py new file mode 100644 index 0000000..5013ecb --- /dev/null +++ b/helpers/settings.py @@ -0,0 +1,69 @@ +from pathlib import Path +from typing import Any, Optional + +from helpers.repository import YamlRepository +from helpers.observables import ObservableObject + + +class Settings(ObservableObject): + _default_unset = object() + + def _ensure_root(self) -> dict[str, Any]: + value = super()._get_value() + if isinstance(value, dict): + return value + + value = {} + super()._set_value(value) + + return value + + def _persist_default(self, attributes: tuple[str, ...], default: Any) -> Any: + if not attributes: + return super()._get_value() + + root = self._ensure_root() + current = root + + for attribute in attributes[:-1]: + nested = current.get(attribute) + if not isinstance(nested, dict): + nested = {} + current[attribute] = nested + current = nested + + last_attribute = attributes[-1] + if current.get(last_attribute) is None: + current[last_attribute] = default + super()._set_value(root) + + return current.get(last_attribute) + + def get_value(self, *attributes: str, default: Any = _default_unset) -> Any: + value = super().get_value(*attributes) + if value is not None: + return value + + if default is self._default_unset: + return None + + return self._persist_default(attributes, default) + + +class SettingsRepository(YamlRepository[Settings]): + def __init__(self, config_file: Path): + super().__init__(config_file) + self.settings: Optional[Settings] = None + + def _write(self) -> None: + if self.settings is None: + return + + data = dict(self.settings.get_value(default={})) + self._write_yaml(data) + + def load(self) -> Settings: + data = self._read_yaml() + self.settings = Settings(data) + self.settings.subscribe(lambda _: self._write()) + return self.settings \ No newline at end of file diff --git a/locale/de_DE/LC_MESSAGES/petersql.mo b/locale/de_DE/LC_MESSAGES/petersql.mo index 120161d..a5c6beb 100644 Binary files a/locale/de_DE/LC_MESSAGES/petersql.mo and b/locale/de_DE/LC_MESSAGES/petersql.mo differ diff --git a/locale/de_DE/LC_MESSAGES/petersql.po b/locale/de_DE/LC_MESSAGES/petersql.po index 9ce809a..22cec8b 100644 --- a/locale/de_DE/LC_MESSAGES/petersql.po +++ b/locale/de_DE/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-10 18:23+0100\n" +"POT-Creation-Date: 2026-03-23 10:07+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: de_DE\n" @@ -43,38 +43,79 @@ msgctxt "unit" msgid "TB" msgstr "TB" -#: structures/ssh_tunnel.py:166 +#: structures/ssh_tunnel.py:177 msgid "OpenSSH client not found." msgstr "OpenSSH-Client nicht gefunden." -#: windows/dialogs/connections/view.py:395 -#: windows/dialogs/connections/view.py:738 windows/main/controller.py:296 +#: structures/engines/mariadb/context.py:611 +#: structures/engines/mysql/context.py:622 +#: structures/engines/postgresql/context.py:645 +#: structures/engines/sqlite/context.py:524 +#, python-brace-format +msgid "Table{table_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:639 +#: structures/engines/mysql/context.py:650 +#: structures/engines/postgresql/context.py:670 +#: structures/engines/sqlite/context.py:548 +#, python-brace-format +msgid "Column{column_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:657 +#: structures/engines/mysql/context.py:668 +#: structures/engines/postgresql/context.py:688 +#: structures/engines/sqlite/context.py:566 +#, python-brace-format +msgid "Index{index_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:697 +#: structures/engines/mysql/context.py:706 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:608 +#, python-brace-format +msgid "ForeignKey{foreign_key_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:730 +#: structures/engines/mysql/context.py:737 +#: structures/engines/postgresql/context.py:758 +#: structures/engines/sqlite/context.py:636 +#, python-brace-format +msgid "View{view_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:781 +#, python-brace-format +msgid "Trigger{trigger_index:03}" +msgstr "" + +#: windows/dialogs/connections/view.py:415 +#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 #: windows/views.py:33 msgid "Connection" msgstr "Verbindung" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:956 -#: windows/views.py:1306 windows/views.py:1413 windows/views.py:1796 -#: windows/views.py:2707 windows/views.py:2730 windows/views.py:2731 -#: windows/views.py:2732 windows/views.py:2733 windows/views.py:2734 -#: windows/views.py:2735 windows/views.py:2736 windows/views.py:2737 -#: windows/views.py:2738 windows/views.py:2742 windows/views.py:2936 -#: windows/views.py:3137 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1006 +#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 +#: windows/views.py:2821 msgid "Name" msgstr "Name" -#: windows/views.py:48 windows/views.py:381 +#: windows/views.py:48 windows/views.py:428 msgid "Last connection" msgstr "Letzte Verbindung" -#: windows/dialogs/connections/view.py:631 windows/views.py:61 +#: windows/dialogs/connections/view.py:653 windows/views.py:61 msgid "New directory" msgstr "Neues Verzeichnis" -#: windows/dialogs/connections/model.py:187 -#: windows/dialogs/connections/view.py:591 windows/views.py:65 +#: windows/dialogs/connections/model.py:206 +#: windows/dialogs/connections/view.py:613 windows/views.py:65 msgid "New connection" msgstr "Neue Verbindung" @@ -88,14 +129,14 @@ msgstr "Name" msgid "Clone connection" msgstr "Neue Verbindung" -#: windows/views.py:81 windows/views.py:556 windows/views.py:1290 -#: windows/views.py:1331 windows/views.py:1706 windows/views.py:1738 -#: windows/views.py:1997 windows/views.py:3273 windows/views.py:3305 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1322 +#: windows/views.py:1365 windows/views.py:1773 windows/views.py:2032 +#: windows/views.py:2726 windows/views.py:2957 windows/views.py:2989 msgid "Delete" msgstr "Löschen" -#: windows/views.py:111 windows/views.py:1311 windows/views.py:1468 -#: windows/views.py:2747 windows/views.py:3192 +#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 +#: windows/views.py:2876 msgid "Engine" msgstr "Engine" @@ -107,542 +148,548 @@ msgstr "Host + Port" msgid "Username" msgstr "Benutzername" -#: windows/views.py:161 windows/views.py:1100 +#: windows/views.py:161 windows/views.py:1132 msgid "Password" msgstr "Passwort" -#: windows/views.py:177 +#: windows/views.py:174 +#, fuzzy +msgid "Connection timeout" +msgstr "Verbindung verloren" + +#: windows/views.py:192 msgid "Use TLS" msgstr "TLS verwenden" -#: windows/views.py:180 +#: windows/views.py:203 msgid "Use SSH tunnel" msgstr "SSH-Tunnel verwenden" -#: windows/views.py:202 windows/views.py:2668 +#: windows/views.py:214 +msgid "Compressed client/server protocol" +msgstr "" + +#: windows/views.py:233 msgid "Filename" msgstr "Dateiname" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2673 -#: windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 msgid "Select a file" msgstr "Datei auswählen" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 #, fuzzy msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:221 windows/views.py:1313 windows/views.py:1426 -#: windows/views.py:2748 windows/views.py:3034 windows/views.py:3150 +#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 +#: windows/views.py:2834 msgid "Comments" msgstr "Kommentare" -#: windows/main/controller.py:219 windows/views.py:235 windows/views.py:683 -#: windows/views.py:837 +#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 +#: windows/views.py:884 msgid "Settings" msgstr "Einstellungen" -#: windows/views.py:244 +#: windows/views.py:278 msgid "SSH executable" msgstr "SSH-Executable" -#: windows/views.py:249 +#: windows/views.py:283 msgid "ssh" msgstr "ssh" -#: windows/views.py:257 +#: windows/views.py:291 msgid "SSH host + port" msgstr "SSH-Host + Port" -#: windows/views.py:269 +#: windows/views.py:303 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "SSH-Host + Port (der SSH-Server, der den Verkehr zur DB weiterleitet)" -#: windows/views.py:278 +#: windows/views.py:312 msgid "SSH username" msgstr "SSH-Benutzername" -#: windows/views.py:291 +#: windows/views.py:325 msgid "SSH password" msgstr "SSH-Passwort" -#: windows/views.py:304 +#: windows/views.py:338 msgid "Local port" msgstr "Lokaler Port" -#: windows/views.py:310 +#: windows/views.py:344 msgid "if the value is set to 0, the first available port will be used" msgstr "wenn der Wert auf 0 gesetzt ist, wird der erste verfügbare Port verwendet" -#: windows/views.py:319 +#: windows/views.py:353 msgid "Identity file" msgstr "Identitätsdatei" -#: windows/views.py:335 +#: windows/views.py:369 #, fuzzy msgid "Remote host + port" msgstr "Host + Port" -#: windows/views.py:347 +#: windows/views.py:381 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "Remote-Host/Port ist das eigentliche DB-Ziel (standardmäßig DB-Host/Port)." -#: windows/views.py:358 +#: windows/views.py:390 +msgid "SSH extra args" +msgstr "" + +#: windows/views.py:405 msgid "SSH Tunnel" msgstr "SSH-Tunnel" -#: windows/views.py:364 windows/views.py:1309 windows/views.py:2745 +#: windows/views.py:411 windows/views.py:1341 msgid "Created at" msgstr "Erstellt am" -#: windows/views.py:398 +#: windows/views.py:445 msgid "Successful connections" msgstr "Erfolgreiche Verbindungen" -#: windows/views.py:415 +#: windows/views.py:462 #, fuzzy msgid "Last successful connection" msgstr "Erfolgreiche Verbindungen" -#: windows/views.py:432 +#: windows/views.py:479 msgid "Unsuccessful connections" msgstr "Erfolglose Verbindungen" -#: windows/views.py:449 +#: windows/views.py:496 msgid "Last failure reason" msgstr "" -#: windows/views.py:466 +#: windows/views.py:513 #, fuzzy msgid "Total connection attempts" msgstr "Letzte Verbindung" -#: windows/views.py:483 +#: windows/views.py:530 #, fuzzy -msgid " Average connection time (ms)" +msgid "Average connection time (ms)" msgstr "Wiederverbindung fehlgeschlagen:" -#: windows/views.py:500 +#: windows/views.py:547 #, fuzzy -msgid " Most recent connection duration" +msgid "Most recent connection duration" msgstr "Verbindungsmanager öffnen" -#: windows/views.py:519 +#: windows/views.py:566 msgid "Statistics" msgstr "Statistiken" -#: windows/views.py:537 windows/views.py:1672 +#: windows/views.py:584 windows/views.py:1730 msgid "Create" msgstr "Erstellen" -#: windows/views.py:541 +#: windows/views.py:588 #, fuzzy msgid "Create connection" msgstr "Letzte Verbindung" -#: windows/views.py:544 +#: windows/views.py:591 #, fuzzy msgid "Create directory" msgstr "Neues Verzeichnis" -#: windows/views.py:573 windows/views.py:797 windows/views.py:1328 -#: windows/views.py:1741 windows/views.py:2002 windows/views.py:2078 -#: windows/views.py:3081 windows/views.py:3308 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1360 +#: windows/views.py:1776 windows/views.py:2037 windows/views.py:2093 +#: windows/views.py:2702 windows/views.py:2992 windows/views.py:3129 msgid "Cancel" msgstr "Abbrechen" -#: windows/views.py:578 windows/views.py:2007 windows/views.py:3091 -#: windows/views.py:3313 +#: windows/main/controller.py:283 windows/main/controller.py:300 +#: windows/main/controller.py:301 windows/views.py:625 windows/views.py:2042 +#: windows/views.py:2235 windows/views.py:2997 windows/views.py:3135 msgid "Save" msgstr "Speichern" -#: windows/views.py:585 +#: windows/views.py:632 msgid "Test" msgstr "Testen" -#: windows/views.py:592 +#: windows/views.py:639 msgid "Connect" msgstr "Verbinden" -#: windows/views.py:695 +#: windows/views.py:742 msgid "Language" msgstr "Sprache" -#: windows/views.py:700 +#: windows/views.py:747 msgid "English" msgstr "Englisch" -#: windows/views.py:700 +#: windows/views.py:747 msgid "Italian" msgstr "Italienisch" -#: windows/views.py:700 +#: windows/views.py:747 msgid "French" msgstr "Französisch" -#: windows/views.py:712 +#: windows/views.py:759 msgid "Locale" msgstr "Lokale" -#: windows/views.py:733 -msgid "Edit Value" -msgstr "Wert bearbeiten" +#: windows/views.py:780 +#, fuzzy +msgid "Column content" +msgstr "Neue Verbindung" -#: windows/views.py:743 +#: windows/views.py:790 msgid "Syntax" msgstr "Syntax" -#: windows/views.py:800 +#: windows/views.py:847 msgid "Ok" msgstr "Ok" -#: windows/views.py:831 +#: windows/views.py:878 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:840 +#: windows/views.py:887 msgid "File" msgstr "Datei" -#: windows/views.py:843 +#: windows/views.py:890 msgid "About" msgstr "Über" -#: windows/views.py:846 +#: windows/views.py:893 msgid "Help" msgstr "Hilfe" -#: windows/views.py:851 +#: windows/views.py:898 msgid "Open connection manager" msgstr "Verbindungsmanager öffnen" -#: windows/views.py:853 +#: windows/views.py:902 msgid "Disconnect from server" msgstr "Vom Server trennen" -#: windows/views.py:857 +#: windows/views.py:904 msgid "tool" msgstr "Werkzeug" -#: windows/views.py:857 +#: windows/views.py:904 msgid "Refresh" msgstr "Aktualisieren" -#: windows/views.py:861 windows/views.py:863 +#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 +#: windows/views.py:2077 windows/views.py:2221 msgid "Add" msgstr "Hinzufügen" -#: windows/views.py:897 windows/views.py:901 windows/views.py:2166 -#: windows/views.py:2654 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 msgid "MyMenuItem" msgstr "MeinMenüElement" -#: windows/views.py:904 windows/views.py:1769 windows/views.py:3336 +#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 msgid "MyMenu" msgstr "MeinMenü" -#: windows/views.py:919 windows/views.py:1350 windows/views.py:1357 -#: windows/views.py:1364 +#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 +#: windows/views.py:1402 msgid "MyLabel" msgstr "MeinLabel" -#: windows/views.py:925 +#: windows/views.py:972 msgid "Databases" msgstr "Datenbanken" -#: windows/views.py:926 windows/views.py:1308 windows/views.py:2716 -#: windows/views.py:2744 +#: windows/views.py:973 windows/views.py:1340 msgid "Size" msgstr "Größe" -#: windows/views.py:927 +#: windows/views.py:974 msgid "Elements" msgstr "Elemente" -#: windows/views.py:928 +#: windows/views.py:975 msgid "Modified at" msgstr "Geändert am" -#: windows/views.py:929 +#: windows/views.py:976 msgid "Tables" msgstr "Tabellen" -#: windows/views.py:936 +#: windows/views.py:983 msgid "System" msgstr "System" -#: windows/views.py:979 -#, fuzzy -msgid "Character set" -msgstr "Erstellt am" - #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1000 -#: windows/views.py:1312 windows/views.py:2980 +#: windows/components/dataview.py:89 windows/views.py:1029 +#: windows/views.py:1344 msgid "Collation" msgstr "Sortierung" -#: windows/views.py:1026 windows/views.py:2862 +#: windows/views.py:1058 msgid "Encryption" msgstr "" -#: windows/views.py:1038 +#: windows/views.py:1070 msgid "Read Only" msgstr "" -#: windows/views.py:1055 +#: windows/views.py:1087 #, fuzzy msgid "Tablespace" msgstr "Tabellen" -#: windows/views.py:1076 +#: windows/views.py:1108 #, fuzzy msgid "Connection limit" msgstr "Verbindung verloren" -#: windows/views.py:1119 +#: windows/views.py:1151 #, fuzzy msgid "Profile" msgstr "Datei" -#: windows/views.py:1145 +#: windows/views.py:1177 #, fuzzy msgid "Default tablespace" msgstr "Tabelle löschen" -#: windows/views.py:1166 +#: windows/views.py:1198 #, fuzzy msgid "Temporary tablespace" msgstr "Temporär" -#: windows/views.py:1192 +#: windows/views.py:1224 msgid "Quota" msgstr "" -#: windows/views.py:1211 +#: windows/views.py:1243 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1228 +#: windows/views.py:1260 msgid "Account status" msgstr "" -#: windows/views.py:1249 +#: windows/views.py:1281 #, fuzzy msgid "Password expire" msgstr "Passwort" -#: windows/views.py:1270 +#: windows/views.py:1302 msgid "Table:" msgstr "Tabelle:" -#: windows/views.py:1278 windows/views.py:1551 windows/views.py:1595 -#: windows/views.py:1701 windows/views.py:3268 +#: windows/views.py:1310 windows/views.py:1609 windows/views.py:1653 +#: windows/views.py:2721 windows/views.py:2952 msgid "Insert" msgstr "Einfügen" -#: windows/views.py:1283 +#: windows/views.py:1315 msgid "Clone" msgstr "Klonen" -#: windows/views.py:1307 +#: windows/views.py:1339 msgid "Rows" msgstr "Zeilen" -#: windows/views.py:1310 windows/views.py:2746 +#: windows/views.py:1342 msgid "Updated at" msgstr "Aktualisiert am" -#: windows/views.py:1334 windows/views.py:1746 windows/views.py:2085 -#: windows/views.py:2140 +#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 +#: windows/views.py:2173 windows/views.py:2709 msgid "Apply" msgstr "Anwenden" -#: windows/views.py:1344 windows/views.py:1503 windows/views.py:1956 -#: windows/views.py:3225 +#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 +#: windows/views.py:2909 msgid "Options" msgstr "Optionen" -#: windows/views.py:1375 +#: windows/views.py:1413 msgid "Diagram" msgstr "Diagramm" -#: windows/views.py:1386 windows/views.py:2715 +#: windows/views.py:1424 msgid "Database" msgstr "Datenbank" -#: windows/views.py:1441 windows/views.py:3165 +#: windows/views.py:1479 windows/views.py:2849 msgid "Base" msgstr "Basis" -#: windows/views.py:1455 windows/views.py:3179 +#: windows/views.py:1493 windows/views.py:2863 msgid "Auto Increment" msgstr "Auto Inkrement" -#: windows/views.py:1483 windows/views.py:3207 +#: windows/views.py:1521 windows/views.py:2891 msgid "Default Collation" msgstr "Standard-Sortierung" -#: windows/views.py:1515 windows/views.py:1556 windows/views.py:1600 +#: windows/views.py:1531 +msgid "Convert data" +msgstr "" + +#: windows/views.py:1539 +msgid "Row format" +msgstr "" + +#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 +#: windows/views.py:1756 windows/views.py:2081 msgid "Remove" msgstr "Entfernen" -#: windows/views.py:1522 windows/views.py:1563 windows/views.py:1607 +#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 msgid "Clear" msgstr "Löschen" -#: windows/views.py:1537 windows/views.py:3239 +#: windows/views.py:1595 windows/views.py:2923 msgid "Indexes" msgstr "Indizes" -#: windows/views.py:1581 +#: windows/views.py:1639 msgid "Foreign Keys" msgstr "Fremdschlüssel" -#: windows/views.py:1625 +#: windows/views.py:1683 msgid "Checks" msgstr "Prüfungen" -#: windows/views.py:1693 windows/views.py:3260 +#: windows/views.py:1750 windows/views.py:2944 msgid "Columns:" msgstr "Spalten:" -#: windows/views.py:1713 windows/views.py:3280 -msgid "Up" -msgstr "Hoch" +#: windows/views.py:1760 +#, fuzzy +msgid "Move Up" +msgstr "Nach oben bewegen\tCTRL+UP" -#: windows/views.py:1720 windows/views.py:3287 -msgid "Down" -msgstr "Runter" +#: windows/views.py:1762 +#, fuzzy +msgid "Move Down" +msgstr "Nach unten bewegen\tCTRL+D" -#: windows/views.py:1759 windows/views.py:1766 windows/views.py:3326 -#: windows/views.py:3333 +#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 +#: windows/views.py:3017 msgid "Add Index" msgstr "Index hinzufügen" -#: windows/views.py:1763 windows/views.py:3330 +#: windows/views.py:1798 windows/views.py:3014 msgid "Add PrimaryKey" msgstr "Primärschlüssel hinzufügen" -#: windows/views.py:1780 +#: windows/views.py:1815 msgid "Table" msgstr "Tabelle" -#: windows/views.py:1816 +#: windows/views.py:1851 #, fuzzy msgid "Definer" msgstr "Einfügen" -#: windows/views.py:1836 +#: windows/views.py:1871 msgid "Schema" msgstr "" -#: windows/views.py:1862 +#: windows/views.py:1897 msgid "SQL security" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1904 #, fuzzy msgid "DEFINER" msgstr "Einfügen" -#: windows/views.py:1869 +#: windows/views.py:1904 #, fuzzy msgid "INVOKER" msgstr "Einfügen" -#: windows/views.py:1881 windows/views.py:2558 windows/views.py:2577 -#: windows/views.py:2820 +#: windows/views.py:1916 msgid "Algorithm" msgstr "" -#: windows/views.py:1883 windows/views.py:2543 windows/views.py:2576 -#: windows/views.py:2825 +#: windows/views.py:1918 #, fuzzy msgid "UNDEFINED" msgstr "Ohne Vorzeichen" -#: windows/views.py:1886 windows/views.py:2546 windows/views.py:2576 -#: windows/views.py:2828 +#: windows/views.py:1921 msgid "MERGE" msgstr "" -#: windows/views.py:1889 windows/views.py:2555 windows/views.py:2576 -#: windows/views.py:2831 +#: windows/views.py:1924 #, fuzzy msgid "TEMPTABLE" msgstr "Tabelle" -#: windows/views.py:1899 windows/views.py:2582 +#: windows/views.py:1934 msgid "View constraint" msgstr "" -#: windows/views.py:1901 windows/views.py:2581 +#: windows/views.py:1936 #, fuzzy msgid "None" msgstr "Klonen" -#: windows/views.py:1904 windows/views.py:2581 +#: windows/views.py:1939 #, fuzzy msgid "LOCAL" msgstr "Lokale" -#: windows/views.py:1907 +#: windows/views.py:1942 #, fuzzy msgid "CASCADE" msgstr "Abbrechen" -#: windows/views.py:1910 +#: windows/views.py:1945 #, fuzzy msgid "CHECK ONLY" msgstr "Prüfen" -#: windows/views.py:1913 windows/views.py:2581 +#: windows/views.py:1948 msgid "READ ONLY" msgstr "" -#: windows/views.py:1925 +#: windows/views.py:1960 msgid "Force" msgstr "" -#: windows/views.py:1937 +#: windows/views.py:1972 msgid "Security barrier" msgstr "" -#: windows/views.py:2019 +#: windows/views.py:2054 msgid "Views" msgstr "Ansichten" -#: windows/views.py:2027 +#: windows/views.py:2062 msgid "Triggers" msgstr "Trigger" -#: windows/views.py:2039 -#, python-format -msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" -msgstr "" -"Tabelle `%(database_name)s`.`%(table_name)s`: %(total_rows) Zeilen " -"insgesamt" - -#: windows/views.py:2049 -msgid "Insert record" -msgstr "Datensatz einfügen" +#: windows/views.py:2073 +#, fuzzy +msgid "Refrsh" +msgstr "Aktualisieren" -#: windows/views.py:2054 -msgid "Duplicate record" +#: windows/views.py:2079 +#, fuzzy +msgid "Duplicate" msgstr "Datensatz duplizieren" -#: windows/views.py:2061 -msgid "Delete record" -msgstr "Datensatz löschen" - -#: windows/views.py:2071 +#: windows/views.py:2085 msgid "Apply changes automatically" msgstr "Änderungen automatisch anwenden" -#: windows/views.py:2073 windows/views.py:2074 +#: windows/views.py:2087 windows/views.py:2088 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -650,161 +697,152 @@ msgstr "" "Wenn aktiviert, werden Tabellenbearbeitungen sofort angewendet, ohne auf " "Anwenden oder Abbrechen zu drücken" -#: windows/views.py:2095 -msgid "Next" -msgstr "Weiter" +#: windows/views.py:2101 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" -#: windows/views.py:2103 +#: windows/views.py:2109 +#, fuzzy +msgid "First" +msgstr "Filter" + +#: windows/views.py:2127 +msgid "Last" +msgstr "" + +#: windows/views.py:2136 msgid "Filters" msgstr "Filter" -#: windows/views.py:2143 +#: windows/views.py:2176 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2163 +#: windows/views.py:2196 msgid "Insert row" msgstr "Zeile einfügen" -#: windows/views.py:2171 +#: windows/views.py:2204 msgid "Data" msgstr "Daten" -#: windows/views.py:2225 windows/views.py:2275 -msgid "New" -msgstr "Neu" - -#: windows/views.py:2252 -msgid "Query" +#: windows/main/controller.py:278 windows/main/controller.py:287 +#: windows/main/controller.py:288 windows/views.py:2221 +#, fuzzy +msgid "New query" msgstr "Abfrage" -#: windows/views.py:2272 +#: windows/views.py:2223 windows/views.py:2660 msgid "Close" msgstr "Schließen" -#: windows/views.py:2285 -msgid "Query #2" -msgstr "Abfrage #2" +#: windows/main/controller.py:279 windows/main/controller.py:289 +#: windows/main/controller.py:290 windows/views.py:2223 +#, fuzzy +msgid "Close query" +msgstr "Abfrage" -#: windows/views.py:2537 -msgid "Column5" -msgstr "Spalte5" +#: windows/views.py:2227 +msgid "Run" +msgstr "" -#: windows/views.py:2548 -msgid "Import" -msgstr "Importieren" +#: windows/main/controller.py:280 windows/main/controller.py:292 +#: windows/main/controller.py:293 windows/views.py:2227 +#, fuzzy +msgid "Execute" +msgstr "SSH-Executable" -#: windows/views.py:2573 -msgid "Read only" +#: windows/views.py:2229 +msgid "Run all" msgstr "" -#: windows/views.py:2581 -msgid "CASCADED" +#: windows/main/controller.py:295 windows/views.py:2229 +msgid "Execute all statements" msgstr "" -#: windows/views.py:2581 -#, fuzzy -msgid "CHECK OPTION" -msgstr "Verbindung" +#: windows/main/controller.py:282 windows/main/controller.py:297 +#: windows/main/controller.py:298 windows/views.py:2231 +msgid "Stop" +msgstr "" -#: windows/views.py:2613 -msgid "collapsible" -msgstr "zusammenklappbar" +#: windows/views.py:2287 +msgid "a page" +msgstr "" -#: windows/views.py:2629 -msgid "Column3" -msgstr "Spalte3" +#: windows/views.py:2315 +msgid "Query" +msgstr "Abfrage" -#: windows/views.py:2630 -msgid "Column4" -msgstr "Spalte4" +#: windows/views.py:2626 +#, fuzzy +msgid "Character set" +msgstr "Erstellt am" -#: windows/views.py:2673 -msgid "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" -msgstr "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#: windows/views.py:2644 windows/views.py:2663 +msgid "New" +msgstr "Neu" -#: windows/views.py:2685 -msgid "Port" -msgstr "Port" +#: windows/views.py:2683 +msgid "Insert record" +msgstr "Datensatz einfügen" -#: windows/views.py:2708 -msgid "Usage" -msgstr "Verwendung" +#: windows/views.py:2688 +msgid "Duplicate record" +msgstr "Datensatz duplizieren" -#: windows/views.py:2719 -#, python-format -msgid "%(total_rows)s" -msgstr "%(total_rows)s" +#: windows/views.py:2695 +msgid "Delete record" +msgstr "Datensatz löschen" -#: windows/views.py:2724 -msgid "rows total" -msgstr "Zeilen insgesamt" +#: windows/views.py:2733 windows/views.py:2964 +msgid "Up" +msgstr "Hoch" -#: windows/views.py:2743 -msgid "Lines" -msgstr "Zeilen" +#: windows/views.py:2740 windows/views.py:2971 +msgid "Down" +msgstr "Runter" -#: windows/views.py:2775 -msgid "Temporary" -msgstr "Temporär" +#: windows/views.py:3100 +msgid "Save Starments" +msgstr "" -#: windows/views.py:2786 +#: windows/views.py:3108 #, fuzzy -msgid "Engine options" -msgstr "Optionen" +msgid "Location" +msgstr "Sortierung" -#: windows/views.py:2845 -msgid "RadioBtn" +#: windows/views.py:3115 +msgid "*.sql" msgstr "" -#: windows/views.py:2928 -msgid "Edit Column" -msgstr "Spalte bearbeiten" - -#: windows/views.py:2944 -msgid "Datatype" -msgstr "Datentyp" - -#: windows/components/dataview.py:121 windows/views.py:2959 -msgid "Length/Set" -msgstr "Länge/Menge" - -#: windows/components/dataview.py:51 windows/views.py:2998 -msgid "Unsigned" -msgstr "Ohne Vorzeichen" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3004 +#: windows/components/dataview.py:75 msgid "Allow NULL" msgstr "NULL erlauben" -#: windows/views.py:3010 -msgid "Zero Fill" -msgstr "Nullfüllung" +#: windows/components/dataview.py:28 +msgid "Check" +msgstr "Prüfen" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3021 +#: windows/components/dataview.py:78 msgid "Default" msgstr "Standard" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3047 +#: windows/components/dataview.py:82 msgid "Virtuality" msgstr "Virtualität" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3062 msgid "Expression" msgstr "Ausdruck" -#: windows/components/dataview.py:28 -msgid "Check" -msgstr "Prüfen" +#: windows/components/dataview.py:51 +msgid "Unsigned" +msgstr "Ohne Vorzeichen" #: windows/components/dataview.py:53 msgid "Zerofill" @@ -818,6 +856,10 @@ msgstr "#" msgid "Data type" msgstr "Datentyp" +#: windows/components/dataview.py:121 +msgid "Length/Set" +msgstr "Länge/Menge" + #: windows/components/dataview.py:155 msgid "Add column\tCTRL+INS" msgstr "Spalte hinzufügen\tCTRL+INS" @@ -870,11 +912,11 @@ msgstr "Bei UPDATE" msgid "On DELETE" msgstr "Bei DELETE" -#: windows/components/dataview.py:299 +#: windows/components/dataview.py:298 msgid "Add foreign key" msgstr "Fremdschlüssel hinzufügen" -#: windows/components/dataview.py:305 +#: windows/components/dataview.py:304 msgid "Remove foreign key" msgstr "Fremdschlüssel entfernen" @@ -894,75 +936,164 @@ msgstr "AUTO INCREMENT" msgid "Text/Expression" msgstr "Text/Ausdruck" -#: windows/dialogs/connections/view.py:119 windows/main/tabs/query.py:376 +#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:394 +#: windows/dialogs/connections/view.py:414 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:407 +#: windows/dialogs/connections/view.py:426 +#, python-brace-format +msgid "Do you want save the connection {connection_name}?" +msgstr "" + +#: windows/dialogs/connections/view.py:429 msgid "Confirm save" msgstr "Speichern bestätigen" -#: windows/dialogs/connections/view.py:459 +#: windows/dialogs/connections/view.py:481 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:461 +#: windows/dialogs/connections/view.py:483 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:736 +#: windows/dialogs/connections/view.py:750 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:775 +#, fuzzy, python-brace-format +msgid "" +"Connection error:\n" +"{error}" +msgstr "Verbindungsfehler" + +#: windows/dialogs/connections/view.py:776 msgid "Connection error" msgstr "Verbindungsfehler" -#: windows/dialogs/connections/view.py:788 -#: windows/dialogs/connections/view.py:803 +#: windows/dialogs/connections/view.py:802 +#, fuzzy, python-brace-format +msgid "Do you want to delete the connection '{connection_name}'?" +msgstr "Möchten Sie die Datensätze löschen?" + +#: windows/dialogs/connections/view.py:805 +#: windows/dialogs/connections/view.py:822 msgid "Confirm delete" msgstr "Löschen bestätigen" -#: windows/main/controller.py:172 +#: windows/dialogs/connections/view.py:819 +#, fuzzy, python-brace-format +msgid "Do you want to delete the directory '{directory_name}'?" +msgstr "Möchten Sie die Datensätze löschen?" + +#: windows/main/controller.py:275 +#, python-brace-format +msgid "{text} ({shortcut})" +msgstr "" + +#: windows/main/controller.py:281 windows/main/controller.py:294 +#, fuzzy +msgid "Execute all" +msgstr "SSH-Executable" + +#: windows/main/controller.py:471 windows/main/controller.py:479 +#, fuzzy +msgid "Query (1)" +msgstr "Abfrage" + +#: windows/main/controller.py:497 +#, python-brace-format +msgid "Query ({query_number})" +msgstr "" + +#: windows/main/controller.py:530 +msgid "You have unsaved changes. Save before closing?" +msgstr "" + +#: windows/main/controller.py:531 +msgid "Unsaved query" +msgstr "" + +#: windows/main/controller.py:576 +#, fuzzy +msgid "Save query" +msgstr "Abfrage" + +#: windows/main/controller.py:579 +msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" +msgstr "" + +#: windows/main/controller.py:616 windows/main/controller.py:642 +#: windows/main/database/list.py:84 windows/main/database/view.py:256 +#: windows/main/database/view.py:282 windows/main/query/controller.py:177 +msgid "Error" +msgstr "Fehler" + +#: windows/main/controller.py:622 +#, python-brace-format +msgid "-- Saved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:647 +#, python-brace-format +msgid "-- Autosaved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:704 msgid "days" msgstr "Tage" -#: windows/main/controller.py:173 +#: windows/main/controller.py:705 msgid "hours" msgstr "Stunden" -#: windows/main/controller.py:174 +#: windows/main/controller.py:706 msgid "minutes" msgstr "Minuten" -#: windows/main/controller.py:175 +#: windows/main/controller.py:707 msgid "seconds" msgstr "Sekunden" -#: windows/main/controller.py:183 +#: windows/main/controller.py:715 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Verwendeter Speicher: {used} ({percentage:.2%})" -#: windows/main/controller.py:219 +#: windows/main/controller.py:751 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:298 +#: windows/main/controller.py:952 +#, python-brace-format +msgid "~{estimated} (Loading...)" +msgstr "" + +#: windows/main/controller.py:954 +msgid "~ (Loading...)" +msgstr "" + +#: windows/main/controller.py:1119 msgid "Version" msgstr "Version" -#: windows/main/controller.py:300 +#: windows/main/controller.py:1121 msgid "Uptime" msgstr "Betriebszeit" -#: windows/main/controller.py:399 +#: windows/main/controller.py:1199 +#, python-brace-format +msgid "Do you want discard the change to {database_name}?" +msgstr "" + +#: windows/main/controller.py:1232 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -972,138 +1103,177 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:404 windows/main/controller.py:425 +#: windows/main/controller.py:1237 windows/main/controller.py:1258 #, fuzzy msgid "Delete database" msgstr "Tabelle löschen" -#: windows/main/controller.py:410 +#: windows/main/controller.py:1243 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:411 +#: windows/main/controller.py:1244 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:424 +#: windows/main/controller.py:1257 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:439 +#: windows/main/controller.py:1272 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:440 windows/main/tabs/view.py:253 -#: windows/main/tabs/view.py:279 +#: windows/main/controller.py:1273 windows/main/database/view.py:253 +#: windows/main/database/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:582 +#: windows/main/controller.py:1392 +#, python-brace-format +msgid "Do you want discard the change to {table_name}?" +msgstr "" + +#: windows/main/controller.py:1418 +#, fuzzy, python-brace-format +msgid "Do you want delete the table {table_name}?" +msgstr "Möchten Sie die Datensätze löschen?" + +#: windows/main/controller.py:1421 msgid "Delete table" msgstr "Tabelle löschen" -#: windows/main/controller.py:699 +#: windows/main/controller.py:1440 +#, python-brace-format +msgid "{table_name} (COPY)" +msgstr "" + +#: windows/main/controller.py:1563 msgid "Do you want delete the records?" msgstr "Möchten Sie die Datensätze löschen?" -#: windows/main/tabs/database.py:71 +#: windows/main/database/list.py:69 msgid "The connection to the database was lost." msgstr "Die Verbindung zur Datenbank wurde verloren." -#: windows/main/tabs/database.py:73 +#: windows/main/database/list.py:71 msgid "Do you want to reconnect?" msgstr "Möchten Sie erneut verbinden?" -#: windows/main/tabs/database.py:75 +#: windows/main/database/list.py:73 msgid "Connection lost" msgstr "Verbindung verloren" -#: windows/main/tabs/database.py:85 +#: windows/main/database/list.py:83 msgid "Reconnection failed:" msgstr "Wiederverbindung fehlgeschlagen:" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:450 -#: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 -msgid "Error" -msgstr "Fehler" - -#: windows/main/tabs/query.py:305 -#, python-brace-format -msgid "{} rows affected" +#: windows/main/database/view.py:252 +msgid "View created successfully" msgstr "" -#: windows/main/tabs/query.py:309 windows/main/tabs/query.py:331 -#, fuzzy, python-brace-format -msgid "Query {}" -msgstr "Abfrage" +#: windows/main/database/view.py:252 +msgid "View updated successfully" +msgstr "" -#: windows/main/tabs/query.py:314 +#: windows/main/database/view.py:256 #, python-brace-format -msgid "Query {} (Error)" +msgid "Error saving view: {}" msgstr "" -#: windows/main/tabs/query.py:326 +#: windows/main/database/view.py:269 #, python-brace-format -msgid "Query {} ({} rows × {} cols)" +msgid "Are you sure you want to delete view '{}'?" msgstr "" -#: windows/main/tabs/query.py:353 -#, fuzzy, python-brace-format -msgid "{} rows" -msgstr "Zeilen" +#: windows/main/database/view.py:270 +#, fuzzy +msgid "Confirm Delete" +msgstr "Löschen bestätigen" + +#: windows/main/database/view.py:279 +msgid "View deleted successfully" +msgstr "" + +#: windows/main/database/view.py:282 +#, python-brace-format +msgid "Error deleting view: {}" +msgstr "" -#: windows/main/tabs/query.py:355 +#: windows/main/query/controller.py:110 #, python-brace-format -msgid "{:.1f} ms" +msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/tabs/query.py:358 +#: windows/main/query/controller.py:112 #, python-brace-format -msgid "{} warnings" +msgid "{elapsed_s:.2f} s" msgstr "" -#: windows/main/tabs/query.py:370 +#: windows/main/query/controller.py:115 #, fuzzy -msgid "Error:" -msgstr "Fehler" +msgid "none" +msgstr "Klonen" + +#: windows/main/query/controller.py:121 +#, python-brace-format +msgid "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." +msgstr "" + +#: windows/main/query/controller.py:134 +msgid "Query execution cancelled" +msgstr "" -#: windows/main/tabs/query.py:449 +#: windows/main/query/controller.py:176 #, fuzzy msgid "No active database connection" msgstr "Neue Verbindung" -#: windows/main/tabs/view.py:252 -msgid "View created successfully" +#: windows/main/query/renderer.py:53 +#, python-brace-format +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View updated successfully" +#: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 +#, python-brace-format +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/view.py:256 +#: windows/main/query/renderer.py:65 #, python-brace-format -msgid "Error saving view: {}" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/view.py:269 +#: windows/main/query/renderer.py:79 #, python-brace-format -msgid "Are you sure you want to delete view '{}'?" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/view.py:270 -#, fuzzy -msgid "Confirm Delete" -msgstr "Löschen bestätigen" +#: windows/main/query/renderer.py:165 +#, python-brace-format +msgid "{rows_count} rows" +msgstr "" -#: windows/main/tabs/view.py:279 -msgid "View deleted successfully" +#: windows/main/query/renderer.py:167 +#, python-brace-format +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/view.py:282 +#: windows/main/query/renderer.py:171 #, python-brace-format -msgid "Error deleting view: {}" +msgid "{warnings_count} warnings" msgstr "" +#: windows/main/query/renderer.py:186 +#, fuzzy +msgid "Error:" +msgstr "Fehler" + #~ msgid "Created at:" #~ msgstr "" @@ -1140,3 +1310,102 @@ msgstr "" #~ msgid "directory" #~ msgstr "Verzeichnis" +#~ msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#~ msgstr "" +#~ "Tabelle `%(database_name)s`.`%(table_name)s`: " +#~ "%(total_rows) Zeilen insgesamt" + +#~ msgid "Next" +#~ msgstr "Weiter" + +#~ msgid "{} rows affected" +#~ msgstr "" + +#~ msgid "Query {}" +#~ msgstr "Abfrage" + +#~ msgid "Query {} (Error)" +#~ msgstr "" + +#~ msgid "Query {} ({} rows × {} cols)" +#~ msgstr "" + +#~ msgid "{} rows" +#~ msgstr "Zeilen" + +#~ msgid "{:.1f} ms" +#~ msgstr "" + +#~ msgid "{} warnings" +#~ msgstr "" + +#~ msgid "Edit Value" +#~ msgstr "Wert bearbeiten" + +#~ msgid "Query #2" +#~ msgstr "Abfrage #2" + +#~ msgid "Column5" +#~ msgstr "Spalte5" + +#~ msgid "Import" +#~ msgstr "Importieren" + +#~ msgid "Read only" +#~ msgstr "" + +#~ msgid "CASCADED" +#~ msgstr "" + +#~ msgid "CHECK OPTION" +#~ msgstr "Verbindung" + +#~ msgid "collapsible" +#~ msgstr "zusammenklappbar" + +#~ msgid "Column3" +#~ msgstr "Spalte3" + +#~ msgid "Column4" +#~ msgstr "Spalte4" + +#~ msgid "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#~ msgstr "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" + +#~ msgid "Port" +#~ msgstr "Port" + +#~ msgid "Usage" +#~ msgstr "Verwendung" + +#~ msgid "%(total_rows)s" +#~ msgstr "%(total_rows)s" + +#~ msgid "rows total" +#~ msgstr "Zeilen insgesamt" + +#~ msgid "Lines" +#~ msgstr "Zeilen" + +#~ msgid "Temporary" +#~ msgstr "Temporär" + +#~ msgid "Engine options" +#~ msgstr "Optionen" + +#~ msgid "RadioBtn" +#~ msgstr "" + +#~ msgid "Edit Column" +#~ msgstr "Spalte bearbeiten" + +#~ msgid "Datatype" +#~ msgstr "Datentyp" + +#~ msgid "Zero Fill" +#~ msgstr "Nullfüllung" + diff --git a/locale/en_US/LC_MESSAGES/petersql.mo b/locale/en_US/LC_MESSAGES/petersql.mo index 7e69f11..a950b1a 100644 Binary files a/locale/en_US/LC_MESSAGES/petersql.mo and b/locale/en_US/LC_MESSAGES/petersql.mo differ diff --git a/locale/en_US/LC_MESSAGES/petersql.po b/locale/en_US/LC_MESSAGES/petersql.po index 89d304b..5e23444 100644 --- a/locale/en_US/LC_MESSAGES/petersql.po +++ b/locale/en_US/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-10 18:23+0100\n" +"POT-Creation-Date: 2026-03-23 10:07+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: en_US\n" @@ -43,38 +43,79 @@ msgctxt "unit" msgid "TB" msgstr "TB" -#: structures/ssh_tunnel.py:166 +#: structures/ssh_tunnel.py:177 msgid "OpenSSH client not found." msgstr "OpenSSH client not found." -#: windows/dialogs/connections/view.py:395 -#: windows/dialogs/connections/view.py:738 windows/main/controller.py:296 +#: structures/engines/mariadb/context.py:611 +#: structures/engines/mysql/context.py:622 +#: structures/engines/postgresql/context.py:645 +#: structures/engines/sqlite/context.py:524 +#, python-brace-format +msgid "Table{table_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:639 +#: structures/engines/mysql/context.py:650 +#: structures/engines/postgresql/context.py:670 +#: structures/engines/sqlite/context.py:548 +#, python-brace-format +msgid "Column{column_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:657 +#: structures/engines/mysql/context.py:668 +#: structures/engines/postgresql/context.py:688 +#: structures/engines/sqlite/context.py:566 +#, python-brace-format +msgid "Index{index_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:697 +#: structures/engines/mysql/context.py:706 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:608 +#, python-brace-format +msgid "ForeignKey{foreign_key_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:730 +#: structures/engines/mysql/context.py:737 +#: structures/engines/postgresql/context.py:758 +#: structures/engines/sqlite/context.py:636 +#, python-brace-format +msgid "View{view_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:781 +#, python-brace-format +msgid "Trigger{trigger_index:03}" +msgstr "" + +#: windows/dialogs/connections/view.py:415 +#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 #: windows/views.py:33 msgid "Connection" msgstr "Connection" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:956 -#: windows/views.py:1306 windows/views.py:1413 windows/views.py:1796 -#: windows/views.py:2707 windows/views.py:2730 windows/views.py:2731 -#: windows/views.py:2732 windows/views.py:2733 windows/views.py:2734 -#: windows/views.py:2735 windows/views.py:2736 windows/views.py:2737 -#: windows/views.py:2738 windows/views.py:2742 windows/views.py:2936 -#: windows/views.py:3137 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1006 +#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 +#: windows/views.py:2821 msgid "Name" msgstr "Name" -#: windows/views.py:48 windows/views.py:381 +#: windows/views.py:48 windows/views.py:428 msgid "Last connection" msgstr "Last connection" -#: windows/dialogs/connections/view.py:631 windows/views.py:61 +#: windows/dialogs/connections/view.py:653 windows/views.py:61 msgid "New directory" msgstr "New directory" -#: windows/dialogs/connections/model.py:187 -#: windows/dialogs/connections/view.py:591 windows/views.py:65 +#: windows/dialogs/connections/model.py:206 +#: windows/dialogs/connections/view.py:613 windows/views.py:65 msgid "New connection" msgstr "New connection" @@ -86,14 +127,14 @@ msgstr "Rename" msgid "Clone connection" msgstr "Clone connection" -#: windows/views.py:81 windows/views.py:556 windows/views.py:1290 -#: windows/views.py:1331 windows/views.py:1706 windows/views.py:1738 -#: windows/views.py:1997 windows/views.py:3273 windows/views.py:3305 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1322 +#: windows/views.py:1365 windows/views.py:1773 windows/views.py:2032 +#: windows/views.py:2726 windows/views.py:2957 windows/views.py:2989 msgid "Delete" msgstr "Delete" -#: windows/views.py:111 windows/views.py:1311 windows/views.py:1468 -#: windows/views.py:2747 windows/views.py:3192 +#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 +#: windows/views.py:2876 msgid "Engine" msgstr "Engine" @@ -105,671 +146,668 @@ msgstr "Host + port" msgid "Username" msgstr "Username" -#: windows/views.py:161 windows/views.py:1100 +#: windows/views.py:161 windows/views.py:1132 msgid "Password" msgstr "Password" -#: windows/views.py:177 +#: windows/views.py:174 +#, fuzzy +msgid "Connection timeout" +msgstr "Connection" + +#: windows/views.py:192 msgid "Use TLS" msgstr "Use TLS" -#: windows/views.py:180 +#: windows/views.py:203 msgid "Use SSH tunnel" msgstr "Use SSH tunnel" -#: windows/views.py:202 windows/views.py:2668 +#: windows/views.py:214 +msgid "Compressed client/server protocol" +msgstr "" + +#: windows/views.py:233 msgid "Filename" msgstr "Filename" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2673 -#: windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 msgid "Select a file" msgstr "Select a file" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 msgid "*.*" msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:221 windows/views.py:1313 windows/views.py:1426 -#: windows/views.py:2748 windows/views.py:3034 windows/views.py:3150 +#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 +#: windows/views.py:2834 msgid "Comments" msgstr "Comments" -#: windows/main/controller.py:219 windows/views.py:235 windows/views.py:683 -#: windows/views.py:837 +#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 +#: windows/views.py:884 msgid "Settings" msgstr "Settings" -#: windows/views.py:244 +#: windows/views.py:278 msgid "SSH executable" msgstr "SSH executable" -#: windows/views.py:249 +#: windows/views.py:283 msgid "ssh" msgstr "ssh" -#: windows/views.py:257 +#: windows/views.py:291 msgid "SSH host + port" msgstr "SSH host + port" -#: windows/views.py:269 +#: windows/views.py:303 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "SSH host + port (the SSH server that forwards traffic to the DB)" -#: windows/views.py:278 +#: windows/views.py:312 msgid "SSH username" msgstr "SSH username" -#: windows/views.py:291 +#: windows/views.py:325 msgid "SSH password" msgstr "SSH password" -#: windows/views.py:304 +#: windows/views.py:338 msgid "Local port" msgstr "Local port" -#: windows/views.py:310 +#: windows/views.py:344 msgid "if the value is set to 0, the first available port will be used" msgstr "if the value is set to 0, the first available port will be used" -#: windows/views.py:319 +#: windows/views.py:353 msgid "Identity file" msgstr "Identity file" -#: windows/views.py:335 +#: windows/views.py:369 msgid "Remote host + port" msgstr "Remote host + port" -#: windows/views.py:347 +#: windows/views.py:381 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "Remote host/port is the real DB target (defaults to DB Host/Port)." -#: windows/views.py:358 +#: windows/views.py:390 +msgid "SSH extra args" +msgstr "" + +#: windows/views.py:405 msgid "SSH Tunnel" msgstr "SSH Tunnel" -#: windows/views.py:364 windows/views.py:1309 windows/views.py:2745 +#: windows/views.py:411 windows/views.py:1341 msgid "Created at" msgstr "Created at" -#: windows/views.py:398 +#: windows/views.py:445 msgid "Successful connections" msgstr "" -#: windows/views.py:415 +#: windows/views.py:462 msgid "Last successful connection" msgstr "" -#: windows/views.py:432 +#: windows/views.py:479 msgid "Unsuccessful connections" msgstr "" -#: windows/views.py:449 +#: windows/views.py:496 msgid "Last failure reason" msgstr "" -#: windows/views.py:466 +#: windows/views.py:513 msgid "Total connection attempts" msgstr "" -#: windows/views.py:483 -msgid " Average connection time (ms)" -msgstr "" +#: windows/views.py:530 +#, fuzzy +msgid "Average connection time (ms)" +msgstr "Connection" -#: windows/views.py:500 -msgid " Most recent connection duration" +#: windows/views.py:547 +msgid "Most recent connection duration" msgstr "" -#: windows/views.py:519 +#: windows/views.py:566 msgid "Statistics" msgstr "" -#: windows/views.py:537 windows/views.py:1672 +#: windows/views.py:584 windows/views.py:1730 msgid "Create" msgstr "" -#: windows/views.py:541 +#: windows/views.py:588 msgid "Create connection" msgstr "" -#: windows/views.py:544 +#: windows/views.py:591 msgid "Create directory" msgstr "" -#: windows/views.py:573 windows/views.py:797 windows/views.py:1328 -#: windows/views.py:1741 windows/views.py:2002 windows/views.py:2078 -#: windows/views.py:3081 windows/views.py:3308 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1360 +#: windows/views.py:1776 windows/views.py:2037 windows/views.py:2093 +#: windows/views.py:2702 windows/views.py:2992 windows/views.py:3129 msgid "Cancel" msgstr "" -#: windows/views.py:578 windows/views.py:2007 windows/views.py:3091 -#: windows/views.py:3313 +#: windows/main/controller.py:283 windows/main/controller.py:300 +#: windows/main/controller.py:301 windows/views.py:625 windows/views.py:2042 +#: windows/views.py:2235 windows/views.py:2997 windows/views.py:3135 msgid "Save" msgstr "" -#: windows/views.py:585 +#: windows/views.py:632 msgid "Test" msgstr "" -#: windows/views.py:592 +#: windows/views.py:639 msgid "Connect" msgstr "" -#: windows/views.py:695 +#: windows/views.py:742 msgid "Language" msgstr "" -#: windows/views.py:700 +#: windows/views.py:747 msgid "English" msgstr "" -#: windows/views.py:700 +#: windows/views.py:747 msgid "Italian" msgstr "" -#: windows/views.py:700 +#: windows/views.py:747 msgid "French" msgstr "" -#: windows/views.py:712 +#: windows/views.py:759 msgid "Locale" msgstr "" -#: windows/views.py:733 -msgid "Edit Value" -msgstr "" +#: windows/views.py:780 +#, fuzzy +msgid "Column content" +msgstr "Clone connection" -#: windows/views.py:743 +#: windows/views.py:790 msgid "Syntax" msgstr "" -#: windows/views.py:800 +#: windows/views.py:847 msgid "Ok" msgstr "" -#: windows/views.py:831 +#: windows/views.py:878 msgid "PeterSQL" msgstr "" -#: windows/views.py:840 +#: windows/views.py:887 msgid "File" msgstr "" -#: windows/views.py:843 +#: windows/views.py:890 msgid "About" msgstr "" -#: windows/views.py:846 +#: windows/views.py:893 msgid "Help" msgstr "" -#: windows/views.py:851 +#: windows/views.py:898 msgid "Open connection manager" msgstr "" -#: windows/views.py:853 +#: windows/views.py:902 msgid "Disconnect from server" msgstr "" -#: windows/views.py:857 +#: windows/views.py:904 msgid "tool" msgstr "" -#: windows/views.py:857 +#: windows/views.py:904 msgid "Refresh" msgstr "" -#: windows/views.py:861 windows/views.py:863 +#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 +#: windows/views.py:2077 windows/views.py:2221 msgid "Add" msgstr "" -#: windows/views.py:897 windows/views.py:901 windows/views.py:2166 -#: windows/views.py:2654 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 msgid "MyMenuItem" msgstr "" -#: windows/views.py:904 windows/views.py:1769 windows/views.py:3336 +#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 msgid "MyMenu" msgstr "" -#: windows/views.py:919 windows/views.py:1350 windows/views.py:1357 -#: windows/views.py:1364 +#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 +#: windows/views.py:1402 msgid "MyLabel" msgstr "" -#: windows/views.py:925 +#: windows/views.py:972 msgid "Databases" msgstr "" -#: windows/views.py:926 windows/views.py:1308 windows/views.py:2716 -#: windows/views.py:2744 +#: windows/views.py:973 windows/views.py:1340 msgid "Size" msgstr "" -#: windows/views.py:927 +#: windows/views.py:974 msgid "Elements" msgstr "" -#: windows/views.py:928 +#: windows/views.py:975 msgid "Modified at" msgstr "" -#: windows/views.py:929 +#: windows/views.py:976 msgid "Tables" msgstr "" -#: windows/views.py:936 +#: windows/views.py:983 msgid "System" msgstr "" -#: windows/views.py:979 -msgid "Character set" -msgstr "" - #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1000 -#: windows/views.py:1312 windows/views.py:2980 +#: windows/components/dataview.py:89 windows/views.py:1029 +#: windows/views.py:1344 msgid "Collation" msgstr "" -#: windows/views.py:1026 windows/views.py:2862 +#: windows/views.py:1058 msgid "Encryption" msgstr "" -#: windows/views.py:1038 +#: windows/views.py:1070 msgid "Read Only" msgstr "" -#: windows/views.py:1055 +#: windows/views.py:1087 msgid "Tablespace" msgstr "" -#: windows/views.py:1076 +#: windows/views.py:1108 msgid "Connection limit" msgstr "" -#: windows/views.py:1119 +#: windows/views.py:1151 msgid "Profile" msgstr "" -#: windows/views.py:1145 +#: windows/views.py:1177 msgid "Default tablespace" msgstr "" -#: windows/views.py:1166 +#: windows/views.py:1198 msgid "Temporary tablespace" msgstr "" -#: windows/views.py:1192 +#: windows/views.py:1224 msgid "Quota" msgstr "" -#: windows/views.py:1211 +#: windows/views.py:1243 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1228 +#: windows/views.py:1260 msgid "Account status" msgstr "" -#: windows/views.py:1249 +#: windows/views.py:1281 msgid "Password expire" msgstr "" -#: windows/views.py:1270 +#: windows/views.py:1302 msgid "Table:" msgstr "" -#: windows/views.py:1278 windows/views.py:1551 windows/views.py:1595 -#: windows/views.py:1701 windows/views.py:3268 +#: windows/views.py:1310 windows/views.py:1609 windows/views.py:1653 +#: windows/views.py:2721 windows/views.py:2952 msgid "Insert" msgstr "" -#: windows/views.py:1283 +#: windows/views.py:1315 msgid "Clone" msgstr "" -#: windows/views.py:1307 +#: windows/views.py:1339 msgid "Rows" msgstr "" -#: windows/views.py:1310 windows/views.py:2746 +#: windows/views.py:1342 msgid "Updated at" msgstr "" -#: windows/views.py:1334 windows/views.py:1746 windows/views.py:2085 -#: windows/views.py:2140 +#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 +#: windows/views.py:2173 windows/views.py:2709 msgid "Apply" msgstr "" -#: windows/views.py:1344 windows/views.py:1503 windows/views.py:1956 -#: windows/views.py:3225 +#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 +#: windows/views.py:2909 msgid "Options" msgstr "" -#: windows/views.py:1375 +#: windows/views.py:1413 msgid "Diagram" msgstr "" -#: windows/views.py:1386 windows/views.py:2715 +#: windows/views.py:1424 msgid "Database" msgstr "" -#: windows/views.py:1441 windows/views.py:3165 +#: windows/views.py:1479 windows/views.py:2849 msgid "Base" msgstr "" -#: windows/views.py:1455 windows/views.py:3179 +#: windows/views.py:1493 windows/views.py:2863 msgid "Auto Increment" msgstr "" -#: windows/views.py:1483 windows/views.py:3207 +#: windows/views.py:1521 windows/views.py:2891 msgid "Default Collation" msgstr "" -#: windows/views.py:1515 windows/views.py:1556 windows/views.py:1600 +#: windows/views.py:1531 +msgid "Convert data" +msgstr "" + +#: windows/views.py:1539 +msgid "Row format" +msgstr "" + +#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 +#: windows/views.py:1756 windows/views.py:2081 msgid "Remove" msgstr "" -#: windows/views.py:1522 windows/views.py:1563 windows/views.py:1607 +#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 msgid "Clear" msgstr "" -#: windows/views.py:1537 windows/views.py:3239 +#: windows/views.py:1595 windows/views.py:2923 msgid "Indexes" msgstr "" -#: windows/views.py:1581 +#: windows/views.py:1639 msgid "Foreign Keys" msgstr "" -#: windows/views.py:1625 +#: windows/views.py:1683 msgid "Checks" msgstr "" -#: windows/views.py:1693 windows/views.py:3260 +#: windows/views.py:1750 windows/views.py:2944 msgid "Columns:" msgstr "" -#: windows/views.py:1713 windows/views.py:3280 -msgid "Up" +#: windows/views.py:1760 +msgid "Move Up" msgstr "" -#: windows/views.py:1720 windows/views.py:3287 -msgid "Down" +#: windows/views.py:1762 +msgid "Move Down" msgstr "" -#: windows/views.py:1759 windows/views.py:1766 windows/views.py:3326 -#: windows/views.py:3333 +#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 +#: windows/views.py:3017 msgid "Add Index" msgstr "" -#: windows/views.py:1763 windows/views.py:3330 +#: windows/views.py:1798 windows/views.py:3014 msgid "Add PrimaryKey" msgstr "" -#: windows/views.py:1780 +#: windows/views.py:1815 msgid "Table" msgstr "" -#: windows/views.py:1816 +#: windows/views.py:1851 msgid "Definer" msgstr "" -#: windows/views.py:1836 +#: windows/views.py:1871 msgid "Schema" msgstr "" -#: windows/views.py:1862 +#: windows/views.py:1897 msgid "SQL security" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1904 msgid "DEFINER" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1904 msgid "INVOKER" msgstr "" -#: windows/views.py:1881 windows/views.py:2558 windows/views.py:2577 -#: windows/views.py:2820 +#: windows/views.py:1916 msgid "Algorithm" msgstr "" -#: windows/views.py:1883 windows/views.py:2543 windows/views.py:2576 -#: windows/views.py:2825 +#: windows/views.py:1918 msgid "UNDEFINED" msgstr "" -#: windows/views.py:1886 windows/views.py:2546 windows/views.py:2576 -#: windows/views.py:2828 +#: windows/views.py:1921 msgid "MERGE" msgstr "" -#: windows/views.py:1889 windows/views.py:2555 windows/views.py:2576 -#: windows/views.py:2831 +#: windows/views.py:1924 msgid "TEMPTABLE" msgstr "" -#: windows/views.py:1899 windows/views.py:2582 +#: windows/views.py:1934 msgid "View constraint" msgstr "" -#: windows/views.py:1901 windows/views.py:2581 +#: windows/views.py:1936 msgid "None" msgstr "" -#: windows/views.py:1904 windows/views.py:2581 +#: windows/views.py:1939 msgid "LOCAL" msgstr "" -#: windows/views.py:1907 +#: windows/views.py:1942 msgid "CASCADE" msgstr "" -#: windows/views.py:1910 +#: windows/views.py:1945 msgid "CHECK ONLY" msgstr "" -#: windows/views.py:1913 windows/views.py:2581 +#: windows/views.py:1948 msgid "READ ONLY" msgstr "" -#: windows/views.py:1925 +#: windows/views.py:1960 msgid "Force" msgstr "" -#: windows/views.py:1937 +#: windows/views.py:1972 msgid "Security barrier" msgstr "" -#: windows/views.py:2019 +#: windows/views.py:2054 msgid "Views" msgstr "" -#: windows/views.py:2027 +#: windows/views.py:2062 msgid "Triggers" msgstr "" -#: windows/views.py:2039 -#, python-format -msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#: windows/views.py:2073 +msgid "Refrsh" msgstr "" -#: windows/views.py:2049 -msgid "Insert record" +#: windows/views.py:2079 +msgid "Duplicate" msgstr "" -#: windows/views.py:2054 -msgid "Duplicate record" -msgstr "" - -#: windows/views.py:2061 -msgid "Delete record" -msgstr "" - -#: windows/views.py:2071 +#: windows/views.py:2085 msgid "Apply changes automatically" msgstr "" -#: windows/views.py:2073 windows/views.py:2074 +#: windows/views.py:2087 windows/views.py:2088 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" msgstr "" -#: windows/views.py:2095 -msgid "Next" +#: windows/views.py:2101 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" + +#: windows/views.py:2109 +msgid "First" msgstr "" -#: windows/views.py:2103 +#: windows/views.py:2127 +msgid "Last" +msgstr "" + +#: windows/views.py:2136 msgid "Filters" msgstr "" -#: windows/views.py:2143 +#: windows/views.py:2176 msgid "CTRL+ENTER" msgstr "" -#: windows/views.py:2163 +#: windows/views.py:2196 msgid "Insert row" msgstr "" -#: windows/views.py:2171 +#: windows/views.py:2204 msgid "Data" msgstr "" -#: windows/views.py:2225 windows/views.py:2275 -msgid "New" -msgstr "" - -#: windows/views.py:2252 -msgid "Query" -msgstr "" +#: windows/main/controller.py:278 windows/main/controller.py:287 +#: windows/main/controller.py:288 windows/views.py:2221 +#, fuzzy +msgid "New query" +msgstr "New directory" -#: windows/views.py:2272 +#: windows/views.py:2223 windows/views.py:2660 msgid "Close" msgstr "" -#: windows/views.py:2285 -msgid "Query #2" -msgstr "" - -#: windows/views.py:2537 -msgid "Column5" -msgstr "" - -#: windows/views.py:2548 -msgid "Import" -msgstr "" - -#: windows/views.py:2573 -msgid "Read only" +#: windows/main/controller.py:279 windows/main/controller.py:289 +#: windows/main/controller.py:290 windows/views.py:2223 +msgid "Close query" msgstr "" -#: windows/views.py:2581 -msgid "CASCADED" +#: windows/views.py:2227 +msgid "Run" msgstr "" -#: windows/views.py:2581 -msgid "CHECK OPTION" -msgstr "" - -#: windows/views.py:2613 -msgid "collapsible" -msgstr "" +#: windows/main/controller.py:280 windows/main/controller.py:292 +#: windows/main/controller.py:293 windows/views.py:2227 +#, fuzzy +msgid "Execute" +msgstr "SSH executable" -#: windows/views.py:2629 -msgid "Column3" +#: windows/views.py:2229 +msgid "Run all" msgstr "" -#: windows/views.py:2630 -msgid "Column4" +#: windows/main/controller.py:295 windows/views.py:2229 +msgid "Execute all statements" msgstr "" -#: windows/views.py:2673 -msgid "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#: windows/main/controller.py:282 windows/main/controller.py:297 +#: windows/main/controller.py:298 windows/views.py:2231 +msgid "Stop" msgstr "" -#: windows/views.py:2685 -msgid "Port" +#: windows/views.py:2287 +msgid "a page" msgstr "" -#: windows/views.py:2708 -msgid "Usage" +#: windows/views.py:2315 +msgid "Query" msgstr "" -#: windows/views.py:2719 -#, python-format -msgid "%(total_rows)s" +#: windows/views.py:2626 +msgid "Character set" msgstr "" -#: windows/views.py:2724 -msgid "rows total" +#: windows/views.py:2644 windows/views.py:2663 +msgid "New" msgstr "" -#: windows/views.py:2743 -msgid "Lines" +#: windows/views.py:2683 +msgid "Insert record" msgstr "" -#: windows/views.py:2775 -msgid "Temporary" +#: windows/views.py:2688 +msgid "Duplicate record" msgstr "" -#: windows/views.py:2786 -msgid "Engine options" +#: windows/views.py:2695 +msgid "Delete record" msgstr "" -#: windows/views.py:2845 -msgid "RadioBtn" +#: windows/views.py:2733 windows/views.py:2964 +msgid "Up" msgstr "" -#: windows/views.py:2928 -msgid "Edit Column" +#: windows/views.py:2740 windows/views.py:2971 +msgid "Down" msgstr "" -#: windows/views.py:2944 -msgid "Datatype" +#: windows/views.py:3100 +msgid "Save Starments" msgstr "" -#: windows/components/dataview.py:121 windows/views.py:2959 -msgid "Length/Set" +#: windows/views.py:3108 +msgid "Location" msgstr "" -#: windows/components/dataview.py:51 windows/views.py:2998 -msgid "Unsigned" +#: windows/views.py:3115 +msgid "*.sql" msgstr "" #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3004 +#: windows/components/dataview.py:75 msgid "Allow NULL" msgstr "" -#: windows/views.py:3010 -msgid "Zero Fill" +#: windows/components/dataview.py:28 +msgid "Check" msgstr "" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3021 +#: windows/components/dataview.py:78 msgid "Default" msgstr "" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3047 +#: windows/components/dataview.py:82 msgid "Virtuality" msgstr "" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3062 msgid "Expression" msgstr "" -#: windows/components/dataview.py:28 -msgid "Check" +#: windows/components/dataview.py:51 +msgid "Unsigned" msgstr "" #: windows/components/dataview.py:53 @@ -784,6 +822,10 @@ msgstr "" msgid "Data type" msgstr "" +#: windows/components/dataview.py:121 +msgid "Length/Set" +msgstr "" + #: windows/components/dataview.py:155 msgid "Add column\tCTRL+INS" msgstr "" @@ -836,11 +878,11 @@ msgstr "" msgid "On DELETE" msgstr "" -#: windows/components/dataview.py:299 +#: windows/components/dataview.py:298 msgid "Add foreign key" msgstr "" -#: windows/components/dataview.py:305 +#: windows/components/dataview.py:304 msgid "Remove foreign key" msgstr "" @@ -860,75 +902,162 @@ msgstr "" msgid "Text/Expression" msgstr "" -#: windows/dialogs/connections/view.py:119 windows/main/tabs/query.py:376 +#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:394 +#: windows/dialogs/connections/view.py:414 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:407 +#: windows/dialogs/connections/view.py:426 +#, python-brace-format +msgid "Do you want save the connection {connection_name}?" +msgstr "" + +#: windows/dialogs/connections/view.py:429 msgid "Confirm save" msgstr "" -#: windows/dialogs/connections/view.py:459 +#: windows/dialogs/connections/view.py:481 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:461 +#: windows/dialogs/connections/view.py:483 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:736 +#: windows/dialogs/connections/view.py:750 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:775 +#, python-brace-format +msgid "" +"Connection error:\n" +"{error}" +msgstr "" + +#: windows/dialogs/connections/view.py:776 msgid "Connection error" msgstr "" -#: windows/dialogs/connections/view.py:788 -#: windows/dialogs/connections/view.py:803 +#: windows/dialogs/connections/view.py:802 +#, python-brace-format +msgid "Do you want to delete the connection '{connection_name}'?" +msgstr "" + +#: windows/dialogs/connections/view.py:805 +#: windows/dialogs/connections/view.py:822 msgid "Confirm delete" msgstr "" -#: windows/main/controller.py:172 +#: windows/dialogs/connections/view.py:819 +#, python-brace-format +msgid "Do you want to delete the directory '{directory_name}'?" +msgstr "" + +#: windows/main/controller.py:275 +#, python-brace-format +msgid "{text} ({shortcut})" +msgstr "" + +#: windows/main/controller.py:281 windows/main/controller.py:294 +#, fuzzy +msgid "Execute all" +msgstr "SSH executable" + +#: windows/main/controller.py:471 windows/main/controller.py:479 +msgid "Query (1)" +msgstr "" + +#: windows/main/controller.py:497 +#, python-brace-format +msgid "Query ({query_number})" +msgstr "" + +#: windows/main/controller.py:530 +msgid "You have unsaved changes. Save before closing?" +msgstr "" + +#: windows/main/controller.py:531 +msgid "Unsaved query" +msgstr "" + +#: windows/main/controller.py:576 +msgid "Save query" +msgstr "" + +#: windows/main/controller.py:579 +msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" +msgstr "" + +#: windows/main/controller.py:616 windows/main/controller.py:642 +#: windows/main/database/list.py:84 windows/main/database/view.py:256 +#: windows/main/database/view.py:282 windows/main/query/controller.py:177 +msgid "Error" +msgstr "" + +#: windows/main/controller.py:622 +#, python-brace-format +msgid "-- Saved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:647 +#, python-brace-format +msgid "-- Autosaved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:704 msgid "days" msgstr "" -#: windows/main/controller.py:173 +#: windows/main/controller.py:705 msgid "hours" msgstr "" -#: windows/main/controller.py:174 +#: windows/main/controller.py:706 msgid "minutes" msgstr "" -#: windows/main/controller.py:175 +#: windows/main/controller.py:707 msgid "seconds" msgstr "" -#: windows/main/controller.py:183 +#: windows/main/controller.py:715 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "" -#: windows/main/controller.py:219 +#: windows/main/controller.py:751 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:298 +#: windows/main/controller.py:952 +#, python-brace-format +msgid "~{estimated} (Loading...)" +msgstr "" + +#: windows/main/controller.py:954 +msgid "~ (Loading...)" +msgstr "" + +#: windows/main/controller.py:1119 msgid "Version" msgstr "" -#: windows/main/controller.py:300 +#: windows/main/controller.py:1121 msgid "Uptime" msgstr "" -#: windows/main/controller.py:399 +#: windows/main/controller.py:1199 +#, python-brace-format +msgid "Do you want discard the change to {database_name}?" +msgstr "" + +#: windows/main/controller.py:1232 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -938,132 +1067,171 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:404 windows/main/controller.py:425 +#: windows/main/controller.py:1237 windows/main/controller.py:1258 msgid "Delete database" msgstr "" -#: windows/main/controller.py:410 +#: windows/main/controller.py:1243 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:411 +#: windows/main/controller.py:1244 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:424 +#: windows/main/controller.py:1257 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:439 +#: windows/main/controller.py:1272 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:440 windows/main/tabs/view.py:253 -#: windows/main/tabs/view.py:279 +#: windows/main/controller.py:1273 windows/main/database/view.py:253 +#: windows/main/database/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:582 +#: windows/main/controller.py:1392 +#, python-brace-format +msgid "Do you want discard the change to {table_name}?" +msgstr "" + +#: windows/main/controller.py:1418 +#, python-brace-format +msgid "Do you want delete the table {table_name}?" +msgstr "" + +#: windows/main/controller.py:1421 msgid "Delete table" msgstr "" -#: windows/main/controller.py:699 +#: windows/main/controller.py:1440 +#, python-brace-format +msgid "{table_name} (COPY)" +msgstr "" + +#: windows/main/controller.py:1563 msgid "Do you want delete the records?" msgstr "" -#: windows/main/tabs/database.py:71 +#: windows/main/database/list.py:69 msgid "The connection to the database was lost." msgstr "" -#: windows/main/tabs/database.py:73 +#: windows/main/database/list.py:71 msgid "Do you want to reconnect?" msgstr "" -#: windows/main/tabs/database.py:75 +#: windows/main/database/list.py:73 msgid "Connection lost" msgstr "" -#: windows/main/tabs/database.py:85 +#: windows/main/database/list.py:83 msgid "Reconnection failed:" msgstr "" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:450 -#: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 -msgid "Error" +#: windows/main/database/view.py:252 +msgid "View created successfully" msgstr "" -#: windows/main/tabs/query.py:305 -#, python-brace-format -msgid "{} rows affected" +#: windows/main/database/view.py:252 +msgid "View updated successfully" msgstr "" -#: windows/main/tabs/query.py:309 windows/main/tabs/query.py:331 +#: windows/main/database/view.py:256 #, python-brace-format -msgid "Query {}" +msgid "Error saving view: {}" msgstr "" -#: windows/main/tabs/query.py:314 +#: windows/main/database/view.py:269 #, python-brace-format -msgid "Query {} (Error)" +msgid "Are you sure you want to delete view '{}'?" +msgstr "" + +#: windows/main/database/view.py:270 +msgid "Confirm Delete" msgstr "" -#: windows/main/tabs/query.py:326 +#: windows/main/database/view.py:279 +msgid "View deleted successfully" +msgstr "" + +#: windows/main/database/view.py:282 #, python-brace-format -msgid "Query {} ({} rows × {} cols)" +msgid "Error deleting view: {}" msgstr "" -#: windows/main/tabs/query.py:353 +#: windows/main/query/controller.py:110 #, python-brace-format -msgid "{} rows" +msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/tabs/query.py:355 +#: windows/main/query/controller.py:112 #, python-brace-format -msgid "{:.1f} ms" +msgid "{elapsed_s:.2f} s" msgstr "" -#: windows/main/tabs/query.py:358 +#: windows/main/query/controller.py:115 +#, fuzzy +msgid "none" +msgstr "Engine" + +#: windows/main/query/controller.py:121 #, python-brace-format -msgid "{} warnings" +msgid "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." msgstr "" -#: windows/main/tabs/query.py:370 -msgid "Error:" +#: windows/main/query/controller.py:134 +msgid "Query execution cancelled" msgstr "" -#: windows/main/tabs/query.py:449 +#: windows/main/query/controller.py:176 msgid "No active database connection" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View created successfully" +#: windows/main/query/renderer.py:53 +#, python-brace-format +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View updated successfully" +#: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 +#, python-brace-format +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/view.py:256 +#: windows/main/query/renderer.py:65 #, python-brace-format -msgid "Error saving view: {}" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/view.py:269 +#: windows/main/query/renderer.py:79 #, python-brace-format -msgid "Are you sure you want to delete view '{}'?" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/view.py:270 -msgid "Confirm Delete" +#: windows/main/query/renderer.py:165 +#, python-brace-format +msgid "{rows_count} rows" msgstr "" -#: windows/main/tabs/view.py:279 -msgid "View deleted successfully" +#: windows/main/query/renderer.py:167 +#, python-brace-format +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/view.py:282 +#: windows/main/query/renderer.py:171 #, python-brace-format -msgid "Error deleting view: {}" +msgid "{warnings_count} warnings" +msgstr "" + +#: windows/main/query/renderer.py:186 +msgid "Error:" msgstr "" #~ msgid "Created at:" @@ -1105,3 +1273,104 @@ msgstr "" #~ msgid "directory" #~ msgstr "" +#~ msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#~ msgstr "" + +#~ msgid "Next" +#~ msgstr "" + +#~ msgid "{} rows affected" +#~ msgstr "" + +#~ msgid "Query {}" +#~ msgstr "" + +#~ msgid "Query {} (Error)" +#~ msgstr "" + +#~ msgid "Query {} ({} rows × {} cols)" +#~ msgstr "" + +#~ msgid "{} rows" +#~ msgstr "" + +#~ msgid "{:.1f} ms" +#~ msgstr "" + +#~ msgid "{} warnings" +#~ msgstr "" + +#~ msgid " Average connection time (ms)" +#~ msgstr "" + +#~ msgid " Most recent connection duration" +#~ msgstr "" + +#~ msgid "Edit Value" +#~ msgstr "" + +#~ msgid "Query #2" +#~ msgstr "" + +#~ msgid "Column5" +#~ msgstr "" + +#~ msgid "Import" +#~ msgstr "" + +#~ msgid "Read only" +#~ msgstr "" + +#~ msgid "CASCADED" +#~ msgstr "" + +#~ msgid "CHECK OPTION" +#~ msgstr "" + +#~ msgid "collapsible" +#~ msgstr "" + +#~ msgid "Column3" +#~ msgstr "" + +#~ msgid "Column4" +#~ msgstr "" + +#~ msgid "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#~ msgstr "" + +#~ msgid "Port" +#~ msgstr "" + +#~ msgid "Usage" +#~ msgstr "" + +#~ msgid "%(total_rows)s" +#~ msgstr "" + +#~ msgid "rows total" +#~ msgstr "" + +#~ msgid "Lines" +#~ msgstr "" + +#~ msgid "Temporary" +#~ msgstr "" + +#~ msgid "Engine options" +#~ msgstr "" + +#~ msgid "RadioBtn" +#~ msgstr "" + +#~ msgid "Edit Column" +#~ msgstr "" + +#~ msgid "Datatype" +#~ msgstr "" + +#~ msgid "Zero Fill" +#~ msgstr "" + diff --git a/locale/es_ES/LC_MESSAGES/petersql.mo b/locale/es_ES/LC_MESSAGES/petersql.mo index eca2649..3da8b8a 100644 Binary files a/locale/es_ES/LC_MESSAGES/petersql.mo and b/locale/es_ES/LC_MESSAGES/petersql.mo differ diff --git a/locale/es_ES/LC_MESSAGES/petersql.po b/locale/es_ES/LC_MESSAGES/petersql.po index 9cf4c67..e3de2d7 100644 --- a/locale/es_ES/LC_MESSAGES/petersql.po +++ b/locale/es_ES/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-10 18:23+0100\n" +"POT-Creation-Date: 2026-03-23 10:07+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: es_ES\n" @@ -43,38 +43,79 @@ msgctxt "unit" msgid "TB" msgstr "TB" -#: structures/ssh_tunnel.py:166 +#: structures/ssh_tunnel.py:177 msgid "OpenSSH client not found." msgstr "Cliente OpenSSH no encontrado." -#: windows/dialogs/connections/view.py:395 -#: windows/dialogs/connections/view.py:738 windows/main/controller.py:296 +#: structures/engines/mariadb/context.py:611 +#: structures/engines/mysql/context.py:622 +#: structures/engines/postgresql/context.py:645 +#: structures/engines/sqlite/context.py:524 +#, python-brace-format +msgid "Table{table_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:639 +#: structures/engines/mysql/context.py:650 +#: structures/engines/postgresql/context.py:670 +#: structures/engines/sqlite/context.py:548 +#, python-brace-format +msgid "Column{column_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:657 +#: structures/engines/mysql/context.py:668 +#: structures/engines/postgresql/context.py:688 +#: structures/engines/sqlite/context.py:566 +#, python-brace-format +msgid "Index{index_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:697 +#: structures/engines/mysql/context.py:706 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:608 +#, python-brace-format +msgid "ForeignKey{foreign_key_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:730 +#: structures/engines/mysql/context.py:737 +#: structures/engines/postgresql/context.py:758 +#: structures/engines/sqlite/context.py:636 +#, python-brace-format +msgid "View{view_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:781 +#, python-brace-format +msgid "Trigger{trigger_index:03}" +msgstr "" + +#: windows/dialogs/connections/view.py:415 +#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 #: windows/views.py:33 msgid "Connection" msgstr "Conexión" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:956 -#: windows/views.py:1306 windows/views.py:1413 windows/views.py:1796 -#: windows/views.py:2707 windows/views.py:2730 windows/views.py:2731 -#: windows/views.py:2732 windows/views.py:2733 windows/views.py:2734 -#: windows/views.py:2735 windows/views.py:2736 windows/views.py:2737 -#: windows/views.py:2738 windows/views.py:2742 windows/views.py:2936 -#: windows/views.py:3137 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1006 +#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 +#: windows/views.py:2821 msgid "Name" msgstr "Nombre" -#: windows/views.py:48 windows/views.py:381 +#: windows/views.py:48 windows/views.py:428 msgid "Last connection" msgstr "Última conexión" -#: windows/dialogs/connections/view.py:631 windows/views.py:61 +#: windows/dialogs/connections/view.py:653 windows/views.py:61 msgid "New directory" msgstr "Nuevo directorio" -#: windows/dialogs/connections/model.py:187 -#: windows/dialogs/connections/view.py:591 windows/views.py:65 +#: windows/dialogs/connections/model.py:206 +#: windows/dialogs/connections/view.py:613 windows/views.py:65 msgid "New connection" msgstr "Nueva conexión" @@ -88,14 +129,14 @@ msgstr "Nombre" msgid "Clone connection" msgstr "Nueva conexión" -#: windows/views.py:81 windows/views.py:556 windows/views.py:1290 -#: windows/views.py:1331 windows/views.py:1706 windows/views.py:1738 -#: windows/views.py:1997 windows/views.py:3273 windows/views.py:3305 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1322 +#: windows/views.py:1365 windows/views.py:1773 windows/views.py:2032 +#: windows/views.py:2726 windows/views.py:2957 windows/views.py:2989 msgid "Delete" msgstr "Eliminar" -#: windows/views.py:111 windows/views.py:1311 windows/views.py:1468 -#: windows/views.py:2747 windows/views.py:3192 +#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 +#: windows/views.py:2876 msgid "Engine" msgstr "Motor" @@ -107,540 +148,550 @@ msgstr "Host + puerto" msgid "Username" msgstr "Nombre de usuario" -#: windows/views.py:161 windows/views.py:1100 +#: windows/views.py:161 windows/views.py:1132 msgid "Password" msgstr "Contraseña" -#: windows/views.py:177 +#: windows/views.py:174 +#, fuzzy +msgid "Connection timeout" +msgstr "Conexión perdida" + +#: windows/views.py:192 msgid "Use TLS" msgstr "Usar TLS" -#: windows/views.py:180 +#: windows/views.py:203 msgid "Use SSH tunnel" msgstr "Usar túnel SSH" -#: windows/views.py:202 windows/views.py:2668 +#: windows/views.py:214 +msgid "Compressed client/server protocol" +msgstr "" + +#: windows/views.py:233 msgid "Filename" msgstr "Nombre de archivo" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2673 -#: windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 msgid "Select a file" msgstr "Seleccionar un archivo" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 #, fuzzy msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:221 windows/views.py:1313 windows/views.py:1426 -#: windows/views.py:2748 windows/views.py:3034 windows/views.py:3150 +#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 +#: windows/views.py:2834 msgid "Comments" msgstr "Comentarios" -#: windows/main/controller.py:219 windows/views.py:235 windows/views.py:683 -#: windows/views.py:837 +#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 +#: windows/views.py:884 msgid "Settings" msgstr "Configuraciones" -#: windows/views.py:244 +#: windows/views.py:278 msgid "SSH executable" msgstr "Ejecutable SSH" -#: windows/views.py:249 +#: windows/views.py:283 msgid "ssh" msgstr "ssh" -#: windows/views.py:257 +#: windows/views.py:291 msgid "SSH host + port" msgstr "Host SSH + puerto" -#: windows/views.py:269 +#: windows/views.py:303 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "Host SSH + puerto (el servidor SSH que reenvía el tráfico a la BD)" -#: windows/views.py:278 +#: windows/views.py:312 msgid "SSH username" msgstr "Nombre de usuario SSH" -#: windows/views.py:291 +#: windows/views.py:325 msgid "SSH password" msgstr "Contraseña SSH" -#: windows/views.py:304 +#: windows/views.py:338 msgid "Local port" msgstr "Puerto local" -#: windows/views.py:310 +#: windows/views.py:344 msgid "if the value is set to 0, the first available port will be used" msgstr "si el valor se establece en 0, se utilizará el primer puerto disponible" -#: windows/views.py:319 +#: windows/views.py:353 msgid "Identity file" msgstr "Archivo de identidad" -#: windows/views.py:335 +#: windows/views.py:369 #, fuzzy msgid "Remote host + port" msgstr "Host + puerto" -#: windows/views.py:347 +#: windows/views.py:381 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." -msgstr "Host/puerto remoto es el objetivo real de la BD (por defecto Host/Puerto BD)." +msgstr "" +"Host/puerto remoto es el objetivo real de la BD (por defecto Host/Puerto " +"BD)." + +#: windows/views.py:390 +msgid "SSH extra args" +msgstr "" -#: windows/views.py:358 +#: windows/views.py:405 msgid "SSH Tunnel" msgstr "Túnel SSH" -#: windows/views.py:364 windows/views.py:1309 windows/views.py:2745 +#: windows/views.py:411 windows/views.py:1341 msgid "Created at" msgstr "Creado en" -#: windows/views.py:398 +#: windows/views.py:445 msgid "Successful connections" msgstr "Conexiones exitosas" -#: windows/views.py:415 +#: windows/views.py:462 #, fuzzy msgid "Last successful connection" msgstr "Conexiones exitosas" -#: windows/views.py:432 +#: windows/views.py:479 msgid "Unsuccessful connections" msgstr "Conexiones fallidas" -#: windows/views.py:449 +#: windows/views.py:496 msgid "Last failure reason" msgstr "" -#: windows/views.py:466 +#: windows/views.py:513 #, fuzzy msgid "Total connection attempts" msgstr "Última conexión" -#: windows/views.py:483 +#: windows/views.py:530 #, fuzzy -msgid " Average connection time (ms)" +msgid "Average connection time (ms)" msgstr "Reconexión fallida:" -#: windows/views.py:500 +#: windows/views.py:547 #, fuzzy -msgid " Most recent connection duration" +msgid "Most recent connection duration" msgstr "Abrir administrador de conexiones" -#: windows/views.py:519 +#: windows/views.py:566 msgid "Statistics" msgstr "Estadísticas" -#: windows/views.py:537 windows/views.py:1672 +#: windows/views.py:584 windows/views.py:1730 msgid "Create" msgstr "Crear" -#: windows/views.py:541 +#: windows/views.py:588 #, fuzzy msgid "Create connection" msgstr "Última conexión" -#: windows/views.py:544 +#: windows/views.py:591 #, fuzzy msgid "Create directory" msgstr "Nuevo directorio" -#: windows/views.py:573 windows/views.py:797 windows/views.py:1328 -#: windows/views.py:1741 windows/views.py:2002 windows/views.py:2078 -#: windows/views.py:3081 windows/views.py:3308 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1360 +#: windows/views.py:1776 windows/views.py:2037 windows/views.py:2093 +#: windows/views.py:2702 windows/views.py:2992 windows/views.py:3129 msgid "Cancel" msgstr "Cancelar" -#: windows/views.py:578 windows/views.py:2007 windows/views.py:3091 -#: windows/views.py:3313 +#: windows/main/controller.py:283 windows/main/controller.py:300 +#: windows/main/controller.py:301 windows/views.py:625 windows/views.py:2042 +#: windows/views.py:2235 windows/views.py:2997 windows/views.py:3135 msgid "Save" msgstr "Guardar" -#: windows/views.py:585 +#: windows/views.py:632 msgid "Test" msgstr "Probar" -#: windows/views.py:592 +#: windows/views.py:639 msgid "Connect" msgstr "Conectar" -#: windows/views.py:695 +#: windows/views.py:742 msgid "Language" msgstr "Idioma" -#: windows/views.py:700 +#: windows/views.py:747 msgid "English" msgstr "Inglés" -#: windows/views.py:700 +#: windows/views.py:747 msgid "Italian" msgstr "Italiano" -#: windows/views.py:700 +#: windows/views.py:747 msgid "French" msgstr "Francés" -#: windows/views.py:712 +#: windows/views.py:759 msgid "Locale" msgstr "Localización" -#: windows/views.py:733 -msgid "Edit Value" -msgstr "Editar valor" +#: windows/views.py:780 +#, fuzzy +msgid "Column content" +msgstr "Nueva conexión" -#: windows/views.py:743 +#: windows/views.py:790 msgid "Syntax" msgstr "Sintaxis" -#: windows/views.py:800 +#: windows/views.py:847 msgid "Ok" msgstr "Ok" -#: windows/views.py:831 +#: windows/views.py:878 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:840 +#: windows/views.py:887 msgid "File" msgstr "Archivo" -#: windows/views.py:843 +#: windows/views.py:890 msgid "About" msgstr "Acerca de" -#: windows/views.py:846 +#: windows/views.py:893 msgid "Help" msgstr "Ayuda" -#: windows/views.py:851 +#: windows/views.py:898 msgid "Open connection manager" msgstr "Abrir administrador de conexiones" -#: windows/views.py:853 +#: windows/views.py:902 msgid "Disconnect from server" msgstr "Desconectar del servidor" -#: windows/views.py:857 +#: windows/views.py:904 msgid "tool" msgstr "herramienta" -#: windows/views.py:857 +#: windows/views.py:904 msgid "Refresh" msgstr "Actualizar" -#: windows/views.py:861 windows/views.py:863 +#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 +#: windows/views.py:2077 windows/views.py:2221 msgid "Add" msgstr "Agregar" -#: windows/views.py:897 windows/views.py:901 windows/views.py:2166 -#: windows/views.py:2654 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 msgid "MyMenuItem" msgstr "MiElementoMenu" -#: windows/views.py:904 windows/views.py:1769 windows/views.py:3336 +#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 msgid "MyMenu" msgstr "MiMenu" -#: windows/views.py:919 windows/views.py:1350 windows/views.py:1357 -#: windows/views.py:1364 +#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 +#: windows/views.py:1402 msgid "MyLabel" msgstr "MiEtiqueta" -#: windows/views.py:925 +#: windows/views.py:972 msgid "Databases" msgstr "Bases de datos" -#: windows/views.py:926 windows/views.py:1308 windows/views.py:2716 -#: windows/views.py:2744 +#: windows/views.py:973 windows/views.py:1340 msgid "Size" msgstr "Tamaño" -#: windows/views.py:927 +#: windows/views.py:974 msgid "Elements" msgstr "Elementos" -#: windows/views.py:928 +#: windows/views.py:975 msgid "Modified at" msgstr "Modificado en" -#: windows/views.py:929 +#: windows/views.py:976 msgid "Tables" msgstr "Tablas" -#: windows/views.py:936 +#: windows/views.py:983 msgid "System" msgstr "Sistema" -#: windows/views.py:979 -#, fuzzy -msgid "Character set" -msgstr "Creado en" - #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1000 -#: windows/views.py:1312 windows/views.py:2980 +#: windows/components/dataview.py:89 windows/views.py:1029 +#: windows/views.py:1344 msgid "Collation" msgstr "Intercalación" -#: windows/views.py:1026 windows/views.py:2862 +#: windows/views.py:1058 msgid "Encryption" msgstr "" -#: windows/views.py:1038 +#: windows/views.py:1070 msgid "Read Only" msgstr "" -#: windows/views.py:1055 +#: windows/views.py:1087 #, fuzzy msgid "Tablespace" msgstr "Tablas" -#: windows/views.py:1076 +#: windows/views.py:1108 #, fuzzy msgid "Connection limit" msgstr "Conexión perdida" -#: windows/views.py:1119 +#: windows/views.py:1151 #, fuzzy msgid "Profile" msgstr "Archivo" -#: windows/views.py:1145 +#: windows/views.py:1177 #, fuzzy msgid "Default tablespace" msgstr "Eliminar tabla" -#: windows/views.py:1166 +#: windows/views.py:1198 #, fuzzy msgid "Temporary tablespace" msgstr "Temporal" -#: windows/views.py:1192 +#: windows/views.py:1224 msgid "Quota" msgstr "" -#: windows/views.py:1211 +#: windows/views.py:1243 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1228 +#: windows/views.py:1260 msgid "Account status" msgstr "" -#: windows/views.py:1249 +#: windows/views.py:1281 #, fuzzy msgid "Password expire" msgstr "Contraseña" -#: windows/views.py:1270 +#: windows/views.py:1302 msgid "Table:" msgstr "Tabla:" -#: windows/views.py:1278 windows/views.py:1551 windows/views.py:1595 -#: windows/views.py:1701 windows/views.py:3268 +#: windows/views.py:1310 windows/views.py:1609 windows/views.py:1653 +#: windows/views.py:2721 windows/views.py:2952 msgid "Insert" msgstr "Insertar" -#: windows/views.py:1283 +#: windows/views.py:1315 msgid "Clone" msgstr "Clonar" -#: windows/views.py:1307 +#: windows/views.py:1339 msgid "Rows" msgstr "Filas" -#: windows/views.py:1310 windows/views.py:2746 +#: windows/views.py:1342 msgid "Updated at" msgstr "Actualizado en" -#: windows/views.py:1334 windows/views.py:1746 windows/views.py:2085 -#: windows/views.py:2140 +#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 +#: windows/views.py:2173 windows/views.py:2709 msgid "Apply" msgstr "Aplicar" -#: windows/views.py:1344 windows/views.py:1503 windows/views.py:1956 -#: windows/views.py:3225 +#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 +#: windows/views.py:2909 msgid "Options" msgstr "Opciones" -#: windows/views.py:1375 +#: windows/views.py:1413 msgid "Diagram" msgstr "Diagrama" -#: windows/views.py:1386 windows/views.py:2715 +#: windows/views.py:1424 msgid "Database" msgstr "Base de datos" -#: windows/views.py:1441 windows/views.py:3165 +#: windows/views.py:1479 windows/views.py:2849 msgid "Base" msgstr "Base" -#: windows/views.py:1455 windows/views.py:3179 +#: windows/views.py:1493 windows/views.py:2863 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1483 windows/views.py:3207 +#: windows/views.py:1521 windows/views.py:2891 msgid "Default Collation" msgstr "Intercalación predeterminada" -#: windows/views.py:1515 windows/views.py:1556 windows/views.py:1600 +#: windows/views.py:1531 +msgid "Convert data" +msgstr "" + +#: windows/views.py:1539 +msgid "Row format" +msgstr "" + +#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 +#: windows/views.py:1756 windows/views.py:2081 msgid "Remove" msgstr "Eliminar" -#: windows/views.py:1522 windows/views.py:1563 windows/views.py:1607 +#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 msgid "Clear" msgstr "Limpiar" -#: windows/views.py:1537 windows/views.py:3239 +#: windows/views.py:1595 windows/views.py:2923 msgid "Indexes" msgstr "Índices" -#: windows/views.py:1581 +#: windows/views.py:1639 msgid "Foreign Keys" msgstr "Claves foráneas" -#: windows/views.py:1625 +#: windows/views.py:1683 msgid "Checks" msgstr "Comprobaciones" -#: windows/views.py:1693 windows/views.py:3260 +#: windows/views.py:1750 windows/views.py:2944 msgid "Columns:" msgstr "Columnas:" -#: windows/views.py:1713 windows/views.py:3280 -msgid "Up" -msgstr "Arriba" +#: windows/views.py:1760 +#, fuzzy +msgid "Move Up" +msgstr "Mover arriba\tCTRL+UP" -#: windows/views.py:1720 windows/views.py:3287 -msgid "Down" -msgstr "Abajo" +#: windows/views.py:1762 +#, fuzzy +msgid "Move Down" +msgstr "Mover abajo\tCTRL+D" -#: windows/views.py:1759 windows/views.py:1766 windows/views.py:3326 -#: windows/views.py:3333 +#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 +#: windows/views.py:3017 msgid "Add Index" msgstr "Agregar índice" -#: windows/views.py:1763 windows/views.py:3330 +#: windows/views.py:1798 windows/views.py:3014 msgid "Add PrimaryKey" msgstr "Agregar clave primaria" -#: windows/views.py:1780 +#: windows/views.py:1815 msgid "Table" msgstr "Tabla" -#: windows/views.py:1816 +#: windows/views.py:1851 #, fuzzy msgid "Definer" msgstr "Insertar" -#: windows/views.py:1836 +#: windows/views.py:1871 msgid "Schema" msgstr "" -#: windows/views.py:1862 +#: windows/views.py:1897 msgid "SQL security" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1904 #, fuzzy msgid "DEFINER" msgstr "Insertar" -#: windows/views.py:1869 +#: windows/views.py:1904 #, fuzzy msgid "INVOKER" msgstr "Insertar" -#: windows/views.py:1881 windows/views.py:2558 windows/views.py:2577 -#: windows/views.py:2820 +#: windows/views.py:1916 msgid "Algorithm" msgstr "" -#: windows/views.py:1883 windows/views.py:2543 windows/views.py:2576 -#: windows/views.py:2825 +#: windows/views.py:1918 #, fuzzy msgid "UNDEFINED" msgstr "Sin signo" -#: windows/views.py:1886 windows/views.py:2546 windows/views.py:2576 -#: windows/views.py:2828 +#: windows/views.py:1921 msgid "MERGE" msgstr "" -#: windows/views.py:1889 windows/views.py:2555 windows/views.py:2576 -#: windows/views.py:2831 +#: windows/views.py:1924 #, fuzzy msgid "TEMPTABLE" msgstr "Tabla" -#: windows/views.py:1899 windows/views.py:2582 +#: windows/views.py:1934 msgid "View constraint" msgstr "" -#: windows/views.py:1901 windows/views.py:2581 +#: windows/views.py:1936 #, fuzzy msgid "None" msgstr "Clonar" -#: windows/views.py:1904 windows/views.py:2581 +#: windows/views.py:1939 #, fuzzy msgid "LOCAL" msgstr "Localización" -#: windows/views.py:1907 +#: windows/views.py:1942 #, fuzzy msgid "CASCADE" msgstr "Cancelar" -#: windows/views.py:1910 +#: windows/views.py:1945 #, fuzzy msgid "CHECK ONLY" msgstr "Verificar" -#: windows/views.py:1913 windows/views.py:2581 +#: windows/views.py:1948 msgid "READ ONLY" msgstr "" -#: windows/views.py:1925 +#: windows/views.py:1960 msgid "Force" msgstr "" -#: windows/views.py:1937 +#: windows/views.py:1972 msgid "Security barrier" msgstr "" -#: windows/views.py:2019 +#: windows/views.py:2054 msgid "Views" msgstr "Vistas" -#: windows/views.py:2027 +#: windows/views.py:2062 msgid "Triggers" msgstr "Disparadores" -#: windows/views.py:2039 -#, python-format -msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" -msgstr "Tabla `%(database_name)s`.`%(table_name)s`: %(total_rows) filas en total" - -#: windows/views.py:2049 -msgid "Insert record" -msgstr "Insertar registro" +#: windows/views.py:2073 +#, fuzzy +msgid "Refrsh" +msgstr "Actualizar" -#: windows/views.py:2054 -msgid "Duplicate record" +#: windows/views.py:2079 +#, fuzzy +msgid "Duplicate" msgstr "Duplicar registro" -#: windows/views.py:2061 -msgid "Delete record" -msgstr "Eliminar registro" - -#: windows/views.py:2071 +#: windows/views.py:2085 msgid "Apply changes automatically" msgstr "Aplicar cambios automáticamente" -#: windows/views.py:2073 windows/views.py:2074 +#: windows/views.py:2087 windows/views.py:2088 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -648,161 +699,152 @@ msgstr "" "Si está habilitado, las ediciones de la tabla se aplican inmediatamente " "sin presionar Aplicar o Cancelar" -#: windows/views.py:2095 -msgid "Next" -msgstr "Siguiente" +#: windows/views.py:2101 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" + +#: windows/views.py:2109 +#, fuzzy +msgid "First" +msgstr "Filtros" -#: windows/views.py:2103 +#: windows/views.py:2127 +msgid "Last" +msgstr "" + +#: windows/views.py:2136 msgid "Filters" msgstr "Filtros" -#: windows/views.py:2143 +#: windows/views.py:2176 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2163 +#: windows/views.py:2196 msgid "Insert row" msgstr "Insertar fila" -#: windows/views.py:2171 +#: windows/views.py:2204 msgid "Data" msgstr "Datos" -#: windows/views.py:2225 windows/views.py:2275 -msgid "New" -msgstr "Nuevo" - -#: windows/views.py:2252 -msgid "Query" +#: windows/main/controller.py:278 windows/main/controller.py:287 +#: windows/main/controller.py:288 windows/views.py:2221 +#, fuzzy +msgid "New query" msgstr "Consulta" -#: windows/views.py:2272 +#: windows/views.py:2223 windows/views.py:2660 msgid "Close" msgstr "Cerrar" -#: windows/views.py:2285 -msgid "Query #2" -msgstr "Consulta #2" +#: windows/main/controller.py:279 windows/main/controller.py:289 +#: windows/main/controller.py:290 windows/views.py:2223 +#, fuzzy +msgid "Close query" +msgstr "Consulta" -#: windows/views.py:2537 -msgid "Column5" -msgstr "Columna5" +#: windows/views.py:2227 +msgid "Run" +msgstr "" -#: windows/views.py:2548 -msgid "Import" -msgstr "Importar" +#: windows/main/controller.py:280 windows/main/controller.py:292 +#: windows/main/controller.py:293 windows/views.py:2227 +#, fuzzy +msgid "Execute" +msgstr "Ejecutable SSH" -#: windows/views.py:2573 -msgid "Read only" +#: windows/views.py:2229 +msgid "Run all" msgstr "" -#: windows/views.py:2581 -msgid "CASCADED" +#: windows/main/controller.py:295 windows/views.py:2229 +msgid "Execute all statements" msgstr "" -#: windows/views.py:2581 -#, fuzzy -msgid "CHECK OPTION" -msgstr "conexión" +#: windows/main/controller.py:282 windows/main/controller.py:297 +#: windows/main/controller.py:298 windows/views.py:2231 +msgid "Stop" +msgstr "" -#: windows/views.py:2613 -msgid "collapsible" -msgstr "colapsable" +#: windows/views.py:2287 +msgid "a page" +msgstr "" -#: windows/views.py:2629 -msgid "Column3" -msgstr "Columna3" +#: windows/views.py:2315 +msgid "Query" +msgstr "Consulta" -#: windows/views.py:2630 -msgid "Column4" -msgstr "Columna4" +#: windows/views.py:2626 +#, fuzzy +msgid "Character set" +msgstr "Creado en" -#: windows/views.py:2673 -msgid "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" -msgstr "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#: windows/views.py:2644 windows/views.py:2663 +msgid "New" +msgstr "Nuevo" -#: windows/views.py:2685 -msgid "Port" -msgstr "Puerto" +#: windows/views.py:2683 +msgid "Insert record" +msgstr "Insertar registro" -#: windows/views.py:2708 -msgid "Usage" -msgstr "Uso" +#: windows/views.py:2688 +msgid "Duplicate record" +msgstr "Duplicar registro" -#: windows/views.py:2719 -#, python-format -msgid "%(total_rows)s" -msgstr "%(total_rows)s" +#: windows/views.py:2695 +msgid "Delete record" +msgstr "Eliminar registro" -#: windows/views.py:2724 -msgid "rows total" -msgstr "filas en total" +#: windows/views.py:2733 windows/views.py:2964 +msgid "Up" +msgstr "Arriba" -#: windows/views.py:2743 -msgid "Lines" -msgstr "Líneas" +#: windows/views.py:2740 windows/views.py:2971 +msgid "Down" +msgstr "Abajo" -#: windows/views.py:2775 -msgid "Temporary" -msgstr "Temporal" +#: windows/views.py:3100 +msgid "Save Starments" +msgstr "" -#: windows/views.py:2786 +#: windows/views.py:3108 #, fuzzy -msgid "Engine options" -msgstr "Opciones" +msgid "Location" +msgstr "Intercalación" -#: windows/views.py:2845 -msgid "RadioBtn" +#: windows/views.py:3115 +msgid "*.sql" msgstr "" -#: windows/views.py:2928 -msgid "Edit Column" -msgstr "Editar columna" - -#: windows/views.py:2944 -msgid "Datatype" -msgstr "Tipo de datos" - -#: windows/components/dataview.py:121 windows/views.py:2959 -msgid "Length/Set" -msgstr "Longitud/Conjunto" - -#: windows/components/dataview.py:51 windows/views.py:2998 -msgid "Unsigned" -msgstr "Sin signo" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3004 +#: windows/components/dataview.py:75 msgid "Allow NULL" msgstr "Permitir NULL" -#: windows/views.py:3010 -msgid "Zero Fill" -msgstr "Relleno cero" +#: windows/components/dataview.py:28 +msgid "Check" +msgstr "Verificar" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3021 +#: windows/components/dataview.py:78 msgid "Default" msgstr "Predeterminado" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3047 +#: windows/components/dataview.py:82 msgid "Virtuality" msgstr "Virtualidad" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3062 msgid "Expression" msgstr "Expresión" -#: windows/components/dataview.py:28 -msgid "Check" -msgstr "Verificar" +#: windows/components/dataview.py:51 +msgid "Unsigned" +msgstr "Sin signo" #: windows/components/dataview.py:53 msgid "Zerofill" @@ -816,6 +858,10 @@ msgstr "#" msgid "Data type" msgstr "Tipo de datos" +#: windows/components/dataview.py:121 +msgid "Length/Set" +msgstr "Longitud/Conjunto" + #: windows/components/dataview.py:155 msgid "Add column\tCTRL+INS" msgstr "Agregar columna\tCTRL+INS" @@ -868,11 +914,11 @@ msgstr "En UPDATE" msgid "On DELETE" msgstr "En DELETE" -#: windows/components/dataview.py:299 +#: windows/components/dataview.py:298 msgid "Add foreign key" msgstr "Agregar clave foránea" -#: windows/components/dataview.py:305 +#: windows/components/dataview.py:304 msgid "Remove foreign key" msgstr "Eliminar clave foránea" @@ -892,75 +938,164 @@ msgstr "AUTO INCREMENTO" msgid "Text/Expression" msgstr "Texto/Expresión" -#: windows/dialogs/connections/view.py:119 windows/main/tabs/query.py:376 +#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:394 +#: windows/dialogs/connections/view.py:414 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:407 +#: windows/dialogs/connections/view.py:426 +#, python-brace-format +msgid "Do you want save the connection {connection_name}?" +msgstr "" + +#: windows/dialogs/connections/view.py:429 msgid "Confirm save" msgstr "Confirmar guardar" -#: windows/dialogs/connections/view.py:459 +#: windows/dialogs/connections/view.py:481 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:461 +#: windows/dialogs/connections/view.py:483 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:736 +#: windows/dialogs/connections/view.py:750 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:775 +#, fuzzy, python-brace-format +msgid "" +"Connection error:\n" +"{error}" +msgstr "Error de conexión" + +#: windows/dialogs/connections/view.py:776 msgid "Connection error" msgstr "Error de conexión" -#: windows/dialogs/connections/view.py:788 -#: windows/dialogs/connections/view.py:803 +#: windows/dialogs/connections/view.py:802 +#, fuzzy, python-brace-format +msgid "Do you want to delete the connection '{connection_name}'?" +msgstr "¿Quieres eliminar los registros?" + +#: windows/dialogs/connections/view.py:805 +#: windows/dialogs/connections/view.py:822 msgid "Confirm delete" msgstr "Confirmar eliminar" -#: windows/main/controller.py:172 +#: windows/dialogs/connections/view.py:819 +#, fuzzy, python-brace-format +msgid "Do you want to delete the directory '{directory_name}'?" +msgstr "¿Quieres eliminar los registros?" + +#: windows/main/controller.py:275 +#, python-brace-format +msgid "{text} ({shortcut})" +msgstr "" + +#: windows/main/controller.py:281 windows/main/controller.py:294 +#, fuzzy +msgid "Execute all" +msgstr "Ejecutable SSH" + +#: windows/main/controller.py:471 windows/main/controller.py:479 +#, fuzzy +msgid "Query (1)" +msgstr "Consulta" + +#: windows/main/controller.py:497 +#, python-brace-format +msgid "Query ({query_number})" +msgstr "" + +#: windows/main/controller.py:530 +msgid "You have unsaved changes. Save before closing?" +msgstr "" + +#: windows/main/controller.py:531 +msgid "Unsaved query" +msgstr "" + +#: windows/main/controller.py:576 +#, fuzzy +msgid "Save query" +msgstr "Consulta" + +#: windows/main/controller.py:579 +msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" +msgstr "" + +#: windows/main/controller.py:616 windows/main/controller.py:642 +#: windows/main/database/list.py:84 windows/main/database/view.py:256 +#: windows/main/database/view.py:282 windows/main/query/controller.py:177 +msgid "Error" +msgstr "Error" + +#: windows/main/controller.py:622 +#, python-brace-format +msgid "-- Saved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:647 +#, python-brace-format +msgid "-- Autosaved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:704 msgid "days" msgstr "días" -#: windows/main/controller.py:173 +#: windows/main/controller.py:705 msgid "hours" msgstr "horas" -#: windows/main/controller.py:174 +#: windows/main/controller.py:706 msgid "minutes" msgstr "minutos" -#: windows/main/controller.py:175 +#: windows/main/controller.py:707 msgid "seconds" msgstr "segundos" -#: windows/main/controller.py:183 +#: windows/main/controller.py:715 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizada: {used} ({percentage:.2%})" -#: windows/main/controller.py:219 +#: windows/main/controller.py:751 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:298 +#: windows/main/controller.py:952 +#, python-brace-format +msgid "~{estimated} (Loading...)" +msgstr "" + +#: windows/main/controller.py:954 +msgid "~ (Loading...)" +msgstr "" + +#: windows/main/controller.py:1119 msgid "Version" msgstr "Versión" -#: windows/main/controller.py:300 +#: windows/main/controller.py:1121 msgid "Uptime" msgstr "Tiempo de actividad" -#: windows/main/controller.py:399 +#: windows/main/controller.py:1199 +#, python-brace-format +msgid "Do you want discard the change to {database_name}?" +msgstr "" + +#: windows/main/controller.py:1232 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -970,138 +1105,177 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:404 windows/main/controller.py:425 +#: windows/main/controller.py:1237 windows/main/controller.py:1258 #, fuzzy msgid "Delete database" msgstr "Eliminar tabla" -#: windows/main/controller.py:410 +#: windows/main/controller.py:1243 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:411 +#: windows/main/controller.py:1244 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:424 +#: windows/main/controller.py:1257 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:439 +#: windows/main/controller.py:1272 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:440 windows/main/tabs/view.py:253 -#: windows/main/tabs/view.py:279 +#: windows/main/controller.py:1273 windows/main/database/view.py:253 +#: windows/main/database/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:582 +#: windows/main/controller.py:1392 +#, python-brace-format +msgid "Do you want discard the change to {table_name}?" +msgstr "" + +#: windows/main/controller.py:1418 +#, fuzzy, python-brace-format +msgid "Do you want delete the table {table_name}?" +msgstr "¿Quieres eliminar los registros?" + +#: windows/main/controller.py:1421 msgid "Delete table" msgstr "Eliminar tabla" -#: windows/main/controller.py:699 +#: windows/main/controller.py:1440 +#, python-brace-format +msgid "{table_name} (COPY)" +msgstr "" + +#: windows/main/controller.py:1563 msgid "Do you want delete the records?" msgstr "¿Quieres eliminar los registros?" -#: windows/main/tabs/database.py:71 +#: windows/main/database/list.py:69 msgid "The connection to the database was lost." msgstr "Se perdió la conexión a la base de datos." -#: windows/main/tabs/database.py:73 +#: windows/main/database/list.py:71 msgid "Do you want to reconnect?" msgstr "¿Quieres reconectar?" -#: windows/main/tabs/database.py:75 +#: windows/main/database/list.py:73 msgid "Connection lost" msgstr "Conexión perdida" -#: windows/main/tabs/database.py:85 +#: windows/main/database/list.py:83 msgid "Reconnection failed:" msgstr "Reconexión fallida:" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:450 -#: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 -msgid "Error" -msgstr "Error" - -#: windows/main/tabs/query.py:305 -#, python-brace-format -msgid "{} rows affected" +#: windows/main/database/view.py:252 +msgid "View created successfully" msgstr "" -#: windows/main/tabs/query.py:309 windows/main/tabs/query.py:331 -#, fuzzy, python-brace-format -msgid "Query {}" -msgstr "Consulta" +#: windows/main/database/view.py:252 +msgid "View updated successfully" +msgstr "" -#: windows/main/tabs/query.py:314 +#: windows/main/database/view.py:256 #, python-brace-format -msgid "Query {} (Error)" +msgid "Error saving view: {}" msgstr "" -#: windows/main/tabs/query.py:326 +#: windows/main/database/view.py:269 #, python-brace-format -msgid "Query {} ({} rows × {} cols)" +msgid "Are you sure you want to delete view '{}'?" msgstr "" -#: windows/main/tabs/query.py:353 -#, fuzzy, python-brace-format -msgid "{} rows" -msgstr "Filas" +#: windows/main/database/view.py:270 +#, fuzzy +msgid "Confirm Delete" +msgstr "Confirmar eliminar" + +#: windows/main/database/view.py:279 +msgid "View deleted successfully" +msgstr "" + +#: windows/main/database/view.py:282 +#, python-brace-format +msgid "Error deleting view: {}" +msgstr "" -#: windows/main/tabs/query.py:355 +#: windows/main/query/controller.py:110 #, python-brace-format -msgid "{:.1f} ms" +msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/tabs/query.py:358 +#: windows/main/query/controller.py:112 #, python-brace-format -msgid "{} warnings" +msgid "{elapsed_s:.2f} s" msgstr "" -#: windows/main/tabs/query.py:370 +#: windows/main/query/controller.py:115 #, fuzzy -msgid "Error:" -msgstr "Error" +msgid "none" +msgstr "Clonar" + +#: windows/main/query/controller.py:121 +#, python-brace-format +msgid "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." +msgstr "" + +#: windows/main/query/controller.py:134 +msgid "Query execution cancelled" +msgstr "" -#: windows/main/tabs/query.py:449 +#: windows/main/query/controller.py:176 #, fuzzy msgid "No active database connection" msgstr "Nueva conexión" -#: windows/main/tabs/view.py:252 -msgid "View created successfully" +#: windows/main/query/renderer.py:53 +#, python-brace-format +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View updated successfully" +#: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 +#, python-brace-format +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/view.py:256 +#: windows/main/query/renderer.py:65 #, python-brace-format -msgid "Error saving view: {}" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/view.py:269 +#: windows/main/query/renderer.py:79 #, python-brace-format -msgid "Are you sure you want to delete view '{}'?" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/view.py:270 -#, fuzzy -msgid "Confirm Delete" -msgstr "Confirmar eliminar" +#: windows/main/query/renderer.py:165 +#, python-brace-format +msgid "{rows_count} rows" +msgstr "" -#: windows/main/tabs/view.py:279 -msgid "View deleted successfully" +#: windows/main/query/renderer.py:167 +#, python-brace-format +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/view.py:282 +#: windows/main/query/renderer.py:171 #, python-brace-format -msgid "Error deleting view: {}" +msgid "{warnings_count} warnings" msgstr "" +#: windows/main/query/renderer.py:186 +#, fuzzy +msgid "Error:" +msgstr "Error" + #~ msgid "Created at:" #~ msgstr "" @@ -1138,3 +1312,102 @@ msgstr "" #~ msgid "directory" #~ msgstr "directorio" +#~ msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#~ msgstr "" +#~ "Tabla `%(database_name)s`.`%(table_name)s`: %(total_rows)" +#~ " filas en total" + +#~ msgid "Next" +#~ msgstr "Siguiente" + +#~ msgid "{} rows affected" +#~ msgstr "" + +#~ msgid "Query {}" +#~ msgstr "Consulta" + +#~ msgid "Query {} (Error)" +#~ msgstr "" + +#~ msgid "Query {} ({} rows × {} cols)" +#~ msgstr "" + +#~ msgid "{} rows" +#~ msgstr "Filas" + +#~ msgid "{:.1f} ms" +#~ msgstr "" + +#~ msgid "{} warnings" +#~ msgstr "" + +#~ msgid "Edit Value" +#~ msgstr "Editar valor" + +#~ msgid "Query #2" +#~ msgstr "Consulta #2" + +#~ msgid "Column5" +#~ msgstr "Columna5" + +#~ msgid "Import" +#~ msgstr "Importar" + +#~ msgid "Read only" +#~ msgstr "" + +#~ msgid "CASCADED" +#~ msgstr "" + +#~ msgid "CHECK OPTION" +#~ msgstr "conexión" + +#~ msgid "collapsible" +#~ msgstr "colapsable" + +#~ msgid "Column3" +#~ msgstr "Columna3" + +#~ msgid "Column4" +#~ msgstr "Columna4" + +#~ msgid "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#~ msgstr "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" + +#~ msgid "Port" +#~ msgstr "Puerto" + +#~ msgid "Usage" +#~ msgstr "Uso" + +#~ msgid "%(total_rows)s" +#~ msgstr "%(total_rows)s" + +#~ msgid "rows total" +#~ msgstr "filas en total" + +#~ msgid "Lines" +#~ msgstr "Líneas" + +#~ msgid "Temporary" +#~ msgstr "Temporal" + +#~ msgid "Engine options" +#~ msgstr "Opciones" + +#~ msgid "RadioBtn" +#~ msgstr "" + +#~ msgid "Edit Column" +#~ msgstr "Editar columna" + +#~ msgid "Datatype" +#~ msgstr "Tipo de datos" + +#~ msgid "Zero Fill" +#~ msgstr "Relleno cero" + diff --git a/locale/fr_FR/LC_MESSAGES/petersql.mo b/locale/fr_FR/LC_MESSAGES/petersql.mo index 13d3905..b40cd26 100644 Binary files a/locale/fr_FR/LC_MESSAGES/petersql.mo and b/locale/fr_FR/LC_MESSAGES/petersql.mo differ diff --git a/locale/fr_FR/LC_MESSAGES/petersql.po b/locale/fr_FR/LC_MESSAGES/petersql.po index 028bb82..5ba12c3 100644 --- a/locale/fr_FR/LC_MESSAGES/petersql.po +++ b/locale/fr_FR/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-10 18:23+0100\n" +"POT-Creation-Date: 2026-03-23 10:07+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: fr_FR\n" @@ -43,38 +43,79 @@ msgctxt "unit" msgid "TB" msgstr "To" -#: structures/ssh_tunnel.py:166 +#: structures/ssh_tunnel.py:177 msgid "OpenSSH client not found." msgstr "Client OpenSSH introuvable." -#: windows/dialogs/connections/view.py:395 -#: windows/dialogs/connections/view.py:738 windows/main/controller.py:296 +#: structures/engines/mariadb/context.py:611 +#: structures/engines/mysql/context.py:622 +#: structures/engines/postgresql/context.py:645 +#: structures/engines/sqlite/context.py:524 +#, python-brace-format +msgid "Table{table_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:639 +#: structures/engines/mysql/context.py:650 +#: structures/engines/postgresql/context.py:670 +#: structures/engines/sqlite/context.py:548 +#, python-brace-format +msgid "Column{column_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:657 +#: structures/engines/mysql/context.py:668 +#: structures/engines/postgresql/context.py:688 +#: structures/engines/sqlite/context.py:566 +#, python-brace-format +msgid "Index{index_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:697 +#: structures/engines/mysql/context.py:706 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:608 +#, python-brace-format +msgid "ForeignKey{foreign_key_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:730 +#: structures/engines/mysql/context.py:737 +#: structures/engines/postgresql/context.py:758 +#: structures/engines/sqlite/context.py:636 +#, python-brace-format +msgid "View{view_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:781 +#, python-brace-format +msgid "Trigger{trigger_index:03}" +msgstr "" + +#: windows/dialogs/connections/view.py:415 +#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 #: windows/views.py:33 msgid "Connection" msgstr "Connexion" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:956 -#: windows/views.py:1306 windows/views.py:1413 windows/views.py:1796 -#: windows/views.py:2707 windows/views.py:2730 windows/views.py:2731 -#: windows/views.py:2732 windows/views.py:2733 windows/views.py:2734 -#: windows/views.py:2735 windows/views.py:2736 windows/views.py:2737 -#: windows/views.py:2738 windows/views.py:2742 windows/views.py:2936 -#: windows/views.py:3137 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1006 +#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 +#: windows/views.py:2821 msgid "Name" msgstr "Nom" -#: windows/views.py:48 windows/views.py:381 +#: windows/views.py:48 windows/views.py:428 msgid "Last connection" msgstr "Dernière connexion" -#: windows/dialogs/connections/view.py:631 windows/views.py:61 +#: windows/dialogs/connections/view.py:653 windows/views.py:61 msgid "New directory" msgstr "Nouveau répertoire" -#: windows/dialogs/connections/model.py:187 -#: windows/dialogs/connections/view.py:591 windows/views.py:65 +#: windows/dialogs/connections/model.py:206 +#: windows/dialogs/connections/view.py:613 windows/views.py:65 msgid "New connection" msgstr "Nouvelle connexion" @@ -88,14 +129,14 @@ msgstr "Nom" msgid "Clone connection" msgstr "Nouvelle connexion" -#: windows/views.py:81 windows/views.py:556 windows/views.py:1290 -#: windows/views.py:1331 windows/views.py:1706 windows/views.py:1738 -#: windows/views.py:1997 windows/views.py:3273 windows/views.py:3305 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1322 +#: windows/views.py:1365 windows/views.py:1773 windows/views.py:2032 +#: windows/views.py:2726 windows/views.py:2957 windows/views.py:2989 msgid "Delete" msgstr "Supprimer" -#: windows/views.py:111 windows/views.py:1311 windows/views.py:1468 -#: windows/views.py:2747 windows/views.py:3192 +#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 +#: windows/views.py:2876 msgid "Engine" msgstr "Moteur" @@ -107,540 +148,548 @@ msgstr "Hôte + port" msgid "Username" msgstr "Nom d'utilisateur" -#: windows/views.py:161 windows/views.py:1100 +#: windows/views.py:161 windows/views.py:1132 msgid "Password" msgstr "Mot de passe" -#: windows/views.py:177 +#: windows/views.py:174 +#, fuzzy +msgid "Connection timeout" +msgstr "Connexion perdue" + +#: windows/views.py:192 msgid "Use TLS" msgstr "" -#: windows/views.py:180 +#: windows/views.py:203 msgid "Use SSH tunnel" msgstr "Utiliser un tunnel SSH" -#: windows/views.py:202 windows/views.py:2668 +#: windows/views.py:214 +msgid "Compressed client/server protocol" +msgstr "" + +#: windows/views.py:233 msgid "Filename" msgstr "Nom de fichier" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2673 -#: windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 msgid "Select a file" msgstr "Sélectionner un fichier" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 #, fuzzy msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:221 windows/views.py:1313 windows/views.py:1426 -#: windows/views.py:2748 windows/views.py:3034 windows/views.py:3150 +#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 +#: windows/views.py:2834 msgid "Comments" msgstr "Commentaires" -#: windows/main/controller.py:219 windows/views.py:235 windows/views.py:683 -#: windows/views.py:837 +#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 +#: windows/views.py:884 msgid "Settings" msgstr "Paramètres" -#: windows/views.py:244 +#: windows/views.py:278 msgid "SSH executable" msgstr "Exécutable SSH" -#: windows/views.py:249 +#: windows/views.py:283 msgid "ssh" msgstr "ssh" -#: windows/views.py:257 +#: windows/views.py:291 msgid "SSH host + port" msgstr "Hôte SSH + port" -#: windows/views.py:269 +#: windows/views.py:303 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "" -#: windows/views.py:278 +#: windows/views.py:312 msgid "SSH username" msgstr "Nom d'utilisateur SSH" -#: windows/views.py:291 +#: windows/views.py:325 msgid "SSH password" msgstr "Mot de passe SSH" -#: windows/views.py:304 +#: windows/views.py:338 msgid "Local port" msgstr "Port local" -#: windows/views.py:310 +#: windows/views.py:344 msgid "if the value is set to 0, the first available port will be used" msgstr "si la valeur est définie à 0, le premier port disponible sera utilisé" -#: windows/views.py:319 +#: windows/views.py:353 msgid "Identity file" msgstr "" -#: windows/views.py:335 +#: windows/views.py:369 #, fuzzy msgid "Remote host + port" msgstr "Hôte + port" -#: windows/views.py:347 +#: windows/views.py:381 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "" -#: windows/views.py:358 +#: windows/views.py:390 +msgid "SSH extra args" +msgstr "" + +#: windows/views.py:405 msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:364 windows/views.py:1309 windows/views.py:2745 +#: windows/views.py:411 windows/views.py:1341 msgid "Created at" msgstr "Créé le" -#: windows/views.py:398 +#: windows/views.py:445 msgid "Successful connections" msgstr "Connexions réussies" -#: windows/views.py:415 +#: windows/views.py:462 #, fuzzy msgid "Last successful connection" msgstr "Connexions réussies" -#: windows/views.py:432 +#: windows/views.py:479 msgid "Unsuccessful connections" msgstr "Connexions échouées" -#: windows/views.py:449 +#: windows/views.py:496 msgid "Last failure reason" msgstr "" -#: windows/views.py:466 +#: windows/views.py:513 #, fuzzy msgid "Total connection attempts" msgstr "Dernière connexion" -#: windows/views.py:483 +#: windows/views.py:530 #, fuzzy -msgid " Average connection time (ms)" +msgid "Average connection time (ms)" msgstr "Échec de la reconnexion :" -#: windows/views.py:500 +#: windows/views.py:547 #, fuzzy -msgid " Most recent connection duration" +msgid "Most recent connection duration" msgstr "Ouvrir le gestionnaire de connexions" -#: windows/views.py:519 +#: windows/views.py:566 msgid "Statistics" msgstr "Statistiques" -#: windows/views.py:537 windows/views.py:1672 +#: windows/views.py:584 windows/views.py:1730 msgid "Create" msgstr "Créer" -#: windows/views.py:541 +#: windows/views.py:588 #, fuzzy msgid "Create connection" msgstr "Dernière connexion" -#: windows/views.py:544 +#: windows/views.py:591 #, fuzzy msgid "Create directory" msgstr "Nouveau répertoire" -#: windows/views.py:573 windows/views.py:797 windows/views.py:1328 -#: windows/views.py:1741 windows/views.py:2002 windows/views.py:2078 -#: windows/views.py:3081 windows/views.py:3308 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1360 +#: windows/views.py:1776 windows/views.py:2037 windows/views.py:2093 +#: windows/views.py:2702 windows/views.py:2992 windows/views.py:3129 msgid "Cancel" msgstr "Annuler" -#: windows/views.py:578 windows/views.py:2007 windows/views.py:3091 -#: windows/views.py:3313 +#: windows/main/controller.py:283 windows/main/controller.py:300 +#: windows/main/controller.py:301 windows/views.py:625 windows/views.py:2042 +#: windows/views.py:2235 windows/views.py:2997 windows/views.py:3135 msgid "Save" msgstr "Enregistrer" -#: windows/views.py:585 +#: windows/views.py:632 msgid "Test" msgstr "Tester" -#: windows/views.py:592 +#: windows/views.py:639 msgid "Connect" msgstr "Connecter" -#: windows/views.py:695 +#: windows/views.py:742 msgid "Language" msgstr "Langue" -#: windows/views.py:700 +#: windows/views.py:747 msgid "English" msgstr "Anglais" -#: windows/views.py:700 +#: windows/views.py:747 msgid "Italian" msgstr "Italien" -#: windows/views.py:700 +#: windows/views.py:747 msgid "French" msgstr "Français" -#: windows/views.py:712 +#: windows/views.py:759 msgid "Locale" msgstr "Localisation" -#: windows/views.py:733 -msgid "Edit Value" -msgstr "Modifier la valeur" +#: windows/views.py:780 +#, fuzzy +msgid "Column content" +msgstr "Nouvelle connexion" -#: windows/views.py:743 +#: windows/views.py:790 msgid "Syntax" msgstr "Syntaxe" -#: windows/views.py:800 +#: windows/views.py:847 msgid "Ok" msgstr "Ok" -#: windows/views.py:831 +#: windows/views.py:878 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:840 +#: windows/views.py:887 msgid "File" msgstr "Fichier" -#: windows/views.py:843 +#: windows/views.py:890 msgid "About" msgstr "À propos" -#: windows/views.py:846 +#: windows/views.py:893 msgid "Help" msgstr "Aide" -#: windows/views.py:851 +#: windows/views.py:898 msgid "Open connection manager" msgstr "Ouvrir le gestionnaire de connexions" -#: windows/views.py:853 +#: windows/views.py:902 msgid "Disconnect from server" msgstr "Se déconnecter du serveur" -#: windows/views.py:857 +#: windows/views.py:904 msgid "tool" msgstr "outil" -#: windows/views.py:857 +#: windows/views.py:904 msgid "Refresh" msgstr "Actualiser" -#: windows/views.py:861 windows/views.py:863 +#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 +#: windows/views.py:2077 windows/views.py:2221 msgid "Add" msgstr "Ajouter" -#: windows/views.py:897 windows/views.py:901 windows/views.py:2166 -#: windows/views.py:2654 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 msgid "MyMenuItem" msgstr "MonÉlémentMenu" -#: windows/views.py:904 windows/views.py:1769 windows/views.py:3336 +#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 msgid "MyMenu" msgstr "MonMenu" -#: windows/views.py:919 windows/views.py:1350 windows/views.py:1357 -#: windows/views.py:1364 +#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 +#: windows/views.py:1402 msgid "MyLabel" msgstr "MonÉtiquette" -#: windows/views.py:925 +#: windows/views.py:972 msgid "Databases" msgstr "Bases de données" -#: windows/views.py:926 windows/views.py:1308 windows/views.py:2716 -#: windows/views.py:2744 +#: windows/views.py:973 windows/views.py:1340 msgid "Size" msgstr "Taille" -#: windows/views.py:927 +#: windows/views.py:974 msgid "Elements" msgstr "Éléments" -#: windows/views.py:928 +#: windows/views.py:975 msgid "Modified at" msgstr "Modifié le" -#: windows/views.py:929 +#: windows/views.py:976 msgid "Tables" msgstr "Tables" -#: windows/views.py:936 +#: windows/views.py:983 msgid "System" msgstr "Système" -#: windows/views.py:979 -#, fuzzy -msgid "Character set" -msgstr "Créé le" - #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1000 -#: windows/views.py:1312 windows/views.py:2980 +#: windows/components/dataview.py:89 windows/views.py:1029 +#: windows/views.py:1344 msgid "Collation" msgstr "Classement" -#: windows/views.py:1026 windows/views.py:2862 +#: windows/views.py:1058 msgid "Encryption" msgstr "" -#: windows/views.py:1038 +#: windows/views.py:1070 msgid "Read Only" msgstr "" -#: windows/views.py:1055 +#: windows/views.py:1087 #, fuzzy msgid "Tablespace" msgstr "Tables" -#: windows/views.py:1076 +#: windows/views.py:1108 #, fuzzy msgid "Connection limit" msgstr "Connexion perdue" -#: windows/views.py:1119 +#: windows/views.py:1151 #, fuzzy msgid "Profile" msgstr "Fichier" -#: windows/views.py:1145 +#: windows/views.py:1177 #, fuzzy msgid "Default tablespace" msgstr "Supprimer la table" -#: windows/views.py:1166 +#: windows/views.py:1198 #, fuzzy msgid "Temporary tablespace" msgstr "Temporaire" -#: windows/views.py:1192 +#: windows/views.py:1224 msgid "Quota" msgstr "" -#: windows/views.py:1211 +#: windows/views.py:1243 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1228 +#: windows/views.py:1260 msgid "Account status" msgstr "" -#: windows/views.py:1249 +#: windows/views.py:1281 #, fuzzy msgid "Password expire" msgstr "Mot de passe" -#: windows/views.py:1270 +#: windows/views.py:1302 msgid "Table:" msgstr "Table :" -#: windows/views.py:1278 windows/views.py:1551 windows/views.py:1595 -#: windows/views.py:1701 windows/views.py:3268 +#: windows/views.py:1310 windows/views.py:1609 windows/views.py:1653 +#: windows/views.py:2721 windows/views.py:2952 msgid "Insert" msgstr "Insérer" -#: windows/views.py:1283 +#: windows/views.py:1315 msgid "Clone" msgstr "Cloner" -#: windows/views.py:1307 +#: windows/views.py:1339 msgid "Rows" msgstr "Lignes" -#: windows/views.py:1310 windows/views.py:2746 +#: windows/views.py:1342 msgid "Updated at" msgstr "Mis à jour le" -#: windows/views.py:1334 windows/views.py:1746 windows/views.py:2085 -#: windows/views.py:2140 +#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 +#: windows/views.py:2173 windows/views.py:2709 msgid "Apply" msgstr "Appliquer" -#: windows/views.py:1344 windows/views.py:1503 windows/views.py:1956 -#: windows/views.py:3225 +#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 +#: windows/views.py:2909 msgid "Options" msgstr "Options" -#: windows/views.py:1375 +#: windows/views.py:1413 msgid "Diagram" msgstr "Diagramme" -#: windows/views.py:1386 windows/views.py:2715 +#: windows/views.py:1424 msgid "Database" msgstr "Base de données" -#: windows/views.py:1441 windows/views.py:3165 +#: windows/views.py:1479 windows/views.py:2849 msgid "Base" msgstr "Base" -#: windows/views.py:1455 windows/views.py:3179 +#: windows/views.py:1493 windows/views.py:2863 msgid "Auto Increment" msgstr "Auto incrément" -#: windows/views.py:1483 windows/views.py:3207 +#: windows/views.py:1521 windows/views.py:2891 msgid "Default Collation" msgstr "Classement par défaut" -#: windows/views.py:1515 windows/views.py:1556 windows/views.py:1600 +#: windows/views.py:1531 +msgid "Convert data" +msgstr "" + +#: windows/views.py:1539 +msgid "Row format" +msgstr "" + +#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 +#: windows/views.py:1756 windows/views.py:2081 msgid "Remove" msgstr "Supprimer" -#: windows/views.py:1522 windows/views.py:1563 windows/views.py:1607 +#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 msgid "Clear" msgstr "Effacer" -#: windows/views.py:1537 windows/views.py:3239 +#: windows/views.py:1595 windows/views.py:2923 msgid "Indexes" msgstr "Index" -#: windows/views.py:1581 +#: windows/views.py:1639 msgid "Foreign Keys" msgstr "Clés étrangères" -#: windows/views.py:1625 +#: windows/views.py:1683 msgid "Checks" msgstr "Contrôles" -#: windows/views.py:1693 windows/views.py:3260 +#: windows/views.py:1750 windows/views.py:2944 msgid "Columns:" msgstr "Colonnes :" -#: windows/views.py:1713 windows/views.py:3280 -msgid "Up" -msgstr "Haut" +#: windows/views.py:1760 +#, fuzzy +msgid "Move Up" +msgstr "Déplacer vers le haut\tCTRL+UP" -#: windows/views.py:1720 windows/views.py:3287 -msgid "Down" -msgstr "Bas" +#: windows/views.py:1762 +#, fuzzy +msgid "Move Down" +msgstr "Déplacer vers le bas\tCTRL+D" -#: windows/views.py:1759 windows/views.py:1766 windows/views.py:3326 -#: windows/views.py:3333 +#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 +#: windows/views.py:3017 msgid "Add Index" msgstr "Ajouter un index" -#: windows/views.py:1763 windows/views.py:3330 +#: windows/views.py:1798 windows/views.py:3014 msgid "Add PrimaryKey" msgstr "Ajouter une clé primaire" -#: windows/views.py:1780 +#: windows/views.py:1815 msgid "Table" msgstr "Table" -#: windows/views.py:1816 +#: windows/views.py:1851 #, fuzzy msgid "Definer" msgstr "Insérer" -#: windows/views.py:1836 +#: windows/views.py:1871 msgid "Schema" msgstr "" -#: windows/views.py:1862 +#: windows/views.py:1897 msgid "SQL security" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1904 #, fuzzy msgid "DEFINER" msgstr "Insérer" -#: windows/views.py:1869 +#: windows/views.py:1904 #, fuzzy msgid "INVOKER" msgstr "Insérer" -#: windows/views.py:1881 windows/views.py:2558 windows/views.py:2577 -#: windows/views.py:2820 +#: windows/views.py:1916 msgid "Algorithm" msgstr "" -#: windows/views.py:1883 windows/views.py:2543 windows/views.py:2576 -#: windows/views.py:2825 +#: windows/views.py:1918 #, fuzzy msgid "UNDEFINED" msgstr "Non signé" -#: windows/views.py:1886 windows/views.py:2546 windows/views.py:2576 -#: windows/views.py:2828 +#: windows/views.py:1921 msgid "MERGE" msgstr "" -#: windows/views.py:1889 windows/views.py:2555 windows/views.py:2576 -#: windows/views.py:2831 +#: windows/views.py:1924 #, fuzzy msgid "TEMPTABLE" msgstr "Table" -#: windows/views.py:1899 windows/views.py:2582 +#: windows/views.py:1934 msgid "View constraint" msgstr "" -#: windows/views.py:1901 windows/views.py:2581 +#: windows/views.py:1936 #, fuzzy msgid "None" msgstr "Cloner" -#: windows/views.py:1904 windows/views.py:2581 +#: windows/views.py:1939 #, fuzzy msgid "LOCAL" msgstr "Localisation" -#: windows/views.py:1907 +#: windows/views.py:1942 #, fuzzy msgid "CASCADE" msgstr "Annuler" -#: windows/views.py:1910 +#: windows/views.py:1945 #, fuzzy msgid "CHECK ONLY" msgstr "Vérifier" -#: windows/views.py:1913 windows/views.py:2581 +#: windows/views.py:1948 msgid "READ ONLY" msgstr "" -#: windows/views.py:1925 +#: windows/views.py:1960 msgid "Force" msgstr "" -#: windows/views.py:1937 +#: windows/views.py:1972 msgid "Security barrier" msgstr "" -#: windows/views.py:2019 +#: windows/views.py:2054 msgid "Views" msgstr "Vues" -#: windows/views.py:2027 +#: windows/views.py:2062 msgid "Triggers" msgstr "Déclencheurs" -#: windows/views.py:2039 -#, python-format -msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" -msgstr "Table `%(database_name)s`.`%(table_name)s` : %(total_rows) lignes au total" - -#: windows/views.py:2049 -msgid "Insert record" -msgstr "Insérer un enregistrement" +#: windows/views.py:2073 +#, fuzzy +msgid "Refrsh" +msgstr "Actualiser" -#: windows/views.py:2054 -msgid "Duplicate record" +#: windows/views.py:2079 +#, fuzzy +msgid "Duplicate" msgstr "Dupliquer un enregistrement" -#: windows/views.py:2061 -msgid "Delete record" -msgstr "Supprimer un enregistrement" - -#: windows/views.py:2071 +#: windows/views.py:2085 msgid "Apply changes automatically" msgstr "Appliquer les modifications automatiquement" -#: windows/views.py:2073 windows/views.py:2074 +#: windows/views.py:2087 windows/views.py:2088 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -648,161 +697,152 @@ msgstr "" "Si activé, les modifications de la table sont appliquées immédiatement " "sans appuyer sur Appliquer ou Annuler" -#: windows/views.py:2095 -msgid "Next" -msgstr "Suivant" +#: windows/views.py:2101 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" -#: windows/views.py:2103 +#: windows/views.py:2109 +#, fuzzy +msgid "First" +msgstr "Filtres" + +#: windows/views.py:2127 +msgid "Last" +msgstr "" + +#: windows/views.py:2136 msgid "Filters" msgstr "Filtres" -#: windows/views.py:2143 +#: windows/views.py:2176 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2163 +#: windows/views.py:2196 msgid "Insert row" msgstr "Insérer une ligne" -#: windows/views.py:2171 +#: windows/views.py:2204 msgid "Data" msgstr "Données" -#: windows/views.py:2225 windows/views.py:2275 -msgid "New" -msgstr "Nouveau" - -#: windows/views.py:2252 -msgid "Query" +#: windows/main/controller.py:278 windows/main/controller.py:287 +#: windows/main/controller.py:288 windows/views.py:2221 +#, fuzzy +msgid "New query" msgstr "Requête" -#: windows/views.py:2272 +#: windows/views.py:2223 windows/views.py:2660 msgid "Close" msgstr "Fermer" -#: windows/views.py:2285 -msgid "Query #2" -msgstr "Requête #2" +#: windows/main/controller.py:279 windows/main/controller.py:289 +#: windows/main/controller.py:290 windows/views.py:2223 +#, fuzzy +msgid "Close query" +msgstr "Requête" -#: windows/views.py:2537 -msgid "Column5" -msgstr "Colonne5" +#: windows/views.py:2227 +msgid "Run" +msgstr "" -#: windows/views.py:2548 -msgid "Import" -msgstr "Importer" +#: windows/main/controller.py:280 windows/main/controller.py:292 +#: windows/main/controller.py:293 windows/views.py:2227 +#, fuzzy +msgid "Execute" +msgstr "Exécutable SSH" -#: windows/views.py:2573 -msgid "Read only" +#: windows/views.py:2229 +msgid "Run all" msgstr "" -#: windows/views.py:2581 -msgid "CASCADED" +#: windows/main/controller.py:295 windows/views.py:2229 +msgid "Execute all statements" msgstr "" -#: windows/views.py:2581 -#, fuzzy -msgid "CHECK OPTION" -msgstr "connexion" +#: windows/main/controller.py:282 windows/main/controller.py:297 +#: windows/main/controller.py:298 windows/views.py:2231 +msgid "Stop" +msgstr "" -#: windows/views.py:2613 -msgid "collapsible" -msgstr "rétractable" +#: windows/views.py:2287 +msgid "a page" +msgstr "" -#: windows/views.py:2629 -msgid "Column3" -msgstr "Colonne3" +#: windows/views.py:2315 +msgid "Query" +msgstr "Requête" -#: windows/views.py:2630 -msgid "Column4" -msgstr "Colonne4" +#: windows/views.py:2626 +#, fuzzy +msgid "Character set" +msgstr "Créé le" -#: windows/views.py:2673 -msgid "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" -msgstr "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#: windows/views.py:2644 windows/views.py:2663 +msgid "New" +msgstr "Nouveau" -#: windows/views.py:2685 -msgid "Port" -msgstr "Port" +#: windows/views.py:2683 +msgid "Insert record" +msgstr "Insérer un enregistrement" -#: windows/views.py:2708 -msgid "Usage" -msgstr "Utilisation" +#: windows/views.py:2688 +msgid "Duplicate record" +msgstr "Dupliquer un enregistrement" -#: windows/views.py:2719 -#, python-format -msgid "%(total_rows)s" -msgstr "%(total_rows)s" +#: windows/views.py:2695 +msgid "Delete record" +msgstr "Supprimer un enregistrement" -#: windows/views.py:2724 -msgid "rows total" -msgstr "lignes au total" +#: windows/views.py:2733 windows/views.py:2964 +msgid "Up" +msgstr "Haut" -#: windows/views.py:2743 -msgid "Lines" -msgstr "Lignes" +#: windows/views.py:2740 windows/views.py:2971 +msgid "Down" +msgstr "Bas" -#: windows/views.py:2775 -msgid "Temporary" -msgstr "Temporaire" +#: windows/views.py:3100 +msgid "Save Starments" +msgstr "" -#: windows/views.py:2786 +#: windows/views.py:3108 #, fuzzy -msgid "Engine options" -msgstr "Options" +msgid "Location" +msgstr "Classement" -#: windows/views.py:2845 -msgid "RadioBtn" +#: windows/views.py:3115 +msgid "*.sql" msgstr "" -#: windows/views.py:2928 -msgid "Edit Column" -msgstr "Modifier la colonne" - -#: windows/views.py:2944 -msgid "Datatype" -msgstr "Type de données" - -#: windows/components/dataview.py:121 windows/views.py:2959 -msgid "Length/Set" -msgstr "Longueur/Ensemble" - -#: windows/components/dataview.py:51 windows/views.py:2998 -msgid "Unsigned" -msgstr "Non signé" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3004 +#: windows/components/dataview.py:75 msgid "Allow NULL" msgstr "Autoriser NULL" -#: windows/views.py:3010 -msgid "Zero Fill" -msgstr "Remplissage zéro" +#: windows/components/dataview.py:28 +msgid "Check" +msgstr "Vérifier" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3021 +#: windows/components/dataview.py:78 msgid "Default" msgstr "Par défaut" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3047 +#: windows/components/dataview.py:82 msgid "Virtuality" msgstr "Virtualité" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3062 msgid "Expression" msgstr "Expression" -#: windows/components/dataview.py:28 -msgid "Check" -msgstr "Vérifier" +#: windows/components/dataview.py:51 +msgid "Unsigned" +msgstr "Non signé" #: windows/components/dataview.py:53 msgid "Zerofill" @@ -816,6 +856,10 @@ msgstr "#" msgid "Data type" msgstr "Type de données" +#: windows/components/dataview.py:121 +msgid "Length/Set" +msgstr "Longueur/Ensemble" + #: windows/components/dataview.py:155 msgid "Add column\tCTRL+INS" msgstr "Ajouter une colonne\tCTRL+INS" @@ -868,11 +912,11 @@ msgstr "Sur UPDATE" msgid "On DELETE" msgstr "Sur DELETE" -#: windows/components/dataview.py:299 +#: windows/components/dataview.py:298 msgid "Add foreign key" msgstr "Ajouter une clé étrangère" -#: windows/components/dataview.py:305 +#: windows/components/dataview.py:304 msgid "Remove foreign key" msgstr "Supprimer une clé étrangère" @@ -892,75 +936,164 @@ msgstr "AUTO INCREMENT" msgid "Text/Expression" msgstr "Texte/Expression" -#: windows/dialogs/connections/view.py:119 windows/main/tabs/query.py:376 +#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:394 +#: windows/dialogs/connections/view.py:414 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:407 +#: windows/dialogs/connections/view.py:426 +#, python-brace-format +msgid "Do you want save the connection {connection_name}?" +msgstr "" + +#: windows/dialogs/connections/view.py:429 msgid "Confirm save" msgstr "Confirmer la sauvegarde" -#: windows/dialogs/connections/view.py:459 +#: windows/dialogs/connections/view.py:481 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:461 +#: windows/dialogs/connections/view.py:483 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:736 +#: windows/dialogs/connections/view.py:750 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:775 +#, fuzzy, python-brace-format +msgid "" +"Connection error:\n" +"{error}" +msgstr "Erreur de connexion" + +#: windows/dialogs/connections/view.py:776 msgid "Connection error" msgstr "Erreur de connexion" -#: windows/dialogs/connections/view.py:788 -#: windows/dialogs/connections/view.py:803 +#: windows/dialogs/connections/view.py:802 +#, fuzzy, python-brace-format +msgid "Do you want to delete the connection '{connection_name}'?" +msgstr "Voulez-vous supprimer les enregistrements ?" + +#: windows/dialogs/connections/view.py:805 +#: windows/dialogs/connections/view.py:822 msgid "Confirm delete" msgstr "Confirmer la suppression" -#: windows/main/controller.py:172 +#: windows/dialogs/connections/view.py:819 +#, fuzzy, python-brace-format +msgid "Do you want to delete the directory '{directory_name}'?" +msgstr "Voulez-vous supprimer les enregistrements ?" + +#: windows/main/controller.py:275 +#, python-brace-format +msgid "{text} ({shortcut})" +msgstr "" + +#: windows/main/controller.py:281 windows/main/controller.py:294 +#, fuzzy +msgid "Execute all" +msgstr "Exécutable SSH" + +#: windows/main/controller.py:471 windows/main/controller.py:479 +#, fuzzy +msgid "Query (1)" +msgstr "Requête" + +#: windows/main/controller.py:497 +#, python-brace-format +msgid "Query ({query_number})" +msgstr "" + +#: windows/main/controller.py:530 +msgid "You have unsaved changes. Save before closing?" +msgstr "" + +#: windows/main/controller.py:531 +msgid "Unsaved query" +msgstr "" + +#: windows/main/controller.py:576 +#, fuzzy +msgid "Save query" +msgstr "Requête" + +#: windows/main/controller.py:579 +msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" +msgstr "" + +#: windows/main/controller.py:616 windows/main/controller.py:642 +#: windows/main/database/list.py:84 windows/main/database/view.py:256 +#: windows/main/database/view.py:282 windows/main/query/controller.py:177 +msgid "Error" +msgstr "Erreur" + +#: windows/main/controller.py:622 +#, python-brace-format +msgid "-- Saved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:647 +#, python-brace-format +msgid "-- Autosaved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:704 msgid "days" msgstr "jours" -#: windows/main/controller.py:173 +#: windows/main/controller.py:705 msgid "hours" msgstr "heures" -#: windows/main/controller.py:174 +#: windows/main/controller.py:706 msgid "minutes" msgstr "minutes" -#: windows/main/controller.py:175 +#: windows/main/controller.py:707 msgid "seconds" msgstr "secondes" -#: windows/main/controller.py:183 +#: windows/main/controller.py:715 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Mémoire utilisée : {used} ({percentage:.2%})" -#: windows/main/controller.py:219 +#: windows/main/controller.py:751 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:298 +#: windows/main/controller.py:952 +#, python-brace-format +msgid "~{estimated} (Loading...)" +msgstr "" + +#: windows/main/controller.py:954 +msgid "~ (Loading...)" +msgstr "" + +#: windows/main/controller.py:1119 msgid "Version" msgstr "Version" -#: windows/main/controller.py:300 +#: windows/main/controller.py:1121 msgid "Uptime" msgstr "Temps de fonctionnement" -#: windows/main/controller.py:399 +#: windows/main/controller.py:1199 +#, python-brace-format +msgid "Do you want discard the change to {database_name}?" +msgstr "" + +#: windows/main/controller.py:1232 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -970,138 +1103,177 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:404 windows/main/controller.py:425 +#: windows/main/controller.py:1237 windows/main/controller.py:1258 #, fuzzy msgid "Delete database" msgstr "Supprimer la table" -#: windows/main/controller.py:410 +#: windows/main/controller.py:1243 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:411 +#: windows/main/controller.py:1244 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:424 +#: windows/main/controller.py:1257 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:439 +#: windows/main/controller.py:1272 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:440 windows/main/tabs/view.py:253 -#: windows/main/tabs/view.py:279 +#: windows/main/controller.py:1273 windows/main/database/view.py:253 +#: windows/main/database/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:582 +#: windows/main/controller.py:1392 +#, python-brace-format +msgid "Do you want discard the change to {table_name}?" +msgstr "" + +#: windows/main/controller.py:1418 +#, fuzzy, python-brace-format +msgid "Do you want delete the table {table_name}?" +msgstr "Voulez-vous supprimer les enregistrements ?" + +#: windows/main/controller.py:1421 msgid "Delete table" msgstr "Supprimer la table" -#: windows/main/controller.py:699 +#: windows/main/controller.py:1440 +#, python-brace-format +msgid "{table_name} (COPY)" +msgstr "" + +#: windows/main/controller.py:1563 msgid "Do you want delete the records?" msgstr "Voulez-vous supprimer les enregistrements ?" -#: windows/main/tabs/database.py:71 +#: windows/main/database/list.py:69 msgid "The connection to the database was lost." msgstr "La connexion à la base de données a été perdue." -#: windows/main/tabs/database.py:73 +#: windows/main/database/list.py:71 msgid "Do you want to reconnect?" msgstr "Voulez-vous vous reconnecter ?" -#: windows/main/tabs/database.py:75 +#: windows/main/database/list.py:73 msgid "Connection lost" msgstr "Connexion perdue" -#: windows/main/tabs/database.py:85 +#: windows/main/database/list.py:83 msgid "Reconnection failed:" msgstr "Échec de la reconnexion :" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:450 -#: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 -msgid "Error" -msgstr "Erreur" - -#: windows/main/tabs/query.py:305 -#, python-brace-format -msgid "{} rows affected" +#: windows/main/database/view.py:252 +msgid "View created successfully" msgstr "" -#: windows/main/tabs/query.py:309 windows/main/tabs/query.py:331 -#, fuzzy, python-brace-format -msgid "Query {}" -msgstr "Requête" +#: windows/main/database/view.py:252 +msgid "View updated successfully" +msgstr "" -#: windows/main/tabs/query.py:314 +#: windows/main/database/view.py:256 #, python-brace-format -msgid "Query {} (Error)" +msgid "Error saving view: {}" msgstr "" -#: windows/main/tabs/query.py:326 +#: windows/main/database/view.py:269 #, python-brace-format -msgid "Query {} ({} rows × {} cols)" +msgid "Are you sure you want to delete view '{}'?" msgstr "" -#: windows/main/tabs/query.py:353 -#, fuzzy, python-brace-format -msgid "{} rows" -msgstr "Lignes" +#: windows/main/database/view.py:270 +#, fuzzy +msgid "Confirm Delete" +msgstr "Confirmer la suppression" + +#: windows/main/database/view.py:279 +msgid "View deleted successfully" +msgstr "" + +#: windows/main/database/view.py:282 +#, python-brace-format +msgid "Error deleting view: {}" +msgstr "" -#: windows/main/tabs/query.py:355 +#: windows/main/query/controller.py:110 #, python-brace-format -msgid "{:.1f} ms" +msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/tabs/query.py:358 +#: windows/main/query/controller.py:112 #, python-brace-format -msgid "{} warnings" +msgid "{elapsed_s:.2f} s" msgstr "" -#: windows/main/tabs/query.py:370 +#: windows/main/query/controller.py:115 #, fuzzy -msgid "Error:" -msgstr "Erreur" +msgid "none" +msgstr "Cloner" + +#: windows/main/query/controller.py:121 +#, python-brace-format +msgid "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." +msgstr "" + +#: windows/main/query/controller.py:134 +msgid "Query execution cancelled" +msgstr "" -#: windows/main/tabs/query.py:449 +#: windows/main/query/controller.py:176 #, fuzzy msgid "No active database connection" msgstr "Nouvelle connexion" -#: windows/main/tabs/view.py:252 -msgid "View created successfully" +#: windows/main/query/renderer.py:53 +#, python-brace-format +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View updated successfully" +#: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 +#, python-brace-format +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/view.py:256 +#: windows/main/query/renderer.py:65 #, python-brace-format -msgid "Error saving view: {}" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/view.py:269 +#: windows/main/query/renderer.py:79 #, python-brace-format -msgid "Are you sure you want to delete view '{}'?" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/view.py:270 -#, fuzzy -msgid "Confirm Delete" -msgstr "Confirmer la suppression" +#: windows/main/query/renderer.py:165 +#, python-brace-format +msgid "{rows_count} rows" +msgstr "" -#: windows/main/tabs/view.py:279 -msgid "View deleted successfully" +#: windows/main/query/renderer.py:167 +#, python-brace-format +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/view.py:282 +#: windows/main/query/renderer.py:171 #, python-brace-format -msgid "Error deleting view: {}" +msgid "{warnings_count} warnings" msgstr "" +#: windows/main/query/renderer.py:186 +#, fuzzy +msgid "Error:" +msgstr "Erreur" + #~ msgid "Created at:" #~ msgstr "" @@ -1138,3 +1310,102 @@ msgstr "" #~ msgid "directory" #~ msgstr "répertoire" +#~ msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#~ msgstr "" +#~ "Table `%(database_name)s`.`%(table_name)s` : " +#~ "%(total_rows) lignes au total" + +#~ msgid "Next" +#~ msgstr "Suivant" + +#~ msgid "{} rows affected" +#~ msgstr "" + +#~ msgid "Query {}" +#~ msgstr "Requête" + +#~ msgid "Query {} (Error)" +#~ msgstr "" + +#~ msgid "Query {} ({} rows × {} cols)" +#~ msgstr "" + +#~ msgid "{} rows" +#~ msgstr "Lignes" + +#~ msgid "{:.1f} ms" +#~ msgstr "" + +#~ msgid "{} warnings" +#~ msgstr "" + +#~ msgid "Edit Value" +#~ msgstr "Modifier la valeur" + +#~ msgid "Query #2" +#~ msgstr "Requête #2" + +#~ msgid "Column5" +#~ msgstr "Colonne5" + +#~ msgid "Import" +#~ msgstr "Importer" + +#~ msgid "Read only" +#~ msgstr "" + +#~ msgid "CASCADED" +#~ msgstr "" + +#~ msgid "CHECK OPTION" +#~ msgstr "connexion" + +#~ msgid "collapsible" +#~ msgstr "rétractable" + +#~ msgid "Column3" +#~ msgstr "Colonne3" + +#~ msgid "Column4" +#~ msgstr "Colonne4" + +#~ msgid "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#~ msgstr "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" + +#~ msgid "Port" +#~ msgstr "Port" + +#~ msgid "Usage" +#~ msgstr "Utilisation" + +#~ msgid "%(total_rows)s" +#~ msgstr "%(total_rows)s" + +#~ msgid "rows total" +#~ msgstr "lignes au total" + +#~ msgid "Lines" +#~ msgstr "Lignes" + +#~ msgid "Temporary" +#~ msgstr "Temporaire" + +#~ msgid "Engine options" +#~ msgstr "Options" + +#~ msgid "RadioBtn" +#~ msgstr "" + +#~ msgid "Edit Column" +#~ msgstr "Modifier la colonne" + +#~ msgid "Datatype" +#~ msgstr "Type de données" + +#~ msgid "Zero Fill" +#~ msgstr "Remplissage zéro" + diff --git a/locale/it_IT/LC_MESSAGES/petersql.mo b/locale/it_IT/LC_MESSAGES/petersql.mo index 2efb70e..bbf4054 100644 Binary files a/locale/it_IT/LC_MESSAGES/petersql.mo and b/locale/it_IT/LC_MESSAGES/petersql.mo differ diff --git a/locale/it_IT/LC_MESSAGES/petersql.po b/locale/it_IT/LC_MESSAGES/petersql.po index 94bfe10..9855aa5 100644 --- a/locale/it_IT/LC_MESSAGES/petersql.po +++ b/locale/it_IT/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-10 18:23+0100\n" +"POT-Creation-Date: 2026-03-23 10:07+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: it_IT\n" @@ -43,59 +43,98 @@ msgctxt "unit" msgid "TB" msgstr "TB" -#: structures/ssh_tunnel.py:166 +#: structures/ssh_tunnel.py:177 msgid "OpenSSH client not found." msgstr "Client OpenSSH non trovato." -#: windows/dialogs/connections/view.py:395 -#: windows/dialogs/connections/view.py:738 windows/main/controller.py:296 +#: structures/engines/mariadb/context.py:611 +#: structures/engines/mysql/context.py:622 +#: structures/engines/postgresql/context.py:645 +#: structures/engines/sqlite/context.py:524 +#, python-brace-format +msgid "Table{table_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:639 +#: structures/engines/mysql/context.py:650 +#: structures/engines/postgresql/context.py:670 +#: structures/engines/sqlite/context.py:548 +#, python-brace-format +msgid "Column{column_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:657 +#: structures/engines/mysql/context.py:668 +#: structures/engines/postgresql/context.py:688 +#: structures/engines/sqlite/context.py:566 +#, python-brace-format +msgid "Index{index_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:697 +#: structures/engines/mysql/context.py:706 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:608 +#, python-brace-format +msgid "ForeignKey{foreign_key_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:730 +#: structures/engines/mysql/context.py:737 +#: structures/engines/postgresql/context.py:758 +#: structures/engines/sqlite/context.py:636 +#, python-brace-format +msgid "View{view_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:781 +#, python-brace-format +msgid "Trigger{trigger_index:03}" +msgstr "" + +#: windows/dialogs/connections/view.py:415 +#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 #: windows/views.py:33 msgid "Connection" msgstr "Connessione" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:956 -#: windows/views.py:1306 windows/views.py:1413 windows/views.py:1796 -#: windows/views.py:2707 windows/views.py:2730 windows/views.py:2731 -#: windows/views.py:2732 windows/views.py:2733 windows/views.py:2734 -#: windows/views.py:2735 windows/views.py:2736 windows/views.py:2737 -#: windows/views.py:2738 windows/views.py:2742 windows/views.py:2936 -#: windows/views.py:3137 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1006 +#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 +#: windows/views.py:2821 msgid "Name" msgstr "Nome" -#: windows/views.py:48 windows/views.py:381 +#: windows/views.py:48 windows/views.py:428 msgid "Last connection" msgstr "Ultima connessione" -#: windows/dialogs/connections/view.py:631 windows/views.py:61 +#: windows/dialogs/connections/view.py:653 windows/views.py:61 msgid "New directory" msgstr "Nuova directory" -#: windows/dialogs/connections/model.py:187 -#: windows/dialogs/connections/view.py:591 windows/views.py:65 +#: windows/dialogs/connections/model.py:206 +#: windows/dialogs/connections/view.py:613 windows/views.py:65 msgid "New connection" msgstr "Nuova connessione" #: windows/views.py:71 -#, fuzzy msgid "Rename" -msgstr "Nome" +msgstr "Rinomina" #: windows/views.py:76 -#, fuzzy msgid "Clone connection" -msgstr "Nuova connessione" +msgstr "Chiudi connessione" -#: windows/views.py:81 windows/views.py:556 windows/views.py:1290 -#: windows/views.py:1331 windows/views.py:1706 windows/views.py:1738 -#: windows/views.py:1997 windows/views.py:3273 windows/views.py:3305 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1322 +#: windows/views.py:1365 windows/views.py:1773 windows/views.py:2032 +#: windows/views.py:2726 windows/views.py:2957 windows/views.py:2989 msgid "Delete" msgstr "Elimina" -#: windows/views.py:111 windows/views.py:1311 windows/views.py:1468 -#: windows/views.py:2747 windows/views.py:3192 +#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 +#: windows/views.py:2876 msgid "Engine" msgstr "Motore" @@ -107,539 +146,539 @@ msgstr "Host + porta" msgid "Username" msgstr "Nome utente" -#: windows/views.py:161 windows/views.py:1100 +#: windows/views.py:161 windows/views.py:1132 msgid "Password" msgstr "Password" -#: windows/views.py:177 +#: windows/views.py:174 +msgid "Connection timeout" +msgstr "Timeout connessione" + +#: windows/views.py:192 msgid "Use TLS" msgstr "" -#: windows/views.py:180 +#: windows/views.py:203 msgid "Use SSH tunnel" msgstr "Usa tunnel SSH" -#: windows/views.py:202 windows/views.py:2668 +#: windows/views.py:214 +msgid "Compressed client/server protocol" +msgstr "" + +#: windows/views.py:233 msgid "Filename" msgstr "Nome file" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2673 -#: windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 msgid "Select a file" msgstr "Seleziona un file" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 msgid "*.*" msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:221 windows/views.py:1313 windows/views.py:1426 -#: windows/views.py:2748 windows/views.py:3034 windows/views.py:3150 +#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 +#: windows/views.py:2834 msgid "Comments" msgstr "Commenti" -#: windows/main/controller.py:219 windows/views.py:235 windows/views.py:683 -#: windows/views.py:837 +#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 +#: windows/views.py:884 msgid "Settings" msgstr "Impostazioni" -#: windows/views.py:244 +#: windows/views.py:278 msgid "SSH executable" msgstr "Eseguibile SSH" -#: windows/views.py:249 +#: windows/views.py:283 msgid "ssh" msgstr "ssh" -#: windows/views.py:257 +#: windows/views.py:291 msgid "SSH host + port" msgstr "Host SSH + porta" -#: windows/views.py:269 +#: windows/views.py:303 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "" -#: windows/views.py:278 +#: windows/views.py:312 msgid "SSH username" msgstr "Nome utente SSH" -#: windows/views.py:291 +#: windows/views.py:325 msgid "SSH password" msgstr "Password SSH" -#: windows/views.py:304 +#: windows/views.py:338 msgid "Local port" msgstr "Porta locale" -#: windows/views.py:310 +#: windows/views.py:344 msgid "if the value is set to 0, the first available port will be used" msgstr "se il valore è impostato a 0, verrà utilizzata la prima porta disponibile" -#: windows/views.py:319 +#: windows/views.py:353 msgid "Identity file" msgstr "" -#: windows/views.py:335 -#, fuzzy +#: windows/views.py:369 msgid "Remote host + port" -msgstr "Host + porta" +msgstr "Remoto host + porta" -#: windows/views.py:347 +#: windows/views.py:381 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "" -#: windows/views.py:358 +#: windows/views.py:390 +msgid "SSH extra args" +msgstr "" + +#: windows/views.py:405 msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:364 windows/views.py:1309 windows/views.py:2745 +#: windows/views.py:411 windows/views.py:1341 msgid "Created at" msgstr "Creato il" -#: windows/views.py:398 +#: windows/views.py:445 msgid "Successful connections" msgstr "Connessioni riuscite" -#: windows/views.py:415 -#, fuzzy +#: windows/views.py:462 msgid "Last successful connection" -msgstr "Connessioni riuscite" +msgstr "Ultima connessione riuscita" -#: windows/views.py:432 +#: windows/views.py:479 msgid "Unsuccessful connections" msgstr "Connessioni non riuscite" -#: windows/views.py:449 +#: windows/views.py:496 msgid "Last failure reason" msgstr "" -#: windows/views.py:466 -#, fuzzy +#: windows/views.py:513 msgid "Total connection attempts" -msgstr "Ultima connessione" +msgstr "Totale connessioni" -#: windows/views.py:483 -#, fuzzy -msgid " Average connection time (ms)" -msgstr "Riconnessione fallita:" +#: windows/views.py:530 +msgid "Average connection time (ms)" +msgstr "Media in ms del tempo di connessione" -#: windows/views.py:500 -#, fuzzy -msgid " Most recent connection duration" -msgstr "Apri gestore connessioni" +#: windows/views.py:547 +msgid "Most recent connection duration" +msgstr "" -#: windows/views.py:519 +#: windows/views.py:566 msgid "Statistics" msgstr "Statistiche" -#: windows/views.py:537 windows/views.py:1672 +#: windows/views.py:584 windows/views.py:1730 msgid "Create" msgstr "Crea" -#: windows/views.py:541 -#, fuzzy +#: windows/views.py:588 msgid "Create connection" -msgstr "Ultima connessione" +msgstr "Nuova connessione" -#: windows/views.py:544 -#, fuzzy +#: windows/views.py:591 msgid "Create directory" msgstr "Nuova directory" -#: windows/views.py:573 windows/views.py:797 windows/views.py:1328 -#: windows/views.py:1741 windows/views.py:2002 windows/views.py:2078 -#: windows/views.py:3081 windows/views.py:3308 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1360 +#: windows/views.py:1776 windows/views.py:2037 windows/views.py:2093 +#: windows/views.py:2702 windows/views.py:2992 windows/views.py:3129 msgid "Cancel" msgstr "Annulla" -#: windows/views.py:578 windows/views.py:2007 windows/views.py:3091 -#: windows/views.py:3313 +#: windows/main/controller.py:283 windows/main/controller.py:300 +#: windows/main/controller.py:301 windows/views.py:625 windows/views.py:2042 +#: windows/views.py:2235 windows/views.py:2997 windows/views.py:3135 msgid "Save" msgstr "Salva" -#: windows/views.py:585 +#: windows/views.py:632 msgid "Test" msgstr "Testa" -#: windows/views.py:592 +#: windows/views.py:639 msgid "Connect" msgstr "Connetti" -#: windows/views.py:695 +#: windows/views.py:742 msgid "Language" msgstr "Lingua" -#: windows/views.py:700 +#: windows/views.py:747 msgid "English" msgstr "Inglese" -#: windows/views.py:700 +#: windows/views.py:747 msgid "Italian" msgstr "Italiano" -#: windows/views.py:700 +#: windows/views.py:747 msgid "French" msgstr "Francese" -#: windows/views.py:712 +#: windows/views.py:759 msgid "Locale" msgstr "Localizzazione" -#: windows/views.py:733 -msgid "Edit Value" -msgstr "Modifica valore" +#: windows/views.py:780 +#, fuzzy +msgid "Column content" +msgstr "Chiudi connessione" -#: windows/views.py:743 +#: windows/views.py:790 msgid "Syntax" msgstr "Sintassi" -#: windows/views.py:800 +#: windows/views.py:847 msgid "Ok" msgstr "Ok" -#: windows/views.py:831 +#: windows/views.py:878 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:840 +#: windows/views.py:887 msgid "File" msgstr "File" -#: windows/views.py:843 +#: windows/views.py:890 msgid "About" msgstr "Informazioni" -#: windows/views.py:846 +#: windows/views.py:893 msgid "Help" msgstr "Aiuto" -#: windows/views.py:851 +#: windows/views.py:898 msgid "Open connection manager" msgstr "Apri gestore connessioni" -#: windows/views.py:853 +#: windows/views.py:902 msgid "Disconnect from server" msgstr "Disconnetti dal server" -#: windows/views.py:857 +#: windows/views.py:904 msgid "tool" msgstr "strumento" -#: windows/views.py:857 +#: windows/views.py:904 msgid "Refresh" msgstr "Aggiorna" -#: windows/views.py:861 windows/views.py:863 +#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 +#: windows/views.py:2077 windows/views.py:2221 msgid "Add" msgstr "Aggiungi" -#: windows/views.py:897 windows/views.py:901 windows/views.py:2166 -#: windows/views.py:2654 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 msgid "MyMenuItem" msgstr "IlMioElementoMenu" -#: windows/views.py:904 windows/views.py:1769 windows/views.py:3336 +#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 msgid "MyMenu" msgstr "IlMioMenu" -#: windows/views.py:919 windows/views.py:1350 windows/views.py:1357 -#: windows/views.py:1364 +#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 +#: windows/views.py:1402 msgid "MyLabel" msgstr "LaMiaEtichetta" -#: windows/views.py:925 +#: windows/views.py:972 msgid "Databases" msgstr "Database" -#: windows/views.py:926 windows/views.py:1308 windows/views.py:2716 -#: windows/views.py:2744 +#: windows/views.py:973 windows/views.py:1340 msgid "Size" msgstr "Dimensione" -#: windows/views.py:927 +#: windows/views.py:974 msgid "Elements" msgstr "Elementi" -#: windows/views.py:928 +#: windows/views.py:975 msgid "Modified at" msgstr "Modificato il" -#: windows/views.py:929 +#: windows/views.py:976 msgid "Tables" msgstr "Tabelle" -#: windows/views.py:936 +#: windows/views.py:983 msgid "System" msgstr "Sistema" -#: windows/views.py:979 -#, fuzzy -msgid "Character set" -msgstr "Creato il" - #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1000 -#: windows/views.py:1312 windows/views.py:2980 +#: windows/components/dataview.py:89 windows/views.py:1029 +#: windows/views.py:1344 msgid "Collation" msgstr "Ordinamento" -#: windows/views.py:1026 windows/views.py:2862 +#: windows/views.py:1058 msgid "Encryption" msgstr "" -#: windows/views.py:1038 +#: windows/views.py:1070 msgid "Read Only" msgstr "" -#: windows/views.py:1055 +#: windows/views.py:1087 #, fuzzy msgid "Tablespace" msgstr "Tabelle" -#: windows/views.py:1076 +#: windows/views.py:1108 #, fuzzy msgid "Connection limit" msgstr "Connessione persa" -#: windows/views.py:1119 +#: windows/views.py:1151 #, fuzzy msgid "Profile" msgstr "File" -#: windows/views.py:1145 +#: windows/views.py:1177 #, fuzzy msgid "Default tablespace" msgstr "Elimina tabella" -#: windows/views.py:1166 +#: windows/views.py:1198 #, fuzzy msgid "Temporary tablespace" msgstr "Temporaneo" -#: windows/views.py:1192 +#: windows/views.py:1224 msgid "Quota" msgstr "" -#: windows/views.py:1211 +#: windows/views.py:1243 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1228 +#: windows/views.py:1260 msgid "Account status" msgstr "" -#: windows/views.py:1249 +#: windows/views.py:1281 #, fuzzy msgid "Password expire" msgstr "Password" -#: windows/views.py:1270 +#: windows/views.py:1302 msgid "Table:" msgstr "Tabella:" -#: windows/views.py:1278 windows/views.py:1551 windows/views.py:1595 -#: windows/views.py:1701 windows/views.py:3268 +#: windows/views.py:1310 windows/views.py:1609 windows/views.py:1653 +#: windows/views.py:2721 windows/views.py:2952 msgid "Insert" msgstr "Inserisci" -#: windows/views.py:1283 +#: windows/views.py:1315 msgid "Clone" msgstr "Clona" -#: windows/views.py:1307 +#: windows/views.py:1339 msgid "Rows" msgstr "Righe" -#: windows/views.py:1310 windows/views.py:2746 +#: windows/views.py:1342 msgid "Updated at" msgstr "Aggiornato il" -#: windows/views.py:1334 windows/views.py:1746 windows/views.py:2085 -#: windows/views.py:2140 +#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 +#: windows/views.py:2173 windows/views.py:2709 msgid "Apply" msgstr "Applica" -#: windows/views.py:1344 windows/views.py:1503 windows/views.py:1956 -#: windows/views.py:3225 +#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 +#: windows/views.py:2909 msgid "Options" msgstr "Opzioni" -#: windows/views.py:1375 +#: windows/views.py:1413 msgid "Diagram" msgstr "Diagramma" -#: windows/views.py:1386 windows/views.py:2715 +#: windows/views.py:1424 msgid "Database" msgstr "Database" -#: windows/views.py:1441 windows/views.py:3165 +#: windows/views.py:1479 windows/views.py:2849 msgid "Base" msgstr "Base" -#: windows/views.py:1455 windows/views.py:3179 +#: windows/views.py:1493 windows/views.py:2863 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1483 windows/views.py:3207 +#: windows/views.py:1521 windows/views.py:2891 msgid "Default Collation" msgstr "Ordinamento predefinito" -#: windows/views.py:1515 windows/views.py:1556 windows/views.py:1600 +#: windows/views.py:1531 +msgid "Convert data" +msgstr "" + +#: windows/views.py:1539 +msgid "Row format" +msgstr "" + +#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 +#: windows/views.py:1756 windows/views.py:2081 msgid "Remove" msgstr "Rimuovi" -#: windows/views.py:1522 windows/views.py:1563 windows/views.py:1607 +#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 msgid "Clear" msgstr "Pulisci" -#: windows/views.py:1537 windows/views.py:3239 +#: windows/views.py:1595 windows/views.py:2923 msgid "Indexes" msgstr "Indici" -#: windows/views.py:1581 +#: windows/views.py:1639 msgid "Foreign Keys" msgstr "Chiavi esterne" -#: windows/views.py:1625 +#: windows/views.py:1683 msgid "Checks" msgstr "Vincoli" -#: windows/views.py:1693 windows/views.py:3260 +#: windows/views.py:1750 windows/views.py:2944 msgid "Columns:" msgstr "Colonne:" -#: windows/views.py:1713 windows/views.py:3280 -msgid "Up" -msgstr "Su" +#: windows/views.py:1760 +#, fuzzy +msgid "Move Up" +msgstr "Sposta su\tCTRL+UP" -#: windows/views.py:1720 windows/views.py:3287 -msgid "Down" -msgstr "Giù" +#: windows/views.py:1762 +#, fuzzy +msgid "Move Down" +msgstr "Sposta giù\tCTRL+D" -#: windows/views.py:1759 windows/views.py:1766 windows/views.py:3326 -#: windows/views.py:3333 +#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 +#: windows/views.py:3017 msgid "Add Index" msgstr "Aggiungi indice" -#: windows/views.py:1763 windows/views.py:3330 +#: windows/views.py:1798 windows/views.py:3014 msgid "Add PrimaryKey" msgstr "Aggiungi chiave primaria" -#: windows/views.py:1780 +#: windows/views.py:1815 msgid "Table" msgstr "Tabella" -#: windows/views.py:1816 +#: windows/views.py:1851 #, fuzzy msgid "Definer" msgstr "Inserisci" -#: windows/views.py:1836 +#: windows/views.py:1871 msgid "Schema" msgstr "" -#: windows/views.py:1862 +#: windows/views.py:1897 msgid "SQL security" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1904 #, fuzzy msgid "DEFINER" msgstr "Inserisci" -#: windows/views.py:1869 +#: windows/views.py:1904 #, fuzzy msgid "INVOKER" msgstr "Inserisci" -#: windows/views.py:1881 windows/views.py:2558 windows/views.py:2577 -#: windows/views.py:2820 +#: windows/views.py:1916 msgid "Algorithm" msgstr "" -#: windows/views.py:1883 windows/views.py:2543 windows/views.py:2576 -#: windows/views.py:2825 +#: windows/views.py:1918 #, fuzzy msgid "UNDEFINED" msgstr "Senza segno" -#: windows/views.py:1886 windows/views.py:2546 windows/views.py:2576 -#: windows/views.py:2828 +#: windows/views.py:1921 msgid "MERGE" msgstr "" -#: windows/views.py:1889 windows/views.py:2555 windows/views.py:2576 -#: windows/views.py:2831 +#: windows/views.py:1924 #, fuzzy msgid "TEMPTABLE" msgstr "Tabella" -#: windows/views.py:1899 windows/views.py:2582 +#: windows/views.py:1934 msgid "View constraint" msgstr "" -#: windows/views.py:1901 windows/views.py:2581 +#: windows/views.py:1936 #, fuzzy msgid "None" msgstr "Clona" -#: windows/views.py:1904 windows/views.py:2581 +#: windows/views.py:1939 #, fuzzy msgid "LOCAL" msgstr "Localizzazione" -#: windows/views.py:1907 +#: windows/views.py:1942 #, fuzzy msgid "CASCADE" msgstr "Annulla" -#: windows/views.py:1910 +#: windows/views.py:1945 #, fuzzy msgid "CHECK ONLY" msgstr "Verifica" -#: windows/views.py:1913 windows/views.py:2581 +#: windows/views.py:1948 msgid "READ ONLY" msgstr "" -#: windows/views.py:1925 +#: windows/views.py:1960 msgid "Force" msgstr "" -#: windows/views.py:1937 +#: windows/views.py:1972 msgid "Security barrier" msgstr "" -#: windows/views.py:2019 +#: windows/views.py:2054 msgid "Views" msgstr "Viste" -#: windows/views.py:2027 +#: windows/views.py:2062 msgid "Triggers" msgstr "Trigger" -#: windows/views.py:2039 -#, python-format -msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" -msgstr "Tabella `%(database_name)s`.`%(table_name)s`: %(total_rows) righe totali" - -#: windows/views.py:2049 -msgid "Insert record" -msgstr "Inserisci record" +#: windows/views.py:2073 +#, fuzzy +msgid "Refrsh" +msgstr "Aggiorna" -#: windows/views.py:2054 -msgid "Duplicate record" +#: windows/views.py:2079 +#, fuzzy +msgid "Duplicate" msgstr "Duplica record" -#: windows/views.py:2061 -msgid "Delete record" -msgstr "Elimina record" - -#: windows/views.py:2071 +#: windows/views.py:2085 msgid "Apply changes automatically" msgstr "Applica modifiche automaticamente" -#: windows/views.py:2073 windows/views.py:2074 +#: windows/views.py:2087 windows/views.py:2088 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -647,161 +686,152 @@ msgstr "" "Se abilitato, le modifiche alla tabella vengono applicate immediatamente " "senza premere Applica o Annulla" -#: windows/views.py:2095 -msgid "Next" -msgstr "Avanti" +#: windows/views.py:2101 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" + +#: windows/views.py:2109 +#, fuzzy +msgid "First" +msgstr "Filtri" -#: windows/views.py:2103 +#: windows/views.py:2127 +msgid "Last" +msgstr "" + +#: windows/views.py:2136 msgid "Filters" msgstr "Filtri" -#: windows/views.py:2143 +#: windows/views.py:2176 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2163 +#: windows/views.py:2196 msgid "Insert row" msgstr "Inserisci riga" -#: windows/views.py:2171 +#: windows/views.py:2204 msgid "Data" msgstr "Dati" -#: windows/views.py:2225 windows/views.py:2275 -msgid "New" -msgstr "Nuovo" - -#: windows/views.py:2252 -msgid "Query" +#: windows/main/controller.py:278 windows/main/controller.py:287 +#: windows/main/controller.py:288 windows/views.py:2221 +#, fuzzy +msgid "New query" msgstr "Query" -#: windows/views.py:2272 +#: windows/views.py:2223 windows/views.py:2660 msgid "Close" msgstr "Chiudi" -#: windows/views.py:2285 -msgid "Query #2" -msgstr "Query #2" +#: windows/main/controller.py:279 windows/main/controller.py:289 +#: windows/main/controller.py:290 windows/views.py:2223 +#, fuzzy +msgid "Close query" +msgstr "Query" -#: windows/views.py:2537 -msgid "Column5" -msgstr "Colonna5" +#: windows/views.py:2227 +msgid "Run" +msgstr "" -#: windows/views.py:2548 -msgid "Import" -msgstr "Importa" +#: windows/main/controller.py:280 windows/main/controller.py:292 +#: windows/main/controller.py:293 windows/views.py:2227 +#, fuzzy +msgid "Execute" +msgstr "Eseguibile SSH" -#: windows/views.py:2573 -msgid "Read only" +#: windows/views.py:2229 +msgid "Run all" msgstr "" -#: windows/views.py:2581 -msgid "CASCADED" +#: windows/main/controller.py:295 windows/views.py:2229 +msgid "Execute all statements" msgstr "" -#: windows/views.py:2581 -#, fuzzy -msgid "CHECK OPTION" -msgstr "connessione" +#: windows/main/controller.py:282 windows/main/controller.py:297 +#: windows/main/controller.py:298 windows/views.py:2231 +msgid "Stop" +msgstr "" -#: windows/views.py:2613 -msgid "collapsible" -msgstr "collassabile" +#: windows/views.py:2287 +msgid "a page" +msgstr "" -#: windows/views.py:2629 -msgid "Column3" -msgstr "Colonna3" +#: windows/views.py:2315 +msgid "Query" +msgstr "Query" -#: windows/views.py:2630 -msgid "Column4" -msgstr "Colonna4" +#: windows/views.py:2626 +#, fuzzy +msgid "Character set" +msgstr "Creato il" -#: windows/views.py:2673 -msgid "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" -msgstr "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#: windows/views.py:2644 windows/views.py:2663 +msgid "New" +msgstr "Nuovo" -#: windows/views.py:2685 -msgid "Port" -msgstr "Porta" +#: windows/views.py:2683 +msgid "Insert record" +msgstr "Inserisci record" -#: windows/views.py:2708 -msgid "Usage" -msgstr "Utilizzo" +#: windows/views.py:2688 +msgid "Duplicate record" +msgstr "Duplica record" -#: windows/views.py:2719 -#, python-format -msgid "%(total_rows)s" -msgstr "%(total_rows)s" +#: windows/views.py:2695 +msgid "Delete record" +msgstr "Elimina record" -#: windows/views.py:2724 -msgid "rows total" -msgstr "righe totali" +#: windows/views.py:2733 windows/views.py:2964 +msgid "Up" +msgstr "Su" -#: windows/views.py:2743 -msgid "Lines" -msgstr "Righe" +#: windows/views.py:2740 windows/views.py:2971 +msgid "Down" +msgstr "Giù" -#: windows/views.py:2775 -msgid "Temporary" -msgstr "Temporaneo" +#: windows/views.py:3100 +msgid "Save Starments" +msgstr "" -#: windows/views.py:2786 +#: windows/views.py:3108 #, fuzzy -msgid "Engine options" -msgstr "Opzioni" +msgid "Location" +msgstr "Ordinamento" -#: windows/views.py:2845 -msgid "RadioBtn" +#: windows/views.py:3115 +msgid "*.sql" msgstr "" -#: windows/views.py:2928 -msgid "Edit Column" -msgstr "Modifica colonna" - -#: windows/views.py:2944 -msgid "Datatype" -msgstr "Tipo di dati" - -#: windows/components/dataview.py:121 windows/views.py:2959 -msgid "Length/Set" -msgstr "Lunghezza/Insieme" - -#: windows/components/dataview.py:51 windows/views.py:2998 -msgid "Unsigned" -msgstr "Senza segno" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3004 +#: windows/components/dataview.py:75 msgid "Allow NULL" msgstr "Consenti NULL" -#: windows/views.py:3010 -msgid "Zero Fill" -msgstr "Riempimento zero" +#: windows/components/dataview.py:28 +msgid "Check" +msgstr "Verifica" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3021 +#: windows/components/dataview.py:78 msgid "Default" msgstr "Predefinito" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3047 +#: windows/components/dataview.py:82 msgid "Virtuality" msgstr "Virtualità" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3062 msgid "Expression" msgstr "Espressione" -#: windows/components/dataview.py:28 -msgid "Check" -msgstr "Verifica" +#: windows/components/dataview.py:51 +msgid "Unsigned" +msgstr "Senza segno" #: windows/components/dataview.py:53 msgid "Zerofill" @@ -815,6 +845,10 @@ msgstr "#" msgid "Data type" msgstr "Tipo di dati" +#: windows/components/dataview.py:121 +msgid "Length/Set" +msgstr "Lunghezza/Insieme" + #: windows/components/dataview.py:155 msgid "Add column\tCTRL+INS" msgstr "Aggiungi colonna\tCTRL+INS" @@ -867,11 +901,11 @@ msgstr "Su AGGIORNAMENTO" msgid "On DELETE" msgstr "Su ELIMINA" -#: windows/components/dataview.py:299 +#: windows/components/dataview.py:298 msgid "Add foreign key" msgstr "Aggiungi chiave esterna" -#: windows/components/dataview.py:305 +#: windows/components/dataview.py:304 msgid "Remove foreign key" msgstr "Rimuovi chiave esterna" @@ -891,75 +925,164 @@ msgstr "AUTO INCREMENTO" msgid "Text/Expression" msgstr "Testo/Espressione" -#: windows/dialogs/connections/view.py:119 windows/main/tabs/query.py:376 +#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:394 +#: windows/dialogs/connections/view.py:414 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:407 +#: windows/dialogs/connections/view.py:426 +#, python-brace-format +msgid "Do you want save the connection {connection_name}?" +msgstr "" + +#: windows/dialogs/connections/view.py:429 msgid "Confirm save" msgstr "Conferma salvataggio" -#: windows/dialogs/connections/view.py:459 +#: windows/dialogs/connections/view.py:481 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:461 +#: windows/dialogs/connections/view.py:483 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:736 +#: windows/dialogs/connections/view.py:750 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:775 +#, fuzzy, python-brace-format +msgid "" +"Connection error:\n" +"{error}" +msgstr "Errore di connessione" + +#: windows/dialogs/connections/view.py:776 msgid "Connection error" msgstr "Errore di connessione" -#: windows/dialogs/connections/view.py:788 -#: windows/dialogs/connections/view.py:803 +#: windows/dialogs/connections/view.py:802 +#, fuzzy, python-brace-format +msgid "Do you want to delete the connection '{connection_name}'?" +msgstr "Vuoi eliminare i record?" + +#: windows/dialogs/connections/view.py:805 +#: windows/dialogs/connections/view.py:822 msgid "Confirm delete" msgstr "Conferma eliminazione" -#: windows/main/controller.py:172 +#: windows/dialogs/connections/view.py:819 +#, fuzzy, python-brace-format +msgid "Do you want to delete the directory '{directory_name}'?" +msgstr "Vuoi eliminare i record?" + +#: windows/main/controller.py:275 +#, python-brace-format +msgid "{text} ({shortcut})" +msgstr "" + +#: windows/main/controller.py:281 windows/main/controller.py:294 +#, fuzzy +msgid "Execute all" +msgstr "Eseguibile SSH" + +#: windows/main/controller.py:471 windows/main/controller.py:479 +#, fuzzy +msgid "Query (1)" +msgstr "Query" + +#: windows/main/controller.py:497 +#, python-brace-format +msgid "Query ({query_number})" +msgstr "" + +#: windows/main/controller.py:530 +msgid "You have unsaved changes. Save before closing?" +msgstr "" + +#: windows/main/controller.py:531 +msgid "Unsaved query" +msgstr "" + +#: windows/main/controller.py:576 +#, fuzzy +msgid "Save query" +msgstr "Query" + +#: windows/main/controller.py:579 +msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" +msgstr "" + +#: windows/main/controller.py:616 windows/main/controller.py:642 +#: windows/main/database/list.py:84 windows/main/database/view.py:256 +#: windows/main/database/view.py:282 windows/main/query/controller.py:177 +msgid "Error" +msgstr "Errore" + +#: windows/main/controller.py:622 +#, python-brace-format +msgid "-- Saved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:647 +#, python-brace-format +msgid "-- Autosaved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:704 msgid "days" msgstr "giorni" -#: windows/main/controller.py:173 +#: windows/main/controller.py:705 msgid "hours" msgstr "ore" -#: windows/main/controller.py:174 +#: windows/main/controller.py:706 msgid "minutes" msgstr "minuti" -#: windows/main/controller.py:175 +#: windows/main/controller.py:707 msgid "seconds" msgstr "secondi" -#: windows/main/controller.py:183 +#: windows/main/controller.py:715 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizzata: {used} ({percentage:.2%})" -#: windows/main/controller.py:219 +#: windows/main/controller.py:751 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:298 +#: windows/main/controller.py:952 +#, python-brace-format +msgid "~{estimated} (Loading...)" +msgstr "" + +#: windows/main/controller.py:954 +msgid "~ (Loading...)" +msgstr "" + +#: windows/main/controller.py:1119 msgid "Version" msgstr "Versione" -#: windows/main/controller.py:300 +#: windows/main/controller.py:1121 msgid "Uptime" msgstr "Tempo di attività" -#: windows/main/controller.py:399 +#: windows/main/controller.py:1199 +#, python-brace-format +msgid "Do you want discard the change to {database_name}?" +msgstr "" + +#: windows/main/controller.py:1232 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -969,138 +1092,177 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:404 windows/main/controller.py:425 +#: windows/main/controller.py:1237 windows/main/controller.py:1258 #, fuzzy msgid "Delete database" msgstr "Elimina tabella" -#: windows/main/controller.py:410 +#: windows/main/controller.py:1243 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:411 +#: windows/main/controller.py:1244 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:424 +#: windows/main/controller.py:1257 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:439 +#: windows/main/controller.py:1272 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:440 windows/main/tabs/view.py:253 -#: windows/main/tabs/view.py:279 +#: windows/main/controller.py:1273 windows/main/database/view.py:253 +#: windows/main/database/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:582 +#: windows/main/controller.py:1392 +#, python-brace-format +msgid "Do you want discard the change to {table_name}?" +msgstr "" + +#: windows/main/controller.py:1418 +#, fuzzy, python-brace-format +msgid "Do you want delete the table {table_name}?" +msgstr "Vuoi eliminare i record?" + +#: windows/main/controller.py:1421 msgid "Delete table" msgstr "Elimina tabella" -#: windows/main/controller.py:699 +#: windows/main/controller.py:1440 +#, python-brace-format +msgid "{table_name} (COPY)" +msgstr "" + +#: windows/main/controller.py:1563 msgid "Do you want delete the records?" msgstr "Vuoi eliminare i record?" -#: windows/main/tabs/database.py:71 +#: windows/main/database/list.py:69 msgid "The connection to the database was lost." msgstr "La connessione al database è stata persa." -#: windows/main/tabs/database.py:73 +#: windows/main/database/list.py:71 msgid "Do you want to reconnect?" msgstr "Vuoi riconnetterti?" -#: windows/main/tabs/database.py:75 +#: windows/main/database/list.py:73 msgid "Connection lost" msgstr "Connessione persa" -#: windows/main/tabs/database.py:85 +#: windows/main/database/list.py:83 msgid "Reconnection failed:" msgstr "Riconnessione fallita:" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:450 -#: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 -msgid "Error" -msgstr "Errore" - -#: windows/main/tabs/query.py:305 -#, python-brace-format -msgid "{} rows affected" +#: windows/main/database/view.py:252 +msgid "View created successfully" msgstr "" -#: windows/main/tabs/query.py:309 windows/main/tabs/query.py:331 -#, fuzzy, python-brace-format -msgid "Query {}" -msgstr "Query" +#: windows/main/database/view.py:252 +msgid "View updated successfully" +msgstr "" -#: windows/main/tabs/query.py:314 +#: windows/main/database/view.py:256 #, python-brace-format -msgid "Query {} (Error)" +msgid "Error saving view: {}" msgstr "" -#: windows/main/tabs/query.py:326 +#: windows/main/database/view.py:269 #, python-brace-format -msgid "Query {} ({} rows × {} cols)" +msgid "Are you sure you want to delete view '{}'?" msgstr "" -#: windows/main/tabs/query.py:353 -#, fuzzy, python-brace-format -msgid "{} rows" -msgstr "Righe" +#: windows/main/database/view.py:270 +#, fuzzy +msgid "Confirm Delete" +msgstr "Conferma eliminazione" + +#: windows/main/database/view.py:279 +msgid "View deleted successfully" +msgstr "" -#: windows/main/tabs/query.py:355 +#: windows/main/database/view.py:282 #, python-brace-format -msgid "{:.1f} ms" +msgid "Error deleting view: {}" msgstr "" -#: windows/main/tabs/query.py:358 +#: windows/main/query/controller.py:110 #, python-brace-format -msgid "{} warnings" +msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/tabs/query.py:370 +#: windows/main/query/controller.py:112 +#, python-brace-format +msgid "{elapsed_s:.2f} s" +msgstr "" + +#: windows/main/query/controller.py:115 #, fuzzy -msgid "Error:" -msgstr "Errore" +msgid "none" +msgstr "Clona" + +#: windows/main/query/controller.py:121 +#, python-brace-format +msgid "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." +msgstr "" -#: windows/main/tabs/query.py:449 +#: windows/main/query/controller.py:134 +msgid "Query execution cancelled" +msgstr "" + +#: windows/main/query/controller.py:176 #, fuzzy msgid "No active database connection" msgstr "Nuova connessione" -#: windows/main/tabs/view.py:252 -msgid "View created successfully" +#: windows/main/query/renderer.py:53 +#, python-brace-format +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View updated successfully" +#: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 +#, python-brace-format +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/view.py:256 +#: windows/main/query/renderer.py:65 #, python-brace-format -msgid "Error saving view: {}" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/view.py:269 +#: windows/main/query/renderer.py:79 #, python-brace-format -msgid "Are you sure you want to delete view '{}'?" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/view.py:270 -#, fuzzy -msgid "Confirm Delete" -msgstr "Conferma eliminazione" +#: windows/main/query/renderer.py:165 +#, python-brace-format +msgid "{rows_count} rows" +msgstr "" -#: windows/main/tabs/view.py:279 -msgid "View deleted successfully" +#: windows/main/query/renderer.py:167 +#, python-brace-format +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/view.py:282 +#: windows/main/query/renderer.py:171 #, python-brace-format -msgid "Error deleting view: {}" +msgid "{warnings_count} warnings" msgstr "" +#: windows/main/query/renderer.py:186 +#, fuzzy +msgid "Error:" +msgstr "Errore" + #~ msgid "Created at:" #~ msgstr "" @@ -1131,3 +1293,102 @@ msgstr "" #~ msgid "directory" #~ msgstr "directory" +#~ msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#~ msgstr "" +#~ "Tabella `%(database_name)s`.`%(table_name)s`: " +#~ "%(total_rows) righe totali" + +#~ msgid "Next" +#~ msgstr "Avanti" + +#~ msgid "{} rows affected" +#~ msgstr "" + +#~ msgid "Query {}" +#~ msgstr "Query" + +#~ msgid "Query {} (Error)" +#~ msgstr "" + +#~ msgid "Query {} ({} rows × {} cols)" +#~ msgstr "" + +#~ msgid "{} rows" +#~ msgstr "Righe" + +#~ msgid "{:.1f} ms" +#~ msgstr "" + +#~ msgid "{} warnings" +#~ msgstr "" + +#~ msgid "Edit Value" +#~ msgstr "Modifica valore" + +#~ msgid "Query #2" +#~ msgstr "Query #2" + +#~ msgid "Column5" +#~ msgstr "Colonna5" + +#~ msgid "Import" +#~ msgstr "Importa" + +#~ msgid "Read only" +#~ msgstr "" + +#~ msgid "CASCADED" +#~ msgstr "" + +#~ msgid "CHECK OPTION" +#~ msgstr "connessione" + +#~ msgid "collapsible" +#~ msgstr "collassabile" + +#~ msgid "Column3" +#~ msgstr "Colonna3" + +#~ msgid "Column4" +#~ msgstr "Colonna4" + +#~ msgid "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#~ msgstr "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" + +#~ msgid "Port" +#~ msgstr "Porta" + +#~ msgid "Usage" +#~ msgstr "Utilizzo" + +#~ msgid "%(total_rows)s" +#~ msgstr "%(total_rows)s" + +#~ msgid "rows total" +#~ msgstr "righe totali" + +#~ msgid "Lines" +#~ msgstr "Righe" + +#~ msgid "Temporary" +#~ msgstr "Temporaneo" + +#~ msgid "Engine options" +#~ msgstr "Opzioni" + +#~ msgid "RadioBtn" +#~ msgstr "" + +#~ msgid "Edit Column" +#~ msgstr "Modifica colonna" + +#~ msgid "Datatype" +#~ msgstr "Tipo di dati" + +#~ msgid "Zero Fill" +#~ msgstr "Riempimento zero" + diff --git a/locale/petersql.pot b/locale/petersql.pot index da6114d..fcc7476 100644 --- a/locale/petersql.pot +++ b/locale/petersql.pot @@ -23,38 +23,79 @@ msgctxt "unit" msgid "TB" msgstr "" -#: structures/ssh_tunnel.py:166 +#: structures/ssh_tunnel.py:177 msgid "OpenSSH client not found." msgstr "" -#: windows/dialogs/connections/view.py:395 -#: windows/dialogs/connections/view.py:738 windows/main/controller.py:296 +#: structures/engines/mariadb/context.py:611 +#: structures/engines/mysql/context.py:622 +#: structures/engines/postgresql/context.py:645 +#: structures/engines/sqlite/context.py:524 +#, python-brace-format +msgid "Table{table_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:639 +#: structures/engines/mysql/context.py:650 +#: structures/engines/postgresql/context.py:670 +#: structures/engines/sqlite/context.py:548 +#, python-brace-format +msgid "Column{column_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:657 +#: structures/engines/mysql/context.py:668 +#: structures/engines/postgresql/context.py:688 +#: structures/engines/sqlite/context.py:566 +#, python-brace-format +msgid "Index{index_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:697 +#: structures/engines/mysql/context.py:706 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:608 +#, python-brace-format +msgid "ForeignKey{foreign_key_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:730 +#: structures/engines/mysql/context.py:737 +#: structures/engines/postgresql/context.py:758 +#: structures/engines/sqlite/context.py:636 +#, python-brace-format +msgid "View{view_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:781 +#, python-brace-format +msgid "Trigger{trigger_index:03}" +msgstr "" + +#: windows/dialogs/connections/view.py:415 +#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 #: windows/views.py:33 msgid "Connection" msgstr "" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:956 -#: windows/views.py:1306 windows/views.py:1413 windows/views.py:1796 -#: windows/views.py:2707 windows/views.py:2730 windows/views.py:2731 -#: windows/views.py:2732 windows/views.py:2733 windows/views.py:2734 -#: windows/views.py:2735 windows/views.py:2736 windows/views.py:2737 -#: windows/views.py:2738 windows/views.py:2742 windows/views.py:2936 -#: windows/views.py:3137 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1006 +#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 +#: windows/views.py:2821 msgid "Name" msgstr "" -#: windows/views.py:48 windows/views.py:381 +#: windows/views.py:48 windows/views.py:428 msgid "Last connection" msgstr "" -#: windows/dialogs/connections/view.py:631 windows/views.py:61 +#: windows/dialogs/connections/view.py:653 windows/views.py:61 msgid "New directory" msgstr "" -#: windows/dialogs/connections/model.py:187 -#: windows/dialogs/connections/view.py:591 windows/views.py:65 +#: windows/dialogs/connections/model.py:206 +#: windows/dialogs/connections/view.py:613 windows/views.py:65 msgid "New connection" msgstr "" @@ -66,14 +107,14 @@ msgstr "" msgid "Clone connection" msgstr "" -#: windows/views.py:81 windows/views.py:556 windows/views.py:1290 -#: windows/views.py:1331 windows/views.py:1706 windows/views.py:1738 -#: windows/views.py:1997 windows/views.py:3273 windows/views.py:3305 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1322 +#: windows/views.py:1365 windows/views.py:1773 windows/views.py:2032 +#: windows/views.py:2726 windows/views.py:2957 windows/views.py:2989 msgid "Delete" msgstr "" -#: windows/views.py:111 windows/views.py:1311 windows/views.py:1468 -#: windows/views.py:2747 windows/views.py:3192 +#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 +#: windows/views.py:2876 msgid "Engine" msgstr "" @@ -85,671 +126,663 @@ msgstr "" msgid "Username" msgstr "" -#: windows/views.py:161 windows/views.py:1100 +#: windows/views.py:161 windows/views.py:1132 msgid "Password" msgstr "" -#: windows/views.py:177 +#: windows/views.py:174 +msgid "Connection timeout" +msgstr "" + +#: windows/views.py:192 msgid "Use TLS" msgstr "" -#: windows/views.py:180 +#: windows/views.py:203 msgid "Use SSH tunnel" msgstr "" -#: windows/views.py:202 windows/views.py:2668 +#: windows/views.py:214 +msgid "Compressed client/server protocol" +msgstr "" + +#: windows/views.py:233 msgid "Filename" msgstr "" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2673 -#: windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 msgid "Select a file" msgstr "" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 msgid "*.*" msgstr "" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:221 windows/views.py:1313 windows/views.py:1426 -#: windows/views.py:2748 windows/views.py:3034 windows/views.py:3150 +#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 +#: windows/views.py:2834 msgid "Comments" msgstr "" -#: windows/main/controller.py:219 windows/views.py:235 windows/views.py:683 -#: windows/views.py:837 +#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 +#: windows/views.py:884 msgid "Settings" msgstr "" -#: windows/views.py:244 +#: windows/views.py:278 msgid "SSH executable" msgstr "" -#: windows/views.py:249 +#: windows/views.py:283 msgid "ssh" msgstr "" -#: windows/views.py:257 +#: windows/views.py:291 msgid "SSH host + port" msgstr "" -#: windows/views.py:269 +#: windows/views.py:303 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "" -#: windows/views.py:278 +#: windows/views.py:312 msgid "SSH username" msgstr "" -#: windows/views.py:291 +#: windows/views.py:325 msgid "SSH password" msgstr "" -#: windows/views.py:304 +#: windows/views.py:338 msgid "Local port" msgstr "" -#: windows/views.py:310 +#: windows/views.py:344 msgid "if the value is set to 0, the first available port will be used" msgstr "" -#: windows/views.py:319 +#: windows/views.py:353 msgid "Identity file" msgstr "" -#: windows/views.py:335 +#: windows/views.py:369 msgid "Remote host + port" msgstr "" -#: windows/views.py:347 +#: windows/views.py:381 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "" -#: windows/views.py:358 +#: windows/views.py:390 +msgid "SSH extra args" +msgstr "" + +#: windows/views.py:405 msgid "SSH Tunnel" msgstr "" -#: windows/views.py:364 windows/views.py:1309 windows/views.py:2745 +#: windows/views.py:411 windows/views.py:1341 msgid "Created at" msgstr "" -#: windows/views.py:398 +#: windows/views.py:445 msgid "Successful connections" msgstr "" -#: windows/views.py:415 +#: windows/views.py:462 msgid "Last successful connection" msgstr "" -#: windows/views.py:432 +#: windows/views.py:479 msgid "Unsuccessful connections" msgstr "" -#: windows/views.py:449 +#: windows/views.py:496 msgid "Last failure reason" msgstr "" -#: windows/views.py:466 +#: windows/views.py:513 msgid "Total connection attempts" msgstr "" -#: windows/views.py:483 -msgid " Average connection time (ms)" +#: windows/views.py:530 +msgid "Average connection time (ms)" msgstr "" -#: windows/views.py:500 -msgid " Most recent connection duration" +#: windows/views.py:547 +msgid "Most recent connection duration" msgstr "" -#: windows/views.py:519 +#: windows/views.py:566 msgid "Statistics" msgstr "" -#: windows/views.py:537 windows/views.py:1672 +#: windows/views.py:584 windows/views.py:1730 msgid "Create" msgstr "" -#: windows/views.py:541 +#: windows/views.py:588 msgid "Create connection" msgstr "" -#: windows/views.py:544 +#: windows/views.py:591 msgid "Create directory" msgstr "" -#: windows/views.py:573 windows/views.py:797 windows/views.py:1328 -#: windows/views.py:1741 windows/views.py:2002 windows/views.py:2078 -#: windows/views.py:3081 windows/views.py:3308 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1360 +#: windows/views.py:1776 windows/views.py:2037 windows/views.py:2093 +#: windows/views.py:2702 windows/views.py:2992 windows/views.py:3129 msgid "Cancel" msgstr "" -#: windows/views.py:578 windows/views.py:2007 windows/views.py:3091 -#: windows/views.py:3313 +#: windows/main/controller.py:283 windows/main/controller.py:300 +#: windows/main/controller.py:301 windows/views.py:625 windows/views.py:2042 +#: windows/views.py:2235 windows/views.py:2997 windows/views.py:3135 msgid "Save" msgstr "" -#: windows/views.py:585 +#: windows/views.py:632 msgid "Test" msgstr "" -#: windows/views.py:592 +#: windows/views.py:639 msgid "Connect" msgstr "" -#: windows/views.py:695 +#: windows/views.py:742 msgid "Language" msgstr "" -#: windows/views.py:700 +#: windows/views.py:747 msgid "English" msgstr "" -#: windows/views.py:700 +#: windows/views.py:747 msgid "Italian" msgstr "" -#: windows/views.py:700 +#: windows/views.py:747 msgid "French" msgstr "" -#: windows/views.py:712 +#: windows/views.py:759 msgid "Locale" msgstr "" -#: windows/views.py:733 -msgid "Edit Value" +#: windows/views.py:780 +msgid "Column content" msgstr "" -#: windows/views.py:743 +#: windows/views.py:790 msgid "Syntax" msgstr "" -#: windows/views.py:800 +#: windows/views.py:847 msgid "Ok" msgstr "" -#: windows/views.py:831 +#: windows/views.py:878 msgid "PeterSQL" msgstr "" -#: windows/views.py:840 +#: windows/views.py:887 msgid "File" msgstr "" -#: windows/views.py:843 +#: windows/views.py:890 msgid "About" msgstr "" -#: windows/views.py:846 +#: windows/views.py:893 msgid "Help" msgstr "" -#: windows/views.py:851 +#: windows/views.py:898 msgid "Open connection manager" msgstr "" -#: windows/views.py:853 +#: windows/views.py:902 msgid "Disconnect from server" msgstr "" -#: windows/views.py:857 +#: windows/views.py:904 msgid "tool" msgstr "" -#: windows/views.py:857 +#: windows/views.py:904 msgid "Refresh" msgstr "" -#: windows/views.py:861 windows/views.py:863 +#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 +#: windows/views.py:2077 windows/views.py:2221 msgid "Add" msgstr "" -#: windows/views.py:897 windows/views.py:901 windows/views.py:2166 -#: windows/views.py:2654 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 msgid "MyMenuItem" msgstr "" -#: windows/views.py:904 windows/views.py:1769 windows/views.py:3336 +#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 msgid "MyMenu" msgstr "" -#: windows/views.py:919 windows/views.py:1350 windows/views.py:1357 -#: windows/views.py:1364 +#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 +#: windows/views.py:1402 msgid "MyLabel" msgstr "" -#: windows/views.py:925 +#: windows/views.py:972 msgid "Databases" msgstr "" -#: windows/views.py:926 windows/views.py:1308 windows/views.py:2716 -#: windows/views.py:2744 +#: windows/views.py:973 windows/views.py:1340 msgid "Size" msgstr "" -#: windows/views.py:927 +#: windows/views.py:974 msgid "Elements" msgstr "" -#: windows/views.py:928 +#: windows/views.py:975 msgid "Modified at" msgstr "" -#: windows/views.py:929 +#: windows/views.py:976 msgid "Tables" msgstr "" -#: windows/views.py:936 +#: windows/views.py:983 msgid "System" msgstr "" -#: windows/views.py:979 -msgid "Character set" -msgstr "" - #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1000 -#: windows/views.py:1312 windows/views.py:2980 +#: windows/components/dataview.py:89 windows/views.py:1029 +#: windows/views.py:1344 msgid "Collation" msgstr "" -#: windows/views.py:1026 windows/views.py:2862 +#: windows/views.py:1058 msgid "Encryption" msgstr "" -#: windows/views.py:1038 +#: windows/views.py:1070 msgid "Read Only" msgstr "" -#: windows/views.py:1055 +#: windows/views.py:1087 msgid "Tablespace" msgstr "" -#: windows/views.py:1076 +#: windows/views.py:1108 msgid "Connection limit" msgstr "" -#: windows/views.py:1119 +#: windows/views.py:1151 msgid "Profile" msgstr "" -#: windows/views.py:1145 +#: windows/views.py:1177 msgid "Default tablespace" msgstr "" -#: windows/views.py:1166 +#: windows/views.py:1198 msgid "Temporary tablespace" msgstr "" -#: windows/views.py:1192 +#: windows/views.py:1224 msgid "Quota" msgstr "" -#: windows/views.py:1211 +#: windows/views.py:1243 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1228 +#: windows/views.py:1260 msgid "Account status" msgstr "" -#: windows/views.py:1249 +#: windows/views.py:1281 msgid "Password expire" msgstr "" -#: windows/views.py:1270 +#: windows/views.py:1302 msgid "Table:" msgstr "" -#: windows/views.py:1278 windows/views.py:1551 windows/views.py:1595 -#: windows/views.py:1701 windows/views.py:3268 +#: windows/views.py:1310 windows/views.py:1609 windows/views.py:1653 +#: windows/views.py:2721 windows/views.py:2952 msgid "Insert" msgstr "" -#: windows/views.py:1283 +#: windows/views.py:1315 msgid "Clone" msgstr "" -#: windows/views.py:1307 +#: windows/views.py:1339 msgid "Rows" msgstr "" -#: windows/views.py:1310 windows/views.py:2746 +#: windows/views.py:1342 msgid "Updated at" msgstr "" -#: windows/views.py:1334 windows/views.py:1746 windows/views.py:2085 -#: windows/views.py:2140 +#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 +#: windows/views.py:2173 windows/views.py:2709 msgid "Apply" msgstr "" -#: windows/views.py:1344 windows/views.py:1503 windows/views.py:1956 -#: windows/views.py:3225 +#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 +#: windows/views.py:2909 msgid "Options" msgstr "" -#: windows/views.py:1375 +#: windows/views.py:1413 msgid "Diagram" msgstr "" -#: windows/views.py:1386 windows/views.py:2715 +#: windows/views.py:1424 msgid "Database" msgstr "" -#: windows/views.py:1441 windows/views.py:3165 +#: windows/views.py:1479 windows/views.py:2849 msgid "Base" msgstr "" -#: windows/views.py:1455 windows/views.py:3179 +#: windows/views.py:1493 windows/views.py:2863 msgid "Auto Increment" msgstr "" -#: windows/views.py:1483 windows/views.py:3207 +#: windows/views.py:1521 windows/views.py:2891 msgid "Default Collation" msgstr "" -#: windows/views.py:1515 windows/views.py:1556 windows/views.py:1600 +#: windows/views.py:1531 +msgid "Convert data" +msgstr "" + +#: windows/views.py:1539 +msgid "Row format" +msgstr "" + +#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 +#: windows/views.py:1756 windows/views.py:2081 msgid "Remove" msgstr "" -#: windows/views.py:1522 windows/views.py:1563 windows/views.py:1607 +#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 msgid "Clear" msgstr "" -#: windows/views.py:1537 windows/views.py:3239 +#: windows/views.py:1595 windows/views.py:2923 msgid "Indexes" msgstr "" -#: windows/views.py:1581 +#: windows/views.py:1639 msgid "Foreign Keys" msgstr "" -#: windows/views.py:1625 +#: windows/views.py:1683 msgid "Checks" msgstr "" -#: windows/views.py:1693 windows/views.py:3260 +#: windows/views.py:1750 windows/views.py:2944 msgid "Columns:" msgstr "" -#: windows/views.py:1713 windows/views.py:3280 -msgid "Up" +#: windows/views.py:1760 +msgid "Move Up" msgstr "" -#: windows/views.py:1720 windows/views.py:3287 -msgid "Down" +#: windows/views.py:1762 +msgid "Move Down" msgstr "" -#: windows/views.py:1759 windows/views.py:1766 windows/views.py:3326 -#: windows/views.py:3333 +#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 +#: windows/views.py:3017 msgid "Add Index" msgstr "" -#: windows/views.py:1763 windows/views.py:3330 +#: windows/views.py:1798 windows/views.py:3014 msgid "Add PrimaryKey" msgstr "" -#: windows/views.py:1780 +#: windows/views.py:1815 msgid "Table" msgstr "" -#: windows/views.py:1816 +#: windows/views.py:1851 msgid "Definer" msgstr "" -#: windows/views.py:1836 +#: windows/views.py:1871 msgid "Schema" msgstr "" -#: windows/views.py:1862 +#: windows/views.py:1897 msgid "SQL security" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1904 msgid "DEFINER" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1904 msgid "INVOKER" msgstr "" -#: windows/views.py:1881 windows/views.py:2558 windows/views.py:2577 -#: windows/views.py:2820 +#: windows/views.py:1916 msgid "Algorithm" msgstr "" -#: windows/views.py:1883 windows/views.py:2543 windows/views.py:2576 -#: windows/views.py:2825 +#: windows/views.py:1918 msgid "UNDEFINED" msgstr "" -#: windows/views.py:1886 windows/views.py:2546 windows/views.py:2576 -#: windows/views.py:2828 +#: windows/views.py:1921 msgid "MERGE" msgstr "" -#: windows/views.py:1889 windows/views.py:2555 windows/views.py:2576 -#: windows/views.py:2831 +#: windows/views.py:1924 msgid "TEMPTABLE" msgstr "" -#: windows/views.py:1899 windows/views.py:2582 +#: windows/views.py:1934 msgid "View constraint" msgstr "" -#: windows/views.py:1901 windows/views.py:2581 +#: windows/views.py:1936 msgid "None" msgstr "" -#: windows/views.py:1904 windows/views.py:2581 +#: windows/views.py:1939 msgid "LOCAL" msgstr "" -#: windows/views.py:1907 +#: windows/views.py:1942 msgid "CASCADE" msgstr "" -#: windows/views.py:1910 +#: windows/views.py:1945 msgid "CHECK ONLY" msgstr "" -#: windows/views.py:1913 windows/views.py:2581 +#: windows/views.py:1948 msgid "READ ONLY" msgstr "" -#: windows/views.py:1925 +#: windows/views.py:1960 msgid "Force" msgstr "" -#: windows/views.py:1937 +#: windows/views.py:1972 msgid "Security barrier" msgstr "" -#: windows/views.py:2019 +#: windows/views.py:2054 msgid "Views" msgstr "" -#: windows/views.py:2027 +#: windows/views.py:2062 msgid "Triggers" msgstr "" -#: windows/views.py:2039 -#, python-format -msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#: windows/views.py:2073 +msgid "Refrsh" msgstr "" -#: windows/views.py:2049 -msgid "Insert record" -msgstr "" - -#: windows/views.py:2054 -msgid "Duplicate record" +#: windows/views.py:2079 +msgid "Duplicate" msgstr "" -#: windows/views.py:2061 -msgid "Delete record" -msgstr "" - -#: windows/views.py:2071 +#: windows/views.py:2085 msgid "Apply changes automatically" msgstr "" -#: windows/views.py:2073 windows/views.py:2074 +#: windows/views.py:2087 windows/views.py:2088 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" msgstr "" -#: windows/views.py:2095 -msgid "Next" +#: windows/views.py:2101 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" + +#: windows/views.py:2109 +msgid "First" msgstr "" -#: windows/views.py:2103 +#: windows/views.py:2127 +msgid "Last" +msgstr "" + +#: windows/views.py:2136 msgid "Filters" msgstr "" -#: windows/views.py:2143 +#: windows/views.py:2176 msgid "CTRL+ENTER" msgstr "" -#: windows/views.py:2163 +#: windows/views.py:2196 msgid "Insert row" msgstr "" -#: windows/views.py:2171 +#: windows/views.py:2204 msgid "Data" msgstr "" -#: windows/views.py:2225 windows/views.py:2275 -msgid "New" +#: windows/main/controller.py:278 windows/main/controller.py:287 +#: windows/main/controller.py:288 windows/views.py:2221 +msgid "New query" msgstr "" -#: windows/views.py:2252 -msgid "Query" -msgstr "" - -#: windows/views.py:2272 +#: windows/views.py:2223 windows/views.py:2660 msgid "Close" msgstr "" -#: windows/views.py:2285 -msgid "Query #2" -msgstr "" - -#: windows/views.py:2537 -msgid "Column5" -msgstr "" - -#: windows/views.py:2548 -msgid "Import" +#: windows/main/controller.py:279 windows/main/controller.py:289 +#: windows/main/controller.py:290 windows/views.py:2223 +msgid "Close query" msgstr "" -#: windows/views.py:2573 -msgid "Read only" +#: windows/views.py:2227 +msgid "Run" msgstr "" -#: windows/views.py:2581 -msgid "CASCADED" +#: windows/main/controller.py:280 windows/main/controller.py:292 +#: windows/main/controller.py:293 windows/views.py:2227 +msgid "Execute" msgstr "" -#: windows/views.py:2581 -msgid "CHECK OPTION" +#: windows/views.py:2229 +msgid "Run all" msgstr "" -#: windows/views.py:2613 -msgid "collapsible" +#: windows/main/controller.py:295 windows/views.py:2229 +msgid "Execute all statements" msgstr "" -#: windows/views.py:2629 -msgid "Column3" -msgstr "" - -#: windows/views.py:2630 -msgid "Column4" -msgstr "" - -#: windows/views.py:2673 -msgid "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#: windows/main/controller.py:282 windows/main/controller.py:297 +#: windows/main/controller.py:298 windows/views.py:2231 +msgid "Stop" msgstr "" -#: windows/views.py:2685 -msgid "Port" +#: windows/views.py:2287 +msgid "a page" msgstr "" -#: windows/views.py:2708 -msgid "Usage" +#: windows/views.py:2315 +msgid "Query" msgstr "" -#: windows/views.py:2719 -#, python-format -msgid "%(total_rows)s" +#: windows/views.py:2626 +msgid "Character set" msgstr "" -#: windows/views.py:2724 -msgid "rows total" +#: windows/views.py:2644 windows/views.py:2663 +msgid "New" msgstr "" -#: windows/views.py:2743 -msgid "Lines" +#: windows/views.py:2683 +msgid "Insert record" msgstr "" -#: windows/views.py:2775 -msgid "Temporary" +#: windows/views.py:2688 +msgid "Duplicate record" msgstr "" -#: windows/views.py:2786 -msgid "Engine options" +#: windows/views.py:2695 +msgid "Delete record" msgstr "" -#: windows/views.py:2845 -msgid "RadioBtn" +#: windows/views.py:2733 windows/views.py:2964 +msgid "Up" msgstr "" -#: windows/views.py:2928 -msgid "Edit Column" +#: windows/views.py:2740 windows/views.py:2971 +msgid "Down" msgstr "" -#: windows/views.py:2944 -msgid "Datatype" +#: windows/views.py:3100 +msgid "Save Starments" msgstr "" -#: windows/components/dataview.py:121 windows/views.py:2959 -msgid "Length/Set" +#: windows/views.py:3108 +msgid "Location" msgstr "" -#: windows/components/dataview.py:51 windows/views.py:2998 -msgid "Unsigned" +#: windows/views.py:3115 +msgid "*.sql" msgstr "" #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3004 +#: windows/components/dataview.py:75 msgid "Allow NULL" msgstr "" -#: windows/views.py:3010 -msgid "Zero Fill" +#: windows/components/dataview.py:28 +msgid "Check" msgstr "" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3021 +#: windows/components/dataview.py:78 msgid "Default" msgstr "" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3047 +#: windows/components/dataview.py:82 msgid "Virtuality" msgstr "" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3062 msgid "Expression" msgstr "" -#: windows/components/dataview.py:28 -msgid "Check" +#: windows/components/dataview.py:51 +msgid "Unsigned" msgstr "" #: windows/components/dataview.py:53 @@ -764,6 +797,10 @@ msgstr "" msgid "Data type" msgstr "" +#: windows/components/dataview.py:121 +msgid "Length/Set" +msgstr "" + #: windows/components/dataview.py:155 msgid "Add column\tCTRL+INS" msgstr "" @@ -816,11 +853,11 @@ msgstr "" msgid "On DELETE" msgstr "" -#: windows/components/dataview.py:299 +#: windows/components/dataview.py:298 msgid "Add foreign key" msgstr "" -#: windows/components/dataview.py:305 +#: windows/components/dataview.py:304 msgid "Remove foreign key" msgstr "" @@ -840,75 +877,161 @@ msgstr "" msgid "Text/Expression" msgstr "" -#: windows/dialogs/connections/view.py:119 windows/main/tabs/query.py:376 +#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:394 +#: windows/dialogs/connections/view.py:414 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:407 +#: windows/dialogs/connections/view.py:426 +#, python-brace-format +msgid "Do you want save the connection {connection_name}?" +msgstr "" + +#: windows/dialogs/connections/view.py:429 msgid "Confirm save" msgstr "" -#: windows/dialogs/connections/view.py:459 +#: windows/dialogs/connections/view.py:481 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:461 +#: windows/dialogs/connections/view.py:483 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:736 +#: windows/dialogs/connections/view.py:750 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:775 +#, python-brace-format +msgid "" +"Connection error:\n" +"{error}" +msgstr "" + +#: windows/dialogs/connections/view.py:776 msgid "Connection error" msgstr "" -#: windows/dialogs/connections/view.py:788 -#: windows/dialogs/connections/view.py:803 +#: windows/dialogs/connections/view.py:802 +#, python-brace-format +msgid "Do you want to delete the connection '{connection_name}'?" +msgstr "" + +#: windows/dialogs/connections/view.py:805 +#: windows/dialogs/connections/view.py:822 msgid "Confirm delete" msgstr "" -#: windows/main/controller.py:172 +#: windows/dialogs/connections/view.py:819 +#, python-brace-format +msgid "Do you want to delete the directory '{directory_name}'?" +msgstr "" + +#: windows/main/controller.py:275 +#, python-brace-format +msgid "{text} ({shortcut})" +msgstr "" + +#: windows/main/controller.py:281 windows/main/controller.py:294 +msgid "Execute all" +msgstr "" + +#: windows/main/controller.py:471 windows/main/controller.py:479 +msgid "Query (1)" +msgstr "" + +#: windows/main/controller.py:497 +#, python-brace-format +msgid "Query ({query_number})" +msgstr "" + +#: windows/main/controller.py:530 +msgid "You have unsaved changes. Save before closing?" +msgstr "" + +#: windows/main/controller.py:531 +msgid "Unsaved query" +msgstr "" + +#: windows/main/controller.py:576 +msgid "Save query" +msgstr "" + +#: windows/main/controller.py:579 +msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" +msgstr "" + +#: windows/main/controller.py:616 windows/main/controller.py:642 +#: windows/main/database/list.py:84 windows/main/database/view.py:256 +#: windows/main/database/view.py:282 windows/main/query/controller.py:177 +msgid "Error" +msgstr "" + +#: windows/main/controller.py:622 +#, python-brace-format +msgid "-- Saved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:647 +#, python-brace-format +msgid "-- Autosaved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:704 msgid "days" msgstr "" -#: windows/main/controller.py:173 +#: windows/main/controller.py:705 msgid "hours" msgstr "" -#: windows/main/controller.py:174 +#: windows/main/controller.py:706 msgid "minutes" msgstr "" -#: windows/main/controller.py:175 +#: windows/main/controller.py:707 msgid "seconds" msgstr "" -#: windows/main/controller.py:183 +#: windows/main/controller.py:715 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "" -#: windows/main/controller.py:219 +#: windows/main/controller.py:751 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:298 +#: windows/main/controller.py:952 +#, python-brace-format +msgid "~{estimated} (Loading...)" +msgstr "" + +#: windows/main/controller.py:954 +msgid "~ (Loading...)" +msgstr "" + +#: windows/main/controller.py:1119 msgid "Version" msgstr "" -#: windows/main/controller.py:300 +#: windows/main/controller.py:1121 msgid "Uptime" msgstr "" -#: windows/main/controller.py:399 +#: windows/main/controller.py:1199 +#, python-brace-format +msgid "Do you want discard the change to {database_name}?" +msgstr "" + +#: windows/main/controller.py:1232 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -918,131 +1041,169 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:404 windows/main/controller.py:425 +#: windows/main/controller.py:1237 windows/main/controller.py:1258 msgid "Delete database" msgstr "" -#: windows/main/controller.py:410 +#: windows/main/controller.py:1243 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:411 +#: windows/main/controller.py:1244 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:424 +#: windows/main/controller.py:1257 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:439 +#: windows/main/controller.py:1272 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:440 windows/main/tabs/view.py:253 -#: windows/main/tabs/view.py:279 +#: windows/main/controller.py:1273 windows/main/database/view.py:253 +#: windows/main/database/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:582 +#: windows/main/controller.py:1392 +#, python-brace-format +msgid "Do you want discard the change to {table_name}?" +msgstr "" + +#: windows/main/controller.py:1418 +#, python-brace-format +msgid "Do you want delete the table {table_name}?" +msgstr "" + +#: windows/main/controller.py:1421 msgid "Delete table" msgstr "" -#: windows/main/controller.py:699 +#: windows/main/controller.py:1440 +#, python-brace-format +msgid "{table_name} (COPY)" +msgstr "" + +#: windows/main/controller.py:1563 msgid "Do you want delete the records?" msgstr "" -#: windows/main/tabs/database.py:71 +#: windows/main/database/list.py:69 msgid "The connection to the database was lost." msgstr "" -#: windows/main/tabs/database.py:73 +#: windows/main/database/list.py:71 msgid "Do you want to reconnect?" msgstr "" -#: windows/main/tabs/database.py:75 +#: windows/main/database/list.py:73 msgid "Connection lost" msgstr "" -#: windows/main/tabs/database.py:85 +#: windows/main/database/list.py:83 msgid "Reconnection failed:" msgstr "" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:450 -#: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 -msgid "Error" +#: windows/main/database/view.py:252 +msgid "View created successfully" msgstr "" -#: windows/main/tabs/query.py:305 -#, python-brace-format -msgid "{} rows affected" +#: windows/main/database/view.py:252 +msgid "View updated successfully" msgstr "" -#: windows/main/tabs/query.py:309 windows/main/tabs/query.py:331 +#: windows/main/database/view.py:256 #, python-brace-format -msgid "Query {}" +msgid "Error saving view: {}" msgstr "" -#: windows/main/tabs/query.py:314 +#: windows/main/database/view.py:269 #, python-brace-format -msgid "Query {} (Error)" +msgid "Are you sure you want to delete view '{}'?" +msgstr "" + +#: windows/main/database/view.py:270 +msgid "Confirm Delete" +msgstr "" + +#: windows/main/database/view.py:279 +msgid "View deleted successfully" msgstr "" -#: windows/main/tabs/query.py:326 +#: windows/main/database/view.py:282 #, python-brace-format -msgid "Query {} ({} rows × {} cols)" +msgid "Error deleting view: {}" msgstr "" -#: windows/main/tabs/query.py:353 +#: windows/main/query/controller.py:110 #, python-brace-format -msgid "{} rows" +msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/tabs/query.py:355 +#: windows/main/query/controller.py:112 #, python-brace-format -msgid "{:.1f} ms" +msgid "{elapsed_s:.2f} s" msgstr "" -#: windows/main/tabs/query.py:358 +#: windows/main/query/controller.py:115 +msgid "none" +msgstr "" + +#: windows/main/query/controller.py:121 #, python-brace-format -msgid "{} warnings" +msgid "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." msgstr "" -#: windows/main/tabs/query.py:370 -msgid "Error:" +#: windows/main/query/controller.py:134 +msgid "Query execution cancelled" msgstr "" -#: windows/main/tabs/query.py:449 +#: windows/main/query/controller.py:176 msgid "No active database connection" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View created successfully" +#: windows/main/query/renderer.py:53 +#, python-brace-format +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View updated successfully" +#: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 +#, python-brace-format +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/view.py:256 +#: windows/main/query/renderer.py:65 #, python-brace-format -msgid "Error saving view: {}" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/view.py:269 +#: windows/main/query/renderer.py:79 #, python-brace-format -msgid "Are you sure you want to delete view '{}'?" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/view.py:270 -msgid "Confirm Delete" +#: windows/main/query/renderer.py:165 +#, python-brace-format +msgid "{rows_count} rows" msgstr "" -#: windows/main/tabs/view.py:279 -msgid "View deleted successfully" +#: windows/main/query/renderer.py:167 +#, python-brace-format +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/view.py:282 +#: windows/main/query/renderer.py:171 #, python-brace-format -msgid "Error deleting view: {}" +msgid "{warnings_count} warnings" +msgstr "" + +#: windows/main/query/renderer.py:186 +msgid "Error:" msgstr "" diff --git a/main.py b/main.py index 0fee084..325e4f3 100755 --- a/main.py +++ b/main.py @@ -11,9 +11,7 @@ from helpers.loader import Loader from helpers.logger import logger -from helpers.observables import ObservableObject - -from windows.dialogs.settings.repository import SettingsRepository +from helpers.settings import Settings, SettingsRepository from windows.components.stc.styles import apply_stc_theme, set_theme_loader from windows.components.stc.themes import ThemeManager @@ -26,7 +24,7 @@ class PeterSQL(wx.App): locale: wx.Locale = wx.Locale() settings_repository = SettingsRepository(WORKDIR / "settings.yml") - settings: ObservableObject = settings_repository.load() + settings: Settings = settings_repository.load() main_frame: wx.Frame = None @@ -53,7 +51,7 @@ def OnInit(self) -> bool: return True def _init_theme_loader(self) -> None: - theme_name = self.settings.get_value("theme", "current") or "petersql" + theme_name = self.settings.get_value("ui", "appearance", "theme", default="petersql") self.theme_loader = ThemeLoader(WORKDIR / "themes") try: self.theme_loader.load_theme(theme_name) @@ -64,10 +62,7 @@ def _init_theme_loader(self) -> None: logger.error(f"Error loading theme: {ex}", exc_info=True) def _init_locale(self): - _locale = self.settings.get_value("locale") - - if _locale is None: - _locale, encoding = locale.getdefaultlocale() + _locale = self.settings.get_value("language", default="en_US") translation = gettext.translation( 'petersql', @@ -101,16 +96,12 @@ def open_main_frame(self) -> None: from windows.main.controller import MainFrameController self.main_frame = MainFrameController() - size = wx.Size( - *list(map(int, self.settings.get_value("window", "size").split(","))) - ) + size_values = self.settings.get_value("ui", "window", "size", default=[1920, 1080]) + size = wx.Size(*list(map(int, size_values))) self.main_frame.SetSize(width=size.width, height=size.height) - position = wx.Point( - *list( - map(int, self.settings.get_value("window", "position").split(",")) - ) - ) + position_values = self.settings.get_value("ui", "window", "position", default=[0, 0]) + position = wx.Point(*list(map(int, position_values))) self.main_frame.SetPosition(position) self.main_frame.Layout() self.main_frame.SetIcon( @@ -125,13 +116,13 @@ def open_main_frame(self) -> None: def _on_size(self, event: wx.SizeEvent) -> None: size = event.GetSize() - self.settings.set_value("window", "size", value=f"{size.Width},{size.Height}") + self.settings.set_value("ui", "window", "size", value=[size.Width, size.Height]) self.main_frame.Layout() def _on_move(self, event: wx.MouseEvent) -> None: position = event.GetPosition() self.settings.set_value( - "window", "position", value=f"{position.x},{position.y}" + "ui", "window", "position", value=[position.x, position.y] ) self.main_frame.Layout() diff --git a/settings.yml b/settings.yml index 9f18920..bb68983 100755 --- a/settings.yml +++ b/settings.yml @@ -1,32 +1,51 @@ -window: - size: 1920,1048 - position: 0,0 -appearance: - theme: petersql - mode: auto -shortcuts: - autocomplete: - force_show: Ctrl+Space - complete: Tab,Enter - cancel: Escape - query_editor: - execute: F5 - execute_selection: Ctrl+Enter -settings: +language: en_US +ui: + window: + size: + - 1769 + - 967 + position: + - 26 + - 23 + appearance: + theme: petersql + mode: auto + dialogs: + connections: + expanded_directories: + - - 0 + - - 3 + - - 18 + shortcuts: + query: + execute_current: Ctrl+Enter + execute_all: Ctrl+Shift+Enter + stop: Esc + new_query: Ctrl+T + close_query: Ctrl+W + save: Ctrl+S + save_as: Ctrl+Shift+S +editor: + statement_separator: ; + trim_whitespace: false + execute_selected_only: false + autoformat: true autocomplete: + enabled: true debounce_ms: 80 min_prefix_length: 1 add_space_after_completion: true popup_width: 300 popup_max_height: 10 -language: en_US -query_editor: - statement_separator: ; - trim_whitespace: false - execute_selected_only: false - autocomplete: true - autoformat: true -advanced: + shortcuts: + execute: F5 + execute_selection: Ctrl+Enter + force_autocomplete: Ctrl+Space + complete: Tab,Enter + cancel: Escape +runtime: connection_timeout: 10 query_timeout: 10 logging_level: INFO +records: + limit: 100 diff --git a/structures/configurations.py b/structures/configurations.py index 9bcca82..f071b3b 100644 --- a/structures/configurations.py +++ b/structures/configurations.py @@ -1,4 +1,4 @@ -from typing import NamedTuple, Optional +from typing import NamedTuple, Optional, Union class CredentialsConfiguration(NamedTuple): @@ -6,7 +6,9 @@ class CredentialsConfiguration(NamedTuple): username: str password: Optional[str] port: int - use_tls_enabled: bool = False + use_tls: bool = False + connect_timeout: int = 10 + compressed_protocol: bool = False class SourceConfiguration(NamedTuple): @@ -24,7 +26,7 @@ class SSHTunnelConfiguration(NamedTuple): remote_host: Optional[str] = None remote_port: Optional[int] = None identity_file: Optional[str] = None - extra_args: Optional[list[str]] = None + extra_args: Optional[Union[str, list[str]]] = None @property def is_enabled(self) -> bool: diff --git a/structures/engines/context.py b/structures/engines/context.py index 1728107..922345d 100755 --- a/structures/engines/context.py +++ b/structures/engines/context.py @@ -33,6 +33,8 @@ class AbstractContext(abc.ABC): + """Base context API for SQL engines.""" + _connection: Any = None _cursor: Any = None _ssh_tunnel: Optional[SSHTunnel] = None @@ -43,6 +45,8 @@ class AbstractContext(abc.ABC): DATATYPE: StandardDataType INDEXTYPE: StandardIndexType COLLATIONS: dict[str, str] = {} + ROW_FORMATS: list[str] = [] + server_version: str = "" IDENTIFIER_QUOTE_CHAR: str = '"' DEFAULT_STATEMENT_SEPARATOR: str = ";" @@ -50,32 +54,29 @@ class AbstractContext(abc.ABC): databases: ObservableLazyList[SQLDatabase] def __init__(self, connection: Connection): + """Initialize the context with the selected connection.""" self.connection = connection self.databases = ObservableLazyList(self.get_databases) def __del__(self): + """Ensure resources are released during object destruction.""" with contextlib.suppress(Exception): self.disconnect() def before_connect(self, *args, **kwargs): + """Prepare transport details before opening the DB connection.""" # SSH tunnel support via connection configuration if hasattr(self.connection, "ssh_tunnel") and self.connection.ssh_tunnel: ssh_config = self.connection.ssh_tunnel if not ssh_config.is_enabled: return - base_host = getattr(self, "_base_host", getattr(self, "host", "127.0.0.1")) - base_port = getattr(self, "_base_port", getattr(self, "port", 0)) - self._base_host = base_host - self._base_port = base_port + self._base_host = getattr(self, "_base_host", getattr(self, "host", "127.0.0.1")) + self._base_port = getattr(self, "_base_port", getattr(self, "port", 0)) - remote_host = getattr(ssh_config, "remote_host", None) or getattr( - self, "_base_host", "127.0.0.1" - ) - remote_port = int( - getattr(ssh_config, "remote_port", 0) or getattr(self, "_base_port", 0) - ) + remote_host = getattr(ssh_config, "remote_host", None) or self._base_host + remote_port = int(getattr(ssh_config, "remote_port", 0) or self._base_port) local_port = int(getattr(ssh_config, "local_port", 0) or 0) logger.debug( "Preparing DB SSH tunnel: connection=%s engine=%s base=%s:%s remote=%s:%s requested_local_port=%s", @@ -110,9 +111,11 @@ def before_connect(self, *args, **kwargs): ) def after_connect(self, *args, **kwargs): + """Run engine-specific setup right after a successful connection.""" pass def before_disconnect(self, *args, **kwargs): + """Release pre-disconnect resources and restore base host settings.""" if self._ssh_tunnel is not None: logger.debug( "Stopping DB SSH tunnel for connection=%s", @@ -128,10 +131,12 @@ def before_disconnect(self, *args, **kwargs): self.port = self._base_port def after_disconnect(self): + """Run engine-specific cleanup after disconnection.""" pass @staticmethod def _extract_spec_names(values: Any) -> list[str]: + """Extract normalized names from spec values.""" if not isinstance(values, list): return [] @@ -149,6 +154,7 @@ def _extract_spec_names(values: Any) -> list[str]: @staticmethod def _load_yaml_file(path: str) -> dict[str, Any]: + """Load a YAML file from the project workspace.""" file_path = WORKDIR / path if not file_path.exists(): return {} @@ -163,8 +169,9 @@ def _load_yaml_file(path: str) -> dict[str, Any]: @staticmethod def _merge_spec_values( - base_values: list[str], add_values: list[str], remove_values: list[str] + base_values: list[str], add_values: list[str], remove_values: list[str] ) -> list[str]: + """Merge additions and removals into a base vocabulary list.""" removed = {value.upper() for value in remove_values} merged = [value for value in base_values if value.upper() not in removed] @@ -179,6 +186,7 @@ def _merge_spec_values( @staticmethod def _extract_major(version: Optional[str]) -> str: + """Extract the first numeric major version from a version string.""" if not version: return "" @@ -189,13 +197,14 @@ def _extract_major(version: Optional[str]) -> str: @staticmethod def _select_version_spec( - versions_map: dict[str, Any], major_version: str + versions_map: dict[str, Any], major_version: str ) -> dict[str, Any]: + """Select the most suitable version spec for a target major version.""" if not versions_map: return {} if major_version in versions_map and isinstance( - versions_map[major_version], dict + versions_map[major_version], dict ): return versions_map[major_version] @@ -223,8 +232,9 @@ def _select_version_spec( return selected_spec def get_engine_vocabulary( - self, engine: str, server_version: Optional[str] + self, engine: str, server_version: Optional[str] ) -> tuple[tuple[str, ...], tuple[str, ...]]: + """Build engine keywords and functions from shared and engine specs.""" global_spec = self._load_yaml_file("structures/engines/specification.yaml") engine_spec = self._load_yaml_file( f"structures/engines/{engine}/specification.yaml" @@ -280,16 +290,19 @@ def get_engine_vocabulary( @property def is_connected(self): + """Return True when both connection and cursor are available.""" return self._connection is not None and self._cursor is not None @property def cursor(self) -> Any: + """Return the active cursor or raise when not connected.""" if self._cursor is None: raise RuntimeError("Not connected to the database. Call connect() first.") return self._cursor @staticmethod def get_temporary_id(container: list[SQLTypeAlias]) -> int: + """Generate a temporary negative identifier for new objects.""" return min([0] + [t.id for t in container]) - 1 @abc.abstractmethod @@ -299,126 +312,154 @@ def connect(self, **connect_kwargs) -> None: @abc.abstractmethod def set_database(self, database: SQLDatabase) -> None: + """Select the active database for subsequent operations.""" raise NotImplementedError @abc.abstractmethod def get_server_version(self) -> str: + """Return the database server version string.""" raise NotImplementedError @abc.abstractmethod def get_server_uptime(self) -> Optional[int]: + """Return the server uptime in seconds when available.""" raise NotImplementedError @abc.abstractmethod def get_databases(self) -> list[SQLDatabase]: + """Return all databases visible to the current connection.""" raise NotImplementedError @abc.abstractmethod def get_views(self, database: SQLDatabase) -> list[SQLView]: + """Return views for the given database.""" raise NotImplementedError @abc.abstractmethod def get_triggers(self, database: SQLDatabase) -> list[SQLTrigger]: + """Return triggers for the given database.""" raise NotImplementedError @abc.abstractmethod def get_tables(self, database: SQLDatabase) -> list[SQLTable]: + """Return tables for the given database.""" raise NotImplementedError @abc.abstractmethod def get_columns(self, table: SQLTable) -> list[SQLColumn]: + """Return columns for the given table.""" raise NotImplementedError @abc.abstractmethod def get_indexes(self, table: SQLTable) -> list[SQLIndex]: + """Return indexes for the given table.""" raise NotImplementedError @abc.abstractmethod def get_foreign_keys(self, table: SQLTable) -> list[SQLForeignKey]: + """Return foreign keys for the given table.""" raise NotImplementedError @abc.abstractmethod def build_empty_table( - self, database: SQLDatabase, /, name: Optional[str] = None, **default_values + self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> SQLTable: + """Build a new in-memory table model with default values.""" raise NotImplementedError @abc.abstractmethod def build_empty_column( - self, - table: SQLTable, - datatype: SQLDataType, - /, - name: Optional[str] = None, - **default_values, + self, + table: SQLTable, + datatype: SQLDataType, + /, + name: Optional[str] = None, + **default_values, ) -> SQLColumn: + """Build a new in-memory column model with default values.""" raise NotImplementedError @abc.abstractmethod def build_empty_index( - self, - table: SQLTable, - indextype: SQLIndexType, - columns: list[str], - /, - name: Optional[str] = None, - **default_values, + self, + table: SQLTable, + indextype: SQLIndexType, + columns: list[str], + /, + name: Optional[str] = None, + **default_values, ) -> SQLIndex: + """Build a new in-memory index model with default values.""" raise NotImplementedError @abc.abstractmethod def build_empty_check( - self, - table: SQLTable, - /, - name: Optional[str] = None, - expression: Optional[str] = None, - **default_values, + self, + table: SQLTable, + /, + name: Optional[str] = None, + expression: Optional[str] = None, + **default_values, ) -> SQLCheck: + """Build a new in-memory check constraint model.""" raise NotImplementedError @abc.abstractmethod def build_empty_foreign_key( - self, - table: SQLTable, - columns: list[str], - /, - name: Optional[str] = None, - **default_values, + self, + table: SQLTable, + columns: list[str], + /, + name: Optional[str] = None, + **default_values, ) -> SQLForeignKey: + """Build a new in-memory foreign key model.""" raise NotImplementedError @abc.abstractmethod def build_empty_record( - self, table: SQLTable, /, *, values: dict[str, Any] + self, table: SQLTable, /, *, values: dict[str, Any] ) -> SQLRecord: + """Build a new in-memory record model.""" raise NotImplementedError @abc.abstractmethod def build_empty_view( - self, database: SQLDatabase, /, name: Optional[str] = None, **default_values + self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> SQLView: + """Build a new in-memory view model.""" raise NotImplementedError @abc.abstractmethod def build_empty_function( - self, database: SQLDatabase, /, name: Optional[str] = None, **default_values + self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> "SQLFunction": + """Build a new in-memory function model.""" raise NotImplementedError @abc.abstractmethod def build_empty_procedure( - self, database: SQLDatabase, /, name: Optional[str] = None, **default_values + self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> "SQLProcedure": + """Build a new in-memory procedure model.""" raise NotImplementedError @abc.abstractmethod def build_empty_trigger( - self, database: SQLDatabase, /, name: Optional[str] = None, **default_values + self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> SQLTrigger: + """Build a new in-memory trigger model.""" + raise NotImplementedError + + @abc.abstractmethod + def get_result_column_datatypes( + self, cursor: Any + ) -> list[Optional[SQLDataType]]: + """Infer SQL data types for result columns from a driver cursor.""" raise NotImplementedError def quote_identifier(self, name: str) -> str: + """Quote an SQL identifier only when needed.""" value = name.strip() if not value: assert False, "Invalid identifier name: %s" % name @@ -432,18 +473,20 @@ def quote_identifier(self, name: str) -> str: return f"{self.IDENTIFIER_QUOTE_CHAR}{escaped_name}{self.IDENTIFIER_QUOTE_CHAR}" def qualify(self, *parts): + """Build a qualified SQL identifier from multiple parts.""" return ".".join(self.quote_identifier(part) for part in parts) def get_records( - self, - table: SQLTable, - /, - *, - filters: Optional[str] = None, - limit: int = 1000, - offset: int = 0, - orders: Optional[str] = None, + self, + table: SQLTable, + /, + *, + filters: Optional[str] = None, + limit: int = 1000, + offset: int = 0, + orders: Optional[str] = None, ) -> list[dict[str, Any]]: + """Fetch records from a table using optional filtering and pagination.""" logger.debug(f"get records for table={table.name}") QUERY_LOGS.append(f"/* get_records for table={table.name} */") if table is None or table.is_new: @@ -479,6 +522,7 @@ def get_records( # EXECUTION def execute(self, query: str) -> bool: + """Execute a SQL query and append it to query logs.""" query_clean = re.sub(r"\s+", " ", str(query)).strip() logger.debug("execute query: %s", query_clean) QUERY_LOGS.append(query_clean) @@ -493,6 +537,7 @@ def execute(self, query: str) -> bool: return True def fetchone(self) -> Any: + """Fetch a single row from the active cursor.""" try: return self.cursor.fetchone() except Exception as ex: @@ -500,6 +545,7 @@ def fetchone(self) -> Any: raise def fetchall(self) -> list[Any]: + """Fetch all rows from the active cursor.""" try: return self.cursor.fetchall() except Exception as ex: @@ -507,6 +553,7 @@ def fetchall(self) -> list[Any]: raise def disconnect(self) -> None: + """Close cursor and connection resources safely.""" self.before_disconnect() if self._cursor is not None: @@ -523,6 +570,7 @@ def disconnect(self) -> None: @contextlib.contextmanager def transaction(self): + """Provide a simple BEGIN/COMMIT/ROLLBACK transaction scope.""" try: self.execute("BEGIN") yield self diff --git a/structures/engines/database.py b/structures/engines/database.py index 70802dc..4238062 100755 --- a/structures/engines/database.py +++ b/structures/engines/database.py @@ -578,6 +578,12 @@ def __eq__(self, other: object) -> bool: return self.values == other.values + def copy(self): + cls = self.__class__ + field_values = {f.name: getattr(self, f.name) for f in dataclasses.fields(cls)} + field_values["values"] = dict(field_values["values"]) + return cls(**field_values) + def __str__(self) -> str: return f"{self.__class__.__name__}(id={self.id}, table={self.table.name}, values={self.values})" diff --git a/structures/engines/mariadb/context.py b/structures/engines/mariadb/context.py index af4284b..b459c06 100755 --- a/structures/engines/mariadb/context.py +++ b/structures/engines/mariadb/context.py @@ -6,6 +6,8 @@ import pymysql +from pymysql.constants import FIELD_TYPE + from helpers.logger import logger from structures.connection import Connection @@ -44,6 +46,8 @@ class MariaDBContext(AbstractContext): IDENTIFIER_QUOTE_CHAR = "`" DEFAULT_STATEMENT_SEPARATOR = ";" + ROW_FORMATS: list[str] = ["DEFAULT", "DYNAMIC", "FIXED", "COMPRESSED", "REDUNDANT", "COMPACT"] + def __init__(self, connection: Connection): super().__init__(connection) @@ -66,9 +70,9 @@ def after_connect(self, *args, **kwargs): self.execute("""SHOW ENGINES;""") self.ENGINES = [dict(row).get("Engine") for row in self.fetchall()] - server_version = self.get_server_version() + self.server_version = self.get_server_version() self.KEYWORDS, builtin_functions = self.get_engine_vocabulary( - "mariadb", server_version + "mariadb", self.server_version ) self.execute(""" @@ -81,6 +85,12 @@ def after_connect(self, *args, **kwargs): self.FUNCTIONS = tuple(dict.fromkeys(builtin_functions + user_functions)) def _parse_type(self, column_type: str): + """Parse a raw COLUMN_TYPE string from information_schema into structured field attributes. + + Used in get_columns() to extract length, precision, scale, set values, and flags + from DDL-style strings such as 'varchar(255)', 'decimal(10,2)', or 'enum('a','b')'. + Returns an empty dict when no pattern matches. + """ types = MariaDBDataType.get_all() type_set = [ x.lower() @@ -116,13 +126,72 @@ def _parse_type(self, column_type: str): return dict() + def _get_field_type_name(self, type_code: Optional[int]) -> Optional[str]: + """Resolve a pymysql FIELD_TYPE integer code to its constant name. + + Used in get_result_column_datatypes() to bridge the driver's numeric type + representation to a named string (e.g. 253 -> 'VAR_STRING'). + Returns None when the code is absent or unrecognised. + """ + if type_code is None: + return None + + for name, value in vars(FIELD_TYPE).items(): + if not name.isupper() or not isinstance(value, int): + continue + + if value == type_code: + return name + + return None + + def get_result_column_datatypes( + self, cursor: pymysql.cursors.Cursor + ) -> list[Optional[SQLDataType]]: + """Map each result column to its SQLDataType using the pymysql cursor description. + + Resolves the driver's numeric type code via _get_field_type_name(), then looks up + the matching SQLDataType by name. Returns None for columns whose type cannot be resolved. + Unlike _parse_type(), this operates on query result metadata, not on DDL column strings. + """ + datatypes: list[Optional[SQLDataType]] = [] + + for description in cursor.description or []: + type_code = description[1] if len(description) > 1 else None + datatype_name = self._get_field_type_name(type_code) + + if datatype_name is None: + datatypes.append(None) + continue + + try: + datatypes.append(self.DATATYPE.get_by_name(datatype_name)) + except Exception: + datatypes.append(None) + + return datatypes + def connect(self, **connect_kwargs) -> None: - if self._connection is None: - self.before_connect() + skip_after_connect = bool(connect_kwargs.pop("skip_after_connect", False)) + skip_before_connect = bool(connect_kwargs.pop("skip_before_connect", False)) - use_tls_enabled = bool( - getattr(self.connection.configuration, "use_tls_enabled", False) + if self._connection is None: + if not skip_before_connect: + self.before_connect() + + connect_timeout_override = connect_kwargs.pop("connect_timeout", None) + compressed_protocol_override = connect_kwargs.pop("compress", None) + compressed_protocol = bool( + compressed_protocol_override + if compressed_protocol_override is not None + else getattr(self.connection.configuration, "compressed_protocol", False) ) + connect_timeout = ( + int(connect_timeout_override) + if connect_timeout_override is not None + else int(getattr(self.connection.configuration, "connect_timeout", 10)) + ) + use_tls = bool(getattr(self.connection.configuration, "use_tls", False)) try: base_kwargs = dict( @@ -131,37 +200,24 @@ def connect(self, **connect_kwargs) -> None: password=self.password, cursorclass=pymysql.cursors.DictCursor, port=self.port, + connect_timeout=connect_timeout, + compress=compressed_protocol, **connect_kwargs, ) - if use_tls_enabled: + if use_tls: base_kwargs["ssl"] = { "cert_reqs": ssl.CERT_NONE, "check_hostname": False, } logger.debug( - "MariaDB connect target host=%s port=%s user=%s use_tls_enabled=%s", + "MariaDB connect target host=%s port=%s user=%s compressed_protocol=%s connect_timeout=%s use_tls=%s", base_kwargs.get("host"), base_kwargs.get("port"), base_kwargs.get("user"), - use_tls_enabled, + compressed_protocol, + connect_timeout, + use_tls, ) - # - # # SSH tunnel support via connection configuration - # if hasattr(self.connection, 'ssh_tunnel') and self.connection.ssh_tunnel: - # ssh_config = self.connection.ssh_tunnel - # self._ssh_tunnel = SSHTunnel( - # ssh_config.hostname, int(ssh_config.port), - # ssh_username=ssh_config.username, - # ssh_password=ssh_config.password, - # remote_port=self.port, - # local_bind_address=(self.host, int(getattr(ssh_config, 'local_port', 0))), - # extra_args=ssh_config.extra_args - # ) - # self._ssh_tunnel.start() - # base_kwargs.update( - # host=self.host, - # port=self._ssh_tunnel.local_port, - # ) self._connection = pymysql.connect(**base_kwargs) self._cursor = self._connection.cursor() @@ -190,7 +246,7 @@ def connect(self, **connect_kwargs) -> None: if hasattr(self.connection, "configuration"): self.connection.configuration = ( - self.connection.configuration._replace(use_tls_enabled=True) + self.connection.configuration._replace(use_tls=True) ) logger.info( "MariaDB connection succeeded after enabling TLS automatically." @@ -199,33 +255,9 @@ def connect(self, **connect_kwargs) -> None: logger.error(f"Failed to connect to MariaDB: {e}", exc_info=True) raise - if self._cursor is not None: + if self._cursor is not None and not skip_after_connect: self.after_connect() - def disconnect(self) -> None: - """Disconnect from database and stop SSH tunnel if active.""" - try: - if self._cursor: - self._cursor.close() - except Exception: - pass - - try: - if self._connection: - self._connection.close() - except Exception: - pass - - try: - if self._ssh_tunnel: - self._ssh_tunnel.stop() - self._ssh_tunnel = None - except Exception: - pass - - self._cursor = None - self._connection = None - def set_database(self, database: SQLDatabase) -> None: self.execute(f"USE {database.quoted_name}") @@ -336,7 +368,7 @@ def get_tables(self, database: SQLDatabase) -> list[SQLTable]: QUERY_LOGS.append(f"/* get_tables for database={database.name} */") self.execute(f""" - SELECT TABLE_NAME, ENGINE, TABLE_COLLATION, TABLE_ROWS, AUTO_INCREMENT, + SELECT TABLE_NAME, ENGINE, TABLE_COLLATION, TABLE_ROWS, AUTO_INCREMENT, ROW_FORMAT, CREATE_TIME, UPDATE_TIME, ROUND(DATA_LENGTH + INDEX_LENGTH, 2) as total_bytes FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{database.name}' @@ -353,6 +385,7 @@ def get_tables(self, database: SQLDatabase) -> list[SQLTable]: database=database, engine=row["ENGINE"], collation_name=row["TABLE_COLLATION"], + row_format=row["ROW_FORMAT"], auto_increment=int(row["AUTO_INCREMENT"] or 0), total_bytes=row["total_bytes"], total_rows=row["TABLE_ROWS"], @@ -575,7 +608,7 @@ def build_empty_table( id = MariaDBContext.get_temporary_id(database.tables) if name is None: - name = _(f"Table{str(id * -1):03}") + name = _("Table{table_index:03}").format(table_index=id * -1) default_values.setdefault("engine", "InnoDB") default_values.setdefault("collation_name", "utf8mb4_general_ci") @@ -603,7 +636,7 @@ def build_empty_column( id = MariaDBContext.get_temporary_id(table.columns) if name is None: - name = _(f"Column{str(id * -1):03}") + name = _("Column{column_index:03}").format(column_index=id * -1) return MariaDBColumn( id=id, name=name, table=table, datatype=datatype, **default_values @@ -621,7 +654,7 @@ def build_empty_index( id = MariaDBContext.get_temporary_id(table.indexes) if name is None: - name = _(f"Index{str(id * -1):03}") + name = _("Index{index_number:03}").format(index_number=id * -1) return MariaDBIndex( id=id, @@ -661,7 +694,9 @@ def build_empty_foreign_key( id = MariaDBContext.get_temporary_id(table.foreign_keys) if name is None: - name = _(f"ForeignKey{str(id * -1):03}") + name = _("ForeignKey{foreign_key_number:03}").format( + foreign_key_number=id * -1 + ) reference_table = default_values.get("reference_table", "") reference_columns = default_values.get("reference_columns", []) @@ -692,7 +727,7 @@ def build_empty_view( id = MariaDBContext.get_temporary_id(database.views) if name is None: - name = _(f"View{str(id * -1):03}") + name = _("View{view_index:03}").format(view_index=id * -1) return MariaDBView( id=id, @@ -743,7 +778,7 @@ def build_empty_trigger( id = MariaDBContext.get_temporary_id(database.triggers) if name is None: - name = _(f"Trigger{str(id * -1):03}") + name = _("Trigger{trigger_index:03}").format(trigger_index=id * -1) return MariaDBTrigger( id=id, diff --git a/structures/engines/mariadb/database.py b/structures/engines/mariadb/database.py index d383b8a..5a52634 100644 --- a/structures/engines/mariadb/database.py +++ b/structures/engines/mariadb/database.py @@ -57,6 +57,9 @@ def drop(self) -> bool: @dataclasses.dataclass(eq=False) class MariaDBTable(SQLTable): + row_format: Optional[str] = None + convert_data: bool = dataclasses.field(default=False, compare=False) + def raw_create(self) -> str: columns = [str(MariaDBColumnBuilder(column)) for column in self.columns] @@ -90,6 +93,12 @@ def alter_engine(self, engine: str): return True + def alter_row_format(self, row_format: str): + statement = f"ALTER TABLE {self.fully_qualified_name} ROW_FORMAT={row_format};" + self.database.context.execute(statement) + + return True + def rename(self, table: Self, new_name: str) -> bool: statement = f"ALTER TABLE {table.fully_qualified_name} RENAME TO `{new_name}`;" self.database.context.execute(statement) @@ -140,9 +149,11 @@ def alter(self) -> bool: if self.auto_increment != original_table.auto_increment: original_table.alter_auto_increment(self.auto_increment) if self.collation_name != original_table.collation_name: - original_table.alter_collation(self.collation_name) + original_table.alter_collation(self.collation_name, convert=self.convert_data) if self.engine != original_table.engine: original_table.alter_engine(self.engine) + if self.row_format != original_table.row_format and self.row_format: + original_table.alter_row_format(self.row_format) for i, (original, current) in enumerate(map_columns): if original is None: diff --git a/structures/engines/mariadb/datatype.py b/structures/engines/mariadb/datatype.py index bb399f7..4060773 100755 --- a/structures/engines/mariadb/datatype.py +++ b/structures/engines/mariadb/datatype.py @@ -3,20 +3,20 @@ class MariaDBDataType(StandardDataType): # Integer types - TINYINT = SQLDataType(name="TINYINT", category=DataTypeCategory.INTEGER, has_unsigned=True) - SMALLINT = SQLDataType(name="SMALLINT", category=DataTypeCategory.INTEGER, has_unsigned=True) - MEDIUMINT = SQLDataType(name="MEDIUMINT", category=DataTypeCategory.INTEGER, has_unsigned=True) - INT = SQLDataType(name="INT", category=DataTypeCategory.INTEGER, alias=["INTEGER"], has_unsigned=True) - BIGINT = SQLDataType(name="BIGINT", category=DataTypeCategory.INTEGER, has_unsigned=True) + TINYINT = SQLDataType(name="TINYINT", category=DataTypeCategory.INTEGER, alias=["TINY"], has_unsigned=True) + SMALLINT = SQLDataType(name="SMALLINT", category=DataTypeCategory.INTEGER, alias=["SHORT"], has_unsigned=True) + MEDIUMINT = SQLDataType(name="MEDIUMINT", category=DataTypeCategory.INTEGER, alias=["INT24"], has_unsigned=True) + INT = SQLDataType(name="INT", category=DataTypeCategory.INTEGER, alias=["INTEGER", "LONG"], has_unsigned=True) + BIGINT = SQLDataType(name="BIGINT", category=DataTypeCategory.INTEGER, alias=["LONGLONG"], has_unsigned=True) # Floating point FLOAT = SQLDataType(name="FLOAT", category=DataTypeCategory.REAL, has_precision=True) DOUBLE = SQLDataType(name="DOUBLE", category=DataTypeCategory.REAL, alias=["REAL"], has_precision=True) - DECIMAL = SQLDataType(name="DECIMAL", category=DataTypeCategory.REAL, alias=["DEC"], has_precision=True, has_scale=True) + DECIMAL = SQLDataType(name="DECIMAL", category=DataTypeCategory.REAL, alias=["DEC", "NEWDECIMAL"], has_precision=True, has_scale=True) # Text types - CHAR = SQLDataType(name="CHAR", category=DataTypeCategory.TEXT, has_length=True, max_size=255) - VARCHAR = SQLDataType(name="VARCHAR", category=DataTypeCategory.TEXT, has_length=True, max_size=65535, format=DataTypeFormat.STRING) + CHAR = SQLDataType(name="CHAR", category=DataTypeCategory.TEXT, alias=["STRING"], has_length=True, max_size=255) + VARCHAR = SQLDataType(name="VARCHAR", category=DataTypeCategory.TEXT, alias=["VAR_STRING"], has_length=True, max_size=65535, format=DataTypeFormat.STRING) TINYTEXT = SQLDataType(name="TINYTEXT", category=DataTypeCategory.TEXT, format=DataTypeFormat.STRING) TEXT = SQLDataType(name="TEXT", category=DataTypeCategory.TEXT, format=DataTypeFormat.STRING) MEDIUMTEXT = SQLDataType(name="MEDIUMTEXT", category=DataTypeCategory.TEXT, format=DataTypeFormat.STRING) @@ -25,10 +25,10 @@ class MariaDBDataType(StandardDataType): # Binary types BINARY = SQLDataType(name="BINARY", category=DataTypeCategory.BINARY, has_length=True, max_size=255) VARBINARY = SQLDataType(name="VARBINARY", category=DataTypeCategory.BINARY, has_length=True, max_size=65535) - TINYBLOB = SQLDataType(name="TINYBLOB", category=DataTypeCategory.BINARY) + TINYBLOB = SQLDataType(name="TINYBLOB", category=DataTypeCategory.BINARY, alias=["TINY_BLOB"]) BLOB = SQLDataType(name="BLOB", category=DataTypeCategory.BINARY) - MEDIUMBLOB = SQLDataType(name="MEDIUMBLOB", category=DataTypeCategory.BINARY) - LONGBLOB = SQLDataType(name="LONGBLOB", category=DataTypeCategory.BINARY) + MEDIUMBLOB = SQLDataType(name="MEDIUMBLOB", category=DataTypeCategory.BINARY, alias=["MEDIUM_BLOB"]) + LONGBLOB = SQLDataType(name="LONGBLOB", category=DataTypeCategory.BINARY, alias=["LONG_BLOB"]) # Date and time DATE = SQLDataType(name="DATE", category=DataTypeCategory.TEMPORAL) diff --git a/structures/engines/mysql/context.py b/structures/engines/mysql/context.py index ffd1012..c8eaf8a 100644 --- a/structures/engines/mysql/context.py +++ b/structures/engines/mysql/context.py @@ -4,6 +4,8 @@ import pymysql +from pymysql.constants import FIELD_TYPE + from gettext import gettext as _ from helpers.logger import logger @@ -30,12 +32,11 @@ MySQLTable, MySQLTrigger, MySQLView, + MySQLCheck, ) from structures.engines.mysql.datatype import MySQLDataType from structures.engines.mysql.indextype import MySQLIndexType -from structures.ssh_tunnel import SSHTunnel - class MySQLContext(AbstractContext): MAP_COLUMN_FIELDS = MAP_COLUMN_FIELDS @@ -46,6 +47,8 @@ class MySQLContext(AbstractContext): IDENTIFIER_QUOTE_CHAR = "`" DEFAULT_STATEMENT_SEPARATOR = ";" + ROW_FORMATS: list[str] = ["DEFAULT", "DYNAMIC", "FIXED", "COMPRESSED", "REDUNDANT", "COMPACT"] + def __init__(self, connection: Connection): super().__init__(connection) @@ -68,9 +71,9 @@ def after_connect(self, *args, **kwargs): self.execute("""SHOW ENGINES;""") self.ENGINES = [dict(row).get("Engine") for row in self.fetchall()] - server_version = self.get_server_version() + self.server_version = self.get_server_version() self.KEYWORDS, builtin_functions = self.get_engine_vocabulary( - "mysql", server_version + "mysql", self.server_version ) self.execute(""" @@ -83,6 +86,12 @@ def after_connect(self, *args, **kwargs): self.FUNCTIONS = tuple(dict.fromkeys(builtin_functions + user_functions)) def _parse_type(self, column_type: str): + """Parse a raw COLUMN_TYPE string from information_schema into structured field attributes. + + Used in get_columns() to extract length, precision, scale, set values, and flags + from DDL-style strings such as 'varchar(255)', 'decimal(10,2)', or 'enum('a','b')'. + Returns an empty dict when no pattern matches. + """ types = MySQLDataType.get_all() type_set = [ x.lower() @@ -118,13 +127,72 @@ def _parse_type(self, column_type: str): return dict() + def _get_field_type_name(self, type_code: Optional[int]) -> Optional[str]: + """Resolve a pymysql FIELD_TYPE integer code to its constant name. + + Used in get_result_column_datatypes() to bridge the driver's numeric type + representation to a named string (e.g. 253 -> 'VAR_STRING'). + Returns None when the code is absent or unrecognised. + """ + if type_code is None: + return None + + for name, value in vars(FIELD_TYPE).items(): + if not name.isupper() or not isinstance(value, int): + continue + + if value == type_code: + return name + + return None + + def get_result_column_datatypes( + self, cursor: pymysql.cursors.Cursor + ) -> list[Optional[SQLDataType]]: + """Map each result column to its SQLDataType using the pymysql cursor description. + + Resolves the driver's numeric type code via _get_field_type_name(), then looks up + the matching SQLDataType by name. Returns None for columns whose type cannot be resolved. + Unlike _parse_type(), this operates on query result metadata, not on DDL column strings. + """ + datatypes: list[Optional[SQLDataType]] = [] + + for description in cursor.description or []: + type_code = description[1] if len(description) > 1 else None + datatype_name = self._get_field_type_name(type_code) + + if datatype_name is None: + datatypes.append(None) + continue + + try: + datatypes.append(self.DATATYPE.get_by_name(datatype_name)) + except Exception: + datatypes.append(None) + + return datatypes + def connect(self, **connect_kwargs) -> None: - if self._connection is None: - self.before_connect() + skip_after_connect = bool(connect_kwargs.pop("skip_after_connect", False)) + skip_before_connect = bool(connect_kwargs.pop("skip_before_connect", False)) - use_tls_enabled = bool( - getattr(self.connection.configuration, "use_tls_enabled", False) + if self._connection is None: + if not skip_before_connect: + self.before_connect() + + connect_timeout_override = connect_kwargs.pop("connect_timeout", None) + compressed_protocol_override = connect_kwargs.pop("compress", None) + compressed_protocol = bool( + compressed_protocol_override + if compressed_protocol_override is not None + else getattr(self.connection.configuration, "compressed_protocol", False) + ) + connect_timeout = ( + int(connect_timeout_override) + if connect_timeout_override is not None + else int(getattr(self.connection.configuration, "connect_timeout", 10)) ) + use_tls = bool(getattr(self.connection.configuration, "use_tls", False)) base_kwargs = dict( host=self.host, @@ -132,19 +200,23 @@ def connect(self, **connect_kwargs) -> None: password=self.password, port=self.port, cursorclass=pymysql.cursors.DictCursor, + connect_timeout=connect_timeout, + compress=compressed_protocol, **connect_kwargs, ) - if use_tls_enabled: + if use_tls: base_kwargs["ssl"] = { "cert_reqs": ssl.CERT_NONE, "check_hostname": False, } logger.debug( - "MySQL connect target host=%s port=%s user=%s use_tls_enabled=%s", + "MySQL connect target host=%s port=%s user=%s compressed_protocol=%s connect_timeout=%s use_tls=%s", base_kwargs.get("host"), base_kwargs.get("port"), base_kwargs.get("user"), - use_tls_enabled, + compressed_protocol, + connect_timeout, + use_tls, ) try: @@ -175,7 +247,7 @@ def connect(self, **connect_kwargs) -> None: if hasattr(self.connection, "configuration"): self.connection.configuration = ( - self.connection.configuration._replace(use_tls_enabled=True) + self.connection.configuration._replace(use_tls=True) ) logger.info( "MySQL connection succeeded after enabling TLS automatically." @@ -184,7 +256,7 @@ def connect(self, **connect_kwargs) -> None: logger.error(f"Failed to connect to MySQL: {e}") raise - if self._cursor is not None: + if self._cursor is not None and not skip_after_connect: self.after_connect() def set_database(self, database: SQLDatabase) -> None: @@ -308,7 +380,7 @@ def get_tables(self, database: SQLDatabase) -> list[SQLTable]: QUERY_LOGS.append(f"/* get_tables for database={database.name} */") self.execute(f""" - SELECT TABLE_NAME, ENGINE, TABLE_COLLATION, TABLE_ROWS, AUTO_INCREMENT, + SELECT TABLE_NAME, ENGINE, TABLE_COLLATION, TABLE_ROWS, AUTO_INCREMENT, ROW_FORMAT, ROUND((DATA_LENGTH + INDEX_LENGTH), 2) as total_bytes FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{database.name}' @@ -325,6 +397,7 @@ def get_tables(self, database: SQLDatabase) -> list[SQLTable]: database=database, engine=row["ENGINE"], collation_name=row["TABLE_COLLATION"], + row_format=row["ROW_FORMAT"], auto_increment=int(row["AUTO_INCREMENT"] or 0), total_bytes=row["total_bytes"], total_rows=row["TABLE_ROWS"], @@ -546,7 +619,7 @@ def build_empty_table( id = MySQLContext.get_temporary_id(database.tables) if name is None: - name = _(f"Table{str(id * -1):03}") + name = _("Table{table_index:03}").format(table_index=id * -1) default_values.setdefault("engine", "InnoDB") default_values.setdefault("collation_name", "utf8mb4_general_ci") @@ -574,7 +647,7 @@ def build_empty_column( id = MySQLContext.get_temporary_id(table.columns) if name is None: - name = _(f"Column{str(id * -1):03}") + name = _("Column{column_index:03}").format(column_index=id * -1) return MySQLColumn( id=id, name=name, table=table, datatype=datatype, **default_values @@ -592,7 +665,7 @@ def build_empty_index( id = MySQLContext.get_temporary_id(table.indexes) if name is None: - name = _(f"Index{str(id * -1):03}") + name = _("Index{index_number:03}").format(index_number=id * -1) return MySQLIndex( id=id, @@ -610,8 +683,6 @@ def build_empty_check( expression: Optional[str] = None, **default_values, ) -> "MySQLCheck": - from structures.engines.mysql.database import MySQLCheck - id = MySQLContext.get_temporary_id(table.checks) if name is None: @@ -632,7 +703,9 @@ def build_empty_foreign_key( id = MySQLContext.get_temporary_id(table.foreign_keys) if name is None: - name = _(f"ForeignKey{str(id * -1):03}") + name = _("ForeignKey{foreign_key_number:03}").format( + foreign_key_number=id * -1 + ) reference_table = default_values.get("reference_table", "") reference_columns = default_values.get("reference_columns", []) @@ -661,7 +734,7 @@ def build_empty_view( id = MySQLContext.get_temporary_id(database.views) if name is None: - name = _(f"View{str(id * -1):03}") + name = _("View{view_index:03}").format(view_index=id * -1) return MySQLView( id=id, diff --git a/structures/engines/mysql/database.py b/structures/engines/mysql/database.py index fd2694d..399f4e3 100644 --- a/structures/engines/mysql/database.py +++ b/structures/engines/mysql/database.py @@ -69,6 +69,9 @@ def drop(self) -> bool: @dataclasses.dataclass(eq=False) class MySQLTable(SQLTable): + row_format: Optional[str] = None + convert_data: bool = dataclasses.field(default=False, compare=False) + def raw_create(self) -> str: columns = [str(MySQLColumnBuilder(column)) for column in self.columns] @@ -90,11 +93,11 @@ def alter_auto_increment(self, auto_increment: int): return True - def alter_collation(self, convert: bool = True): + def alter_collation(self, collation_name: str, convert: bool = True): charset = "" if convert: - charset = f"CONVERT TO CHARACTER SET {self.database.context.COLLATIONS[self.collation_name]}" - return self.database.context.execute(f"""ALTER TABLE `{self.database.name}`.`{self.name}` {charset} COLLATE {self.collation_name};""") + charset = f"CONVERT TO CHARACTER SET {self.database.context.COLLATIONS[collation_name]} " + return self.database.context.execute(f"ALTER TABLE `{self.database.name}`.`{self.name}` {charset}COLLATE {collation_name};") def alter_engine(self, engine: str): statement = f"ALTER TABLE `{self.database.name}`.`{self.name}` ENGINE {engine};" @@ -102,6 +105,12 @@ def alter_engine(self, engine: str): return True + def alter_row_format(self, row_format: str): + statement = f"ALTER TABLE `{self.database.name}`.`{self.name}` ROW_FORMAT={row_format};" + self.database.context.execute(statement) + + return True + def rename(self, table: Self, new_name: str) -> bool: statement = f"ALTER TABLE `{self.database.name}`.`{table.name}` RENAME TO `{new_name}`;" self.database.context.execute(statement) @@ -152,9 +161,11 @@ def alter(self) -> bool: if self.auto_increment != original_table.auto_increment: original_table.alter_auto_increment(self.auto_increment) if self.collation_name != original_table.collation_name: - original_table.alter_collation(self.collation_name) + original_table.alter_collation(self.collation_name, convert=self.convert_data) if self.engine != original_table.engine: original_table.alter_engine(self.engine) + if self.row_format != original_table.row_format and self.row_format: + original_table.alter_row_format(self.row_format) for original, current in map_columns: if original is None: diff --git a/structures/engines/mysql/datatype.py b/structures/engines/mysql/datatype.py index 8a18716..0846df2 100644 --- a/structures/engines/mysql/datatype.py +++ b/structures/engines/mysql/datatype.py @@ -8,20 +8,20 @@ class MySQLDataType(StandardDataType): # Integer types - TINYINT = SQLDataType(name="TINYINT", category=DataTypeCategory.INTEGER, has_unsigned=True) - SMALLINT = SQLDataType(name="SMALLINT", category=DataTypeCategory.INTEGER, has_unsigned=True) - MEDIUMINT = SQLDataType(name="MEDIUMINT", category=DataTypeCategory.INTEGER, has_unsigned=True) - INT = SQLDataType(name="INT", category=DataTypeCategory.INTEGER, alias=["INTEGER"], has_unsigned=True) - BIGINT = SQLDataType(name="BIGINT", category=DataTypeCategory.INTEGER, has_unsigned=True) + TINYINT = SQLDataType(name="TINYINT", category=DataTypeCategory.INTEGER, alias=["TINY"], has_unsigned=True) + SMALLINT = SQLDataType(name="SMALLINT", category=DataTypeCategory.INTEGER, alias=["SHORT"], has_unsigned=True) + MEDIUMINT = SQLDataType(name="MEDIUMINT", category=DataTypeCategory.INTEGER, alias=["INT24"], has_unsigned=True) + INT = SQLDataType(name="INT", category=DataTypeCategory.INTEGER, alias=["INTEGER", "LONG"], has_unsigned=True) + BIGINT = SQLDataType(name="BIGINT", category=DataTypeCategory.INTEGER, alias=["LONGLONG"], has_unsigned=True) # Floating point FLOAT = SQLDataType(name="FLOAT", category=DataTypeCategory.REAL, has_precision=True) DOUBLE = SQLDataType(name="DOUBLE", category=DataTypeCategory.REAL, alias=["REAL"], has_precision=True) - DECIMAL = SQLDataType(name="DECIMAL", category=DataTypeCategory.REAL, alias=["DEC"], has_precision=True, has_scale=True) + DECIMAL = SQLDataType(name="DECIMAL", category=DataTypeCategory.REAL, alias=["DEC", "NEWDECIMAL"], has_precision=True, has_scale=True) # Text types - CHAR = SQLDataType(name="CHAR", category=DataTypeCategory.TEXT, has_length=True, max_size=255) - VARCHAR = SQLDataType(name="VARCHAR", category=DataTypeCategory.TEXT, has_length=True, max_size=65535, format=DataTypeFormat.STRING) + CHAR = SQLDataType(name="CHAR", category=DataTypeCategory.TEXT, alias=["STRING"], has_length=True, max_size=255) + VARCHAR = SQLDataType(name="VARCHAR", category=DataTypeCategory.TEXT, alias=["VAR_STRING"], has_length=True, max_size=65535, format=DataTypeFormat.STRING) TINYTEXT = SQLDataType(name="TINYTEXT", category=DataTypeCategory.TEXT, format=DataTypeFormat.STRING) TEXT = SQLDataType(name="TEXT", category=DataTypeCategory.TEXT, format=DataTypeFormat.STRING) MEDIUMTEXT = SQLDataType(name="MEDIUMTEXT", category=DataTypeCategory.TEXT, format=DataTypeFormat.STRING) @@ -30,10 +30,10 @@ class MySQLDataType(StandardDataType): # Binary types BINARY = SQLDataType(name="BINARY", category=DataTypeCategory.BINARY, has_length=True, max_size=255) VARBINARY = SQLDataType(name="VARBINARY", category=DataTypeCategory.BINARY, has_length=True, max_size=65535) - TINYBLOB = SQLDataType(name="TINYBLOB", category=DataTypeCategory.BINARY) + TINYBLOB = SQLDataType(name="TINYBLOB", category=DataTypeCategory.BINARY, alias=["TINY_BLOB"]) BLOB = SQLDataType(name="BLOB", category=DataTypeCategory.BINARY) - MEDIUMBLOB = SQLDataType(name="MEDIUMBLOB", category=DataTypeCategory.BINARY) - LONGBLOB = SQLDataType(name="LONGBLOB", category=DataTypeCategory.BINARY) + MEDIUMBLOB = SQLDataType(name="MEDIUMBLOB", category=DataTypeCategory.BINARY, alias=["MEDIUM_BLOB"]) + LONGBLOB = SQLDataType(name="LONGBLOB", category=DataTypeCategory.BINARY, alias=["LONG_BLOB"]) # Date and time DATE = SQLDataType(name="DATE", category=DataTypeCategory.TEMPORAL) diff --git a/structures/engines/postgresql/context.py b/structures/engines/postgresql/context.py index d058073..964fd4f 100644 --- a/structures/engines/postgresql/context.py +++ b/structures/engines/postgresql/context.py @@ -1,12 +1,13 @@ import psycopg2 import psycopg2.extras +from psycopg2.extensions import cursor as PostgreSQLCursor + from typing import Any, Optional from gettext import gettext as _ from helpers.logger import logger from structures.connection import Connection -from structures.ssh_tunnel import SSHTunnel from structures.engines.context import QUERY_LOGS, AbstractContext from structures.engines.database import ( @@ -58,9 +59,9 @@ def after_connect(self, *args, **kwargs): self.execute("SELECT collname FROM pg_collation;") self.COLLATIONS = {row["collname"]: row["collname"] for row in self.fetchall()} - server_version = self.get_server_version() + self.server_version = self.get_server_version() self.KEYWORDS, builtin_functions = self.get_engine_vocabulary( - "postgresql", server_version + "postgresql", self.server_version ) self.execute(""" @@ -102,11 +103,81 @@ def _load_custom_types(self) -> None: ) setattr(PostgreSQLDataType, row["typname"].upper(), datatype) + def _extract_description_type_code(self, description: Any) -> Optional[int]: + if hasattr(description, "type_code"): + value = getattr(description, "type_code") + return int(value) if value is not None else None + + if len(description) > 1 and description[1] is not None: + return int(description[1]) + + return None + + def _load_result_type_names_by_oid( + self, cursor: PostgreSQLCursor, oid_values: list[int] + ) -> dict[int, str]: + type_name_by_oid: dict[int, str] = {} + + lookup_cursor = cursor.connection.cursor() + try: + lookup_cursor.execute( + "SELECT oid, format_type(oid, NULL) AS type_name FROM pg_type WHERE oid = ANY(%s)", + (oid_values,), + ) + for oid, type_name in lookup_cursor.fetchall(): + type_name_by_oid[int(oid)] = str(type_name) + finally: + lookup_cursor.close() + + return type_name_by_oid + + def _normalize_result_type_name(self, datatype_name: str) -> str: + normalized = datatype_name.strip().upper() + normalized = normalized.split("(")[0].strip() + normalized = " ".join(normalized.split()) + return normalized + + def get_result_column_datatypes( + self, cursor: PostgreSQLCursor + ) -> list[Optional[SQLDataType]]: + descriptions = list(cursor.description or []) + oids = [self._extract_description_type_code(desc) for desc in descriptions] + oid_values = [oid for oid in oids if oid is not None] + + if not oid_values: + return [None for _ in descriptions] + + type_name_by_oid = self._load_result_type_names_by_oid(cursor, oid_values) + datatypes: list[Optional[SQLDataType]] = [] + + for oid in oids: + if oid is None or oid not in type_name_by_oid: + datatypes.append(None) + continue + + try: + type_name = self._normalize_result_type_name(type_name_by_oid[oid]) + datatypes.append(self.DATATYPE.get_by_name(type_name)) + except Exception: + datatypes.append(None) + + return datatypes + def connect(self, **connect_kwargs) -> None: + skip_after_connect = bool(connect_kwargs.pop("skip_after_connect", False)) + skip_before_connect = bool(connect_kwargs.pop("skip_before_connect", False)) + if self._connection is None: try: - self.before_connect() + if not skip_before_connect: + self.before_connect() database = connect_kwargs.pop("database", "postgres") + connect_timeout_override = connect_kwargs.pop("connect_timeout", None) + connect_timeout = ( + int(connect_timeout_override) + if connect_timeout_override is not None + else int(getattr(self.connection.configuration, "connect_timeout", 10)) + ) base_kwargs = dict( host=self.host, @@ -114,14 +185,16 @@ def connect(self, **connect_kwargs) -> None: password=self.password, database=database, port=self.port, + connect_timeout=connect_timeout, **connect_kwargs, ) logger.debug( - "PostgreSQL connect target host=%s port=%s user=%s database=%s", + "PostgreSQL connect target host=%s port=%s user=%s database=%s connect_timeout=%s", base_kwargs.get("host"), base_kwargs.get("port"), base_kwargs.get("user"), base_kwargs.get("database"), + base_kwargs.get("connect_timeout"), ) self._connection = psycopg2.connect(**base_kwargs) @@ -133,7 +206,8 @@ def connect(self, **connect_kwargs) -> None: logger.error(f"Failed to connect to PostgreSQL: {e}", exc_info=True) raise else: - self.after_connect() + if not skip_after_connect: + self.after_connect() def after_disconnect(self): self._current_database = None @@ -419,7 +493,7 @@ def get_indexes(self, table: SQLTable) -> list[SQLIndex]: return results - def get_checks(self, table: PostgreSQLTable) -> list[PostgreSQLCheck]: + def get_checks(self, table: PostgreSQLTable) -> list["PostgreSQLCheck"]: from structures.engines.postgresql.database import PostgreSQLCheck if table is None or table.is_new: @@ -568,7 +642,7 @@ def build_empty_table( id = PostgreSQLContext.get_temporary_id(database.tables) if name is None: - name = _(f"Table{str(id * -1):03}") + name = _("Table{table_index:03}").format(table_index=id * -1) return PostgreSQLTable( id=id, @@ -593,7 +667,7 @@ def build_empty_column( id = PostgreSQLContext.get_temporary_id(table.columns) if name is None: - name = _(f"Column{str(id * -1):03}") + name = _("Column{column_index:03}").format(column_index=id * -1) return PostgreSQLColumn( id=id, name=name, table=table, datatype=datatype, **default_values @@ -611,7 +685,7 @@ def build_empty_index( id = PostgreSQLContext.get_temporary_id(table.indexes) if name is None: - name = _(f"Index{str(id * -1):03}") + name = _("Index{index_number:03}").format(index_number=id * -1) return PostgreSQLIndex( id=id, @@ -628,7 +702,7 @@ def build_empty_check( name: Optional[str] = None, expression: Optional[str] = None, **default_values, - ) -> PostgreSQLCheck: + ) -> "PostgreSQLCheck": from structures.engines.postgresql.database import PostgreSQLCheck id = PostgreSQLContext.get_temporary_id(table.checks) @@ -651,7 +725,9 @@ def build_empty_foreign_key( id = PostgreSQLContext.get_temporary_id(table.foreign_keys) if name is None: - name = _(f"ForeignKey{str(id * -1):03}") + name = _("ForeignKey{foreign_key_number:03}").format( + foreign_key_number=id * -1 + ) return PostgreSQLForeignKey( id=id, @@ -679,7 +755,7 @@ def build_empty_view( id = PostgreSQLContext.get_temporary_id(database.views) if name is None: - name = _(f"View{str(id * -1):03}") + name = _("View{view_index:03}").format(view_index=id * -1) return PostgreSQLView( id=id, diff --git a/structures/engines/sqlite/context.py b/structures/engines/sqlite/context.py index 23b1ebc..015d55b 100755 --- a/structures/engines/sqlite/context.py +++ b/structures/engines/sqlite/context.py @@ -69,9 +69,9 @@ def __init__(self, connection: Connection): def after_connect(self, *args, **kwargs): super().after_connect(*args, **kwargs) - server_version = self.get_server_version() + self.server_version = self.get_server_version() spec_keywords, spec_functions = self.get_engine_vocabulary( - "sqlite", server_version + "sqlite", self.server_version ) self.KEYWORDS = tuple( dict.fromkeys( @@ -96,8 +96,13 @@ def after_connect(self, *args, **kwargs): # self.execute("PRAGMA page_size = 4096") def connect(self, **connect_kwargs) -> None: + skip_after_connect = bool(connect_kwargs.pop("skip_after_connect", False)) + skip_before_connect = bool(connect_kwargs.pop("skip_before_connect", False)) + if self._connection is None: try: + if not skip_before_connect: + self.before_connect() self._connection = sqlite3.connect(self.filename) except Exception as e: @@ -106,7 +111,8 @@ def connect(self, **connect_kwargs) -> None: else: self._connection.row_factory = sqlite3.Row self._cursor = self._connection.cursor() - self.after_connect() + if not skip_after_connect: + self.after_connect() def set_database(self, database: SQLDatabase) -> None: pass @@ -515,7 +521,7 @@ def build_empty_table( id = SQLiteContext.get_temporary_id(database.tables) if name is None: - name = _(f"Table{str(id * -1):03}") + name = _("Table{table_index:03}").format(table_index=id * -1) return SQLiteTable( id=id, @@ -539,7 +545,7 @@ def build_empty_column( id = SQLiteContext.get_temporary_id(table.columns) if name is None: - name = _(f"Column{str(id * -1):03}") + name = _("Column{column_index:03}").format(column_index=id * -1) return SQLiteColumn( id=id, name=name, table=table, datatype=datatype, **default_values @@ -557,7 +563,7 @@ def build_empty_index( id = SQLiteContext.get_temporary_id(table.indexes) if name is None: - name = _(f"Index{str(id * -1):03}") + name = _("Index{index_number:03}").format(index_number=id * -1) return SQLiteIndex( id=id, @@ -599,7 +605,9 @@ def build_empty_foreign_key( id = SQLiteContext.get_temporary_id(table.foreign_keys) if name is None: - name = _(f"ForeignKey{str(id * -1):03}") + name = _("ForeignKey{foreign_key_number:03}").format( + foreign_key_number=id * -1 + ) return SQLiteForeignKey( id=id, @@ -625,7 +633,7 @@ def build_empty_view( id = SQLiteContext.get_temporary_id(database.views) if name is None: - name = _(f"View{str(id * -1):03}") + name = _("View{view_index:03}").format(view_index=id * -1) return SQLiteView( id=id, @@ -658,3 +666,8 @@ def build_empty_trigger( database=database, statement=default_values.get("statement", ""), ) + + def get_result_column_datatypes( + self, cursor: sqlite3.Cursor + ) -> list[Optional[SQLDataType]]: + return [None for _ in (cursor.description or [])] diff --git a/structures/ssh_tunnel.py b/structures/ssh_tunnel.py index 24b960b..73fc88e 100644 --- a/structures/ssh_tunnel.py +++ b/structures/ssh_tunnel.py @@ -1,12 +1,13 @@ import atexit import os +import shlex import shutil import signal import socket import subprocess import time -from typing import Optional +from typing import Optional, Union from gettext import gettext as _ @@ -27,7 +28,7 @@ def __init__( local_bind_address: tuple[str, int] = ("localhost", 0), ssh_executable: str = "ssh", identity_file: Optional[str] = None, - extra_args: Optional[list[str]] = None, + extra_args: Optional[Union[str, list[str]]] = None, ): self.ssh_hostname = ssh_hostname self.ssh_port = ssh_port @@ -40,9 +41,19 @@ def __init__( self.ssh_executable = ssh_executable self.identity_file = identity_file - self.extra_args = extra_args or [] + self.extra_args = self._normalize_extra_args(extra_args) self._process: Optional[subprocess.Popen] = None + @staticmethod + def _normalize_extra_args(extra_args: Optional[Union[str, list[str]]]) -> list[str]: + if not extra_args: + return [] + + if isinstance(extra_args, list): + return [str(value) for value in extra_args if str(value).strip()] + + return [value for value in shlex.split(extra_args) if value.strip()] + def __enter__(self): self.start() return self.local_port diff --git a/tests/autocomplete/README.md b/tests/autocomplete/README.md index 846956d..7dd6e58 100644 --- a/tests/autocomplete/README.md +++ b/tests/autocomplete/README.md @@ -81,7 +81,7 @@ The system detects which table is on the left of the operator and filters out AL ## Test Coverage Matrix -Golden tests organized by SQL query writing flow (180 base tests, executed across 11 engine/version targets): +Golden tests organized by SQL query writing flow (211 base tests, executed across 11 engine/version targets): - mysql: `8`, `9` - mariadb: `5`, `10`, `11`, `12` @@ -153,7 +153,19 @@ Golden tests organized by SQL query writing flow (180 base tests, executed acros | WINDOW_FUNCTIONS_OVER ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/window_functions_over.json` | 1 | 1 | 0 | 0 | OVER-clause bootstrap suggestions (`PARTITION BY`, `ORDER BY`). | | CURSOR_IN_TOKEN ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/cursor_in_token.json` | 1 | 1 | 0 | 0 | Correct prefix/context when cursor is inside an existing token. | -### 11. Multi-Query & Special Cases +### 11. INSERT, UPDATE, DELETE Operations +| Test Group | File | Total | ✅ | ❌ | ⚠️ | Description | +|------------|------|-------|---|---|---|-------------| +| INSERT ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/insert.json` | 14 | 14 | 0 | 0 | INSERT INTO statements with table selection, column specification, VALUES clauses, and string literal handling. | +| UPDATE ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/update.json` | 16 | 16 | 0 | 0 | UPDATE statements with table selection, SET clauses, WHERE conditions, JOIN operations, and string literal handling. | +| DELETE ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/delete.json` | 15 | 15 | 0 | 0 | DELETE FROM statements with table selection, WHERE conditions, JOIN/USING clauses, subqueries, and string literal handling. | + +### 12. String Literal Handling +| Test Group | File | Total | ✅ | ❌ | ⚠️ | Description | +|------------|------|-------|---|---|---|-------------| +| STRING_LITERALS ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/insert.json`, `cases/update.json`, `cases/delete.json`, `cases/where.json` | 11 | 11 | 0 | 0 | No suggestions when cursor is inside string literals across INSERT, UPDATE, DELETE, and SELECT statements. | + +### 13. Multi-Query & Special Cases | Test Group | File | Total | ✅ | ❌ | ⚠️ | Description | |------------|------|-------|---|---|---|-------------| | DERIVED_TABLES_CTE ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/derived_tables_cte.json` | 9 | 9 | 0 | 0 | Minimal CTE/derived-table scope extraction for FROM/JOIN/WHERE and dot completion. | @@ -165,8 +177,8 @@ Golden tests organized by SQL query writing flow (180 base tests, executed acros | LARGE_SCHEMA_GUARDRAILS ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/perf.json` | 2 | 2 | 0 | 0 | Large-schema guardrails for prefix filtering and noise control. | ### Summary Statistics -- **Total Tests**: 1936 (176 base × 11 engine/version targets) -- **✅ Passing**: 1936 (176 base × 11 targets, 100%) +- **Total Tests**: 2321 (211 base × 11 engine/version targets) +- **✅ Passing**: 2321 (211 base × 11 targets, 100%) - **❌ Failing**: 0 (remaining tests, 0%) - **⚠️ Expected Failures (xfail)**: 0 (0 base × 11 targets, 0%) - **⚪ Not Implemented**: 0 (0%) diff --git a/tests/autocomplete/RULES.md b/tests/autocomplete/RULES.md index 229403e..6a488fc 100644 --- a/tests/autocomplete/RULES.md +++ b/tests/autocomplete/RULES.md @@ -39,18 +39,25 @@ The scope classification determines which columns are suggested in expression co - **SCOPED**: Explicit scope exists via FROM/JOIN clauses in the current statement - Example: `SELECT id, | FROM users` → scope = `users` table - Example: `SELECT * FROM users u JOIN orders o ON u.id = o.user_id; SELECT u.id, |` → scope = `u`, `o` tables - - Behavior: Suggest only columns from scope tables (qualified if multiple tables, unqualified if single table) + - Behavior: Suggest only columns from scope tables + - normally **unqualified** if there is a single explicit table + - **qualified** if there are multiple explicit tables + - if the statement already uses qualified style (for example `users.id` or `u.id`), suggestions must continue in **qualified** form - **VIRTUAL_SCOPED**: Implicit scope inferred from context without explicit FROM/JOIN - - Via qualified columns: `SELECT users.id, |` → virtual scope = `users` (inferred from qualified column) - - Via CURRENT_TABLE: `SELECT id, |` with CURRENT_TABLE=users → virtual scope = `users` - - **CRITICAL:** VIRTUAL_SCOPED requires CURRENT_TABLE to be set in UI (or qualified column present) - - When VIRTUAL_SCOPED via CURRENT_TABLE (no FROM/JOIN), columns MUST be qualified (e.g., `users.id`, not `id`) - - Behavior: Suggest columns from the inferred table(s), but allow DB-wide suggestions with prefix + - Via qualified references already present in the statement: + - `SELECT users.id, |` → virtual scope = `users` + - `SELECT users.*, |` → virtual scope = `users` + - Via CURRENT_TABLE: + - `SELECT |` with CURRENT_TABLE=users → virtual scope = `users` + - **CRITICAL:** VIRTUAL_SCOPED exists when there is no explicit `FROM/JOIN` scope, but there is still a strong table reference intent + - In VIRTUAL_SCOPED, columns MUST be **always qualified** + - `users.id`, not `id` + - Behavior: Suggest columns from the inferred table(s); DB-wide suggestions are allowed only when there is no explicit scope and either a prefix exists or autocomplete is forced - **NO_SCOPED**: No scope information available - No FROM/JOIN in current statement - - No qualified columns to infer scope from + - No qualified references to infer virtual scope from - No CURRENT_TABLE set - Example: `SELECT id, |` with CURRENT_TABLE=null and no qualified columns - Behavior: Suggest only functions (no columns without prefix) @@ -99,8 +106,8 @@ For clarity, examples use the following assumed schema order: ## Context Detection -The autocomplete system uses `sqlglot` to parse the SQL query and determine the current context. -Contexts are defined in the `SQLContext` enum. +The autocomplete system determines the current SQL context (for example `SELECT_LIST`, `WHERE_CLAUSE`, `JOIN_ON`, `ORDER_BY_CLAUSE`). +This document defines the expected behavior for each context, independently of the internal implementation. --- @@ -110,7 +117,7 @@ These examples demonstrate the strict separation between table-selection and exp **Assume:** `CURRENT_TABLE = users` (set in table editor) -### Example 1: SELECT with no FROM → CURRENT_TABLE + DB-wide allowed +### Example 1: SELECT with no FROM → VIRTUAL_SCOPED via CURRENT_TABLE ```sql SELECT u| @@ -119,11 +126,11 @@ SELECT u| **Context:** SELECT_LIST, no scope tables exist **Suggestions:** -- `users.id, users.name, users.email, ...` (CURRENT_TABLE columns first) -- `products.unit_price, ...` (DB-wide columns matching 'u') +- `users.id, users.name, users.email, ...` (CURRENT_TABLE columns first, always qualified in VIRTUAL_SCOPED) +- `products.unit_price, ...` (DB-wide columns matching 'u', only because no explicit scope exists and a prefix is present) - `UPPER, UUID, UNIX_TIMESTAMP` (functions) -**Rationale:** No scope tables exist, so CURRENT_TABLE and DB-wide columns are allowed. +**Rationale:** No explicit scope tables exist, but CURRENT_TABLE creates a VIRTUAL_SCOPED context. Its columns must stay qualified. DB-wide suggestions are allowed here only because there is still no explicit scope and a prefix is present. --- @@ -161,7 +168,7 @@ SELECT * FROM orders WHERE u| - ❌ `users.*` (CURRENT_TABLE not in scope) - ❌ `products.unit_price` (DB-wide column) -**Rationale:** WHERE is an expression context with scope tables. CURRENT_TABLE is not in scope, so it MUST be ignored. DB-wide columns MUST NOT be suggested. +**Rationale:** WHERE is an expression context with explicit scope tables. CURRENT_TABLE is not in scope, so it MUST be ignored. DB-wide columns MUST NOT be suggested when explicit scope exists. ```sql -- Case B: CURRENT_TABLE in scope @@ -191,9 +198,8 @@ The autocomplete resolution follows this strict precedence order: - Show columns of that table/alias (ignore broader context) - Example: `WHERE u.i|` → show columns of `u` starting with `i` -3. **Context Detection** (sqlglot/regex) +3. **Context Detection** - Determine SQL context: SELECT_LIST, WHERE, JOIN ON, ORDER BY, etc. - - Use sqlglot parsing (primary) or regex fallback 4. **Within Context: Prefix Rules** - If prefix exists (token before cursor without `.`) → apply prefix matching @@ -280,7 +286,7 @@ SELECT ui| **Qualification rules:** -1. **Single table in scope (no ambiguity):** +1. **Single explicit table in scope (no ambiguity):** - **No prefix:** Use **unqualified** column names (e.g., `id`, `name`) - **With prefix:** - **Column-name match** (Generic Prefix Matching rule B): Use **unqualified** column names (e.g., `name`) @@ -300,13 +306,15 @@ SELECT ui| - **Implication for autocomplete:** If prefix does not match the alias and does not match any column name, return empty suggestions. Do NOT suggest qualified columns with the original table name. - **Example:** `FROM users u WHERE us|` → NO suggestions (prefix 'us' does not match alias 'u' or any column) -4. **Consistency rule - Qualified context propagation:** If the query already uses at least one qualified column (e.g., `users.id` or `u.id`) in the SELECT list, column suggestions MUST stay qualified for consistency, even for single-table scopes. +4. **Consistency rule - Qualified context propagation:** If the query already uses at least one qualified column (e.g., `users.id` or `u.id`) in the current statement, column suggestions MUST stay qualified for consistency, even for single-table scopes. - This is a style lock: once qualified style is used, autocomplete keeps qualified style. - Applies to all column contexts: SELECT list (after comma), WHERE, JOIN ON, ORDER BY, GROUP BY, HAVING. - For aliased tables, qualification MUST use alias only (never table name). - For non-aliased tables, qualification uses `table.column`. -**Rationale:** When only one table is in scope, qualification usually adds noise. However, table-name prefix expansion and explicit qualified usage both express a clear qualification intent. Once user intent is qualified style, maintaining it across contexts keeps SQL consistent and avoids invalid `table.column` usage when aliases are present. +5. **VIRTUAL_SCOPED rule:** If scope is only virtual (via `CURRENT_TABLE`, `table.column`, or `table.*` without explicit `FROM/JOIN`), suggestions MUST stay **qualified**. + +**Rationale:** When only one explicit table is in scope, qualification usually adds noise. However, table-name prefix expansion, VIRTUAL_SCOPED contexts, and explicit qualified usage all express a clear qualification intent. Once user intent is qualified style, maintaining it across contexts keeps SQL consistent and unambiguous. **Examples:** ```sql @@ -314,6 +322,11 @@ SELECT ui| SELECT * FROM users WHERE | → id, name, email, password, is_enabled, created_at +-- Virtual scoped via CURRENT_TABLE: always qualified +SELECT | +-- CURRENT_TABLE = users +→ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at + -- Single table, prefix matches ONLY column name: unqualified SELECT * FROM users WHERE n| → name (column-name match, unqualified) @@ -344,6 +357,10 @@ SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE | SELECT u.id FROM users u WHERE | → u.id, u.name, u.email, u.status, u.created_at +-- Virtual scoped via qualified reference: always qualified +SELECT users.id, | +→ users.name, users.email, users.password, users.is_enabled, users.created_at + SELECT u.id FROM users u ORDER BY | → u.id, u.name, u.email, ... @@ -505,14 +522,18 @@ These contexts suggest **columns** from scope tables only. #### SELECT_LIST Context (Special Case) **If statement has NO scope tables (no FROM/JOIN yet):** -- Without prefix: `CURRENT_TABLE` columns MUST be included first (if set) +- Without prefix: + - if `CURRENT_TABLE` exists, the context is VIRTUAL_SCOPED and `CURRENT_TABLE` columns MUST be included first in **qualified** form + - if `CURRENT_TABLE` does not exist and there are no qualified references, suggest only functions - With prefix: `CURRENT_TABLE` columns MUST be included ONLY if they match the prefix via: - Column-name match (e.g., `SELECT na|` → `name` from CURRENT_TABLE) - Table-name expansion (e.g., `SELECT u|` and CURRENT_TABLE is `users` → suggest `users.*` columns) -- Database-wide columns MUST be included ONLY if prefix exists (guardrail: avoid noise when no prefix) +- Database-wide columns MUST be included ONLY if: + - no explicit scope exists + - and a prefix exists, **or** autocomplete was explicitly forced - Functions and keywords are included -**If statement HAS scope tables (FROM/JOIN exists):** +**If statement HAS explicit scope tables (FROM/JOIN exists):** - `CURRENT_TABLE` columns MUST be included ONLY if `CURRENT_TABLE` is in scope - If `CURRENT_TABLE` is not in scope, it MUST be ignored - Database-wide columns MUST NOT be suggested @@ -625,7 +646,7 @@ SEL| → SELECT (SINGLE_TOKEN) #### 3a. Without prefix (after SELECT, no FROM/JOIN in query) **Show:** -- `CURRENT_TABLE` columns (if set) +- `CURRENT_TABLE` columns in qualified form (if set) - Functions - Keywords (FROM, WHERE, etc.) - **only if SELECT list already has items** - e.g., `SELECT id |` → show keywords (can continue query) @@ -682,8 +703,8 @@ SELECT * FROM users u JOIN orders o ON u.id = o.user_id; SELECT | **CURRENT_TABLE handling:** - **When NO scope tables exist (no FROM/JOIN):** - - `CURRENT_TABLE` columns MUST be included first (if set) - - Database-wide table-name expansion and column-name matching are included + - `CURRENT_TABLE` columns MUST be included first in qualified form (if set) + - Database-wide table-name expansion and column-name matching are included only if a prefix exists or autocomplete is forced - Functions are included - **When NO scope tables AND NO prefix:** @@ -704,8 +725,8 @@ SELECT * FROM users u JOIN orders o ON u.id = o.user_id; SELECT | ```sql -- Assume CURRENT_TABLE = users SELECT u| -→ users.id, users.name, users.email, ... (CURRENT_TABLE via table-name expansion) -→ user_sessions.* (other tables starting with 'u') +→ users.id, users.name, users.email, ... (CURRENT_TABLE first, VIRTUAL_SCOPED) +→ user_sessions.id, user_sessions.user_id, ... (DB-wide columns allowed because no explicit scope and prefix exists) → orders.user_id, products.unit_price (DB-wide columns starting with 'u') → Functions: UPPER, UUID, UNIX_TIMESTAMP ``` @@ -1550,7 +1571,7 @@ Suggestions are always ordered by priority: **CURRENT_TABLE group inclusion is context-dependent:** - **Expression contexts (JOIN_ON, WHERE, ORDER_BY, GROUP_BY, HAVING):** CURRENT_TABLE group MUST be omitted unless `CURRENT_TABLE` is in scope -- **SELECT_LIST without scope tables:** CURRENT_TABLE group MUST be included (if set) +- **SELECT_LIST without explicit scope tables:** CURRENT_TABLE group MUST be included (if set) - **SELECT_LIST with scope tables:** CURRENT_TABLE group MUST be included ONLY if `CURRENT_TABLE` is in scope - **Table-selection contexts (FROM_CLAUSE, JOIN_CLAUSE):** Not applicable (these suggest tables, not columns) @@ -1563,9 +1584,9 @@ Suggestions are always ordered by priority: **Column ordering reminder:** See "Important Note on Column Ordering in Examples" section at the beginning of this document. All columns preserve schema order (ordinal_position), NOT alphabetical order. -1. **Columns from CURRENT_TABLE** (if set in context, e.g., table editor) - - **Single table in scope:** Unqualified (e.g., `id`, `name`) - - **Multiple tables in scope:** Use `alias.column` format if the table has an alias in the current query, otherwise `table.column` +1. **Columns from CURRENT_TABLE** (if allowed by context rules) + - In **VIRTUAL_SCOPED** they are always **qualified** + - In explicit scope they follow the normal qualification rules of that context - Columns preserve their definition order (ordinal position in the table schema). They must NOT be reordered alphabetically. 2. **Columns from tables in FROM clause** (if any) @@ -1586,17 +1607,16 @@ Suggestions are always ordered by priority: 4. **All table.column from database** (all other tables not in FROM/JOIN) - **CRITICAL: Group 4 eligibility is context-dependent:** - - ✅ **Eligible in SELECT_LIST when NO scope tables exist** (and only with prefix - guardrail against noise) + - ✅ **Eligible in SELECT_LIST when NO explicit scope tables exist** and either a prefix exists or autocomplete is forced - ❌ **NOT eligible in SELECT_LIST when scope tables exist** (scope restriction active) - ❌ **NOT eligible in scope-restricted expression contexts** (WHERE, JOIN_ON, ORDER_BY, GROUP_BY, HAVING) - ❌ **NOT applicable in table-selection contexts** (FROM_CLAUSE, JOIN_CLAUSE suggest tables, not columns) - Always use `table.column` format (no aliases for tables not in query) - Columns preserve their definition order (ordinal position in the table schema). They must NOT be reordered alphabetically. - Database-wide tables follow a deterministic stable order (schema order or internal stable ordering); within each table, preserve column definition order. - - **Performance guardrail (applies ONLY to this group when eligible):** If no prefix and total suggestions exceed threshold (400 items), skip this group to avoid lag in large databases - **No prefix definition:** prefix is `None` OR empty string after trimming whitespace - The cap applies only to group 4 (DB-wide columns). Groups 1-3 (CURRENT_TABLE, FROM, JOIN) are always included in full (already loaded/scoped). - - With prefix: always include this group when eligible (filtered results are manageable) + - With prefix: include this group when eligible (filtered results are manageable) 5. **Functions** - Alphabetically within this group @@ -1712,7 +1732,7 @@ Given a prefix P (token immediately before cursor, without '.'): - **Single table in scope:** Unqualified (e.g., `name`) - **Multiple tables in scope:** Qualified with `alias.column` if table has alias, otherwise `table.column` -**A) Table-name match expansion:** +**Table-name match expansion:* - For EVERY table T whose name startswith(P), return ALL columns of T as column suggestions - **Qualification:** Always qualified with `alias.column` if table has alias, otherwise `table.column` (even for single table) - **Rationale:** Qualified names indicate the match is from table name, helping users discover dot-completion @@ -1739,17 +1759,17 @@ Given a prefix P (token immediately before cursor, without '.'): See **Scope-Restricted Expression Contexts** section for complete rules. -**SELECT_LIST without scope tables:** +**SELECT_LIST without explicit scope tables:** - `CURRENT_TABLE` columns MUST be included first (if set) -- Database-wide table-name expansion and column-name matching are included **ONLY when prefix exists** -- **CRITICAL: When no prefix exists, DB-wide columns MUST NOT be shown (guardrail against noise)** +- Database-wide table-name expansion and column-name matching are included **ONLY when a prefix exists or autocomplete is forced** +- **CRITICAL: When there is no prefix and autocomplete is not forced, DB-wide columns MUST NOT be shown** - **With prefix matching order:** 1. **CURRENT_TABLE table-name expansion** (if CURRENT_TABLE name matches prefix) 2. **Other DB-wide table-name expansions** (tables whose names match prefix) 3. **Column-name matching from all DB tables** (columns whose names match prefix) 4. **Functions** -**SELECT_LIST with scope tables:** +**SELECT_LIST with explicit scope tables:** - `CURRENT_TABLE` columns MUST be included ONLY if `CURRENT_TABLE` is in scope - Database-wide columns MUST NOT be suggested (regardless of prefix) - Scope table columns are included with alias-first qualification @@ -1765,7 +1785,7 @@ See **Scope-Restricted Expression Contexts** section for complete rules. **Examples:** -**SELECT_LIST without scope, with CURRENT_TABLE and prefix:** +**SELECT_LIST without explicit scope, with CURRENT_TABLE and prefix:** ```sql -- Assume CURRENT_TABLE = users, prefix = "u" SELECT u| @@ -1825,57 +1845,6 @@ SELECT * FROM users u JOIN orders o WHERE u| → CURRENT_TABLE priority ignored in this case ``` -**Example in table editor context (CURRENT_TABLE = users, no alias):** -```sql -SELECT u| -→ Context: SELECT_LIST without scope (no FROM/JOIN) -→ Prefix: "u" -→ users.id (CURRENT_TABLE column starting with 'u') -→ users.name (CURRENT_TABLE column - shown for context) -→ orders.user_id (DB-wide column starting with 'u' - allowed because no scope AND prefix exists) -→ products.unit_price (DB-wide column starting with 'u') -→ UPPER (function starting with 'u') -→ UUID (function starting with 'u') -→ UPDATE (keyword starting with 'u') -``` - -**Example in multi-query context (CURRENT_TABLE = users, second query has no scope):** -```sql -SELECT * FROM users u WHERE id = 1; SELECT u| -→ Context: SELECT_LIST without scope (second query has no FROM/JOIN) -→ Prefix: "u" -→ users.id (CURRENT_TABLE column starting with 'u') -→ users.name (CURRENT_TABLE column - shown for context) -→ orders.user_id (DB-wide column starting with 'u' - allowed because no scope AND prefix exists) -→ products.unit_price (DB-wide column starting with 'u') -→ UPPER (function starting with 'u') -→ UUID (function starting with 'u') -→ UPDATE (keyword starting with 'u') -``` - -**Example in query with FROM:** -```sql -SELECT * FROM users WHERE u| -→ Columns: (none - no columns start with 'u') (single table: unqualified) -→ UPPER (function starting with 'u') -→ UPDATE (keyword starting with 'u') -→ (DB-wide columns excluded - WHERE is scope-restricted expression context) -``` - -**Example in query with JOIN (alias-exact-match mode):** -```sql -SELECT * FROM users u JOIN orders o WHERE u| -→ Context: WHERE (scope-restricted) -→ Prefix: "u" -→ Alias-exact-match mode (u == alias 'u') -→ u.id, u.name, u.email, u.password, u.is_enabled, u.created_at (all columns from alias 'u', multiple tables: qualified) -→ UPPER, UUID, UNIX_TIMESTAMP (functions starting with 'u') -→ (DB-wide columns excluded - WHERE is scope-restricted expression context) -→ UPDATE (KEYWORD) -``` - ---- - ### Out-of-Scope Table Hints (SELECT_LIST with Scope) **Applies ONLY in SELECT_LIST when scope tables already exist (FROM/JOIN present).** @@ -1984,7 +1953,7 @@ This table provides a quick reference for implementers to understand the behavio | Context | Scope Required | DB-wide Columns | CURRENT_TABLE | Table Hints | |---------|---------------|-----------------|---------------|-------------| -| **SELECT_LIST (no scope)** | No | Only with prefix | Yes (if set) | No | +| **SELECT_LIST (no scope)** | No | With prefix, or with forced autocomplete | Yes (if set) | No | | **SELECT_LIST (with scope)** | Yes | No | Only if in scope | Yes (if prefix matches) | | **FROM_CLAUSE** | Scope building | N/A | Yes (if set, not present) | N/A | | **JOIN_CLAUSE** | Scope extension | N/A | Yes (if set, not present) | N/A | @@ -2001,125 +1970,37 @@ This table provides a quick reference for implementers to understand the behavio - **Table Hints:** Whether out-of-scope table hints can be suggested **Notes:** -- SELECT_LIST without scope: DB-wide columns included only when prefix exists (guardrail against noise) +- SELECT_LIST without explicit scope: DB-wide columns included only when a prefix exists or autocomplete is forced - SELECT_LIST with scope: DB-wide columns excluded; Out-of-Scope Table Hints shown when prefix matches DB tables but no scope tables/columns - FROM_CLAUSE and JOIN_CLAUSE are table-selection contexts (scope building/extension), not column contexts - All scope-restricted expression contexts (JOIN_ON, WHERE, ORDER_BY, GROUP_BY, HAVING) follow the same rules (see **Scope-Restricted Expression Contexts** section) -- Performance guardrail applies only to DB-wide columns group when no prefix (see Ordering Rules group 4) ---- +## DB-wide policy summary -## Implementation Notes +DB-wide suggestions are allowed only when there is **no explicit scope**. -- Context detection uses `sqlglot.parse_one()` with `ErrorLevel.IGNORE` for incomplete SQL -- Dialect is retrieved from `CURRENT_CONNECTION.get_value().engine.value.dialect` -- `CURRENT_TABLE` is an observable: `CURRENT_TABLE.get_value() -> Optional[SQLTable]` - - Used to prioritize columns from the current table when set - - Can be `None` if no table is currently selected -- Fallback to regex-based context detection if sqlglot parsing fails +They are allowed when at least one of the following is true: +- a prefix exists +- autocomplete was explicitly forced --- -### Architecture Notes - -**Critical:** Centralize resolution logic to avoid duplication, but distinguish between table-selection and expression contexts. - -**Two distinct resolution functions are needed:** - -#### 1. Table Selection (FROM_CLAUSE, JOIN_CLAUSE) - -```python -def resolve_tables_for_table_selection( - context: SQLContext, - scope: QueryScope, - current_table: Optional[SQLTable] = None, - prefix: Optional[str] = None -) -> List[TableSuggestion]: - """ - Resolve table candidates for FROM/JOIN clauses. - - Returns tables in priority order: - 1. CTE names (if available from WITH clause) - 2. Physical tables from database - 3. CURRENT_TABLE (if set and not already present in current statement) - convenience shortcut - - Filtering: - - If prefix provided, filter by startswith(prefix) - - Exclude tables already present in the current statement (query separated by separator) - - Note: This is table-selection, not column resolution. - CURRENT_TABLE can appear even if scope tables already exist. - """ - pass -``` - -#### 2. Expression Contexts (SELECT_LIST, WHERE, JOIN_ON, ORDER_BY, GROUP_BY, HAVING) - -```python -def resolve_columns_for_expression( - context: SQLContext, - scope: QueryScope, - current_table: Optional[SQLTable] = None, - prefix: Optional[str] = None -) -> List[ColumnSuggestion]: - """ - Resolve columns for expression contexts with scope-aware restrictions. - - Behavior depends on context and scope: - - SCOPE-RESTRICTED contexts (WHERE, JOIN_ON, HAVING, ORDER_BY, GROUP_BY): - - See Scope-Restricted Expression Contexts section for complete rules - - Priority: FROM tables > JOIN tables - - SELECT_LIST context: - - If NO scope tables: - * Include CURRENT_TABLE columns (if set) - * Include database-wide columns (only with prefix - guardrail against noise) - - If scope tables exist: - * CURRENT_TABLE included only if in scope; otherwise ignored - * Include scope table columns - * Database-wide columns EXCLUDED (scope restriction active) - * Exception: Out-of-Scope Table Hints if prefix matches DB tables but no scope columns - - All columns use alias.column format when alias exists, otherwise table.column. - """ - pass -``` - -**Benefits:** -- Clear separation between table-selection and expression contexts -- Enforces scope restriction rules consistently -- Single source of truth for each context type -- Easier to test and maintain -- Avoids logic duplication - -**Architectural improvement (optional):** +## Scope and Product Notes -For cleaner architecture, consider using a `QueryScope` object instead of passing multiple parameters: +This file describes the **target autocomplete behavior**. -```python -@dataclass -class QueryScope: - from_tables: List[TableReference] - join_tables: List[TableReference] - derived_tables: List[DerivedTable] - ctes: List[CTE] - current_table: Optional[SQLTable] - aliases: Dict[str, TableReference] # alias -> table mapping +Implementation details such as: +- parser choice +- regex strategy +- internal architecture +- helper functions +- caching strategy -def resolve_columns_in_scope( - scope: QueryScope, - prefix: Optional[str] = None -) -> List[ColumnSuggestion]: - """Pure function - no global context dependency.""" - pass -``` - -This makes the function pure and easier to test. +do **not** belong in `RULES.md`. They belong in technical documentation such as the README or developer docs. --- -**Tables in Scope Definition (with CTEs and Derived Tables):** +## Tables in Scope Definition (with CTEs and Derived Tables) With CTEs and subquery aliases, "tables in scope" is not just physical tables from FROM/JOIN. The priority order is: @@ -2163,9 +2044,9 @@ WHERE | **Important:** Scope handling for subqueries and CTEs. -**v1 subquery scope resolution:** -- **v1 supports inner scope when cursor is inside parentheses of a subquery** (sqlglot typically handles this correctly) -- Fallback to outer scope only when parsing/cursor mapping fails +**Goal:** +- when cursor is inside a subquery, suggestions should use the subquery scope +- when cursor is outside, suggestions should use the outer query scope - When subquery has FROM clause, suggest columns from subquery scope (inner scope) - When cursor is outside subquery parentheses, suggest columns from outer scope @@ -2263,119 +2144,6 @@ SELECT *, ROW_NUMBER() OVER (PARTITION BY status ORDER BY | --- -### Potential Challenges - -**sqlglot Parsing of Incomplete SQL:** - -Test thoroughly with partial queries. You might need a hybrid approach that falls back to regex faster than expected. - -**Examples of challenging cases:** -```sql -SELECT id, name FROM users WHERE | -→ sqlglot may parse successfully - -SELECT id, name FROM users WH| -→ sqlglot may fail, need regex fallback - -SELECT * FROM users WHERE status = '| -→ Incomplete string, sqlglot may fail -``` - -**Recommendation:** -- Use `sqlglot.parse_one()` with `ErrorLevel.IGNORE` as primary approach -- Implement robust regex fallback for common patterns -- Test with many incomplete query variations -- Log parsing failures to identify patterns that need special handling - -**Fallback trigger rule:** -- If sqlglot does not produce a useful AST → fallback to regex -- If cursor position cannot be mapped to an AST node → fallback to regex -- Log: `(dialect, snippet_around_cursor, reason)` for building golden test cases - -**Example logging:** -```python -if not ast or not can_map_cursor_to_node(ast, cursor_pos): - logger.debug( - "sqlglot_fallback", - dialect=dialect, - snippet=text[max(0, cursor_pos-50):cursor_pos+50], - reason="no_useful_ast" if not ast else "cursor_mapping_failed" - ) - return regex_based_context_detection(text, cursor_pos) -``` - -**Benefit:** Build real-world golden tests from production edge cases - -**Cursor Position Context:** - -Make sure context detection knows exactly where the cursor is, not just what's before it. - -**Critical distinction:** -```sql -SELECT | FROM users -→ Context: SELECT_LIST (before FROM) -→ Show: columns, functions - -SELECT id| FROM users -→ Context: After column name (before FROM) -→ Show: FROM, AS, etc. (comma is never suggested) -``` - -**Implementation note:** -- Extract text before cursor: `text[:cursor_pos]` -- Extract text after cursor: `text[cursor_pos:]` (for context validation) -- Check if cursor is immediately after a complete token vs in the middle -- Use both left and right context for accurate detection - ---- - -### Performance Optimization - -**Large Schemas:** - -The 400-item guardrail is good, but additional optimizations are recommended: - -**Debouncing:** -- Delay autocomplete trigger by 150-300ms after last keystroke -- Avoids excessive computation while user is typing rapidly -- Cancel pending autocomplete requests if new input arrives - -**Caching:** -- Cache database schema (tables, columns) in memory -- Refresh only when schema changes (DDL operations detected) -- Cache parsed query structure for current statement -- Invalidate cache when query changes significantly - -**Schema cache invalidation triggers:** -- DDL operations: `CREATE`, `ALTER`, `DROP`, `TRUNCATE` -- Database/schema change (e.g., `USE database`) -- Manual refresh (user-triggered) -- Reconnection to database -- **Best-effort approach:** Some engines (e.g., PostgreSQL) support event listeners for schema changes; if not available, invalidate on DDL keyword detection or periodic refresh - -**Lazy Loading:** -- Load column details only when needed (not all upfront) -- For large tables (>100 columns), load columns on-demand -- Consider pagination for very large suggestion lists - -**Example implementation:** -```python -class AutocompleteCache: - def __init__(self): - self._schema_cache = {} # {database: {table: [columns]}} - self._last_query_hash = None - self._parsed_query_cache = None - - def get_columns(self, table: str) -> List[Column]: - if table not in self._schema_cache: - self._schema_cache[table] = fetch_columns(table) - return self._schema_cache[table] - - def invalidate_schema(self): - self._schema_cache.clear() -``` - ---- ### Statement Separator @@ -2420,19 +2188,7 @@ effective_separator = user_override or engine_default - `"$$"` → invalid (symbols only) - `"GO "` → invalid after trim (contains space before trim) -**Implementation notes:** - -- **Single-character separators:** Simple string split with string/comment awareness -- **Multi-character token separators:** Require word boundary detection using regex `\b{separator}\b` - - Example: `\bGO\b` for SQL Server - - Word boundary `\b` works correctly because multi-char separators are restricted to `[A-Za-z0-9_]+` -- **Both types MUST respect:** - - String literals: `'...'`, `"..."`, `` `...` `` - - Comments: `-- ...`, `/* ... */` - - Dollar-quoted strings (PostgreSQL): `$$...$$`, `$tag$...$tag$` - -All multi-query splitting logic MUST use the effective separator. -Hardcoding `";"` is forbidden. +The separator detection must respect strings, comments, and the active separator rules. --- @@ -2440,10 +2196,10 @@ Hardcoding `";"` is forbidden. **Important:** When multiple queries are present in the editor (separated by the effective statement separator), context detection must operate on the **current query** (where the cursor is), not the entire buffer. -**Implementation approach:** -1. Find statement boundaries by detecting the effective separator -2. Extract the query containing the cursor position -3. Run context detection only on that query +The autocomplete system must: +1. find the current statement using the effective separator +2. extract only the statement containing the cursor +3. resolve context only inside that statement **Edge cases:** - If cursor is on the separator, treat it as "end of previous statement". @@ -2478,11 +2234,6 @@ Context detection should analyze only: `SELECT * FROM orders WHERE |` - Dollar-quoted strings (PostgreSQL: `$$...$$`) - For token separators, word boundaries MUST be respected (e.g., `users_GO` is NOT a separator) -**Recommended approach:** -- Use sqlglot lexer/tokenizer to find statement boundaries (handles strings/comments correctly) -- For token separators: use regex with word boundaries (e.g., `\bGO\b` case-insensitive) -- Or implement robust separator detection with string/comment awareness - ### Multi-Word Keywords Multi-word keywords (e.g., `ORDER BY`, `GROUP BY`, `IS NULL`, `IS NOT NULL`, `NULLS FIRST`) are suggested as a single completion item but inserted verbatim. @@ -2534,7 +2285,7 @@ SELECT * FROM users;;| SELECT * FROM users WHERE name = '| ``` -**Resolution:** sqlglot parsing may fail. Fallback to regex-based context detection. Suggest literal keywords (`NULL`, `TRUE`, `FALSE`) and allow user to complete the string. +**Resolution:** Keep the user in the string-literal context and avoid unrelated suggestions. --- @@ -2546,7 +2297,7 @@ SELECT * FROM users WHERE name = '| SELECT * FROM users WHERE note = 'Price: $10; Discount: 20%' AND | ``` -**Resolution:** The `;` inside the string MUST be ignored. Use sqlglot lexer/tokenizer or implement string/comment-aware separator detection. Context: WHERE clause. +**Resolution:** The `;` inside the string MUST be ignored. Context remains WHERE clause. --- @@ -2627,7 +2378,7 @@ SELECT * FROM active_users WHERE | WITH active_users AS (SELECT * FROM users WHERE status = 'active') ``` -**Resolution:** sqlglot parsing may fail or produce incorrect AST. Fallback to regex-based context detection. CTE `active_users` is not recognized (defined after usage). Treat as physical table or show error hint. +**Resolution:** `active_users` is not yet available in scope. Treat it as unresolved and do not assume CTE scope before its definition. --- diff --git a/tests/autocomplete/cases/delete.json b/tests/autocomplete/cases/delete.json new file mode 100644 index 0000000..6725212 --- /dev/null +++ b/tests/autocomplete/cases/delete.json @@ -0,0 +1,438 @@ +{ + "group": "DELETE", + "cases": [ + { + "case_id": "DELETE_001", + "title": "DELETE FROM without prefix suggests tables", + "sql": "DELETE FROM |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_FROM", + "prefix": null, + "suggestions": [ + "carts", + "customers", + "inventory", + "items", + "orders", + "orders_archive", + "payments", + "products", + "users", + "user_sessions" + ] + } + }, + { + "case_id": "DELETE_002", + "title": "DELETE FROM with prefix filters tables", + "sql": "DELETE FROM u|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "DELETE_FROM", + "prefix": "u", + "suggestions": [ + "users", + "user_sessions" + ] + } + }, + { + "case_id": "DELETE_003", + "title": "DELETE FROM table suggests WHERE", + "sql": "DELETE FROM users |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_CLAUSE", + "prefix": null, + "suggestions": [ + "WHERE", + "JOIN", + "INNER JOIN", + "LEFT JOIN", + "RIGHT JOIN", + "CROSS JOIN", + "USING", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "DELETE_004", + "title": "DELETE FROM table WHERE suggests columns", + "sql": "DELETE FROM users WHERE |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_CONDITIONS", + "prefix": null, + "suggestions": [ + "id", + "name", + "email", + "status", + "created_at", + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "DELETE_005", + "title": "DELETE FROM table WHERE with column prefix filters columns", + "sql": "DELETE FROM users WHERE e|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "DELETE_WHERE_CONDITIONS", + "prefix": "e", + "suggestions": [ + "email" + ] + } + }, + { + "case_id": "DELETE_006", + "title": "DELETE FROM table WHERE column suggests operators", + "sql": "DELETE FROM users WHERE id |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_OPERATORS", + "prefix": null, + "suggestions": [ + "=", + "!=", + "<>", + ">", + "<", + ">=", + "<=", + "IS", + "IS NOT", + "IN", + "NOT IN", + "LIKE", + "NOT LIKE", + "BETWEEN", + "NOT BETWEEN", + "AND", + "OR" + ] + } + }, + { + "case_id": "DELETE_007", + "title": "DELETE FROM table WHERE column = suggests expressions", + "sql": "DELETE FROM users WHERE id = |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_EXPRESSIONS", + "prefix": null, + "suggestions": [ + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "DELETE_008", + "title": "DELETE FROM table WHERE column = value suggests AND/OR", + "sql": "DELETE FROM users WHERE id = 1 |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_CONDITIONS", + "prefix": null, + "suggestions": [ + "AND", + "OR", + "LIMIT", + "ORDER BY", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "DELETE_009", + "title": "DELETE with JOIN suggests ON", + "sql": "DELETE users FROM users u JOIN orders o ON |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_JOIN_ON", + "prefix": null, + "suggestions": [ + "u.id", + "u.name", + "u.email", + "u.status", + "u.created_at", + "o.id", + "o.user_id", + "o.total", + "o.status", + "o.created_at", + "NULL", + "TRUE", + "FALSE", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "DELETE_010", + "title": "DELETE with JOIN ON condition suggests WHERE", + "sql": "DELETE users FROM users u JOIN orders o ON u.id = o.user_id |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_CLAUSE", + "prefix": null, + "suggestions": [ + "WHERE", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "DELETE_011", + "title": "DELETE FROM table USING suggests tables", + "sql": "DELETE FROM users USING |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_USING", + "prefix": null, + "suggestions": [ + "carts", + "customers", + "inventory", + "items", + "orders", + "orders_archive", + "payments", + "products", + "user_sessions" + ] + } + }, + { + "case_id": "DELETE_012", + "title": "DELETE FROM table USING table suggests WHERE", + "sql": "DELETE FROM users USING orders |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_CLAUSE", + "prefix": null, + "suggestions": [ + "WHERE", + "JOIN", + "INNER JOIN", + "LEFT JOIN", + "RIGHT JOIN", + "CROSS JOIN", + "USING", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "DELETE_013", + "title": "DELETE FROM table WHERE IN subquery", + "sql": "DELETE FROM users WHERE id IN (|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_SUBQUERY", + "prefix": null, + "suggestions": [ + "SELECT", + "WITH", + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "DELETE_014", + "title": "DELETE WHERE inside string literal no suggestions", + "sql": "DELETE FROM users WHERE name = '|'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside WHERE string literal should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "DELETE_015", + "title": "DELETE WHERE inside string literal with content no suggestions", + "sql": "DELETE FROM users WHERE name = 'test|'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "DELETE_WHERE_STRING_LITERAL", + "prefix": "test", + "comment": "Cursor inside WHERE string literal with content should not suggest anything", + "suggestions": [] + } + } + ] +} diff --git a/tests/autocomplete/cases/from.json b/tests/autocomplete/cases/from.json index 3fb1c65..fa7635e 100644 --- a/tests/autocomplete/cases/from.json +++ b/tests/autocomplete/cases/from.json @@ -231,6 +231,43 @@ "WHERE" ] } + }, + { + "case_id": "FROM_013", + "title": "Schema-qualified table token supports follow-up clause keywords", + "sql": "SELECT * FROM public.users W|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "FROM_CLAUSE", + "prefix": "W", + "suggestions": [ + "WHERE" + ] + } + }, + { + "case_id": "FROM_014", + "title": "Quoted table token supports follow-up clause keywords", + "sql": "SELECT * FROM \"users\" |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "FROM_CLAUSE", + "prefix": null, + "suggestions_contains": [ + "WHERE", + "JOIN" + ], + "suggestions_not_contains": [ + "users", + "orders" + ] + } } ] } diff --git a/tests/autocomplete/cases/insert.json b/tests/autocomplete/cases/insert.json new file mode 100644 index 0000000..a9621bb --- /dev/null +++ b/tests/autocomplete/cases/insert.json @@ -0,0 +1,340 @@ +{ + "group": "INSERT", + "cases": [ + { + "case_id": "INSERT_001", + "title": "INSERT INTO without prefix suggests tables", + "sql": "INSERT INTO |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_INTO", + "prefix": null, + "suggestions": [ + "carts", + "customers", + "inventory", + "items", + "orders", + "orders_archive", + "payments", + "products", + "users", + "user_sessions" + ] + } + }, + { + "case_id": "INSERT_002", + "title": "INSERT INTO with prefix filters tables", + "sql": "INSERT INTO u|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "INSERT_INTO", + "prefix": "u", + "suggestions": [ + "users", + "user_sessions" + ] + } + }, + { + "case_id": "INSERT_003", + "title": "INSERT INTO table suggests columns", + "sql": "INSERT INTO users (|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_COLUMNS", + "prefix": null, + "suggestions": [ + "id", + "name", + "email", + "status", + "created_at" + ] + } + }, + { + "case_id": "INSERT_004", + "title": "INSERT INTO table with column prefix filters columns", + "sql": "INSERT INTO users (e|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "INSERT_COLUMNS", + "prefix": "e", + "suggestions": [ + "email" + ] + } + }, + { + "case_id": "INSERT_005", + "title": "INSERT INTO table after comma suggests more columns", + "sql": "INSERT INTO users (name, |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_COLUMNS", + "prefix": null, + "suggestions": [ + "id", + "email", + "status", + "created_at" + ] + } + }, + { + "case_id": "INSERT_006", + "title": "INSERT INTO table VALUES suggests keywords", + "sql": "INSERT INTO users (name, email) |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_VALUES", + "prefix": null, + "suggestions": [ + "VALUES", + "SELECT", + "DEFAULT" + ] + } + }, + { + "case_id": "INSERT_007", + "title": "INSERT INTO table VALUES prefix filters keywords", + "sql": "INSERT INTO users (name, email) V|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "INSERT_VALUES", + "prefix": "V", + "suggestions": [ + "VALUES" + ] + } + }, + { + "case_id": "INSERT_008", + "title": "INSERT INTO table VALUES ( suggests literals", + "sql": "INSERT INTO users (name, email) VALUES (|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_VALUE_EXPRESSIONS", + "prefix": null, + "suggestions": [ + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "INSERT_009", + "title": "INSERT INTO table VALUES (value, suggests next value", + "sql": "INSERT INTO users (name, email) VALUES ('test', |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_VALUE_EXPRESSIONS", + "prefix": null, + "suggestions": [ + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "INSERT_010", + "title": "INSERT INTO table VALUES () suggests closing", + "sql": "INSERT INTO users (name, email) VALUES ('test', 'test@example.com')|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "INSERT_COMPLETE", + "prefix": ")", + "suggestions": [] + } + }, + { + "case_id": "INSERT_011", + "title": "INSERT INTO table VALUES () , suggests more inserts", + "sql": "INSERT INTO users (name, email) VALUES ('test', 'test@example.com'), |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_VALUE_EXPRESSIONS", + "prefix": null, + "suggestions": [ + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "INSERT_012", + "title": "INSERT complete suggests ON DUPLICATE KEY etc", + "sql": "INSERT INTO users (name, email) VALUES ('test', 'test@example.com') |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_POST_VALUES", + "prefix": null, + "suggestions": [ + "ON DUPLICATE KEY UPDATE", + "ON CONFLICT", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "INSERT_013", + "title": "INSERT inside string literal no suggestions", + "sql": "INSERT INTO users (name, email) VALUES ('|', 'test@example.com')", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside string literal should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "INSERT_014", + "title": "INSERT inside string literal with content no suggestions", + "sql": "INSERT INTO users (name, email) VALUES ('test|', 'test@example.com')", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "INSERT_STRING_LITERAL", + "prefix": "test", + "comment": "Cursor inside string literal with content should not suggest anything", + "suggestions": [] + } + } + ] +} diff --git a/tests/autocomplete/cases/provider_edges.json b/tests/autocomplete/cases/provider_edges.json new file mode 100644 index 0000000..fe80b72 --- /dev/null +++ b/tests/autocomplete/cases/provider_edges.json @@ -0,0 +1,45 @@ +{ + "group": "PROVIDER_EDGES", + "cases": [ + { + "case_id": "PROVIDER_EDGES_001", + "title": "Cursor on new whitespace-only statement after separator opens EMPTY context", + "sql": "SELECT * FROM users; |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "EMPTY", + "context": "EMPTY", + "prefix": null, + "suggestions_contains": [ + "SELECT", + "INSERT" + ], + "suggestions_not_contains": [ + "users", + "id" + ] + } + }, + { + "case_id": "PROVIDER_EDGES_002", + "title": "Second statement scope is isolated from the previous statement in multi-query buffer", + "sql": "SELECT * FROM users;\nSELECT * FROM orders WHERE st|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "WHERE_CLAUSE", + "prefix": "st", + "suggestions_contains": [ + "status" + ], + "suggestions_not_contains": [ + "users.status" + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/autocomplete/cases/sel.json b/tests/autocomplete/cases/sel.json index a41fba6..698b066 100644 --- a/tests/autocomplete/cases/sel.json +++ b/tests/autocomplete/cases/sel.json @@ -3,7 +3,7 @@ "cases": [ { "case_id": "SEL_LIST_001", - "title": "SELECT_LIST without FROM/JOIN shows functions + clause keywords", + "title": "SELECT_LIST without FROM/JOIN and without CURRENT_TABLE shows functions only", "sql": "SELECT |", "dialect": "generic", "current_table": null, diff --git a/tests/autocomplete/cases/select_column_behavior.json b/tests/autocomplete/cases/select_column_behavior.json index 5734c28..4ca425e 100644 --- a/tests/autocomplete/cases/select_column_behavior.json +++ b/tests/autocomplete/cases/select_column_behavior.json @@ -89,7 +89,7 @@ }, { "case_id": "NO_SCOPED_COMMA_001", - "title": "After column + comma suggests next-item (columns + functions)", + "title": "After column + comma with NO_SCOPED suggests next-item functions only", "sql": "SELECT id, |", "dialect": "generic", "current_table": null, @@ -98,7 +98,7 @@ "mode": "CONTEXT", "context": "SELECT_LIST", "prefix": null, - "comment": "Comma = next-item rules. With no scope and no prefix, expect functions.", + "comment": "Comma = next-item rules. With no explicit scope, no qualified references, and no CURRENT_TABLE, this remains NO_SCOPED so only functions are expected.", "suggestions_contains": [ "COUNT", "UUID" diff --git a/tests/autocomplete/cases/update.json b/tests/autocomplete/cases/update.json new file mode 100644 index 0000000..d2c7bf1 --- /dev/null +++ b/tests/autocomplete/cases/update.json @@ -0,0 +1,413 @@ +{ + "group": "UPDATE", + "cases": [ + { + "case_id": "UPDATE_001", + "title": "UPDATE without prefix suggests tables", + "sql": "UPDATE |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_TABLE", + "prefix": null, + "suggestions": [ + "carts", + "customers", + "inventory", + "items", + "orders", + "orders_archive", + "payments", + "products", + "users", + "user_sessions" + ] + } + }, + { + "case_id": "UPDATE_002", + "title": "UPDATE with prefix filters tables", + "sql": "UPDATE u|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "UPDATE_TABLE", + "prefix": "u", + "suggestions": [ + "users", + "user_sessions" + ] + } + }, + { + "case_id": "UPDATE_003", + "title": "UPDATE table suggests SET", + "sql": "UPDATE users |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_SET_CLAUSE", + "prefix": null, + "suggestions": [ + "SET", + "JOIN", + "INNER JOIN", + "LEFT JOIN", + "RIGHT JOIN", + "CROSS JOIN" + ] + } + }, + { + "case_id": "UPDATE_004", + "title": "UPDATE table SET suggests columns", + "sql": "UPDATE users SET |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_SET_COLUMNS", + "prefix": null, + "suggestions": [ + "id", + "name", + "email", + "status", + "created_at" + ] + } + }, + { + "case_id": "UPDATE_005", + "title": "UPDATE table SET with column prefix filters columns", + "sql": "UPDATE users SET e|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "UPDATE_SET_COLUMNS", + "prefix": "e", + "suggestions": [ + "email" + ] + } + }, + { + "case_id": "UPDATE_006", + "title": "UPDATE table SET column = suggests expressions", + "sql": "UPDATE users SET email = |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_SET_EXPRESSIONS", + "prefix": null, + "suggestions": [ + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "UPDATE_007", + "title": "UPDATE table SET column = value, suggests more columns", + "sql": "UPDATE users SET email = 'test@example.com', |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_SET_COLUMNS", + "prefix": null, + "suggestions": [ + "id", + "name", + "status", + "created_at" + ] + } + }, + { + "case_id": "UPDATE_008", + "title": "UPDATE table SET column = value WHERE suggests WHERE", + "sql": "UPDATE users SET email = 'test@example.com' |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_WHERE_CLAUSE", + "prefix": null, + "suggestions": [ + "WHERE", + "JOIN", + "INNER JOIN", + "LEFT JOIN", + "RIGHT JOIN", + "CROSS JOIN", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "UPDATE_009", + "title": "UPDATE table SET WHERE suggests columns", + "sql": "UPDATE users SET email = 'test@example.com' WHERE |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_WHERE_CONDITIONS", + "prefix": null, + "suggestions": [ + "id", + "name", + "email", + "status", + "created_at", + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "UPDATE_010", + "title": "UPDATE table SET WHERE column = suggests operators", + "sql": "UPDATE users SET email = 'test@example.com' WHERE id |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_WHERE_OPERATORS", + "prefix": null, + "suggestions": [ + "=", + "!=", + "<>", + ">", + "<", + ">=", + "<=", + "IS", + "IS NOT", + "IN", + "NOT IN", + "LIKE", + "NOT LIKE", + "BETWEEN", + "NOT BETWEEN", + "AND", + "OR" + ] + } + }, + { + "case_id": "UPDATE_011", + "title": "UPDATE table SET WHERE column = value suggests AND/OR", + "sql": "UPDATE users SET email = 'test@example.com' WHERE id = 1 |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_WHERE_CONDITIONS", + "prefix": null, + "suggestions": [ + "AND", + "OR", + "LIMIT", + "ORDER BY", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "UPDATE_012", + "title": "UPDATE with JOIN suggests ON", + "sql": "UPDATE users u JOIN orders o ON |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_JOIN_ON", + "prefix": null, + "suggestions": [ + "u.id", + "u.name", + "u.email", + "u.status", + "u.created_at", + "o.id", + "o.user_id", + "o.total", + "o.status", + "o.created_at", + "NULL", + "TRUE", + "FALSE", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "UPDATE_013", + "title": "UPDATE with JOIN ON condition suggests SET", + "sql": "UPDATE users u JOIN orders o ON u.id = o.user_id |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_SET_CLAUSE", + "prefix": null, + "suggestions": [ + "SET" + ] + } + }, + { + "case_id": "UPDATE_014", + "title": "UPDATE inside string literal no suggestions", + "sql": "UPDATE users SET email = '|', name = 'test'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside string literal should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "UPDATE_015", + "title": "UPDATE inside string literal with content no suggestions", + "sql": "UPDATE users SET email = 'test|', name = 'test2'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "UPDATE_STRING_LITERAL", + "prefix": "test", + "comment": "Cursor inside string literal with content should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "UPDATE_016", + "title": "UPDATE WHERE inside string literal no suggestions", + "sql": "UPDATE users SET email = 'test@example.com' WHERE name = '|'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_WHERE_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside WHERE string literal should not suggest anything", + "suggestions": [] + } + } + ] +} diff --git a/tests/autocomplete/cases/where.json b/tests/autocomplete/cases/where.json index bc72adb..c7f204a 100644 --- a/tests/autocomplete/cases/where.json +++ b/tests/autocomplete/cases/where.json @@ -395,6 +395,66 @@ "email" ] } + }, + { + "case_id": "WHERE_014", + "title": "WHERE inside string literal no suggestions", + "sql": "SELECT * FROM users WHERE name = '|'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "WHERE_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside WHERE string literal should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "WHERE_015", + "title": "WHERE inside string literal with content no suggestions", + "sql": "SELECT * FROM users WHERE name = 'test|'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "WHERE_STRING_LITERAL", + "prefix": "test", + "comment": "Cursor inside WHERE string literal with content should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "WHERE_016", + "title": "WHERE IN inside string literal no suggestions", + "sql": "SELECT * FROM users WHERE name IN ('|')", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "WHERE_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside WHERE IN string literal should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "WHERE_017", + "title": "WHERE LIKE inside string literal no suggestions", + "sql": "SELECT * FROM users WHERE name LIKE '|'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "WHERE_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside WHERE LIKE string literal should not suggest anything", + "suggestions": [] + } } ] } diff --git a/tests/autocomplete/test_autocomplete_basic.py b/tests/autocomplete/test_autocomplete_basic.py index 582062f..9da6f0f 100644 --- a/tests/autocomplete/test_autocomplete_basic.py +++ b/tests/autocomplete/test_autocomplete_basic.py @@ -1,8 +1,15 @@ from typing import Optional from unittest.mock import Mock -from windows.components.stc.autocomplete.auto_complete import SQLCompletionProvider +from windows.components.stc.autocomplete.auto_complete import ( + SQLAutoCompleteController, + SQLCompletionProvider, +) +from windows.components.stc.autocomplete.suggestion_builder import SuggestionBuilder from windows.components.stc.autocomplete.completion_types import CompletionItemType +from windows.components.stc.autocomplete.completion_types import CompletionItem +from windows.components.stc.autocomplete.completion_types import CompletionResult +from windows.state import CURRENT_SESSION def create_mock_column(col_id: int, name: str, table): @@ -91,8 +98,6 @@ def test_empty_context(): assert "INSERT" in item_names assert "UPDATE" in item_names - print("✓ GT-010 EMPTY context test passed") - def test_single_token(): database = create_mock_database() @@ -109,8 +114,6 @@ def test_single_token(): item_names = [item.name for item in result.items] assert "SELECT" in item_names - print("✓ GT-011 SINGLE_TOKEN test passed") - def test_select_without_from(): database = create_mock_database() @@ -128,8 +131,6 @@ def test_select_without_from(): assert "SUM" in item_names assert "*" in item_names - print("✓ GT-020 SELECT without FROM test passed") - def test_select_with_from(): database = create_mock_database() @@ -148,8 +149,6 @@ def test_select_with_from(): assert "users.name" in item_names assert "COUNT" in item_names - print("✓ GT-021 SELECT with FROM test passed") - def test_where_basic(): database = create_mock_database() @@ -168,8 +167,6 @@ def test_where_basic(): assert "name" in item_names assert "COUNT" in item_names - print("✓ GT-030 WHERE basic test passed") - def test_from_clause(): database = create_mock_database() @@ -186,8 +183,6 @@ def test_from_clause(): assert "users" in item_names assert "orders" in item_names - print("✓ FROM clause test passed") - def test_dot_completion(): database = create_mock_database() @@ -208,7 +203,61 @@ def test_dot_completion(): for name in item_names: assert "users." not in name - print("✓ GT-002 Dot completion test passed") + +def test_dot_completion_with_prefix_in_select_list(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, get_current_table=lambda: None + ) + + result = provider.get(text="SELECT users.na", pos=len("SELECT users.na")) + + assert result is not None + item_names = [item.name for item in result.items] + assert item_names == ["name"] + + +def test_dot_completion_with_prefix_in_where_clause(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, get_current_table=lambda: None + ) + + sql = "SELECT * FROM users u WHERE u.em" + result = provider.get(text=sql, pos=len(sql)) + + assert result is not None + item_names = [item.name for item in result.items] + assert item_names == ["email"] + + +def test_dot_completion_with_prefix_in_order_by_clause(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, get_current_table=lambda: None + ) + + sql = "SELECT * FROM users u ORDER BY u.na" + result = provider.get(text=sql, pos=len(sql)) + + assert result is not None + item_names = [item.name for item in result.items] + assert item_names == ["name"] + + +def test_non_dot_prefix_keeps_context_suggestions(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, get_current_table=lambda: None + ) + + sql = "SELECT * FROM users WHERE na" + result = provider.get(text=sql, pos=len(sql)) + + assert result is not None + item_names = [item.name for item in result.items] + assert "name" in item_names + assert "email" not in item_names def test_multi_query(): @@ -230,7 +279,175 @@ def test_multi_query(): assert "id" in item_names assert "user_id" in item_names - print("✓ GT-001 Multi-query test passed") + +def test_clamp_position_boundaries(): + assert SQLCompletionProvider._clamp_position(pos=-1, text="SELECT") == 0 + assert SQLCompletionProvider._clamp_position(pos=999, text="SELECT") == 6 + assert SQLCompletionProvider._clamp_position(pos=3, text="SELECT") == 3 + + +def test_rebuilds_context_detector_when_dialect_changes(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, + get_current_table=lambda: None, + ) + + session_mysql = Mock() + session_mysql.engine.value.dialect = "mysql" + + session_postgresql = Mock() + session_postgresql.engine.value.dialect = "postgresql" + + try: + CURRENT_SESSION.set_value(session_mysql) + first_result = provider.get(text="SEL", pos=3) + assert first_result is not None + assert provider._context_detector is not None + first_detector = provider._context_detector + + CURRENT_SESSION.set_value(session_postgresql) + second_result = provider.get(text="SEL", pos=3) + assert second_result is not None + assert provider._context_detector is not None + + assert provider._context_detector is not first_detector + assert provider._context_detector._dialect == "postgresql" + finally: + CURRENT_SESSION.set_value(None) + + +def test_unique_items_keeps_same_name_for_different_types(): + items = ( + CompletionItem(name="COUNT", item_type=CompletionItemType.FUNCTION), + CompletionItem(name="COUNT", item_type=CompletionItemType.KEYWORD), + CompletionItem(name="COUNT", item_type=CompletionItemType.FUNCTION), + ) + + unique = SQLAutoCompleteController._unique_items(items=items) + + assert unique == [ + CompletionItem(name="COUNT", item_type=CompletionItemType.FUNCTION), + CompletionItem(name="COUNT", item_type=CompletionItemType.KEYWORD), + ] + + +def test_show_respects_min_prefix_length_when_not_forced(): + class DummyEditor: + @staticmethod + def GetCurrentPos(): + return 0 + + @staticmethod + def GetText(): + return "a" + + controller = SQLAutoCompleteController.__new__(SQLAutoCompleteController) + controller._is_enabled = True + controller._is_showing = False + controller._editor = DummyEditor() + controller._provider = Mock() + controller._min_prefix_length = 2 + controller._current_result = None + + hidden = {"value": False} + shown_items = [] + + controller._hide_popup = lambda: hidden.__setitem__("value", True) + controller._show_popup = lambda items: shown_items.extend(items) + + controller._provider.get.return_value = CompletionResult( + prefix="a", + prefix_length=1, + items=(CompletionItem(name="alpha", item_type=CompletionItemType.COLUMN),), + ) + + controller.show(force=False) + + assert hidden["value"] is True + assert shown_items == [] + + +def test_show_ignores_min_prefix_length_when_forced(): + class DummyEditor: + @staticmethod + def GetCurrentPos(): + return 0 + + @staticmethod + def GetText(): + return "a" + + controller = SQLAutoCompleteController.__new__(SQLAutoCompleteController) + controller._is_enabled = True + controller._is_showing = False + controller._editor = DummyEditor() + controller._provider = Mock() + controller._min_prefix_length = 2 + controller._current_result = None + + hidden = {"value": False} + shown_items = [] + + controller._hide_popup = lambda: hidden.__setitem__("value", True) + controller._show_popup = lambda items: shown_items.extend(items) + + controller._provider.get.return_value = CompletionResult( + prefix="a", + prefix_length=1, + items=(CompletionItem(name="alpha", item_type=CompletionItemType.COLUMN),), + ) + + controller.show(force=True) + + assert hidden["value"] is False + assert [item.name for item in shown_items] == ["alpha"] + + +def test_schema_qualified_from_followup_keywords(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, get_current_table=lambda: None + ) + + sql = "SELECT * FROM public.users W" + result = provider.get(text=sql, pos=len(sql)) + + assert result is not None + assert "WHERE" in [item.name for item in result.items] + + +def test_quoted_from_followup_keywords(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, get_current_table=lambda: None + ) + + sql = 'SELECT * FROM "users" ' + result = provider.get(text=sql, pos=len(sql)) + + assert result is not None + assert "WHERE" in [item.name for item in result.items] + + +def test_schema_qualified_update_target_table_lookup(): + database = create_mock_database() + builder = SuggestionBuilder(database=database, current_table=None) + + table = builder._find_update_target_table("UPDATE public.users SET na") + + assert table is not None + assert table.name == "users" + + +def test_quoted_update_target_table_lookup(): + database = create_mock_database() + builder = SuggestionBuilder(database=database, current_table=None) + + table = builder._find_update_target_table('UPDATE "users" SET na') + + assert table is not None + assert table.name == "users" if __name__ == "__main__": diff --git a/tests/core/test_view_editor.py b/tests/core/test_view_editor.py index 6cc3b3d..b98397a 100644 --- a/tests/core/test_view_editor.py +++ b/tests/core/test_view_editor.py @@ -8,7 +8,7 @@ class TestEditViewModel: def test_init_creates_observables(self): """Test that EditViewModel initializes all required observables.""" - from windows.main.tabs.view import EditViewModel + from windows.main.database.view import EditViewModel model = EditViewModel() @@ -24,7 +24,7 @@ def test_init_creates_observables(self): def test_load_view_sets_name_observable(self): """Test that _load_view sets name observable from view.""" - from windows.main.tabs.view import EditViewModel + from windows.main.database.view import EditViewModel model = EditViewModel() @@ -32,7 +32,7 @@ def test_load_view_sets_name_observable(self): mock_view.name = "test_view" mock_view.statement = "SELECT * FROM test" - with patch('windows.main.tabs.view.CURRENT_SESSION') as mock_session: + with patch('windows.main.database.view.CURRENT_SESSION') as mock_session: mock_session.get_value.return_value = None model._load_view(mock_view) @@ -41,7 +41,7 @@ def test_load_view_sets_name_observable(self): def test_update_view_sets_name_and_statement(self): """Test that update_view sets view name and statement from observables.""" - from windows.main.tabs.view import EditViewModel + from windows.main.database.view import EditViewModel model = EditViewModel() @@ -49,7 +49,7 @@ def test_update_view_sets_name_and_statement(self): mock_view.name = "" mock_view.statement = "" - with patch('windows.main.tabs.view.CURRENT_VIEW') as mock_current_view: + with patch('windows.main.database.view.CURRENT_VIEW') as mock_current_view: mock_current_view.get_value.return_value = mock_view model.name.set_value("updated_view") @@ -104,10 +104,10 @@ def mock_parent(self): def test_init_binds_controls(self, mock_parent): """Test that controller initializes and binds controls.""" - from windows.main.tabs.view import ViewEditorController + from windows.main.database.view import ViewEditorController - with patch('windows.main.tabs.view.CURRENT_VIEW') as mock_current_view: - with patch('windows.main.tabs.view.wx_call_after_debounce'): + with patch('windows.main.database.view.CURRENT_VIEW') as mock_current_view: + with patch('windows.main.database.view.wx_call_after_debounce'): controller = ViewEditorController(mock_parent) assert controller.parent == mock_parent @@ -116,10 +116,10 @@ def test_init_binds_controls(self, mock_parent): def test_get_original_view_returns_none_for_new_view(self, mock_parent): """Test that _get_original_view returns None for new views.""" - from windows.main.tabs.view import ViewEditorController + from windows.main.database.view import ViewEditorController - with patch('windows.main.tabs.view.CURRENT_VIEW'): - with patch('windows.main.tabs.view.wx_call_after_debounce'): + with patch('windows.main.database.view.CURRENT_VIEW'): + with patch('windows.main.database.view.wx_call_after_debounce'): controller = ViewEditorController(mock_parent) mock_view = Mock() @@ -130,10 +130,10 @@ def test_get_original_view_returns_none_for_new_view(self, mock_parent): def test_has_changes_returns_true_for_new_view(self, mock_parent): """Test that _has_changes returns True for new views.""" - from windows.main.tabs.view import ViewEditorController + from windows.main.database.view import ViewEditorController - with patch('windows.main.tabs.view.CURRENT_VIEW'): - with patch('windows.main.tabs.view.wx_call_after_debounce'): + with patch('windows.main.database.view.CURRENT_VIEW'): + with patch('windows.main.database.view.wx_call_after_debounce'): controller = ViewEditorController(mock_parent) mock_view = Mock() @@ -144,10 +144,10 @@ def test_has_changes_returns_true_for_new_view(self, mock_parent): def test_update_button_states_disables_all_when_no_view(self, mock_parent): """Test that update_button_states disables all buttons when no view.""" - from windows.main.tabs.view import ViewEditorController + from windows.main.database.view import ViewEditorController - with patch('windows.main.tabs.view.CURRENT_VIEW') as mock_current_view: - with patch('windows.main.tabs.view.wx_call_after_debounce'): + with patch('windows.main.database.view.CURRENT_VIEW') as mock_current_view: + with patch('windows.main.database.view.wx_call_after_debounce'): mock_current_view.get_value.return_value = None controller = ViewEditorController(mock_parent) @@ -159,10 +159,10 @@ def test_update_button_states_disables_all_when_no_view(self, mock_parent): def test_update_button_states_enables_save_cancel_for_new_view(self, mock_parent): """Test that update_button_states enables save/cancel for new views.""" - from windows.main.tabs.view import ViewEditorController + from windows.main.database.view import ViewEditorController - with patch('windows.main.tabs.view.CURRENT_VIEW') as mock_current_view: - with patch('windows.main.tabs.view.wx_call_after_debounce'): + with patch('windows.main.database.view.CURRENT_VIEW') as mock_current_view: + with patch('windows.main.database.view.wx_call_after_debounce'): mock_view = Mock() type(mock_view).is_new = PropertyMock(return_value=True) mock_current_view.get_value.return_value = mock_view diff --git a/tests/engines/mariadb/test_context.py b/tests/engines/mariadb/test_context.py index fcdd160..5f8ade7 100644 --- a/tests/engines/mariadb/test_context.py +++ b/tests/engines/mariadb/test_context.py @@ -13,6 +13,8 @@ def test_connect_retries_with_tls_on_auth_error(self, monkeypatch): username="root", password="secret", port=3306, + connect_timeout=4, + compressed_protocol=True, ) connection = Connection( id=1, @@ -41,9 +43,11 @@ def fake_connect(**kwargs): context.connect(connect_timeout=1) assert len(calls) == 2 + assert calls[0]["connect_timeout"] == 1 + assert calls[0]["compress"] is True assert "ssl" not in calls[0] assert "ssl" in calls[1] - assert connection.configuration.use_tls_enabled is True + assert connection.configuration.use_tls is True @pytest.mark.integration diff --git a/tests/engines/mysql/test_context.py b/tests/engines/mysql/test_context.py index 523e7e2..e5d157b 100644 --- a/tests/engines/mysql/test_context.py +++ b/tests/engines/mysql/test_context.py @@ -13,6 +13,8 @@ def test_connect_retries_with_tls_on_auth_error(self, monkeypatch): username="root", password="secret", port=3306, + connect_timeout=3, + compressed_protocol=True, ) connection = Connection( id=1, @@ -41,9 +43,11 @@ def fake_connect(**kwargs): context.connect(connect_timeout=1) assert len(calls) == 2 + assert calls[0]["connect_timeout"] == 1 + assert calls[0]["compress"] is True assert "ssl" not in calls[0] assert "ssl" in calls[1] - assert connection.configuration.use_tls_enabled is True + assert connection.configuration.use_tls is True @pytest.mark.integration diff --git a/tests/test_column_controller.py b/tests/test_column_controller.py index bc475cf..7bfd050 100644 --- a/tests/test_column_controller.py +++ b/tests/test_column_controller.py @@ -3,7 +3,7 @@ from structures.engines.sqlite.context import SQLiteContext from structures.engines.sqlite.database import SQLiteDatabase, SQLiteTable, SQLiteIndex, SQLiteColumn -from windows.main.tabs.column import TableColumnsController +from windows.main.table.column import TableColumnsController @pytest.fixture @@ -37,9 +37,9 @@ def mock_table(mock_session): @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_append_column_index(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): # Setup mocks mock_get_app.return_value = Mock() @@ -72,9 +72,9 @@ def test_append_column_index(mock_new_table, mock_current_table, mock_current_se @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_on_column_insert(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): # Setup mocks mock_get_app.return_value = Mock() @@ -115,9 +115,9 @@ def test_on_column_insert(mock_new_table, mock_current_table, mock_current_sessi @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_on_column_delete(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): # Setup mocks mock_get_app.return_value = Mock() @@ -163,10 +163,10 @@ def test_on_column_delete(mock_new_table, mock_current_table, mock_current_sessi @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.CURRENT_COLUMN') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.CURRENT_COLUMN') +@patch('windows.main.table.column.NEW_TABLE') def test_on_column_move_up(mock_new_table, mock_current_column, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): # Setup mocks mock_get_app.return_value = Mock() @@ -205,9 +205,9 @@ def test_on_column_move_up(mock_new_table, mock_current_column, mock_current_tab @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_insert_column_index(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): # Setup mocks mock_get_app.return_value = Mock() diff --git a/tests/ui/test_column_controller.py b/tests/ui/test_column_controller.py index 209f234..07a341c 100644 --- a/tests/ui/test_column_controller.py +++ b/tests/ui/test_column_controller.py @@ -2,7 +2,7 @@ from unittest.mock import Mock, patch, call from structures.engines.sqlite.database import SQLiteDatabase, SQLiteTable, SQLiteIndex, SQLiteColumn -from windows.main.tabs.column import TableColumnsController +from windows.main.table.column import TableColumnsController @pytest.fixture @@ -27,9 +27,9 @@ def mock_table(sqlite_session): @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_append_column_index(mock_new_table, mock_current_table, mock_current_session, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_session.get_value.return_value = sqlite_session @@ -61,9 +61,9 @@ def test_append_column_index(mock_new_table, mock_current_table, mock_current_se @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_on_column_insert(mock_new_table, mock_current_table, mock_current_session, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_session.get_value.return_value = sqlite_session @@ -96,9 +96,9 @@ def test_on_column_insert(mock_new_table, mock_current_table, mock_current_sessi @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_on_column_delete(mock_new_table, mock_current_table, mock_current_session, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_session.get_value.return_value = sqlite_session @@ -136,10 +136,10 @@ def test_on_column_delete(mock_new_table, mock_current_table, mock_current_sessi @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.CURRENT_COLUMN') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.CURRENT_COLUMN') +@patch('windows.main.table.column.NEW_TABLE') def test_on_column_move_up(mock_new_table, mock_current_column, mock_current_table, mock_current_session, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_session.get_value.return_value = sqlite_session @@ -170,9 +170,9 @@ def test_on_column_move_up(mock_new_table, mock_current_column, mock_current_tab @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_insert_column_index(mock_new_table, mock_current_table, mock_current_session, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_session.get_value.return_value = sqlite_session diff --git a/tests/ui/test_dataview.py b/tests/ui/test_dataview.py index 5326794..8aadca5 100644 --- a/tests/ui/test_dataview.py +++ b/tests/ui/test_dataview.py @@ -1,7 +1,7 @@ import pytest from dataclasses import dataclass -from helpers.dataview import ColumnField, AbstractBaseDataModel +from helpers.dataview import ColumnField, BaseDataModel @dataclass @@ -60,14 +60,14 @@ def test_has_value_false(self): assert field.has_value(item) is False -class ConcreteDataModel(AbstractBaseDataModel): +class ConcreteDataModel(BaseDataModel): """Concrete implementation for testing.""" def set_observable(self, observable): self._observable = observable -class TestAbstractBaseDataModel: +class TestBaseDataModel: """Tests for AbstractBaseDataModel.""" def test_load(self): diff --git a/tests/ui/test_index_controller.py b/tests/ui/test_index_controller.py index d241349..c3727b3 100644 --- a/tests/ui/test_index_controller.py +++ b/tests/ui/test_index_controller.py @@ -2,7 +2,7 @@ from unittest.mock import Mock, patch, call from structures.engines.sqlite.database import SQLiteDatabase, SQLiteTable, SQLiteIndex -from windows.main.tabs.index import TableIndexController +from windows.main.table.index import TableIndexController @pytest.fixture @@ -24,9 +24,9 @@ def mock_table(sqlite_session): @patch('wx.GetApp') -@patch('windows.main.tabs.index.CURRENT_TABLE') -@patch('windows.main.tabs.index.CURRENT_INDEX') -@patch('windows.main.tabs.index.NEW_TABLE') +@patch('windows.main.table.index.CURRENT_TABLE') +@patch('windows.main.table.index.CURRENT_INDEX') +@patch('windows.main.table.index.NEW_TABLE') def test_on_index_delete(mock_new_table, mock_current_index, mock_current_table, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_table.get_value.return_value = mock_table @@ -51,9 +51,9 @@ def test_on_index_delete(mock_new_table, mock_current_index, mock_current_table, @patch('wx.GetApp') -@patch('windows.main.tabs.index.CURRENT_TABLE') -@patch('windows.main.tabs.index.CURRENT_INDEX') -@patch('windows.main.tabs.index.NEW_TABLE') +@patch('windows.main.table.index.CURRENT_TABLE') +@patch('windows.main.table.index.CURRENT_INDEX') +@patch('windows.main.table.index.NEW_TABLE') def test_on_index_clear(mock_new_table, mock_current_index, mock_current_table, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_table.get_value.return_value = mock_table diff --git a/windows/components/__init__.py b/windows/components/__init__.py index cc56dd6..60bcf7d 100644 --- a/windows/components/__init__.py +++ b/windows/components/__init__.py @@ -84,6 +84,9 @@ def RenderText(self, text, x, rect, dc, state): return True def StartEditing(self, item, labelRect): + from windows.main.table.records import NULL_DISPLAY + if self._value == NULL_DISPLAY: + self._value = "" logger.debug("StartEditing") return super().StartEditing(item, labelRect) @@ -134,6 +137,7 @@ class BaseDataViewCtrl(wx.dataview.DataViewCtrl): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.app = wx.GetApp() self.Bind(wx.EVT_CHAR_HOOK, self._on_char_hook) def finish_editing(self, current_column): diff --git a/windows/components/dataview.py b/windows/components/dataview.py index 618b748..c96ba88 100644 --- a/windows/components/dataview.py +++ b/windows/components/dataview.py @@ -287,7 +287,6 @@ def __init__(self, *args, **kwargs): self.Bind(wx.EVT_CONTEXT_MENU, self._on_context_menu) def _on_context_menu(self, event): - from icons import BitmapList selected = self.GetSelection() model = self.GetModel() @@ -297,23 +296,18 @@ def _on_context_menu(self, event): menu = wx.Menu() add_item = wx.MenuItem(menu, wx.ID_ANY, _("Add foreign key"), wx.EmptyString, wx.ITEM_NORMAL) - add_item.SetBitmap(BitmapList.ADD) + add_item.SetBitmap(self.app.icon_registry_16.get_bitmap(IconList.ADD)) menu.Append(add_item) self.Bind(wx.EVT_MENU, self.on_foreign_key_insert, add_item) delete_item = wx.MenuItem(menu, wx.ID_ANY, _("Remove foreign key"), wx.EmptyString, wx.ITEM_NORMAL) - delete_item.SetBitmap(BitmapList.DELETE) + delete_item.SetBitmap(self.app.icon_registry_16.get_bitmap(IconList.DELETE)) menu.Append(delete_item) menu.Enable(delete_item.GetId(), selected.IsOk()) self.Bind(wx.EVT_MENU, self.on_foreign_key_delete, delete_item) - # Forse non necessario, dato che editing è già disponibile - # update_item = wx.MenuItem(menu, wx.ID_ANY, _("Update foreign key"), wx.EmptyString, wx.ITEM_NORMAL) - # menu.Append(update_item) - # self.Bind(wx.EVT_MENU, self.on_foreign_key_update, update_item) - self.PopupMenu(menu) def _load_table_columns(self, popup, column2_render: PopupRenderer) -> list[str]: @@ -335,9 +329,9 @@ def __init__(self, *args, **kwargs): CURRENT_TABLE.subscribe(self._load_table) - def make_advanced_dialog(self, parent, value: str): - from windows.dialogs.advanced_cell_editor import AdvancedCellEditorController - return AdvancedCellEditorController(parent, value) + def make_advanced_dialog(self, parent, value: str, read_only : bool = False): + from windows.dialogs.column_content import ColumnContentDialogController # Lazy import: unavoidable circular dependency + return ColumnContentDialogController(parent, value, read_only) def _get_column_renderer(self, column: SQLColumn) -> wx.dataview.DataViewRenderer: for foreign_key in column.table.foreign_keys: @@ -449,7 +443,44 @@ def autosize_columns_from_content(self, sample_rows: int = 30): class QueryEditorResultsDataViewCtrl(TableRecordsDataViewCtrl): - pass + def __init__(self, *args, **kwargs): + BaseDataViewCtrl.__init__(self, *args, **kwargs) + self.Bind(wx.dataview.EVT_DATAVIEW_ITEM_ACTIVATED, self._on_item_activated) + + def _get_activated_model_column(self, event: wx.dataview.DataViewEvent) -> Optional[int]: + current_column = self.CurrentColumn + if current_column: + return current_column.GetModelColumn() + + model_column = event.GetColumn() + return model_column if model_column >= 0 else None + + def _on_item_activated(self, event: wx.dataview.DataViewEvent) -> None: + item = event.GetItem() + if not item.IsOk(): + event.Skip() + return + + model = self.GetModel() + if not model: + event.Skip() + return + + model_column = self._get_activated_model_column(event) + if model_column is None: + event.Skip() + return + + row = model.GetRow(item) + value = model.GetValueByRow(row, model_column) + + dialog = self.make_advanced_dialog(self, str(value or ""), read_only=True) + try: + dialog.ShowModal() + finally: + dialog.Destroy() + + event.Skip() class DatabaseTablesDataViewCtrl(BaseDataViewCtrl): diff --git a/windows/components/stc/autocomplete/auto_complete.py b/windows/components/stc/autocomplete/auto_complete.py index 65195f4..c5fd783 100644 --- a/windows/components/stc/autocomplete/auto_complete.py +++ b/windows/components/stc/autocomplete/auto_complete.py @@ -25,16 +25,17 @@ class SQLCompletionProvider: def __init__( - self, - get_database: Callable[[], Optional[SQLDatabase]], - get_current_table: Optional[Callable[[], Optional[SQLTable]]] = None, - *, - is_filter_editor: bool = False, + self, + get_database: Callable[[], Optional[SQLDatabase]], + get_current_table: Optional[Callable[[], Optional[SQLTable]]] = None, + *, + is_filter_editor: bool = False, ) -> None: self._get_database = get_database self._get_current_table = get_current_table or (lambda: None) self._is_filter_editor = is_filter_editor self._cached_database_id: Optional[int] = None + self._cached_dialect: Optional[str] = None self._context_detector: Optional[ContextDetector] = None self._dot_handler: Optional[DotCompletionHandler] = None @@ -44,7 +45,12 @@ def _get_current_dialect(self) -> Optional[str]: if session := CURRENT_SESSION.get_value(): return session.engine.value.dialect - def get(self, text: str, pos: int) -> Optional[CompletionResult]: + def get( + self, + text: str, + pos: int, + separator: Optional[str] = None, + ) -> Optional[CompletionResult]: try: database = self._get_database() if database is None: @@ -55,7 +61,11 @@ def get(self, text: str, pos: int) -> Optional[CompletionResult]: safe_pos = self._clamp_position(pos=pos, text=text) statement, relative_pos = ( - self._statement_extractor.extract_current_statement(text, safe_pos) + self._statement_extractor.extract_current_statement( + text, + safe_pos, + separator=separator, + ) ) if not self._context_detector: @@ -99,49 +109,39 @@ def _clamp_position(*, pos: int, text: str) -> int: def _update_cache(self, *, database: SQLDatabase) -> None: database_id = id(database) - if self._cached_database_id != database_id: + dialect = self._get_current_dialect() + + if ( + self._cached_database_id != database_id + or self._cached_dialect != dialect + ): self._cached_database_id = database_id + self._cached_dialect = dialect - dialect = self._get_current_dialect() self._context_detector = ContextDetector(dialect) self._dot_handler = DotCompletionHandler(database, None) class SQLAutoCompleteController: def __init__( - self, - editor: wx.stc.StyledTextCtrl, - provider: SQLCompletionProvider, - *, - settings: Optional[object] = None, - theme_loader: Optional[object] = None, - debounce_ms: int = 80, - is_enabled: bool = True, - min_prefix_length: int = 1, + self, + editor: wx.stc.StyledTextCtrl, + provider: SQLCompletionProvider, + *, + settings: object, + theme_loader: Optional[object] = None, + debounce_ms: int = 80, + is_enabled: bool = True, + min_prefix_length: int = 1, ) -> None: self._editor = editor self._provider = provider self._settings = settings self._theme_loader = theme_loader - if settings: - self._debounce_ms = ( - settings.get_value("settings", "autocomplete", "debounce_ms") - or debounce_ms - ) - self._min_prefix_length = ( - settings.get_value("settings", "autocomplete", "min_prefix_length") - or min_prefix_length - ) - self._add_space_after_completion = settings.get_value( - "settings", "autocomplete", "add_space_after_completion" - ) - if self._add_space_after_completion is None: - self._add_space_after_completion = True - else: - self._debounce_ms = debounce_ms - self._min_prefix_length = min_prefix_length - self._add_space_after_completion = True + self._debounce_ms = settings.get_value("editor", "autocomplete", "debounce_ms", default=debounce_ms) + self._min_prefix_length = settings.get_value("editor", "autocomplete", "min_prefix_length", default=min_prefix_length) + self._add_space_after_completion = settings.get_value("editor", "autocomplete", "add_space_after_completion", default=True) self._is_enabled = is_enabled @@ -160,16 +160,23 @@ def set_enabled(self, is_enabled: bool) -> None: self._hide_popup() def get_effective_separator(self) -> str: - if self._settings: - separator = self._settings.get_value("query_editor", "statement_separator") - if separator: - return separator + user_override = ";" + if settings := getattr(self, "_settings", None): + user_override = settings.get_value( + "editor", + "statement_separator", + default=";", + ) session = CURRENT_SESSION.get_value() + default_separator = ";" if session and hasattr(session, "context"): - return session.context.DEFAULT_STATEMENT_SEPARATOR + default_separator = session.context.DEFAULT_STATEMENT_SEPARATOR - return ";" + return StatementExtractor.normalize_separator( + user_override, + default=default_separator, + ) def show(self, *, force: bool) -> None: if not self._is_enabled: @@ -182,7 +189,11 @@ def show(self, *, force: bool) -> None: pos = self._editor.GetCurrentPos() text = self._editor.GetText() - result = self._provider.get(pos=pos, text=text) + result = self._provider.get( + pos=pos, + text=text, + separator=self.get_effective_separator(), + ) if result is None: self._hide_popup() @@ -192,8 +203,12 @@ def show(self, *, force: bool) -> None: self._hide_popup() return + if not force and result.prefix_length < self._min_prefix_length: + self._hide_popup() + return + self._current_result = result - items = self._unique_sorted_items(items=result.items) + items = self._unique_items(items=result.items) self._show_popup(items) except Exception as ex: logger.error(f"Error in show(): {ex}", exc_info=True) @@ -225,13 +240,17 @@ def _on_item_completed(self, item: CompletionItem) -> None: return current_pos = self._editor.GetCurrentPos() - start_pos = current_pos - self._current_result.prefix_length + start_pos, end_pos = self._get_identifier_bounds_around_cursor(current_pos) + + if start_pos == end_pos: + start_pos = max(0, current_pos - self._current_result.prefix_length) + end_pos = current_pos - self._editor.SetSelection(start_pos, current_pos) + self._editor.SetSelection(start_pos, end_pos) should_add_space = ( - self._add_space_after_completion - and item.item_type == CompletionItemType.KEYWORD + self._add_space_after_completion + and item.item_type == CompletionItemType.KEYWORD ) completion_text = item.name + " " if should_add_space else item.name self._editor.ReplaceSelection(completion_text) @@ -253,6 +272,25 @@ def _on_item_completed(self, item: CompletionItem) -> None: if item.name.upper() in trigger_keywords: wx.CallAfter(lambda: self._schedule_show(force=False)) + @staticmethod + def _is_identifier_char(character: str) -> bool: + return character.isalnum() or character == "_" + + def _get_identifier_bounds_around_cursor(self, cursor_pos: int) -> tuple[int, int]: + text = self._editor.GetText() + text_length = len(text) + + start_pos = min(max(cursor_pos, 0), text_length) + end_pos = start_pos + + while start_pos > 0 and self._is_identifier_char(text[start_pos - 1]): + start_pos -= 1 + + while end_pos < text_length and self._is_identifier_char(text[end_pos]): + end_pos += 1 + + return start_pos, end_pos + def _on_key_down(self, event: wx.KeyEvent) -> None: if not self._is_enabled: event.Skip() @@ -325,15 +363,22 @@ def _cancel_pending(self) -> None: self._pending_call = None @staticmethod - def _unique_sorted_items( - *, items: tuple[CompletionItem, ...] + def _unique_items( + *, items: tuple[CompletionItem, ...] ) -> list[CompletionItem]: - seen_names: set[str] = set() + seen_keys: set[tuple[str, CompletionItemType]] = set() unique_items: list[CompletionItem] = [] for item in items: - if item.name not in seen_names: - seen_names.add(item.name) + key = (item.name, item.item_type) + if key not in seen_keys: + seen_keys.add(key) unique_items.append(item) return unique_items + + @staticmethod + def _unique_sorted_items( + *, items: tuple[CompletionItem, ...] + ) -> list[CompletionItem]: + return SQLAutoCompleteController._unique_items(items=items) diff --git a/windows/components/stc/autocomplete/autocomplete_popup.py b/windows/components/stc/autocomplete/autocomplete_popup.py index e7ada99..671bbb9 100644 --- a/windows/components/stc/autocomplete/autocomplete_popup.py +++ b/windows/components/stc/autocomplete/autocomplete_popup.py @@ -1,5 +1,6 @@ +from typing import Callable, Optional + import wx -import wx.dataview from windows.components.stc.autocomplete.completion_types import CompletionItem, CompletionItemType from windows.components.stc.theme_loader import ThemeLoader @@ -8,81 +9,79 @@ class AutoCompletePopup(wx.PopupWindow): def __init__(self, parent: wx.Window, settings: object = None, theme_loader: ThemeLoader = None) -> None: super().__init__(parent, wx.BORDER_SIMPLE) - - self._selected_index: int = 0 + self._items: list[CompletionItem] = [] - self._on_item_selected: callable = None + self._on_item_selected: Optional[Callable] = None self._settings = settings self._theme_loader = theme_loader - + if settings: - self._popup_width = settings.get_value("settings", "autocomplete", "popup_width") or 300 - self._popup_max_height = settings.get_value("settings", "autocomplete", "popup_max_height") or 10 + self._popup_width = settings.get_value("editor", "autocomplete", "popup_width", default=300) + self._popup_max_height = settings.get_value("editor", "autocomplete", "popup_max_height", default=10) else: self._popup_width = 300 self._popup_max_height = 10 - + self._create_ui() self._bind_events() - + def _create_ui(self) -> None: panel = wx.Panel(self) sizer = wx.BoxSizer(wx.VERTICAL) - + self._list_ctrl = wx.ListCtrl( panel, style=wx.LC_REPORT | wx.LC_NO_HEADER | wx.LC_SINGLE_SEL ) - + self._image_list = wx.ImageList(16, 16) self._list_ctrl.SetImageList(self._image_list, wx.IMAGE_LIST_SMALL) - + self._list_ctrl.InsertColumn(0, "", width=self._popup_width) self._list_ctrl.SetMinSize((self._popup_width, 200)) - + sizer.Add(self._list_ctrl, 1, wx.EXPAND) panel.SetSizer(sizer) - + main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.Add(panel, 1, wx.EXPAND) self.SetSizer(main_sizer) - + def _bind_events(self) -> None: self._list_ctrl.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._on_item_activated) self._list_ctrl.Bind(wx.EVT_KEY_DOWN, self._on_key_down) self.Bind(wx.EVT_KILL_FOCUS, self._on_kill_focus) - + def show_items(self, items: list[CompletionItem], position: wx.Point) -> None: self._items = items - self._selected_index = 0 - + self._list_ctrl.DeleteAllItems() self._image_list.RemoveAll() - + for idx, item in enumerate(items): bitmap = self._get_bitmap_for_type(item.item_type) color = self._get_color_for_type(item.item_type) - + image_idx = self._image_list.Add(bitmap) list_idx = self._list_ctrl.InsertItem(idx, item.name, image_idx) - + if color: self._list_ctrl.SetItemTextColour(list_idx, color) - + if items: self._list_ctrl.Select(0) self._list_ctrl.Focus(0) - + self.SetPosition(position) - + item_count = min(len(items), self._popup_max_height) item_height = 24 height = item_count * item_height + 10 self.SetSize((self._popup_width, height)) - + self.Show() self._list_ctrl.SetFocus() - + def _get_bitmap_for_type(self, item_type: CompletionItemType) -> wx.Bitmap: icon_map = { CompletionItemType.KEYWORD: wx.ART_INFORMATION, @@ -90,17 +89,17 @@ def _get_bitmap_for_type(self, item_type: CompletionItemType) -> wx.Bitmap: CompletionItemType.TABLE: wx.ART_FOLDER, CompletionItemType.COLUMN: wx.ART_NORMAL_FILE, } - + art_id = icon_map.get(item_type, wx.ART_INFORMATION) return wx.ArtProvider.GetBitmap(art_id, wx.ART_MENU, (16, 16)) - + def _get_color_for_type(self, item_type: CompletionItemType) -> wx.Colour: if self._theme_loader: colors = self._theme_loader.get_autocomplete_colors() color_hex = colors.get(item_type.value) if color_hex: return wx.Colour(color_hex) - + color_map = { CompletionItemType.KEYWORD: wx.Colour(0, 0, 255), CompletionItemType.FUNCTION: wx.Colour(128, 0, 128), @@ -108,25 +107,25 @@ def _get_color_for_type(self, item_type: CompletionItemType) -> wx.Colour: CompletionItemType.COLUMN: wx.Colour(0, 0, 0), } return color_map.get(item_type, wx.Colour(0, 0, 0)) - + def _on_item_activated(self, event: wx.Event) -> None: row = self._list_ctrl.GetFirstSelected() if row != wx.NOT_FOUND and row < len(self._items): self._complete_with_item(self._items[row]) - + def _on_key_down(self, event: wx.KeyEvent) -> None: key_code = event.GetKeyCode() - + if key_code == wx.WXK_ESCAPE: self.Hide() return - + if key_code in (wx.WXK_RETURN, wx.WXK_TAB): row = self._list_ctrl.GetFirstSelected() if row != wx.NOT_FOUND and row < len(self._items): self._complete_with_item(self._items[row]) return - + if key_code == wx.WXK_PAGEDOWN: current = self._list_ctrl.GetFirstSelected() if current != wx.NOT_FOUND: @@ -135,7 +134,7 @@ def _on_key_down(self, event: wx.KeyEvent) -> None: self._list_ctrl.Focus(new_index) self._list_ctrl.EnsureVisible(new_index) return - + if key_code == wx.WXK_PAGEUP: current = self._list_ctrl.GetFirstSelected() if current != wx.NOT_FOUND: @@ -144,22 +143,35 @@ def _on_key_down(self, event: wx.KeyEvent) -> None: self._list_ctrl.Focus(new_index) self._list_ctrl.EnsureVisible(new_index) return - + event.Skip() - + + def _owns_window(self, window: Optional[wx.Window]) -> bool: + current = window + while current is not None: + if current is self: + return True + current = current.GetParent() + return False + def _on_kill_focus(self, event: wx.FocusEvent) -> None: + next_focus = event.GetWindow() if hasattr(event, "GetWindow") else None + if self._owns_window(next_focus): + event.Skip() + return + self.Hide() event.Skip() - + def _complete_with_item(self, item: CompletionItem) -> None: if self._on_item_selected: self._on_item_selected(item) self.Hide() - - def set_on_item_selected(self, callback: callable) -> None: + + def set_on_item_selected(self, callback: Callable) -> None: self._on_item_selected = callback - - def get_selected_item(self) -> CompletionItem: + + def get_selected_item(self) -> Optional[CompletionItem]: row = self._list_ctrl.GetFirstSelected() if row != wx.NOT_FOUND and row < len(self._items): return self._items[row] diff --git a/windows/components/stc/autocomplete/context_detector.py b/windows/components/stc/autocomplete/context_detector.py index fdee062..7c60d1b 100644 --- a/windows/components/stc/autocomplete/context_detector.py +++ b/windows/components/stc/autocomplete/context_detector.py @@ -2,9 +2,6 @@ from typing import Optional -import sqlglot -from sqlglot import exp - from helpers.logger import logger from windows.components.stc.autocomplete.query_scope import ( @@ -20,8 +17,12 @@ class ContextDetector: _prefix_pattern = re.compile(r"[A-Za-z_][A-Za-z0-9_]*$") + _identifier_segment_pattern = r"(?:[A-Za-z_][A-Za-z0-9_]*|`[^`]+`|\"[^\"]+\"|\[[^\]]+\])" + _table_name_pattern = ( + _identifier_segment_pattern + r"(?:\." + _identifier_segment_pattern + r")?" + ) _join_after_table_pattern = re.compile( - r"\b(?:(?:INNER|LEFT|RIGHT|FULL|CROSS)(?:\s+OUTER)?\s+)?JOIN\s+([A-Za-z_][A-Za-z0-9_]*)" + r"\b(?:(?:INNER|LEFT|RIGHT|FULL|CROSS)(?:\s+OUTER)?\s+)?JOIN\s+(" + _table_name_pattern + r")" r"\s*(?:(?:AS\s+)?([A-Za-z_][A-Za-z0-9_]*))?\s*$", re.IGNORECASE, ) @@ -72,6 +73,8 @@ def detect( try: context = self._detect_context_with_regex(left_text, prefix) + if context == SQLContext.INSERT_COMPLETE and left_text.rstrip().endswith(")"): + prefix = ")" return context, scope, prefix except Exception as ex: logger.debug(f"context detection error: {ex}") @@ -91,7 +94,10 @@ def _extract_prefix(self, text: str, cursor_pos: int) -> str: return "" return match.group(0) - _dot_pattern = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)?$") + _dot_pattern = re.compile( + r"((?:[A-Za-z_][A-Za-z0-9_]*|`[^`]+`|\"[^\"]+\"|\[[^\]]+\]))" + r"\.([A-Za-z_][A-Za-z0-9_]*)?$" + ) def _check_dot_completion(self, left_text: str, prefix: str) -> Optional[re.Match]: if "." in left_text: @@ -100,8 +106,37 @@ def _check_dot_completion(self, left_text: str, prefix: str) -> Optional[re.Matc def _detect_context_with_regex(self, left_text: str, prefix: str) -> SQLContext: left_upper = left_text.upper() + statement_type, statement_pos = self._detect_statement_type(left_upper) + + if statement_type == "INSERT": + return self._detect_insert_context(left_text, left_upper, prefix, statement_pos) + if statement_type == "UPDATE": + return self._detect_update_context(left_text, left_upper, prefix, statement_pos) + if statement_type == "DELETE": + return self._detect_delete_context(left_text, left_upper, prefix, statement_pos) + if statement_type != "SELECT": + return SQLContext.UNKNOWN + + return self._detect_select_context(left_text, left_upper, prefix) + @staticmethod + def _detect_statement_type(left_upper: str) -> tuple[str, int]: + statement_positions = { + "SELECT": left_upper.rfind("SELECT"), + "INSERT": left_upper.rfind("INSERT"), + "UPDATE": left_upper.rfind("UPDATE"), + "DELETE": left_upper.rfind("DELETE"), + } + statement_type = max(statement_positions, key=lambda key: statement_positions[key]) + return statement_type, statement_positions[statement_type] + + def _detect_select_context( + self, left_text: str, left_upper: str, prefix: str + ) -> SQLContext: select_pos = left_upper.rfind("SELECT") + if select_pos == -1: + return SQLContext.UNKNOWN + from_pos = left_upper.rfind("FROM") where_pos = left_upper.rfind("WHERE") join_pos = left_upper.rfind("JOIN") @@ -112,9 +147,6 @@ def _detect_context_with_regex(self, left_text: str, prefix: str) -> SQLContext: limit_pos = left_upper.rfind("LIMIT") offset_pos = left_upper.rfind("OFFSET") - if select_pos == -1: - return SQLContext.UNKNOWN - if re.search(r"\bOVER\s*(?:\(\s*)?$", left_text, re.IGNORECASE): return SQLContext.WINDOW_OVER @@ -158,6 +190,8 @@ def _detect_context_with_regex(self, left_text: str, prefix: str) -> SQLContext: if where_pos > select_pos and where_pos != -1: if where_pos > max(from_pos, order_by_pos, group_by_pos, -1): + if self._is_inside_string_literal(left_text[where_pos:]): + return SQLContext.WHERE_STRING_LITERAL if self._is_after_where_operator(left_text, where_pos, prefix): return SQLContext.WHERE_AFTER_OPERATOR if self._is_after_where_is(left_text, where_pos, prefix): @@ -172,6 +206,289 @@ def _detect_context_with_regex(self, left_text: str, prefix: str) -> SQLContext: return SQLContext.SELECT_LIST + @staticmethod + def _normalize_identifier(identifier: str) -> str: + normalized = identifier.strip() + if not normalized: + return normalized + if normalized[0] in {'`', '"', '['} and normalized[-1] in {'`', '"', ']'}: + return normalized[1:-1] + return normalized + + # ── INSERT ────────────────────────────────────────────────────── + + @staticmethod + def _is_inside_string_literal(text: str) -> bool: + count = 0 + i = 0 + while i < len(text): + if text[i] == "'" and (i == 0 or text[i - 1] != "\\"): + count += 1 + i += 1 + return count % 2 == 1 + + def _detect_insert_context( + self, left_text: str, left_upper: str, prefix: str, insert_pos: int + ) -> SQLContext: + after_insert = left_text[insert_pos:] + + if self._is_inside_string_literal(after_insert): + return SQLContext.INSERT_STRING_LITERAL + + into_pos = left_upper.rfind("INTO", insert_pos) + values_pos = left_upper.rfind("VALUES", insert_pos) + + # INSERT INTO table (...) VALUES (...) | + if values_pos != -1: + after_values = left_text[values_pos + 6:].strip() + # Inside VALUES parentheses or after comma between value groups + if after_values: + # Check if we have a complete VALUES(...) group + open_count = after_values.count("(") + close_count = after_values.count(")") + if open_count > close_count: + # Inside VALUES(...) + return SQLContext.INSERT_VALUE_EXPRESSIONS + if close_count > 0 and after_values.rstrip().endswith(")"): + # Cursor right after ) — no space + if left_text.endswith(")"): + return SQLContext.INSERT_COMPLETE + # After complete VALUES(...) with space + return SQLContext.INSERT_POST_VALUES + if after_values.rstrip().endswith(","): + # After VALUES(...), + return SQLContext.INSERT_VALUE_EXPRESSIONS + # Just after VALUES keyword + return SQLContext.INSERT_VALUE_EXPRESSIONS + + if into_pos == -1: + return SQLContext.INSERT_INTO + + after_into = left_text[into_pos + 4:].strip() + if not after_into: + return SQLContext.INSERT_INTO + + # Check if we're inside column list parentheses + paren_open = after_into.rfind("(") + paren_close = after_into.rfind(")") + if paren_open != -1 and paren_close < paren_open: + # Inside parentheses — column list + return SQLContext.INSERT_COLUMNS + + if paren_close != -1 and paren_close > paren_open: + # After closed parentheses — expect VALUES/SELECT + return SQLContext.INSERT_VALUES + + # After INSERT INTO, check if table name is present + tokens = after_into.split() + if not tokens: + return SQLContext.INSERT_INTO + + # We have at least a table name + if len(tokens) == 1 and prefix: + return SQLContext.INSERT_INTO + + return SQLContext.INSERT_INTO + + # ── UPDATE ────────────────────────────────────────────────────── + + def _detect_update_context( + self, left_text: str, left_upper: str, prefix: str, update_pos: int + ) -> SQLContext: + after_update = left_text[update_pos:] + + if self._is_inside_string_literal(after_update): + where_pos = left_upper.rfind("WHERE", update_pos) + if where_pos != -1 and self._is_inside_string_literal(left_text[where_pos:]): + return SQLContext.UPDATE_WHERE_STRING_LITERAL + return SQLContext.UPDATE_STRING_LITERAL + + set_pos = left_upper.rfind(" SET ", update_pos) + where_pos = left_upper.rfind("WHERE", update_pos) + join_pos = left_upper.rfind("JOIN", update_pos) + on_pos = left_upper.rfind(" ON ", update_pos) + + # WHERE clause + if where_pos != -1 and where_pos > max(set_pos, on_pos, -1): + after_where = left_text[where_pos + 5:] + after_where_stripped = after_where.strip() + if not after_where_stripped: + return SQLContext.UPDATE_WHERE_CONDITIONS + # After value (column op value) — suggest AND/OR + if self._is_after_complete_condition(after_where, prefix): + return SQLContext.UPDATE_WHERE_CONDITIONS + # After column — suggest operators + if self._is_after_column_name(after_where, prefix): + return SQLContext.UPDATE_WHERE_OPERATORS + return SQLContext.UPDATE_WHERE_CONDITIONS + + # ON clause (JOIN) + if on_pos != -1 and on_pos > max(join_pos, -1) and set_pos == -1: + after_on = left_text[on_pos + 4:].strip() + if self._is_after_complete_join_condition(after_on, prefix): + return SQLContext.UPDATE_SET_CLAUSE + return SQLContext.UPDATE_JOIN_ON + + # SET clause + if set_pos != -1: + after_set = left_text[set_pos + 5:].strip() + if not after_set: + return SQLContext.UPDATE_SET_COLUMNS + # After = operator + if re.search(r"=\s*$", after_set): + return SQLContext.UPDATE_SET_EXPRESSIONS + # After complete assignment (col = value) with trailing space + if self._is_after_complete_assignment(after_set, prefix): + if after_set.rstrip().endswith(","): + return SQLContext.UPDATE_SET_COLUMNS + return SQLContext.UPDATE_WHERE_CLAUSE + # Column prefix + return SQLContext.UPDATE_SET_COLUMNS + + # After UPDATE keyword — table name + after_update_keyword = left_text[update_pos + 6:].strip() + if not after_update_keyword: + return SQLContext.UPDATE_TABLE + + # After table name — suggest SET or JOINs + tokens = after_update_keyword.split() + if len(tokens) >= 1 and not prefix: + if join_pos != -1 and join_pos > update_pos: + # After JOIN table — suggest ON or SET + after_join = left_text[join_pos + 4:].strip() + join_tokens = after_join.split() + if len(join_tokens) >= 1: + return SQLContext.UPDATE_SET_CLAUSE + return SQLContext.UPDATE_TABLE + return SQLContext.UPDATE_SET_CLAUSE + + return SQLContext.UPDATE_TABLE + + # ── DELETE ────────────────────────────────────────────────────── + + def _detect_delete_context( + self, left_text: str, left_upper: str, prefix: str, delete_pos: int + ) -> SQLContext: + after_delete = left_text[delete_pos:] + + if self._is_inside_string_literal(after_delete): + return SQLContext.DELETE_WHERE_STRING_LITERAL + + from_pos = left_upper.rfind("FROM", delete_pos) + where_pos = left_upper.rfind("WHERE", delete_pos) + using_pos = left_upper.rfind("USING", delete_pos) + join_pos = left_upper.rfind("JOIN", delete_pos) + on_pos = left_upper.rfind(" ON ", delete_pos) + in_pos = left_upper.rfind(" IN ", delete_pos) + + # WHERE clause + if where_pos != -1 and where_pos > max(from_pos, using_pos, on_pos, -1): + after_where = left_text[where_pos + 5:] + after_where_stripped = after_where.strip() + if not after_where_stripped: + return SQLContext.DELETE_WHERE_CONDITIONS + # Check for IN ( subquery + if in_pos != -1 and in_pos > where_pos: + after_in = left_text[in_pos + 4:].strip() + if after_in.startswith("(") and after_in.count("(") > after_in.count(")"): + return SQLContext.DELETE_SUBQUERY + # After complete condition + if self._is_after_complete_condition(after_where, prefix): + return SQLContext.DELETE_WHERE_CONDITIONS + # After = operator + if re.search(r"(?:=|!=|<>|<=|>=|<|>)\s*$", after_where): + return SQLContext.DELETE_WHERE_EXPRESSIONS + # After column name + if self._is_after_column_name(after_where, prefix): + return SQLContext.DELETE_WHERE_OPERATORS + return SQLContext.DELETE_WHERE_CONDITIONS + + # ON clause (JOIN) + if on_pos != -1 and on_pos > max(join_pos, from_pos, -1): + after_on = left_text[on_pos + 4:].strip() + if self._is_after_complete_join_condition(after_on, prefix): + return SQLContext.DELETE_WHERE_CLAUSE + return SQLContext.DELETE_JOIN_ON + + # USING clause + if using_pos != -1 and using_pos > max(from_pos, -1): + after_using = left_text[using_pos + 5:].strip() + if not after_using: + return SQLContext.DELETE_USING + # After USING table — suggest WHERE + tokens = after_using.split() + if len(tokens) >= 1 and not prefix: + return SQLContext.DELETE_WHERE_CLAUSE + return SQLContext.DELETE_USING + + # FROM clause + if from_pos != -1 and from_pos > delete_pos: + after_from = left_text[from_pos + 4:].strip() + if not after_from: + return SQLContext.DELETE_FROM + + # After table name + tokens = after_from.split() + if len(tokens) >= 1 and not prefix: + if join_pos != -1 and join_pos > from_pos: + after_join = left_text[join_pos + 4:].strip() + join_tokens = after_join.split() + if len(join_tokens) >= 1: + return SQLContext.DELETE_WHERE_CLAUSE + return SQLContext.DELETE_WHERE_CLAUSE + return SQLContext.DELETE_FROM + + return SQLContext.DELETE_FROM + + # ── Helpers for INSERT/UPDATE/DELETE ───────────────────────────── + + @staticmethod + def _is_after_complete_condition(clause: str, prefix: str) -> bool: + if prefix: + return False + # Match: [word] operator value at end + return bool(re.search( + r"(?:[A-Za-z_][A-Za-z0-9_]*\.)?[A-Za-z_][A-Za-z0-9_]*\s*" + r"(?:=|!=|<>|<=|>=|<|>|LIKE|IN|BETWEEN)\s*" + r"(?:[A-Za-z_][A-Za-z0-9_]*|\d+|'[^']*'|\"[^\"]*\"|NULL|TRUE|FALSE|\w+\([^)]*\))\s*$", + clause, + re.IGNORECASE, + )) + + @staticmethod + def _is_after_column_name(clause: str, prefix: str) -> bool: + if prefix: + return False + # Match: identifier at end (with optional qualifier), followed by space + return bool(re.search( + r"(?:(?:AND|OR)\s+)?(?:[A-Za-z_][A-Za-z0-9_]*\.)?[A-Za-z_][A-Za-z0-9_]*\s+$", + clause, + re.IGNORECASE, + )) + + @staticmethod + def _is_after_complete_assignment(clause: str, prefix: str) -> bool: + if prefix: + return False + return bool(re.search( + r"[A-Za-z_][A-Za-z0-9_]*\s*=\s*" + r"(?:[A-Za-z_][A-Za-z0-9_.]*|\d+|'[^']*'|\"[^\"]*\"|NULL|TRUE|FALSE|\w+\([^)]*\))\s*$", + clause, + re.IGNORECASE, + )) + + @staticmethod + def _is_after_complete_join_condition(clause: str, prefix: str) -> bool: + if prefix: + return False + return bool(re.search( + r"(?:[A-Za-z_][A-Za-z0-9_]*\.)?[A-Za-z_][A-Za-z0-9_]*\s*" + r"(?:=|!=|<>|<=|>=|<|>)\s*" + r"(?:[A-Za-z_][A-Za-z0-9_]*\.)?[A-Za-z_][A-Za-z0-9_]*\s*$", + clause, + re.IGNORECASE, + )) + def _is_after_join_table(self, left_text: str, prefix: str) -> bool: if prefix: return False @@ -349,64 +666,6 @@ def _extract_from_qualifier(self, left_text: str) -> Optional[str]: return table_name - def _extract_scope_from_select( - self, parsed: exp.Select, database: Optional[SQLDatabase] - ) -> QueryScope: - from_tables = [] - join_tables = [] - aliases = {} - - if from_clause := parsed.args.get("from"): - if isinstance(from_clause, exp.From): - for table_exp in from_clause.find_all(exp.Table): - table_name = table_exp.name - alias = ( - table_exp.alias - if hasattr(table_exp, "alias") and table_exp.alias - else None - ) - - table_obj = ( - self._find_table_in_database(table_name, database) - if database - else None - ) - ref = TableReference(name=table_name, alias=alias, table=table_obj) - from_tables.append(ref) - - if alias: - aliases[alias.lower()] = ref - aliases[table_name.lower()] = ref - - for join_exp in parsed.find_all(exp.Join): - if table_exp := join_exp.this: - if isinstance(table_exp, exp.Table): - table_name = table_exp.name - alias = ( - table_exp.alias - if hasattr(table_exp, "alias") and table_exp.alias - else None - ) - - table_obj = ( - self._find_table_in_database(table_name, database) - if database - else None - ) - ref = TableReference(name=table_name, alias=alias, table=table_obj) - join_tables.append(ref) - - if alias: - aliases[alias.lower()] = ref - aliases[table_name.lower()] = ref - - return QueryScope( - from_tables=from_tables, - join_tables=join_tables, - current_table=None, - aliases=aliases, - ) - def _extract_scope_from_text( self, text: str, database: Optional[SQLDatabase] ) -> QueryScope: @@ -449,7 +708,7 @@ def _extract_scope_from_text( } join_pattern = re.compile( - r"\bJOIN\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:(?:AS\s+)?([A-Za-z_][A-Za-z0-9_]*))?\s*(?:\bON\b|\bUSING\b|$)", + r"\bJOIN\s+(" + self._table_name_pattern + r")\s*(?:(?:AS\s+)?([A-Za-z_][A-Za-z0-9_]*))?\s*(?:\bON\b|\bUSING\b|$)", re.IGNORECASE, ) @@ -457,6 +716,30 @@ def _extract_scope_from_text( join_tables = [] aliases = {} + # Extract UPDATE target table into scope (only for UPDATE statements) + update_match = re.match( + r"\s*UPDATE\s+(" + self._table_name_pattern + r")" + r"(?:\s+(?:AS\s+)?([A-Za-z_][A-Za-z0-9_]*))?", + main_text, + re.IGNORECASE, + ) + if update_match: + table_name = update_match.group(1) + alias = update_match.group(2) if update_match.group(2) else None + if alias and alias.upper() in sql_keywords: + alias = None + if table_name.upper() not in sql_keywords: + table_obj = ( + self._find_table_in_database(table_name, database) + if database + else None + ) + ref = TableReference(name=table_name, alias=alias, table=table_obj) + from_tables.append(ref) + if alias: + aliases[alias.lower()] = ref + aliases[table_name.lower()] = ref + for table_name, alias, projected_columns in self._extract_from_table_tokens( main_text ): @@ -550,7 +833,7 @@ def _extract_from_table_tokens( if not ( table_match := re.match( - r"(?P[A-Za-z_][A-Za-z0-9_]*)(?:\s+(?:AS\s+)?(?P[A-Za-z_][A-Za-z0-9_]*))?(?:\s+[A-Za-z_][A-Za-z0-9_]*)?$", + r"(?P
" + ContextDetector._table_name_pattern + r")(?:\s+(?:AS\s+)?(?P[A-Za-z_][A-Za-z0-9_]*))?(?:\s+[A-Za-z_][A-Za-z0-9_]*)?$", part, re.IGNORECASE, ) @@ -685,59 +968,19 @@ def _parse_cte_definitions(self, text: str) -> tuple[list[TableReference], int]: def _find_table_in_database( self, table_name: str, database: SQLDatabase ) -> Optional[SQLTable]: + table_name_candidate = table_name.split(".")[-1] + normalized_candidate = self._normalize_identifier(table_name_candidate) + normalized_full_name = self._normalize_identifier(table_name) try: for table in database.tables: - if table.name.lower() == table_name.lower(): + if table.name.lower() == normalized_full_name.lower(): + return table + if table.name.lower() == normalized_candidate.lower(): return table except Exception: pass return None - def _is_in_where(self, text: str) -> bool: - upper = text.upper() - where_pos = upper.rfind("WHERE") - if where_pos == -1: - return False - - after_where = upper[where_pos:] - return ( - "ORDER BY" not in after_where - and "GROUP BY" not in after_where - and "LIMIT" not in after_where - ) - - def _is_after_from(self, text: str) -> bool: - upper = text.upper() - from_pos = upper.rfind("FROM") - if from_pos == -1: - return False - - after_from = upper[from_pos + 4 :].strip() - return len(after_from) == 0 or ( - len(after_from) > 0 and after_from[-1] in [" ", "\n", "\t"] - ) - - def _is_after_on(self, text: str) -> bool: - upper = text.upper() - on_pos = upper.rfind(" ON ") - if on_pos == -1: - return False - - after_on = upper[on_pos + 4 :].strip() - return len(after_on) == 0 or ( - len(after_on) > 0 - and not after_on.endswith(("WHERE", "ORDER", "GROUP", "LIMIT")) - ) - - def _is_after_order_by(self, text: str) -> bool: - upper = text.upper() - order_by_pos = upper.rfind("ORDER BY") - if order_by_pos == -1: - return False - - after_order_by = upper[order_by_pos + 8 :].strip() - return "LIMIT" not in after_order_by - def _is_after_limit_number( self, left_text: str, limit_pos: int, prefix: str ) -> bool: @@ -794,24 +1037,3 @@ def _is_after_order_by_column( return True return False - def _is_after_group_by(self, text: str) -> bool: - upper = text.upper() - group_by_pos = upper.rfind("GROUP BY") - if group_by_pos == -1: - return False - - after_group_by = upper[group_by_pos + 8 :].strip() - return ( - "HAVING" not in after_group_by - and "ORDER BY" not in after_group_by - and "LIMIT" not in after_group_by - ) - - def _is_in_having(self, text: str) -> bool: - upper = text.upper() - having_pos = upper.rfind("HAVING") - if having_pos == -1: - return False - - after_having = upper[having_pos:] - return "ORDER BY" not in after_having and "LIMIT" not in after_having diff --git a/windows/components/stc/autocomplete/dot_completion_handler.py b/windows/components/stc/autocomplete/dot_completion_handler.py index 0fe4ed4..bd8b0e0 100644 --- a/windows/components/stc/autocomplete/dot_completion_handler.py +++ b/windows/components/stc/autocomplete/dot_completion_handler.py @@ -11,7 +11,21 @@ class DotCompletionHandler: - _token_pattern = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") + _identifier_segment_pattern = ( + r"(?:[A-Za-z_][A-Za-z0-9_]*|`[^`]+`|\"[^\"]+\"|\[[^\]]+\])" + ) + _token_pattern = re.compile(_identifier_segment_pattern) + + @staticmethod + def _normalize_identifier(identifier: str) -> str: + normalized = identifier.strip() + if not normalized: + return normalized + + if normalized[0] in {'`', '"', '['} and normalized[-1] in {'`', '"', ']'}: + return normalized[1:-1] + + return normalized def __init__(self, database: Optional[SQLDatabase], scope: Optional[object] = None): self._database = database @@ -64,6 +78,8 @@ def get_completions( if not table_or_alias: return None, "" + table_or_alias = self._normalize_identifier(table_or_alias) + table = self._find_table(table_or_alias) if not table: return None, "" @@ -116,9 +132,13 @@ def _build_table_index(self) -> None: try: for ref in self._scope.from_tables + self._scope.join_tables: if ref.table: - self._table_index[ref.name.lower()] = ref.table + ref_name = self._normalize_identifier(ref.name).lower() + self._table_index[ref_name] = ref.table + if "." in ref_name: + self._table_index[ref_name.split(".")[-1]] = ref.table if ref.alias: - self._table_index[ref.alias.lower()] = ref.table + alias_name = self._normalize_identifier(ref.alias).lower() + self._table_index[alias_name] = ref.table except (AttributeError, TypeError): pass @@ -127,8 +147,13 @@ def _build_table_index(self) -> None: try: for table in self._database.tables: - if table.name.lower() not in self._table_index: - self._table_index[table.name.lower()] = table + table_name = self._normalize_identifier(table.name).lower() + if table_name not in self._table_index: + self._table_index[table_name] = table + if "." in table_name: + base_name = table_name.split(".")[-1] + if base_name not in self._table_index: + self._table_index[base_name] = table except (AttributeError, TypeError): pass diff --git a/windows/components/stc/autocomplete/query_scope.py b/windows/components/stc/autocomplete/query_scope.py index c2a0814..55961c9 100644 --- a/windows/components/stc/autocomplete/query_scope.py +++ b/windows/components/stc/autocomplete/query_scope.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field -from typing import Optional +from typing import Optional, Union from structures.engines.database import SQLTable @@ -20,7 +20,7 @@ class VirtualTable: class TableReference: name: str alias: Optional[str] = None - table: Optional[SQLTable | VirtualTable] = None + table: Optional[Union[SQLTable, VirtualTable]] = None @dataclass diff --git a/windows/components/stc/autocomplete/sql_context.py b/windows/components/stc/autocomplete/sql_context.py index 80e4286..bd1015e 100644 --- a/windows/components/stc/autocomplete/sql_context.py +++ b/windows/components/stc/autocomplete/sql_context.py @@ -24,4 +24,40 @@ class SQLContext(Enum): WINDOW_OVER = "WINDOW_OVER" LIMIT_OFFSET_CLAUSE = "LIMIT_OFFSET" AFTER_LIMIT_NUMBER = "AFTER_LIMIT_NUMBER" + + # INSERT contexts + INSERT_INTO = "INSERT_INTO" + INSERT_COLUMNS = "INSERT_COLUMNS" + INSERT_VALUES = "INSERT_VALUES" + INSERT_VALUE_EXPRESSIONS = "INSERT_VALUE_EXPRESSIONS" + INSERT_COMPLETE = "INSERT_COMPLETE" + INSERT_POST_VALUES = "INSERT_POST_VALUES" + INSERT_STRING_LITERAL = "INSERT_STRING_LITERAL" + + # UPDATE contexts + UPDATE_TABLE = "UPDATE_TABLE" + UPDATE_SET_CLAUSE = "UPDATE_SET_CLAUSE" + UPDATE_SET_COLUMNS = "UPDATE_SET_COLUMNS" + UPDATE_SET_EXPRESSIONS = "UPDATE_SET_EXPRESSIONS" + UPDATE_WHERE_CLAUSE = "UPDATE_WHERE_CLAUSE" + UPDATE_WHERE_CONDITIONS = "UPDATE_WHERE_CONDITIONS" + UPDATE_WHERE_OPERATORS = "UPDATE_WHERE_OPERATORS" + UPDATE_JOIN_ON = "UPDATE_JOIN_ON" + UPDATE_STRING_LITERAL = "UPDATE_STRING_LITERAL" + UPDATE_WHERE_STRING_LITERAL = "UPDATE_WHERE_STRING_LITERAL" + + # DELETE contexts + DELETE_FROM = "DELETE_FROM" + DELETE_WHERE_CLAUSE = "DELETE_WHERE_CLAUSE" + DELETE_WHERE_CONDITIONS = "DELETE_WHERE_CONDITIONS" + DELETE_WHERE_OPERATORS = "DELETE_WHERE_OPERATORS" + DELETE_WHERE_EXPRESSIONS = "DELETE_WHERE_EXPRESSIONS" + DELETE_JOIN_ON = "DELETE_JOIN_ON" + DELETE_USING = "DELETE_USING" + DELETE_SUBQUERY = "DELETE_SUBQUERY" + DELETE_WHERE_STRING_LITERAL = "DELETE_WHERE_STRING_LITERAL" + + # SELECT WHERE string literal + WHERE_STRING_LITERAL = "WHERE_STRING_LITERAL" + UNKNOWN = "UNKNOWN" diff --git a/windows/components/stc/autocomplete/statement_extractor.py b/windows/components/stc/autocomplete/statement_extractor.py index 82d3804..3e4eb12 100644 --- a/windows/components/stc/autocomplete/statement_extractor.py +++ b/windows/components/stc/autocomplete/statement_extractor.py @@ -6,16 +6,86 @@ class StatementExtractor: _string_pattern = re.compile(r"'(?:[^'\\]|\\.)*'|\"(?:[^\"\\]|\\.)*\"") _comment_pattern = re.compile(r"--[^\n]*|/\*.*?\*/", re.DOTALL) + + @staticmethod + def normalize_separator( + separator: Optional[str], *, default: str = ";" + ) -> str: + raw_separator = (separator or "").strip() + if not raw_separator: + return default + + if len(raw_separator) == 1: + return raw_separator + + if re.fullmatch(r"[A-Za-z0-9_]+", raw_separator): + return raw_separator + + return default + + @staticmethod + def _statement_boundaries(text: str, cleaned_text: str, separator: str) -> list[int]: + boundaries = [0] + + if len(separator) == 1: + for idx, char in enumerate(cleaned_text): + if char == separator: + boundaries.append(idx + 1) + else: + pattern = re.compile(rf"\b{re.escape(separator)}\b", re.IGNORECASE) + for match in pattern.finditer(cleaned_text): + boundaries.append(match.end()) + + boundaries.append(len(text)) + return boundaries + + @staticmethod + def _trim_statement( + statement: str, + cursor_offset: int, + separator: str, + ) -> tuple[str, int]: + statement_without_separator = statement + if len(separator) == 1: + if statement_without_separator.endswith(separator): + statement_without_separator = statement_without_separator[:-1] + else: + separator_pattern = re.compile( + rf"\b{re.escape(separator)}\b\s*$", + re.IGNORECASE, + ) + statement_without_separator = separator_pattern.sub( + "", + statement_without_separator, + count=1, + ) + + if statement_without_separator != statement: + cursor_offset = min(cursor_offset, len(statement_without_separator)) + + statement = statement_without_separator + + leading_whitespace = len(statement) - len(statement.lstrip()) + trimmed_statement = statement.lstrip() + relative_pos = max(0, cursor_offset - leading_whitespace) + relative_pos = min(relative_pos, len(trimmed_statement)) + + return trimmed_statement, relative_pos @staticmethod - def extract_current_statement(text: str, cursor_pos: int) -> tuple[str, int]: + def extract_current_statement( + text: str, + cursor_pos: int, + separator: Optional[str] = None, + ) -> tuple[str, int]: + effective_separator = StatementExtractor.normalize_separator(separator) cleaned_text = StatementExtractor._remove_strings_and_comments(text) - - statement_boundaries = [0] - for i, char in enumerate(cleaned_text): - if char == ';': - statement_boundaries.append(i + 1) - statement_boundaries.append(len(text)) + + statement_boundaries = StatementExtractor._statement_boundaries( + text, + cleaned_text, + effective_separator, + ) for i in range(len(statement_boundaries) - 1): start = statement_boundaries[i] @@ -23,21 +93,56 @@ def extract_current_statement(text: str, cursor_pos: int) -> tuple[str, int]: if start <= cursor_pos <= end: statement = text[start:end] - - if statement.endswith(';'): - statement = statement[:-1] - - statement = statement.lstrip() - - relative_pos = cursor_pos - start - if start > 0: - leading_whitespace = len(text[start:]) - len(text[start:].lstrip()) - relative_pos = cursor_pos - start - leading_whitespace - + + cursor_offset = cursor_pos - start + statement, relative_pos = StatementExtractor._trim_statement( + statement, + cursor_offset, + effective_separator, + ) + return statement, relative_pos return text, cursor_pos + @staticmethod + def extract_all_statements( + text: str, + separator: Optional[str] = None, + ) -> list[tuple[str, int, int]]: + """Return all statements as (text, start_pos, end_pos) tuples.""" + if not text.strip(): + return [] + + effective_separator = StatementExtractor.normalize_separator(separator) + cleaned = StatementExtractor._remove_strings_and_comments(text) + boundaries = StatementExtractor._statement_boundaries( + text, + cleaned, + effective_separator, + ) + + results = [] + for i in range(len(boundaries) - 1): + start, end = boundaries[i], boundaries[i + 1] + statement = text[start:end] + if len(effective_separator) == 1: + if statement.endswith(effective_separator): + statement = statement[:-1] + else: + statement = re.sub( + rf"\b{re.escape(effective_separator)}\b\s*$", + "", + statement, + count=1, + flags=re.IGNORECASE, + ) + statement = statement.strip() + if statement: + results.append((statement, start, end)) + + return results + @staticmethod def _remove_strings_and_comments(text: str) -> str: text = StatementExtractor._string_pattern.sub(lambda m: ' ' * len(m.group(0)), text) diff --git a/windows/components/stc/autocomplete/suggestion_builder.py b/windows/components/stc/autocomplete/suggestion_builder.py index 784f607..7459edd 100644 --- a/windows/components/stc/autocomplete/suggestion_builder.py +++ b/windows/components/stc/autocomplete/suggestion_builder.py @@ -6,6 +6,9 @@ CompletionItem, CompletionItemType, ) +from windows.components.stc.autocomplete.dot_completion_handler import ( + DotCompletionHandler, +) from windows.components.stc.autocomplete.query_scope import QueryScope, TableReference from windows.components.stc.autocomplete.sql_context import SQLContext @@ -13,6 +16,10 @@ class SuggestionBuilder: + _identifier_segment_pattern = r"(?:[A-Za-z_][A-Za-z0-9_]*|`[^`]+`|\"[^\"]+\"|\[[^\]]+\])" + _table_name_pattern = ( + _identifier_segment_pattern + r"(?:\." + _identifier_segment_pattern + r")?" + ) _primary_keywords = { "SELECT", "INSERT", @@ -65,6 +72,12 @@ class SuggestionBuilder: "POWER", } + _literal_functions = { + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + } + _max_database_columns = 400 _scope_restricted_contexts = { @@ -105,114 +118,245 @@ def build( statement: str = "", cursor_pos: Optional[int] = None, ) -> list[CompletionItem]: - if context == SQLContext.EMPTY: - return self._build_empty(prefix) - - if context == SQLContext.SINGLE_TOKEN: - return self._build_single_token(prefix) + if context in (SQLContext.EMPTY, SQLContext.SINGLE_TOKEN): + return self._build_empty(prefix) if context == SQLContext.EMPTY else self._build_single_token(prefix) if context == SQLContext.DOT_COMPLETION: return self._build_dot_completion(scope, prefix, statement) - if context == SQLContext.SELECT_LIST: - return self._build_select_list(scope, prefix, statement, cursor_pos) - - if context == SQLContext.FROM_CLAUSE: - import re - - statement_upper = statement.upper() - - if re.search(r"\bAS\s+$", statement_upper): - return [] - - if prefix and re.search(r"\bAS\s+\w+$", statement_upper): - return [] + if context in { + SQLContext.SELECT_LIST, + SQLContext.FROM_CLAUSE, + SQLContext.JOIN_CLAUSE, + SQLContext.JOIN_AFTER_TABLE, + SQLContext.JOIN_ON, + SQLContext.JOIN_ON_AFTER_OPERATOR, + SQLContext.JOIN_ON_AFTER_EXPRESSION, + SQLContext.WHERE_CLAUSE, + SQLContext.WHERE_AFTER_EXPRESSION, + SQLContext.WHERE_AFTER_OPERATOR, + SQLContext.ORDER_BY_CLAUSE, + SQLContext.ORDER_BY_AFTER_COLUMN, + SQLContext.GROUP_BY_CLAUSE, + SQLContext.HAVING_CLAUSE, + SQLContext.HAVING_AFTER_OPERATOR, + SQLContext.HAVING_AFTER_EXPRESSION, + SQLContext.WINDOW_OVER, + SQLContext.LIMIT_OFFSET_CLAUSE, + SQLContext.AFTER_LIMIT_NUMBER, + SQLContext.WHERE_STRING_LITERAL, + }: + return self._build_select_family_context( + context=context, + scope=scope, + prefix=prefix, + statement=statement, + cursor_pos=cursor_pos, + ) - if ( - prefix - and scope.from_tables - and "," not in statement - and self._is_after_completed_from_table_with_prefix( - statement, prefix, scope - ) - ): - return self._build_from_followup_keywords(prefix, scope) - - if not prefix and scope.from_tables: - if "," in statement: - in_scope_table_names = { - ref.name.lower() for ref in scope.from_tables - } - try: - tables = [ - CompletionItem( - name=table.name, item_type=CompletionItemType.TABLE - ) - for table in self._database.tables - if table.name.lower() not in in_scope_table_names - ] - return sorted( - tables, key=lambda x: self._table_name_sort_key(x.name) - ) - except (AttributeError, TypeError): - return [] - else: - return self._build_from_followup_keywords(prefix, scope) + if context in { + SQLContext.INSERT_INTO, + SQLContext.INSERT_COLUMNS, + SQLContext.INSERT_VALUES, + SQLContext.INSERT_VALUE_EXPRESSIONS, + SQLContext.INSERT_COMPLETE, + SQLContext.INSERT_POST_VALUES, + SQLContext.INSERT_STRING_LITERAL, + }: + return self._build_insert_context(context, scope, prefix, statement) + + if context in { + SQLContext.UPDATE_TABLE, + SQLContext.UPDATE_SET_CLAUSE, + SQLContext.UPDATE_SET_COLUMNS, + SQLContext.UPDATE_SET_EXPRESSIONS, + SQLContext.UPDATE_WHERE_CLAUSE, + SQLContext.UPDATE_WHERE_CONDITIONS, + SQLContext.UPDATE_WHERE_OPERATORS, + SQLContext.UPDATE_JOIN_ON, + SQLContext.UPDATE_STRING_LITERAL, + SQLContext.UPDATE_WHERE_STRING_LITERAL, + }: + return self._build_update_context(context, scope, prefix, statement) + + if context in { + SQLContext.DELETE_FROM, + SQLContext.DELETE_WHERE_CLAUSE, + SQLContext.DELETE_WHERE_CONDITIONS, + SQLContext.DELETE_WHERE_OPERATORS, + SQLContext.DELETE_WHERE_EXPRESSIONS, + SQLContext.DELETE_JOIN_ON, + SQLContext.DELETE_USING, + SQLContext.DELETE_SUBQUERY, + SQLContext.DELETE_WHERE_STRING_LITERAL, + }: + return self._build_delete_context(context, scope, prefix, statement) - return self._build_from_clause(prefix, statement, scope) + return self._build_keywords(prefix) + def _build_select_family_context( + self, + context: SQLContext, + scope: QueryScope, + prefix: str, + statement: str, + cursor_pos: Optional[int], + ) -> list[CompletionItem]: + if context == SQLContext.SELECT_LIST: + return self._build_select_list(scope, prefix, statement, cursor_pos) + if context == SQLContext.FROM_CLAUSE: + return self._build_from_context(scope, prefix, statement) if context == SQLContext.JOIN_CLAUSE: return self._build_join_clause(prefix, scope, statement) - if context == SQLContext.JOIN_AFTER_TABLE: return self._build_join_after_table(scope) - if context == SQLContext.JOIN_ON: return self._build_join_on(scope, prefix, statement) - if context == SQLContext.JOIN_ON_AFTER_OPERATOR: return self._build_join_on_after_operator(scope, prefix, statement) - if context == SQLContext.JOIN_ON_AFTER_EXPRESSION: return self._build_join_on_after_expression(prefix) - if context == SQLContext.WHERE_CLAUSE: return self._build_where_clause(scope, prefix, statement) - if context == SQLContext.WHERE_AFTER_EXPRESSION: return self._build_where_after_expression(prefix, statement) - if context == SQLContext.WHERE_AFTER_OPERATOR: return self._build_where_after_operator(scope, prefix, statement) - if context == SQLContext.ORDER_BY_CLAUSE: return self._build_order_by(scope, prefix, statement) - if context == SQLContext.ORDER_BY_AFTER_COLUMN: return self._build_order_by_after_column(prefix) - if context == SQLContext.GROUP_BY_CLAUSE: return self._build_group_by(scope, prefix, statement, cursor_pos) - if context == SQLContext.HAVING_CLAUSE: return self._build_having(scope, prefix, statement) - if context == SQLContext.HAVING_AFTER_OPERATOR: return self._build_having_after_operator(scope, prefix, statement) - if context == SQLContext.HAVING_AFTER_EXPRESSION: return self._build_having_after_expression(prefix) - if context == SQLContext.WINDOW_OVER: return self._build_window_over(prefix) + if context == SQLContext.AFTER_LIMIT_NUMBER: + return self._build_after_limit_number(prefix) + return [] + + def _build_insert_context( + self, + context: SQLContext, + scope: QueryScope, + prefix: str, + statement: str, + ) -> list[CompletionItem]: + if context == SQLContext.INSERT_INTO: + return self._build_insert_into(prefix) + if context == SQLContext.INSERT_COLUMNS: + return self._build_insert_columns(scope, prefix, statement) + if context == SQLContext.INSERT_VALUES: + return self._build_insert_values(prefix) + if context == SQLContext.INSERT_VALUE_EXPRESSIONS: + return self._build_insert_value_expressions(prefix) + if context == SQLContext.INSERT_POST_VALUES: + return self._build_insert_post_values(prefix) + return [] + + def _build_update_context( + self, + context: SQLContext, + scope: QueryScope, + prefix: str, + statement: str, + ) -> list[CompletionItem]: + if context == SQLContext.UPDATE_TABLE: + return self._build_update_table(prefix) + if context == SQLContext.UPDATE_SET_CLAUSE: + return self._build_update_set_clause(prefix, scope) + if context == SQLContext.UPDATE_SET_COLUMNS: + return self._build_update_set_columns(scope, prefix, statement) + if context == SQLContext.UPDATE_SET_EXPRESSIONS: + return self._build_insert_value_expressions(prefix) + if context == SQLContext.UPDATE_WHERE_CLAUSE: + return self._build_update_where_clause(prefix, scope) + if context == SQLContext.UPDATE_WHERE_CONDITIONS: + return self._build_update_where_conditions(scope, prefix, statement) + if context == SQLContext.UPDATE_WHERE_OPERATORS: + return self._build_update_where_operators(prefix) + if context == SQLContext.UPDATE_JOIN_ON: + return self._build_update_join_on(scope, prefix) + return [] + + def _build_delete_context( + self, + context: SQLContext, + scope: QueryScope, + prefix: str, + statement: str, + ) -> list[CompletionItem]: + if context == SQLContext.DELETE_FROM: + return self._build_delete_from(prefix) + if context == SQLContext.DELETE_WHERE_CLAUSE: + return self._build_delete_where_clause(prefix, scope, statement) + if context == SQLContext.DELETE_WHERE_CONDITIONS: + return self._build_delete_where_conditions(scope, prefix, statement) + if context == SQLContext.DELETE_WHERE_OPERATORS: + return self._build_update_where_operators(prefix) + if context == SQLContext.DELETE_WHERE_EXPRESSIONS: + return self._build_insert_value_expressions(prefix) + if context == SQLContext.DELETE_JOIN_ON: + return self._build_update_join_on(scope, prefix) + if context == SQLContext.DELETE_USING: + return self._build_delete_using(scope, prefix, statement) + if context == SQLContext.DELETE_SUBQUERY: + return self._build_delete_subquery(prefix) + return [] + + def _build_from_context( + self, + scope: QueryScope, + prefix: str, + statement: str, + ) -> list[CompletionItem]: + statement_upper = statement.upper() - if context == SQLContext.LIMIT_OFFSET_CLAUSE: + if re.search(r"\bAS\s+$", statement_upper): return [] - if context == SQLContext.AFTER_LIMIT_NUMBER: - return self._build_after_limit_number(prefix) + if prefix and re.search(r"\bAS\s+\w+$", statement_upper): + return [] - return self._build_keywords(prefix) + if ( + prefix + and scope.from_tables + and "," not in statement + and self._is_after_completed_from_table_with_prefix( + statement, prefix, scope + ) + ): + return self._build_from_followup_keywords(prefix, scope) + + if not prefix and scope.from_tables: + if "," in statement: + in_scope_table_names = {ref.name.lower() for ref in scope.from_tables} + try: + tables = [ + CompletionItem(name=table.name, item_type=CompletionItemType.TABLE) + for table in self._database.tables + if table.name.lower() not in in_scope_table_names + ] + return sorted(tables, key=lambda x: self._table_name_sort_key(x.name)) + except (AttributeError, TypeError): + return [] + return self._build_from_followup_keywords(prefix, scope) + + return self._build_from_clause(prefix, statement, scope) + + @staticmethod + def _normalize_identifier(identifier: str) -> str: + normalized = identifier.strip() + if not normalized: + return normalized + if normalized[0] in {'`', '"', '['} and normalized[-1] in {'`', '"', ']'}: + normalized = normalized[1:-1] + return normalized def _build_empty(self, prefix: str) -> list[CompletionItem]: keywords = [ @@ -229,95 +373,25 @@ def _build_empty(self, prefix: str) -> list[CompletionItem]: def _build_dot_completion( self, scope: QueryScope, prefix: str, statement: str ) -> list[CompletionItem]: - import re - + # Moved to DotCompletionHandler - this method now delegates to the centralized handler + handler = DotCompletionHandler(self._database, scope) cursor_pos = len(statement) - text_before_cursor = statement[:cursor_pos] - - dot_match = re.search( - r"([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)?$", - text_before_cursor, - ) - if not dot_match: + if prefix and statement.endswith(prefix): + cursor_pos -= len(prefix) + items, _resolved_prefix = handler.get_completions(statement, cursor_pos) + if items is None: return [] - - table_alias = dot_match.group(1) - column_prefix = dot_match.group(2) if dot_match.group(2) else "" - - resolved_table = self._resolve_table_alias(table_alias, scope, statement) - if not resolved_table: - return [] - - table_name = resolved_table.name - columns = resolved_table.columns - - try: - ordered_columns = self._order_columns_for_dot_completion(columns) - except (AttributeError, TypeError): - ordered_columns = [] - - column_items = [] - for col in ordered_columns: - col_name = col.name - if not column_prefix or col_name.upper().startswith(column_prefix.upper()): - column_items.append( - CompletionItem(name=col_name, item_type=CompletionItemType.COLUMN) - ) - - return column_items - - @staticmethod - def _order_columns_for_dot_completion(columns: object) -> list[object]: - columns_list = list(columns) - - def key(item_with_index: tuple[int, object]) -> tuple[int, int]: - idx, col = item_with_index - raw_id = getattr(col, "id", None) - if isinstance(raw_id, int): - return (0, raw_id) - if isinstance(raw_id, str) and raw_id.isdigit(): - return (0, int(raw_id)) - return (1, idx) - - return [col for _, col in sorted(enumerate(columns_list), key=key)] - - def _resolve_table_alias(self, table_alias: str, scope: QueryScope, statement: str): - for ref in scope.from_tables: - if ref.alias and ref.alias.lower() == table_alias.lower(): - if ref.table is not None: - return ref.table - return self._get_table_by_name(ref.name) - if ref.name.lower() == table_alias.lower(): - if ref.table is not None: - return ref.table - return self._get_table_by_name(ref.name) - - for ref in scope.join_tables: - if ref.alias and ref.alias.lower() == table_alias.lower(): - if ref.table is not None: - return ref.table - return self._get_table_by_name(ref.name) - if ref.name.lower() == table_alias.lower(): - if ref.table is not None: - return ref.table - return self._get_table_by_name(ref.name) - - for ref in scope.cte_tables: - if ref.name.lower() == table_alias.lower() and ref.table is not None: - return ref.table - - if ( - scope.current_table - and scope.current_table.name.lower() == table_alias.lower() - ): - return scope.current_table - - return self._get_table_by_name(table_alias) + if prefix: + prefix_lower = prefix.lower() + items = [item for item in items if item.name.lower().startswith(prefix_lower)] + return items def _get_table_by_name(self, table_name: str): + raw_name = table_name.split(".")[-1] + normalized_name = self._normalize_identifier(raw_name).lower() try: for table in self._database.tables: - if table.name.lower() == table_name.lower(): + if table.name.lower() == normalized_name: return table except (AttributeError, TypeError): pass @@ -759,8 +833,6 @@ def _build_from_clause( def _is_after_completed_from_table_with_prefix( statement: str, prefix: str, scope: QueryScope ) -> bool: - import re - if not scope.from_tables: return False @@ -773,8 +845,12 @@ def _is_after_completed_from_table_with_prefix( return False prefix_lower = prefix.lower() - matches_completed_table = prefix_lower == last_ref.name.lower() or ( - bool(last_ref.alias) and prefix_lower == last_ref.alias.lower() + last_ref_name = SuggestionBuilder._normalize_identifier(last_ref.name).lower() + last_ref_base_name = last_ref_name.split(".")[-1] + matches_completed_table = ( + prefix_lower == last_ref_name + or prefix_lower == last_ref_base_name + or (bool(last_ref.alias) and prefix_lower == last_ref.alias.lower()) ) if matches_completed_table: @@ -782,7 +858,9 @@ def _is_after_completed_from_table_with_prefix( return bool( re.search( - r"\bFROM\s+[A-Za-z_][A-Za-z0-9_]*(?:\s+(?:AS\s+)?[A-Za-z_][A-Za-z0-9_]*)?\s+[A-Za-z_][A-Za-z0-9_]*$", + r"\bFROM\s+" + + SuggestionBuilder._table_name_pattern + + r"(?:\s+(?:AS\s+)?[A-Za-z_][A-Za-z0-9_]*)?\s+[A-Za-z_][A-Za-z0-9_]*$", statement_trimmed, re.IGNORECASE, ) @@ -915,14 +993,21 @@ def _is_after_completed_join_table_with_prefix( prefix_lower = prefix.lower() - if prefix_lower == last_ref.name.lower() or ( - bool(last_ref.alias) and prefix_lower == last_ref.alias.lower() + last_ref_name = SuggestionBuilder._normalize_identifier(last_ref.name).lower() + last_ref_base_name = last_ref_name.split(".")[-1] + + if ( + prefix_lower == last_ref_name + or prefix_lower == last_ref_base_name + or (bool(last_ref.alias) and prefix_lower == last_ref.alias.lower()) ): return True return bool( re.search( - r"\bJOIN\s+[A-Za-z_][A-Za-z0-9_]*(?:\s+(?:AS\s+)?[A-Za-z_][A-Za-z0-9_]*)?\s+[A-Za-z_][A-Za-z0-9_]*$", + r"\bJOIN\s+" + + SuggestionBuilder._table_name_pattern + + r"(?:\s+(?:AS\s+)?[A-Za-z_][A-Za-z0-9_]*)?\s+[A-Za-z_][A-Za-z0-9_]*$", statement_trimmed, re.IGNORECASE, ) @@ -1282,7 +1367,7 @@ def _build_single_table_where_columns( if not table: return [] - if reference.alias or prefer_qualified: + if reference.alias or prefer_qualified or "." in reference.name: return self._build_qualified_columns_for_reference(reference, prefix) if not prefix: @@ -1387,6 +1472,20 @@ def _build_where_literals(prefix: str) -> list[CompletionItem]: for literal in literals ] + @staticmethod + def _build_join_literals(prefix: str) -> list[CompletionItem]: + literals = ["NULL", "TRUE", "FALSE"] + if prefix: + prefix_upper = prefix.upper() + literals = [ + literal for literal in literals if literal.startswith(prefix_upper) + ] + + return [ + CompletionItem(name=literal, item_type=CompletionItemType.KEYWORD) + for literal in literals + ] + @staticmethod def _build_after_is_keywords(prefix: str) -> list[CompletionItem]: keywords = ["NULL", "NOT NULL", "TRUE", "FALSE"] @@ -1802,7 +1901,9 @@ def _build_select_list_functions(self, prefix: str) -> list[CompletionItem]: if item.name not in self._select_list_excluded_functions ] - def _build_functions(self, prefix: str) -> list[CompletionItem]: + def _build_functions( + self, prefix: str, exclude: Optional[set[str]] = None + ) -> list[CompletionItem]: if not self._database: return [] @@ -1813,6 +1914,7 @@ def _build_functions(self, prefix: str) -> list[CompletionItem]: name=str(func).upper(), item_type=CompletionItemType.FUNCTION ) for func in functions + if not exclude or str(func).upper() not in exclude ] except (AttributeError, TypeError): return [] @@ -2393,3 +2495,283 @@ def _filter_columns_by_prefix( filtered.append(col) return filtered + + # ── INSERT builders ────────────────────────────────────────────── + + def _build_insert_into(self, prefix: str) -> list[CompletionItem]: + return self._build_all_tables(prefix) + + def _build_insert_columns( + self, scope: QueryScope, prefix: str, statement: str + ) -> list[CompletionItem]: + table = self._find_insert_target_table(statement) + if not table: + return [] + + already_listed = self._extract_already_listed_columns(statement) + columns = [] + try: + for col in table.columns: + if col.name.lower() not in already_listed: + if not prefix or col.name.lower().startswith(prefix.lower()): + columns.append( + CompletionItem(name=col.name, item_type=CompletionItemType.COLUMN) + ) + except (AttributeError, TypeError): + pass + return columns + + def _build_insert_values(self, prefix: str) -> list[CompletionItem]: + keywords = ["VALUES", "SELECT", "DEFAULT"] + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + def _build_insert_value_expressions(self, prefix: str) -> list[CompletionItem]: + items = self._build_where_literals(prefix) + items.extend(self._build_functions(prefix, exclude=self._literal_functions)) + return items + + def _build_insert_post_values(self, prefix: str) -> list[CompletionItem]: + keywords = ["ON DUPLICATE KEY UPDATE", "ON CONFLICT", "RETURNING", ";"] + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.upper().startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + # ── UPDATE builders ────────────────────────────────────────────── + + def _build_update_table(self, prefix: str) -> list[CompletionItem]: + return self._build_all_tables(prefix) + + def _build_update_set_clause(self, prefix: str, scope: QueryScope) -> list[CompletionItem]: + keywords = ["SET"] + if not scope.join_tables: + keywords.extend(["JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "CROSS JOIN"]) + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.upper().startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + def _build_update_set_columns( + self, scope: QueryScope, prefix: str, statement: str + ) -> list[CompletionItem]: + table = self._find_update_target_table(statement) + if not table: + return [] + + already_set = self._extract_already_set_columns(statement) + columns = [] + try: + for col in table.columns: + if col.name.lower() not in already_set: + if not prefix or col.name.lower().startswith(prefix.lower()): + columns.append( + CompletionItem(name=col.name, item_type=CompletionItemType.COLUMN) + ) + except (AttributeError, TypeError): + pass + return columns + + def _build_update_where_clause(self, prefix: str, scope: QueryScope) -> list[CompletionItem]: + keywords = ["WHERE"] + if not scope.join_tables: + keywords.extend(["JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "CROSS JOIN"]) + keywords.extend(["RETURNING", ";"]) + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.upper().startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + def _build_update_where_conditions( + self, scope: QueryScope, prefix: str, statement: str + ) -> list[CompletionItem]: + table = self._find_update_target_table(statement) + + # After a complete condition: suggest AND/OR/LIMIT/etc + if self._is_after_where_condition_value(statement): + keywords = ["AND", "OR", "LIMIT", "ORDER BY", "RETURNING", ";"] + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.upper().startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + # Suggest columns + functions + columns = self._get_table_columns_as_items(table, prefix) + items = list(columns) + items.extend(self._build_where_literals(prefix)) + items.extend(self._build_functions(prefix, exclude=self._literal_functions)) + return items + + def _build_update_where_operators(self, prefix: str) -> list[CompletionItem]: + operators = [ + "=", "!=", "<>", ">", "<", ">=", "<=", + "IS", "IS NOT", "IN", "NOT IN", "LIKE", "NOT LIKE", + "BETWEEN", "NOT BETWEEN", "AND", "OR", + ] + if prefix: + prefix_upper = prefix.upper() + operators = [op for op in operators if op.upper().startswith(prefix_upper)] + return [CompletionItem(name=op, item_type=CompletionItemType.KEYWORD) for op in operators] + + def _build_update_join_on(self, scope: QueryScope, prefix: str) -> list[CompletionItem]: + items = self._resolve_columns_in_scope(scope, prefix, SQLContext.JOIN_ON) + items.extend(self._build_join_literals(prefix)) + items.extend(self._build_functions(prefix, exclude=self._literal_functions)) + return items + + # ── DELETE builders ────────────────────────────────────────────── + + def _build_delete_from(self, prefix: str) -> list[CompletionItem]: + return self._build_all_tables(prefix) + + def _build_delete_where_clause( + self, prefix: str, scope: QueryScope, statement: str + ) -> list[CompletionItem]: + keywords = ["WHERE"] + if not scope.join_tables: + keywords.extend(["JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "CROSS JOIN"]) + keywords.append("USING") + keywords.extend(["RETURNING", ";"]) + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.upper().startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + def _build_delete_where_conditions( + self, scope: QueryScope, prefix: str, statement: str + ) -> list[CompletionItem]: + table = self._find_delete_target_table(statement) + + if self._is_after_where_condition_value(statement): + keywords = ["AND", "OR", "LIMIT", "ORDER BY", "RETURNING", ";"] + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.upper().startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + columns = self._get_table_columns_as_items(table, prefix) + items = list(columns) + items.extend(self._build_where_literals(prefix)) + items.extend(self._build_functions(prefix, exclude=self._literal_functions)) + return items + + def _build_delete_using( + self, scope: QueryScope, prefix: str, statement: str + ) -> list[CompletionItem]: + delete_table = self._find_delete_target_table(statement) + exclude = {delete_table.name.lower()} if delete_table else set() + return self._build_all_tables(prefix, exclude=exclude) + + def _build_delete_subquery(self, prefix: str) -> list[CompletionItem]: + keywords = ["SELECT", "WITH"] + items = [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + items.extend(self._build_where_literals(prefix)) + items.extend(self._build_functions(prefix, exclude=self._literal_functions)) + if prefix: + prefix_upper = prefix.upper() + items = [item for item in items if item.name.upper().startswith(prefix_upper)] + return items + + # ── Shared helpers ─────────────────────────────────────────────── + + def _build_all_tables( + self, prefix: str, exclude: Optional[set[str]] = None + ) -> list[CompletionItem]: + if not self._database: + return [] + try: + tables = [] + for table in self._database.tables: + if exclude and table.name.lower() in exclude: + continue + if not prefix or table.name.lower().startswith(prefix.lower()): + tables.append( + CompletionItem(name=table.name, item_type=CompletionItemType.TABLE) + ) + return sorted(tables, key=lambda x: self._table_name_sort_key(x.name)) + except (AttributeError, TypeError): + return [] + + def _find_insert_target_table(self, statement: str) -> Optional[SQLTable]: + match = re.search( + r"\bINSERT\s+INTO\s+(" + self._table_name_pattern + r")", + statement, + re.IGNORECASE, + ) + if match: + return self._get_table_by_name(match.group(1)) + return None + + def _find_update_target_table(self, statement: str) -> Optional[SQLTable]: + match = re.search( + r"\bUPDATE\s+(" + self._table_name_pattern + r")", + statement, + re.IGNORECASE, + ) + if match: + return self._get_table_by_name(match.group(1)) + return None + + def _find_delete_target_table(self, statement: str) -> Optional[SQLTable]: + match = re.search( + r"\bFROM\s+(" + self._table_name_pattern + r")", + statement, + re.IGNORECASE, + ) + if match: + return self._get_table_by_name(match.group(1)) + return None + + def _get_table_columns_as_items( + self, table: Optional[SQLTable], prefix: str + ) -> list[CompletionItem]: + if not table: + return [] + try: + columns = [] + for col in table.columns: + if not prefix or col.name.lower().startswith(prefix.lower()): + columns.append( + CompletionItem(name=col.name, item_type=CompletionItemType.COLUMN) + ) + return columns + except (AttributeError, TypeError): + return [] + + @staticmethod + def _extract_already_listed_columns(statement: str) -> set[str]: + match = re.search(r"\(\s*(.+?)(?:\)|$)", statement, re.IGNORECASE) + if not match: + return set() + column_list = match.group(1) + return {col.strip().lower() for col in column_list.split(",") if col.strip()} + + @staticmethod + def _extract_already_set_columns(statement: str) -> set[str]: + set_match = re.search(r"\bSET\s+(.*)", statement, re.IGNORECASE | re.DOTALL) + if not set_match: + return set() + set_clause = set_match.group(1) + columns = set() + for assignment in set_clause.split(","): + eq_match = re.match(r"\s*(\w+)\s*=", assignment) + if eq_match: + columns.add(eq_match.group(1).lower()) + return columns + + def _is_after_where_condition_value(self, statement: str) -> bool: + where_match = re.search(r"\bWHERE\s+(.*)", statement, re.IGNORECASE | re.DOTALL) + if not where_match: + return False + clause = where_match.group(1).strip() + if not clause: + return False + return bool(re.search( + r"(?:[A-Za-z_][A-Za-z0-9_]*\.)?[A-Za-z_][A-Za-z0-9_]*\s*" + r"(?:=|!=|<>|<=|>=|<|>|LIKE|IN|BETWEEN)\s*" + r"(?:[A-Za-z_][A-Za-z0-9_]*|\d+|'[^']*'|\"[^\"]*\"|NULL|TRUE|FALSE|\w+\([^)]*\))\s*$", + clause, + re.IGNORECASE, + )) diff --git a/windows/dialogs/advanced_cell_editor/__init__.py b/windows/dialogs/advanced_cell_editor/__init__.py deleted file mode 100644 index c844cb4..0000000 --- a/windows/dialogs/advanced_cell_editor/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from windows.dialogs.advanced_cell_editor.controller import AdvancedCellEditorController - -__all__ = ["AdvancedCellEditorController"] diff --git a/windows/dialogs/column_content/__init__.py b/windows/dialogs/column_content/__init__.py new file mode 100644 index 0000000..689438b --- /dev/null +++ b/windows/dialogs/column_content/__init__.py @@ -0,0 +1,3 @@ +from windows.dialogs.column_content.controller import ColumnContentDialogController + +__all__ = ["ColumnContentDialogController"] \ No newline at end of file diff --git a/windows/dialogs/advanced_cell_editor/controller.py b/windows/dialogs/column_content/controller.py similarity index 66% rename from windows/dialogs/advanced_cell_editor/controller.py rename to windows/dialogs/column_content/controller.py index 15ceb12..b09d799 100644 --- a/windows/dialogs/advanced_cell_editor/controller.py +++ b/windows/dialogs/column_content/controller.py @@ -1,17 +1,21 @@ +from typing import Optional + import wx from windows.components.stc.detectors import detect_syntax_id from windows.components.stc.profiles import SyntaxProfile from windows.components.stc.styles import apply_stc_theme -from windows.views import AdvancedCellEditorDialog +from windows.views import ColumnContentDialog -class AdvancedCellEditorController(AdvancedCellEditorDialog): +class ColumnContentDialogController(ColumnContentDialog): app = wx.GetApp() - def __init__(self, parent, value: str): + def __init__(self, parent, value: str, read_only: bool = False): super().__init__(parent) + self._read_only = read_only + self.syntax_choice.AppendItems(self.app.syntax_registry.labels()) self.advanced_stc_editor.SetText(value or "") self.advanced_stc_editor.EmptyUndoBuffer() @@ -21,6 +25,10 @@ def __init__(self, parent, value: str): self.syntax_choice.SetStringSelection(self._auto_syntax_profile().label) self.do_apply_syntax(do_format=True) + self._apply_read_only_state() + + self.m_button48.Bind(wx.EVT_BUTTON, self._on_ok) + self.m_button49.Bind(wx.EVT_BUTTON, self._on_cancel) def _auto_syntax_profile(self) -> SyntaxProfile: text = self.advanced_stc_editor.GetText() @@ -33,8 +41,22 @@ def _get_current_syntax_profile(self) -> SyntaxProfile: return self.app.syntax_registry.get(label) def on_syntax_changed(self, _evt): - label = self.syntax_choice.GetStringSelection() - self.do_apply_syntax(label) + self.do_apply_syntax(do_format=True) + + def _apply_read_only_state(self) -> None: + if not self._read_only: + return + + self.advanced_stc_editor.SetReadOnly(True) + self.m_button49.Hide() + self.m_button48.SetLabel("Close") + self.Layout() + + def _on_ok(self, _evt: wx.Event) -> None: + self.EndModal(wx.ID_OK) + + def _on_cancel(self, _evt: wx.Event) -> None: + self.EndModal(wx.ID_CANCEL) def do_apply_syntax(self, do_format: bool = True): label = self.syntax_choice.GetStringSelection() @@ -58,3 +80,6 @@ def _replace_text_undo_friendly(self, new_text: str): self.advanced_stc_editor.SetText(new_text) finally: self.advanced_stc_editor.EndUndoAction() + + def get_value(self) -> Optional[str]: + return self.advanced_stc_editor.GetText() \ No newline at end of file diff --git a/windows/dialogs/connections/controller.py b/windows/dialogs/connections/controller.py index 7acfbb8..f955086 100644 --- a/windows/dialogs/connections/controller.py +++ b/windows/dialogs/connections/controller.py @@ -3,7 +3,7 @@ import wx import wx.dataview -from helpers.dataview import BaseDataViewTreeModel +from helpers.dataview import BaseObservableDataViewTreeModel from structures.connection import Connection @@ -11,7 +11,7 @@ from windows.dialogs.connections.repository import ConnectionsRepository -class ConnectionsTreeModel(BaseDataViewTreeModel): +class ConnectionsTreeModel(BaseObservableDataViewTreeModel): def __init__(self): super().__init__(column_count=2) self._parent_map = {} @@ -133,8 +133,6 @@ def _on_item_start_editing(self, event): self._allow_next_edit = False def do_filter_connections(self, search_text): - # self.search_text = search_text - # self._update_displayed_connections() self.repository.connections.filter( lambda x: search_text.lower() in x.name.lower() ) diff --git a/windows/dialogs/connections/model.py b/windows/dialogs/connections/model.py index bd9624e..0df3922 100644 --- a/windows/dialogs/connections/model.py +++ b/windows/dialogs/connections/model.py @@ -20,7 +20,9 @@ def __init__(self): self.hostname = Observable[str]() self.username = Observable[str]() self.password = Observable[str]() - self.use_tls_enabled = Observable[bool](initial=False) + self.use_tls = Observable[bool](initial=False) + self.connection_timeout = Observable[int](initial=10) + self.compressed_protocol = Observable[bool](initial=False) self.port = Observable[int](initial=3306) self.filename = Observable[str]() self.comments = Observable[str]("") @@ -45,6 +47,7 @@ def __init__(self): self.ssh_tunnel_identity_file = Observable[str]() self.ssh_tunnel_remote_hostname = Observable[str]() self.ssh_tunnel_remote_port = Observable[int](initial=3306) + self.ssh_tunnel_extra_args = Observable[str]() self.engine.subscribe(self._set_default_port) @@ -54,7 +57,9 @@ def __init__(self): self.hostname, self.username, self.password, - self.use_tls_enabled, + self.use_tls, + self.connection_timeout, + self.compressed_protocol, self.port, self.filename, self.comments, @@ -77,6 +82,7 @@ def __init__(self): self.ssh_tunnel_identity_file, self.ssh_tunnel_remote_hostname, self.ssh_tunnel_remote_port, + self.ssh_tunnel_extra_args, callback=self._build, ) @@ -97,7 +103,9 @@ def clear(self, *args): self.hostname: None, self.username: None, self.password: None, - self.use_tls_enabled: False, + self.use_tls: False, + self.connection_timeout: 10, + self.compressed_protocol: False, self.port: 3306, self.filename: None, self.comments: None, @@ -120,6 +128,7 @@ def clear(self, *args): self.ssh_tunnel_identity_file: None, self.ssh_tunnel_remote_hostname: None, self.ssh_tunnel_remote_port: 3306, + self.ssh_tunnel_extra_args: "", } for observable, value in defaults.items(): @@ -157,8 +166,12 @@ def apply(self, connection: Connection): self.hostname(connection.configuration.hostname) self.username(connection.configuration.username) self.password(connection.configuration.password) - self.use_tls_enabled( - getattr(connection.configuration, "use_tls_enabled", False) + self.use_tls(getattr(connection.configuration, "use_tls", False)) + self.connection_timeout( + getattr(connection.configuration, "connect_timeout", 10) + ) + self.compressed_protocol( + getattr(connection.configuration, "compressed_protocol", False) ) self.port(connection.configuration.port) @@ -178,8 +191,14 @@ def apply(self, connection: Connection): self.ssh_tunnel_remote_port( getattr(ssh_tunnel, "remote_port", None) or self.port() ) + extra_args = getattr(ssh_tunnel, "extra_args", "") + if isinstance(extra_args, list): + self.ssh_tunnel_extra_args(" ".join(extra_args)) + else: + self.ssh_tunnel_extra_args(extra_args or "") else: self.ssh_tunnel_enabled(False) + self.ssh_tunnel_extra_args("") def _build_empty_connection(self): return Connection( @@ -187,7 +206,12 @@ def _build_empty_connection(self): name=self.name() or _("New connection"), engine=ConnectionEngine.MYSQL, configuration=CredentialsConfiguration( - hostname="localhost", username="root", password="", port=3306 + hostname="localhost", + username="root", + password="", + port=3306, + connect_timeout=10, + compressed_protocol=False, ), ) @@ -224,7 +248,9 @@ def _build(self, *args): username=db_username, password=db_password, port=self.port.get_value() or 3306, - use_tls_enabled=bool(self.use_tls_enabled.get_value()), + use_tls=bool(self.use_tls.get_value()), + connect_timeout=self.connection_timeout.get_value() or 10, + compressed_protocol=bool(self.compressed_protocol.get_value()), ) if ssh_tunnel_enabled := bool(self.ssh_tunnel_enabled()): @@ -253,6 +279,10 @@ def _build(self, *args): identity_file=self.ssh_tunnel_identity_file.get_value() or None, remote_host=remote_host, remote_port=remote_port, + extra_args=( + self.ssh_tunnel_extra_args.get_value() or "" + ).strip() + or None, ) elif connection_engine == ConnectionEngine.SQLITE: diff --git a/windows/dialogs/connections/repository.py b/windows/dialogs/connections/repository.py index 5902c22..22f6bcc 100644 --- a/windows/dialogs/connections/repository.py +++ b/windows/dialogs/connections/repository.py @@ -96,7 +96,7 @@ def _connection_from_dict( ConnectionEngine.MARIADB, ConnectionEngine.POSTGRESQL, ]: - configuration = CredentialsConfiguration(**config_data) + configuration = self._build_credentials_configuration(config_data) elif engine == ConnectionEngine.SQLITE: configuration = SourceConfiguration(**config_data) @@ -326,7 +326,44 @@ def _build_ssh_configuration( if data.get("remote_port") else None, identity_file=data.get("identity_file"), - extra_args=data.get("extra_args"), + extra_args=self._normalize_ssh_extra_args(data.get("extra_args")), ) except (TypeError, ValueError): return None + + def _build_credentials_configuration( + self, data: dict[str, Any] + ) -> Optional[CredentialsConfiguration]: + if not data: + return None + + try: + return CredentialsConfiguration( + hostname=str(data.get("hostname", "")), + username=str(data.get("username", "")), + password=data.get("password"), + port=int(data.get("port", 3306)), + use_tls=bool( + data.get("use_tls", data.get("use_tls_enabled", False)) + ), + connect_timeout=int(data.get("connect_timeout", 10)), + compressed_protocol=bool(data.get("compressed_protocol", False)), + ) + except (TypeError, ValueError): + return None + + @staticmethod + def _normalize_ssh_extra_args(extra_args: Any) -> Optional[Union[str, list[str]]]: + if isinstance(extra_args, str): + value = extra_args.strip() + return value if value else None + + if isinstance(extra_args, list): + values = [ + str(value).strip() + for value in extra_args + if isinstance(value, str) and value.strip() + ] + return values if values else None + + return None diff --git a/windows/dialogs/connections/view.py b/windows/dialogs/connections/view.py index 94b4417..4534ecb 100644 --- a/windows/dialogs/connections/view.py +++ b/windows/dialogs/connections/view.py @@ -31,7 +31,9 @@ class ConnectionsManager(ConnectionsDialog): - _SETTINGS_SECTION = "connections_dialog" + _SETTINGS_UI = "ui" + _SETTINGS_DIALOGS = "dialogs" + _SETTINGS_CONNECTIONS = "connections" _SETTINGS_EXPANDED_DIRECTORIES = "expanded_directories" def __init__(self, parent): @@ -55,7 +57,9 @@ def __init__(self, parent): port=self.port, username=self.username, password=self.password, - use_tls_enabled=self.use_tls_enabled, + use_tls=self.use_tls, + connection_timeout=self.connection_timeout, + compressed_protocol=self.compressed_protocol, filename=self.filename, comments=self.comments, created_at=self.created_at, @@ -77,6 +81,7 @@ def __init__(self, parent): ssh_tunnel_identity_file=self.identity_file, ssh_tunnel_remote_hostname=self.remote_hostname, ssh_tunnel_remote_port=self.remote_port, + ssh_tunnel_extra_args=self.ssh_tunnel_extra_args, ) self.connections_model.engine.subscribe(self._on_change_engine) @@ -125,7 +130,9 @@ def _record_connection_attempt( ) if not connection.is_new: + expanded_paths = self._capture_expanded_directory_paths() self._repository.save_connection(connection) + wx.CallAfter(self._restore_expanded_directory_paths, expanded_paths) def _sync_statistics_to_model(self, connection: Connection) -> None: self.connections_model.created_at(connection.created_at or "") @@ -195,6 +202,10 @@ def _on_connection_activated(self, connection: Connection): def _on_change_engine(self, value: str): connection_engine = ConnectionEngine.from_name(value) + supports_compressed_protocol = connection_engine in [ + ConnectionEngine.MYSQL, + ConnectionEngine.MARIADB, + ] self.panel_credentials.Show( connection_engine @@ -214,6 +225,9 @@ def _on_change_engine(self, value: str): ) self.panel_source.Show(connection_engine == ConnectionEngine.SQLITE) + self.compressed_protocol.Enable(supports_compressed_protocol) + if not supports_compressed_protocol: + self.connections_model.compressed_protocol(False) self.panel_source.GetParent().Layout() @@ -281,19 +295,28 @@ def _save_expanded_directory_paths_to_settings(self) -> None: expanded_paths = self._capture_expanded_directory_paths() serialized_paths = self._serialize_expanded_directory_paths(expanded_paths) - if self._app.settings.get_value(self._SETTINGS_SECTION) is None: - self._app.settings.set_value(self._SETTINGS_SECTION, value={}) + self._app.settings.get_value( + self._SETTINGS_UI, + self._SETTINGS_DIALOGS, + self._SETTINGS_CONNECTIONS, + default={}, + ) self._app.settings.set_value( - self._SETTINGS_SECTION, + self._SETTINGS_UI, + self._SETTINGS_DIALOGS, + self._SETTINGS_CONNECTIONS, self._SETTINGS_EXPANDED_DIRECTORIES, value=serialized_paths, ) def _restore_expanded_directory_paths_from_settings(self) -> None: raw_paths = self._app.settings.get_value( - self._SETTINGS_SECTION, + self._SETTINGS_UI, + self._SETTINGS_DIALOGS, + self._SETTINGS_CONNECTIONS, self._SETTINGS_EXPANDED_DIRECTORIES, + default=[], ) expanded_paths = self._deserialize_expanded_directory_paths(raw_paths) self._restore_expanded_directory_paths(expanded_paths) @@ -368,13 +391,10 @@ def _walk(nodes, parent_path=()): _walk(self._repository.connections.get_value()) def do_open_session(self, session: Session): - # CONNECTIONS_LIST.append(connection) - SESSIONS_LIST.append(session) CURRENT_SESSION(session) if not self.GetParent(): - # CURRENT_CONNECTION(connection) self._app.open_main_frame() self.Hide() @@ -403,7 +423,9 @@ def on_save(self, *args): dialog = wx.MessageDialog( None, - message=_(f"Do you want save the connection {connection.name}?"), + message=_("Do you want save the connection {connection_name}?").format( + connection_name=connection.name + ), caption=_("Confirm save"), style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, ) @@ -704,65 +726,57 @@ def on_rename(self, event): def verify_session(self, session: Session): started_at = time.perf_counter() - with Loader.cursor_wait(): - try: - tls_was_enabled = bool( - getattr(session.connection.configuration, "use_tls_enabled", False) - ) + try: + tls_enabled = bool( + getattr(session.connection.configuration, "use_tls", False) + ) - logger.debug( - "Verifying session connection=%s engine=%s host=%s port=%s user=%s use_tls_enabled=%s", - session.connection.name, - session.connection.engine, - getattr(session.connection.configuration, "hostname", None), - getattr(session.connection.configuration, "port", None), - getattr(session.connection.configuration, "username", None), - tls_was_enabled, - ) - session.connect(connect_timeout=10) + connect_timeout = int( + getattr(session.connection.configuration, "connect_timeout", 10) + ) + session.connect(connect_timeout=connect_timeout) - tls_is_enabled = bool( - getattr(session.connection.configuration, "use_tls_enabled", False) - ) - if not tls_was_enabled and tls_is_enabled: - self.connections_model.use_tls_enabled(True) - - if not session.connection.is_new: - self._repository.save_connection(session.connection) - - wx.MessageDialog( - None, - message=_( - "This connection cannot work without TLS. TLS has been enabled automatically." - ), - caption=_("Connection"), - style=wx.OK | wx.ICON_INFORMATION, - ).ShowModal() - - duration_ms = int((time.perf_counter() - started_at) * 1000) - self._record_connection_attempt( - session.connection, - success=True, - duration_ms=duration_ms, - ) - self._sync_statistics_to_model(session.connection) - except Exception as ex: - duration_ms = int((time.perf_counter() - started_at) * 1000) - self._record_connection_attempt( - session.connection, - success=False, - duration_ms=duration_ms, - failure_reason=str(ex), - ) - self._sync_statistics_to_model(session.connection) + if not tls_enabled and bool( + getattr(session.connection.configuration, "use_tls", False) + ): + self.connections_model.use_tls(True) + + if not session.connection.is_new: + self._repository.save_connection(session.connection) wx.MessageDialog( None, - message=_(f"Connection error:\n{str(ex)}"), - caption=_("Connection error"), - style=wx.OK | wx.OK_DEFAULT | wx.ICON_ERROR, + message=_( + "This connection cannot work without TLS. TLS has been enabled automatically." + ), + caption=_("Connection"), + style=wx.OK | wx.ICON_INFORMATION, ).ShowModal() - raise ConnectionError(ex) + + duration_ms = int((time.perf_counter() - started_at) * 1000) + self._record_connection_attempt( + session.connection, + success=True, + duration_ms=duration_ms, + ) + self._sync_statistics_to_model(session.connection) + except Exception as ex: + duration_ms = int((time.perf_counter() - started_at) * 1000) + self._record_connection_attempt( + session.connection, + success=False, + duration_ms=duration_ms, + failure_reason=str(ex), + ) + self._sync_statistics_to_model(session.connection) + + wx.MessageDialog( + None, + message=_("Connection error:\n{error}").format(error=str(ex)), + caption=_("Connection error"), + style=wx.OK | wx.OK_DEFAULT | wx.ICON_ERROR, + ).ShowModal() + raise ConnectionError(ex) def on_connect(self, event): if PENDING_CONNECTION() and not self.on_save(event): @@ -772,19 +786,22 @@ def on_connect(self, event): session = Session(connection) - try: - self.verify_session(session) - except ConnectionError as ex: - logger.info(ex) - except Exception as ex: - logger.error(ex, exc_info=True) - else: - self.do_open_session(session) + with Loader.cursor_wait(): + try: + self.verify_session(session) + except ConnectionError as ex: + logger.info(ex) + except Exception as ex: + logger.error(ex, exc_info=True) + else: + self.do_open_session(session) def on_delete_connection(self, connection: Connection): dialog = wx.MessageDialog( None, - message=_(f"Do you want to delete the connection '{connection.name}'?"), + message=_("Do you want to delete the connection '{connection_name}'?").format( + connection_name=connection.name + ), caption=_("Confirm delete"), style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, ) @@ -799,7 +816,9 @@ def on_delete_connection(self, connection: Connection): def on_delete_directory(self, directory: ConnectionDirectory): dialog = wx.MessageDialog( None, - message=_(f"Do you want to delete the directory '{directory.name}'?"), + message=_("Do you want to delete the directory '{directory_name}'?").format( + directory_name=directory.name + ), caption=_("Confirm delete"), style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, ) diff --git a/windows/dialogs/settings/controller.py b/windows/dialogs/settings/controller.py index f608583..dda60a1 100644 --- a/windows/dialogs/settings/controller.py +++ b/windows/dialogs/settings/controller.py @@ -4,8 +4,8 @@ import wx.dataview from constants import Language, LogLevel +from helpers.settings import Settings -from windows.dialogs.settings.repository import Settings from windows.views import SettingsDialog @@ -36,16 +36,10 @@ def _populate_languages(self) -> None: def _populate_shortcuts(self) -> None: self.dialog.shortcuts_list.DeleteAllItems() - - shortcuts = self.settings.get_value("shortcuts") or {} - for action, shortcut_data in shortcuts.items(): - if isinstance(shortcut_data, dict): - shortcut = shortcut_data.get("key", "") - context = shortcut_data.get("context", "Global") - else: - shortcut = str(shortcut_data) - context = "Global" - + + shortcuts = self.settings.get_value("editor", "shortcuts", default={}) + for action, shortcut in shortcuts.items(): + context = "Global" self.dialog.shortcuts_list.AppendItem([action, shortcut, context]) def _populate_themes(self) -> None: @@ -71,57 +65,57 @@ def _load_settings(self) -> None: def _load_advanced_settings(self) -> None: self.dialog.advanced_connection_timeout.SetValue( - self.settings.get_value("advanced", "connection_timeout") or 60 + self.settings.get_value("runtime", "connection_timeout", default=60) ) self.dialog.advanced_query_timeout.SetValue( - self.settings.get_value("advanced", "query_timeout") or 60 + self.settings.get_value("runtime", "query_timeout", default=60) ) - + levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARNING, LogLevel.ERROR] - logging_level = self.settings.get_value("advanced", "logging_level") or "INFO" + logging_level = self.settings.get_value("runtime", "logging_level", default="INFO") try: selection = next(i for i, level in enumerate(levels) if level.value == logging_level) except StopIteration: selection = 1 self.dialog.advanced_logging_level.SetSelection(selection) - + def _load_appearance_settings(self) -> None: - current_theme = self.settings.get_value("appearance", "theme") or "" + current_theme = self.settings.get_value("ui", "appearance", "theme", default="") if current_theme: idx = self.dialog.theme.FindString(current_theme) if idx != wx.NOT_FOUND: self.dialog.theme.SetSelection(idx) - - appearance_mode = self.settings.get_value("appearance", "mode") or "auto" + + appearance_mode = self.settings.get_value("ui", "appearance", "mode", default="auto") if appearance_mode == "auto": self.dialog.appearance_mode_auto.SetValue(True) elif appearance_mode == "light": self.dialog.appearance_mode_light.SetValue(True) elif appearance_mode == "dark": self.dialog.appearance_mode_dark.SetValue(True) - + def _load_general_settings(self) -> None: language_map = {lang.code: idx for idx, lang in enumerate(Language)} - language = self.settings.get_value("language") or "en_US" + language = self.settings.get_value("language", default="en_US") self.dialog.language.SetSelection(language_map.get(language, 0)) - + def _load_query_editor_settings(self) -> None: self.dialog.query_editor_statement_separator.SetValue( - self.settings.get_value("query_editor", "statement_separator") or ";" + self.settings.get_value("editor", "statement_separator", default=";") ) self.dialog.query_editor_trim_whitespace.SetValue( - self.settings.get_value("query_editor", "trim_whitespace") or False + self.settings.get_value("editor", "trim_whitespace", default=False) ) self.dialog.query_editor_execute_selected_only.SetValue( - self.settings.get_value("query_editor", "execute_selected_only") or False + self.settings.get_value("editor", "execute_selected_only", default=False) ) - - autocomplete = self.settings.get_value("query_editor", "autocomplete") + + autocomplete = self.settings.get_value("editor", "autocomplete", "enabled", default=True) self.dialog.query_editor_autocomplete.SetValue( autocomplete if autocomplete is not None else True ) - - autoformat = self.settings.get_value("query_editor", "autoformat") + + autoformat = self.settings.get_value("editor", "autoformat", default=True) self.dialog.query_editor_format.SetValue( autoformat if autoformat is not None else True ) @@ -133,61 +127,59 @@ def _save_settings(self) -> None: self._save_advanced_settings() def _save_advanced_settings(self) -> None: - if not self.settings.get_value("advanced"): - self.settings.set_value("advanced", value={}) - - self.settings.set_value("advanced", "connection_timeout", + self.settings.get_value("runtime", default={}) + + self.settings.set_value("runtime", "connection_timeout", value=self.dialog.advanced_connection_timeout.GetValue() ) - self.settings.set_value("advanced", "query_timeout", + self.settings.set_value("runtime", "query_timeout", value=self.dialog.advanced_query_timeout.GetValue() ) - + levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARNING, LogLevel.ERROR] selection = self.dialog.advanced_logging_level.GetSelection() - self.settings.set_value("advanced", "logging_level", + self.settings.set_value("runtime", "logging_level", value=levels[selection].value if 0 <= selection < len(levels) else LogLevel.INFO.value ) - + def _save_appearance_settings(self) -> None: - if not self.settings.get_value("appearance"): - self.settings.set_value("appearance", value={}) - + self.settings.get_value("ui", "appearance", default={}) + theme_idx = self.dialog.theme.GetSelection() if theme_idx != wx.NOT_FOUND: - self.settings.set_value("appearance", "theme", value=self.dialog.theme.GetString(theme_idx)) - + self.settings.set_value("ui", "appearance", "theme", value=self.dialog.theme.GetString(theme_idx)) + if self.dialog.appearance_mode_auto.GetValue(): appearance_mode = "auto" elif self.dialog.appearance_mode_light.GetValue(): appearance_mode = "light" else: appearance_mode = "dark" - self.settings.set_value("appearance", "mode", value=appearance_mode) - + self.settings.set_value("ui", "appearance", "mode", value=appearance_mode) + def _save_general_settings(self) -> None: language_map = {idx: lang.code for idx, lang in enumerate(Language)} self.settings.set_value("language", value=language_map.get( self.dialog.language.GetSelection(), "en_US" )) - + def _save_query_editor_settings(self) -> None: - if not self.settings.get_value("query_editor"): - self.settings.set_value("query_editor", value={}) - - self.settings.set_value("query_editor", "statement_separator", + self.settings.get_value("editor", default={}) + self.settings.get_value("editor", "autocomplete", default={}) + + self.settings.set_value("editor", "statement_separator", value=self.dialog.query_editor_statement_separator.GetValue() ) - self.settings.set_value("query_editor", "trim_whitespace", + self.settings.set_value("editor", "trim_whitespace", value=self.dialog.query_editor_trim_whitespace.GetValue() ) - self.settings.set_value("query_editor", "execute_selected_only", + self.settings.set_value("editor", "execute_selected_only", value=self.dialog.query_editor_execute_selected_only.GetValue() ) - self.settings.set_value("query_editor", "autocomplete", + self.settings.set_value("editor", "autocomplete", "enabled", value=self.dialog.query_editor_autocomplete.GetValue() ) - self.settings.set_value("query_editor", "autoformat", + self.settings.set_value("editor", "autoformat", value=self.dialog.query_editor_format.GetValue() ) @@ -200,22 +192,17 @@ def _on_cancel(self, event: wx.Event) -> None: def _on_filter_shortcuts(self, event: wx.Event) -> None: filter_text = self.dialog.shortcuts_filter.GetValue().lower() - + self.dialog.shortcuts_list.DeleteAllItems() - - shortcuts = self.settings.get_value("shortcuts") or {} - - for action, shortcut_data in shortcuts.items(): - if isinstance(shortcut_data, dict): - shortcut = shortcut_data.get("key", "") - context = shortcut_data.get("context", "Global") - else: - shortcut = str(shortcut_data) - context = "Global" - - if (not filter_text or - filter_text in action.lower() or - filter_text in shortcut.lower() or + + shortcuts = self.settings.get_value("editor", "shortcuts", default={}) + + for action, shortcut in shortcuts.items(): + context = "Global" + + if (not filter_text or + filter_text in action.lower() or + filter_text in shortcut.lower() or filter_text in context.lower()): self.dialog.shortcuts_list.AppendItem([action, shortcut, context]) diff --git a/windows/dialogs/settings/repository.py b/windows/dialogs/settings/repository.py index b3ee7b1..b9d001f 100644 --- a/windows/dialogs/settings/repository.py +++ b/windows/dialogs/settings/repository.py @@ -1,28 +1 @@ -from pathlib import Path -from typing import Optional - -from helpers.observables import ObservableObject -from helpers.repository import YamlRepository - - -class SettingsRepository(YamlRepository[ObservableObject]): - def __init__(self, config_file: Path): - super().__init__(config_file) - self.settings: Optional[ObservableObject] = None - - def _write(self) -> None: - if self.settings is None: - return - - data = dict(self.settings.get_value()) - self._write_yaml(data) - - def load(self) -> ObservableObject: - data = self._read_yaml() - self.settings = ObservableObject(data) - self.settings.subscribe(lambda _: self._write()) - return self.settings - - -class Settings(SettingsRepository): - pass +from helpers.settings import Settings, SettingsRepository diff --git a/windows/main/controller.py b/windows/main/controller.py index 214169e..a3d17b5 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -1,11 +1,15 @@ +import contextlib +import dataclasses import math import os +import threading import time from collections import defaultdict from gettext import gettext as _ -from typing import Optional, Union +from typing import Any, Optional, Union +import babel.numbers import psutil import sqlglot import wx.adv @@ -13,6 +17,7 @@ import wx.stc from helpers import bytes_to_human +from helpers.loader import Loader from helpers.logger import logger from helpers.observables import CallbackEvent @@ -29,17 +34,21 @@ from windows.components.stc.template_menu import SQLTemplateMenuController from windows.main import CURRENT_CONNECTION, CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE, CURRENT_COLUMN, CURRENT_INDEX, CURRENT_FOREIGN_KEY, CURRENT_RECORDS, AUTO_APPLY, CURRENT_VIEW, CURRENT_TRIGGER -from windows.main.tabs.query import QueryResultsController -from windows.main.tabs.table import EditTableModel, NEW_TABLE -from windows.main.tabs.index import TableIndexController -from windows.main.tabs.check import TableCheckController -from windows.main.tabs.column import TableColumnsController -from windows.main.tabs.records import TableRecordsController -from windows.main.tabs.database import ListDatabaseTable -from windows.main.tabs.database_options import DatabaseOptionsController -from windows.main.tabs.explorer import TreeExplorerController -from windows.main.tabs.foreign_key import TableForeignKeyController -from windows.main.tabs.view import ViewEditorController + +from windows.main.explorer import TreeExplorerController + +from windows.main.database.list import ListDatabaseTable +from windows.main.database.view import ViewEditorController +from windows.main.database.options import DatabaseOptionsController + +from windows.main.table.check import TableCheckController +from windows.main.table.index import TableIndexController +from windows.main.table.column import TableColumnsController +from windows.main.table.records import TableRecordsController +from windows.main.table.options import EditTableModel, NEW_TABLE +from windows.main.table.foreign_key import TableForeignKeyController + +from windows.main.query.controller import QueryResultsController class MainFrameController(MainFrameView): @@ -49,6 +58,10 @@ def __init__(self): super().__init__(None) self.styled_text_ctrls_name = ["sql_query_logs", "stc_view_select", "sql_query_filters", "sql_create_table", "sql_query_editor"] + self._query_pages: list[wx.Panel] = [] + self._query_page_counter = 1 + self._query_page_meta: dict[wx.Panel, dict[str, Any]] = {} + self._query_shortcuts = self._load_query_shortcuts() self.edit_table_model = EditTableModel() self.edit_table_model.bind_controls( @@ -56,7 +69,9 @@ def __init__(self): comments=self.table_comment, auto_increment=self.table_auto_increment, collation=self.table_collation, + convert_data=self.convert_data_collation, engine=self.table_engine, + row_format=self.table_row_format, ) self.list_database_tables = ListDatabaseTable(self.list_ctrl_database_tables) @@ -72,12 +87,27 @@ def __init__(self): self.controller_list_table_check = TableCheckController(self.dv_table_checks) self.controller_list_table_foreign_key = TableForeignKeyController(self.dv_table_foreign_keys) - self.controller_query_records = QueryResultsController(self.sql_query_editor, self.notebook_sql_results) + self._setup_query_pages() self.controller_view_editor = ViewEditorController(self) + records_limit = self._load_records_limit_from_settings() + self.limit_records.SetValue(records_limit) + + self._records_offset = 0 + self._records_limit = records_limit + self._records_total_rows = 0 + self._records_total_key = None + self._records_total_request_id = 0 + self._records_total_is_loading = False + self._records_label_template = self.name_database_table.GetLabel() + + self.limit_records.Bind(wx.EVT_SPINCTRL, self.on_limit_records_changed) + self._setup_query_editors() + self._setup_database_action_buttons_bindings() + self._setup_subscribers() # Memory update timer @@ -87,6 +117,74 @@ def __init__(self): self.Bind(wx.EVT_SYS_COLOUR_CHANGED, self.on_sys_colour_changed) + def _setup_database_action_buttons_bindings(self) -> None: + model = self.controller_database_options.model + + database_observables = [ + model.database_name, + model.database_collation, + model.database_encryption, + model.database_read_only, + model.database_tablespace, + model.database_connection_limit, + model.database_password, + model.database_profile, + model.database_default_tablespace, + model.database_temporary_tablespace, + model.database_quota, + model.database_unlimited_quota, + model.database_account_status, + model.database_password_expire, + ] + + for observable in database_observables: + observable.subscribe(self._on_database_options_changed) + + CURRENT_SESSION.subscribe(self._on_database_options_changed) + + def _on_database_options_changed(self, _=None) -> None: + self._update_database_action_buttons() + + @staticmethod + def _database_has_changes(database: Optional[SQLDatabase]) -> bool: + if database is None: + return False + + if database.is_new: + return True + + session = CURRENT_SESSION.get_value() + if session is None: + return False + + original_database = next( + (db for db in session.context.databases.get_value() if db.id == database.id), + None, + ) + + if original_database is None: + return True + + for field in dataclasses.fields(database): + if not field.compare: + continue + + if getattr(database, field.name, None) != getattr(original_database, field.name, None): + return True + + return False + + def _update_database_action_buttons(self) -> None: + database = CURRENT_DATABASE.get_value() + + has_database = database is not None + has_changes = self._database_has_changes(database) + is_persisted = bool(database is not None and not database.is_new) + + self.btn_apply_database.Enable(has_database and has_changes) + self.btn_cancel_database.Enable(is_persisted and has_changes) + self.btn_delete_database.Enable(is_persisted) + def on_sys_colour_changed(self, event): self._setup_query_editors() @@ -94,32 +192,460 @@ def on_sys_colour_changed(self, event): event.Skip() def _setup_query_editors(self): + editors = set() + for styled_text_ctrl_name in self.styled_text_ctrls_name: styled_text_ctrl = getattr(self, styled_text_ctrl_name) + self._setup_sql_editor(styled_text_ctrl) + editors.add(styled_text_ctrl) - styled_text_ctrl.EmptyUndoBuffer() + for meta in self._query_page_meta.values(): + styled_text_ctrl = meta["editor"] + if styled_text_ctrl in editors: + continue - wx.GetApp().theme_manager.register(styled_text_ctrl, lambda: wx.GetApp().syntax_registry.get("sql")) + self._setup_sql_editor(styled_text_ctrl) - apply_stc_theme(styled_text_ctrl, SQL) + def _setup_sql_editor(self, styled_text_ctrl: wx.stc.StyledTextCtrl) -> None: + styled_text_ctrl.EmptyUndoBuffer() - sql_completion_provider = SQLCompletionProvider( - get_database=lambda: CURRENT_DATABASE.get_value(), - get_current_table=lambda: CURRENT_TABLE.get_value(), - ) + wx.GetApp().theme_manager.register(styled_text_ctrl, lambda: wx.GetApp().syntax_registry.get("sql")) - sql_autocomplete_controller = SQLAutoCompleteController( - editor=styled_text_ctrl, - provider=sql_completion_provider, - settings=wx.GetApp().settings, - theme_loader=wx.GetApp().theme_loader, - ) - - sql_template_menu = SQLTemplateMenuController( - editor=styled_text_ctrl, - get_database=lambda: CURRENT_DATABASE.get_value(), - get_current_table=lambda: CURRENT_TABLE.get_value(), - ) + apply_stc_theme(styled_text_ctrl, SQL) + + sql_completion_provider = SQLCompletionProvider( + get_database=lambda: CURRENT_DATABASE.get_value(), + get_current_table=lambda: CURRENT_TABLE.get_value(), + ) + + SQLAutoCompleteController( + editor=styled_text_ctrl, + provider=sql_completion_provider, + settings=wx.GetApp().settings, + theme_loader=wx.GetApp().theme_loader, + ) + + SQLTemplateMenuController( + editor=styled_text_ctrl, + get_database=lambda: CURRENT_DATABASE.get_value(), + get_current_table=lambda: CURRENT_TABLE.get_value(), + ) + + def _build_query_editor(self, parent: wx.Window) -> wx.stc.StyledTextCtrl: + editor = wx.stc.StyledTextCtrl(parent, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0) + editor.SetUseTabs(True) + editor.SetTabWidth(4) + editor.SetIndent(4) + editor.SetTabIndents(True) + editor.SetBackSpaceUnIndents(True) + editor.SetViewEOL(False) + editor.SetViewWhiteSpace(False) + editor.SetMarginWidth(2, 0) + editor.SetIndentationGuides(True) + editor.SetReadOnly(False) + editor.SetMarginType(1, wx.stc.STC_MARGIN_SYMBOL) + editor.SetMarginMask(1, wx.stc.STC_MASK_FOLDERS) + editor.SetMarginWidth(1, 16) + editor.SetMarginSensitive(1, True) + editor.SetProperty("fold", "1") + editor.SetFoldFlags(wx.stc.STC_FOLDFLAG_LINEBEFORE_CONTRACTED | wx.stc.STC_FOLDFLAG_LINEAFTER_CONTRACTED) + editor.SetMarginType(0, wx.stc.STC_MARGIN_NUMBER) + editor.SetMarginWidth(0, editor.TextWidth(wx.stc.STC_STYLE_LINENUMBER, "_99999")) + editor.SetSelBackground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)) + editor.SetSelForeground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT)) + return editor + + def _load_query_shortcuts(self) -> dict[str, str]: + settings = wx.GetApp().settings + return { + "execute_current": settings.get_value("ui", "shortcuts", "query", "execute_current", default="Ctrl+Enter"), + "execute_all": settings.get_value("ui", "shortcuts", "query", "execute_all", default="Ctrl+Shift+Enter"), + "stop": settings.get_value("ui", "shortcuts", "query", "stop", default="Esc"), + "new_query": settings.get_value("ui", "shortcuts", "query", "new_query", default="Ctrl+T"), + "close_query": settings.get_value("ui", "shortcuts", "query", "close_query", default="Ctrl+W"), + "save": settings.get_value("ui", "shortcuts", "query", "save", default="Ctrl+S"), + "save_as": settings.get_value("ui", "shortcuts", "query", "save_as", default="Ctrl+Shift+S"), + } + + def _with_shortcut(self, text: str, shortcut_key: str) -> str: + shortcut = self._query_shortcuts.get(shortcut_key) + if not shortcut: + return text + + return _("{text} ({shortcut})").format(text=text, shortcut=shortcut) + + def _apply_query_toolbar_shortcuts(self, toolbar: wx.ToolBar, tool_ids: dict[str, int]) -> None: + toolbar.SetToolShortHelp(tool_ids["new"], self._with_shortcut(_("New query"), "new_query")) + toolbar.SetToolShortHelp(tool_ids["close"], self._with_shortcut(_("Close query"), "close_query")) + toolbar.SetToolShortHelp(tool_ids["execute"], self._with_shortcut(_("Execute"), "execute_current")) + toolbar.SetToolShortHelp(tool_ids["execute_all"], self._with_shortcut(_("Execute all"), "execute_all")) + toolbar.SetToolShortHelp(tool_ids["stop"], self._with_shortcut(_("Stop"), "stop")) + toolbar.SetToolShortHelp(tool_ids["save"], self._with_shortcut(_("Save"), "save")) + + def _build_query_toolbar(self, parent: wx.Window) -> tuple[wx.ToolBar, dict[str, int]]: + toolbar = wx.ToolBar(parent, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL) + new_query = toolbar.AddTool(wx.ID_ANY, _("New query"), wx.Bitmap("icons/16x16/add.png", wx.BITMAP_TYPE_ANY), + wx.NullBitmap, wx.ITEM_NORMAL, _("New query"), wx.EmptyString, None) + close_query = toolbar.AddTool(wx.ID_ANY, _("Close query"), wx.Bitmap("icons/16x16/delete.png", wx.BITMAP_TYPE_ANY), + wx.NullBitmap, wx.ITEM_NORMAL, _("Close query"), wx.EmptyString, None) + toolbar.AddSeparator() + execute_statement = toolbar.AddTool(wx.ID_ANY, _("Execute"), wx.Bitmap("icons/16x16/arrow_right.png", wx.BITMAP_TYPE_ANY), + wx.NullBitmap, wx.ITEM_NORMAL, _("Execute"), wx.EmptyString, None) + execute_all = toolbar.AddTool(wx.ID_ANY, _("Execute all"), wx.Bitmap("icons/16x16/arrows_lefttoright.png", wx.BITMAP_TYPE_ANY), + wx.NullBitmap, wx.ITEM_NORMAL, _("Execute all statements"), wx.EmptyString, None) + toolbar.AddSeparator() + stop_statements = toolbar.AddTool(wx.ID_ANY, _("Stop"), wx.Bitmap("icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY), + wx.NullBitmap, wx.ITEM_NORMAL, _("Stop"), wx.EmptyString, None) + toolbar.AddSeparator() + save_query = toolbar.AddTool(wx.ID_ANY, _("Save"), wx.Bitmap("icons/16x16/disk.png", wx.BITMAP_TYPE_ANY), + wx.NullBitmap, wx.ITEM_NORMAL, _("Save"), wx.EmptyString, None) + toolbar.Realize() + + tool_ids = { + "new": new_query.GetId(), + "close": close_query.GetId(), + "execute": execute_statement.GetId(), + "execute_all": execute_all.GetId(), + "stop": stop_statements.GetId(), + "save": save_query.GetId(), + } + + self._apply_query_toolbar_shortcuts(toolbar, tool_ids) + + toolbar.Bind(wx.EVT_TOOL, self.on_new_query, id=new_query.GetId()) + toolbar.Bind(wx.EVT_TOOL, self.on_close_query, id=close_query.GetId()) + toolbar.Bind(wx.EVT_TOOL, self.on_execute_statement, id=execute_statement.GetId()) + toolbar.Bind(wx.EVT_TOOL, self.on_execute_statements, id=execute_all.GetId()) + toolbar.Bind(wx.EVT_TOOL, self.on_stop_statements, id=stop_statements.GetId()) + toolbar.Bind(wx.EVT_TOOL, self.on_save, id=save_query.GetId()) + return toolbar, tool_ids + + def _build_query_page(self) -> tuple[wx.Panel, wx.stc.StyledTextCtrl, wx.Window, wx.ToolBar, dict[str, int]]: + panel_query = wx.Panel(self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL) + query_sizer = wx.BoxSizer(wx.VERTICAL) + splitter = wx.SplitterWindow(panel_query, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_3D) + + panel_top = wx.Panel(splitter, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL) + top_sizer = wx.BoxSizer(wx.VERTICAL) + toolbar, tool_ids = self._build_query_toolbar(panel_top) + editor = self._build_query_editor(panel_top) + top_sizer.Add(toolbar, 0, wx.EXPAND, 5) + top_sizer.Add(editor, 1, wx.EXPAND | wx.ALL, 5) + panel_top.SetSizer(top_sizer) + + panel_bottom = wx.Panel(splitter, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL) + bottom_sizer = wx.BoxSizer(wx.VERTICAL) + results_notebook_class = self.notebook_sql_results.__class__ + results_notebook = results_notebook_class(panel_bottom, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0) + bottom_sizer.Add(results_notebook, 1, wx.EXPAND | wx.ALL, 5) + panel_bottom.SetSizer(bottom_sizer) + + splitter.SplitHorizontally(panel_top, panel_bottom, -300) + query_sizer.Add(splitter, 1, wx.EXPAND, 5) + panel_query.SetSizer(query_sizer) + return panel_query, editor, results_notebook, toolbar, tool_ids + + def _get_active_query_controller(self) -> Optional[QueryResultsController]: + page = self.notebook_query_editor.GetCurrentPage() + if page is None: + return None + + meta = self._query_page_meta.get(page) + if meta is None: + return None + + return meta["controller"] + + def _register_query_page( + self, + panel: wx.Panel, + editor: wx.stc.StyledTextCtrl, + results_notebook: wx.Window, + toolbar: wx.ToolBar, + tool_ids: dict[str, int], + display_name: str, + ) -> None: + controller = QueryResultsController( + editor, + results_notebook, + cancel_button=None, + on_new_query=self.on_new_query, + on_close_query=self.on_close_query, + on_save_query=self.on_save, + on_save_as_query=self.on_save_as_query, + on_stop_state_changed=lambda enabled: self._set_query_stop_enabled(panel, enabled), + on_before_execute=lambda: self._autosave_query_page_before_execute(panel), + ) + self._query_pages.append(panel) + self._query_page_meta[panel] = { + "editor": editor, + "toolbar": toolbar, + "controller": controller, + "tool_ids": tool_ids, + "file_path": None, + "is_dirty": False, + "display_name": display_name, + } + self._bind_query_editor_events(panel, editor) + self._set_query_stop_enabled(panel, enabled=False) + self._set_query_save_enabled(panel, enabled=False) + + def _set_query_save_enabled(self, page: wx.Panel, enabled: bool) -> None: + meta = self._query_page_meta.get(page) + if meta is None: + return + + toolbar = meta["toolbar"] + tool_ids = meta["tool_ids"] + toolbar.EnableTool(tool_ids["save"], enabled) + + def _set_query_stop_enabled(self, page: wx.Panel, enabled: bool) -> None: + meta = self._query_page_meta.get(page) + if meta is None: + return + + toolbar = meta["toolbar"] + tool_ids = meta["tool_ids"] + toolbar.EnableTool(tool_ids["stop"], enabled) + toolbar.EnableTool(tool_ids["execute"], not enabled) + toolbar.EnableTool(tool_ids["execute_all"], not enabled) + + def _bind_query_editor_events(self, page: wx.Panel, editor: wx.stc.StyledTextCtrl) -> None: + editor.Bind(wx.stc.EVT_STC_CHANGE, lambda event: self._on_query_editor_changed(page, event)) + + def _on_query_editor_changed(self, page: wx.Panel, event: wx.Event) -> None: + self._set_query_dirty(page, is_dirty=True) + event.Skip() + + def _set_query_dirty(self, page: wx.Panel, is_dirty: bool) -> None: + meta = self._query_page_meta.get(page) + if meta is None: + return + + if meta["is_dirty"] == is_dirty: + return + + meta["is_dirty"] = is_dirty + self._update_query_page_title(page) + self._set_query_save_enabled(page, enabled=is_dirty) + + def _update_query_page_title(self, page: wx.Panel) -> None: + meta = self._query_page_meta.get(page) + if meta is None: + return + + page_index = self.notebook_query_editor.FindPage(page) + if page_index < 0: + return + + title = meta["display_name"] + if meta["is_dirty"]: + title = f"{title} *" + + self.notebook_query_editor.SetPageText(page_index, title) + + def _build_query_editor_panel(self) -> tuple[wx.Panel, wx.stc.StyledTextCtrl]: + panel = wx.Panel(self.notebook_query_editor, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL) + sizer = wx.BoxSizer(wx.VERTICAL) + editor = self._build_query_editor(panel) + sizer.Add(editor, 1, wx.EXPAND | wx.ALL, 5) + panel.SetSizer(sizer) + return panel, editor + + def _on_notebook_query_tab_changed(self, event: wx.BookCtrlEvent) -> None: + controller = self._get_active_query_controller() + if controller is not None: + self.controller_query_records = controller + event.Skip() + + def _setup_query_pages(self) -> None: + shared_tool_ids = { + "new": self.new_query.GetId(), + "close": self.close_query.GetId(), + "execute": self.execute_statement.GetId(), + "execute_all": self.execute_all_statements.GetId(), + "stop": self.stop_statements.GetId(), + "save": self.save.GetId(), + } + + self.notebook_query_editor.SetPageText(0, _("Query (1)")) + + self._register_query_page( + panel=self.m_panel63, + editor=self.sql_query_editor, + results_notebook=self.notebook_query_results, + toolbar=self.m_toolBar2, + tool_ids=shared_tool_ids, + display_name=_("Query (1)"), + ) + + self._apply_query_toolbar_shortcuts(self.m_toolBar2, shared_tool_ids) + + self.controller_query_records = self._query_page_meta[self.m_panel63]["controller"] + self.notebook_query_editor.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._on_notebook_query_tab_changed) + self._update_query_close_tools_state() + + def _update_query_close_tools_state(self) -> None: + can_close = self.notebook_query_editor.GetPageCount() > 1 + for meta in self._query_page_meta.values(): + toolbar = meta["toolbar"] + close_query_tool_id = meta["tool_ids"]["close"] + toolbar.EnableTool(close_query_tool_id, can_close) + + def _create_new_query_page(self) -> None: + self._query_page_counter += 1 + label = _("Query ({query_number})").format(query_number=self._query_page_counter) + + panel, editor = self._build_query_editor_panel() + self.notebook_query_editor.AddPage(panel, label, select=True) + + shared_tool_ids = { + "new": self.new_query.GetId(), + "close": self.close_query.GetId(), + "execute": self.execute_statement.GetId(), + "execute_all": self.execute_all_statements.GetId(), + "stop": self.stop_statements.GetId(), + "save": self.save.GetId(), + } + + self._register_query_page( + panel=panel, + editor=editor, + results_notebook=self.notebook_query_results, + toolbar=self.m_toolBar2, + tool_ids=shared_tool_ids, + display_name=label, + ) + + self._setup_sql_editor(editor) + self._update_query_close_tools_state() + + def _confirm_close_query_page(self, page: wx.Panel) -> bool: + meta = self._query_page_meta.get(page) + if meta is None or not meta["is_dirty"]: + return True + + result = wx.MessageDialog( + None, + message=_("You have unsaved changes. Save before closing?"), + caption=_("Unsaved query"), + style=wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION, + ).ShowModal() + + if result == wx.ID_YES: + return self._save_query_page(page, force_save_as=False) + + if result == wx.ID_NO: + return True + + return False + + def _close_active_query_page(self) -> None: + if self.notebook_query_editor.GetPageCount() <= 1: + return + + page = self.notebook_query_editor.GetCurrentPage() + if page is None or page not in self._query_page_meta: + return + + if not self._confirm_close_query_page(page): + return + + meta = self._query_page_meta.pop(page) + self._query_pages.remove(page) + + controller = meta["controller"] + controller.cancel_execution(wx.CommandEvent()) + + query_page_index = self.notebook_query_editor.FindPage(page) + if query_page_index >= 0: + self.notebook_query_editor.DeletePage(query_page_index) + + self._update_query_close_tools_state() + + active_controller = self._get_active_query_controller() + if active_controller is not None: + self.controller_query_records = active_controller + + def _ask_query_save_path(self, file_path: Optional[str] = None) -> Optional[str]: + default_dir = os.path.dirname(file_path) if file_path else os.getcwd() + default_name = os.path.basename(file_path) if file_path else "query.sql" + + dialog = wx.FileDialog( + self, + message=_("Save query"), + defaultDir=default_dir, + defaultFile=default_name, + wildcard=_("SQL files (*.sql)|*.sql|All files (*.*)|*.*"), + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, + ) + + if dialog.ShowModal() != wx.ID_OK: + return None + + return dialog.GetPath() + + @staticmethod + def _write_query_file(file_path: str, content: str) -> None: + with open(file_path, "w", encoding="utf-8") as file_obj: + file_obj.write(content) + + @staticmethod + def _get_query_autosave_path() -> str: + query_dir = os.path.join(os.getcwd(), ".queries") + os.makedirs(query_dir, exist_ok=True) + return os.path.join(query_dir, f"query_{time.strftime('%Y%m%d_%H%M%S')}_{time.time_ns()}.sql") + + def _save_query_page(self, page: wx.Panel, force_save_as: bool) -> bool: + meta = self._query_page_meta.get(page) + if meta is None: + return False + + file_path = meta["file_path"] + if force_save_as or not file_path: + file_path = self._ask_query_save_path(file_path) + if not file_path: + return False + + editor = meta["editor"] + + try: + self._write_query_file(file_path, editor.GetText()) + except Exception as ex: + logger.error(str(ex), exc_info=True) + wx.MessageDialog(None, str(ex), _("Error"), wx.OK | wx.ICON_ERROR).ShowModal() + return False + + meta["file_path"] = file_path + meta["display_name"] = os.path.basename(file_path) + self._set_query_dirty(page, is_dirty=False) + QUERY_LOGS.append(_("-- Saved query to {file_path}").format(file_path=file_path)) + return True + + def _autosave_query_page_before_execute(self, page: wx.Panel) -> bool: + meta = self._query_page_meta.get(page) + if meta is None: + return False + + if meta["file_path"] is not None: + return True + + editor = meta["editor"] + if not editor.GetText().strip(): + return True + + file_path = self._get_query_autosave_path() + try: + self._write_query_file(file_path, editor.GetText()) + except Exception as ex: + logger.error(str(ex), exc_info=True) + wx.MessageDialog(None, str(ex), _("Error"), wx.OK | wx.ICON_ERROR).ShowModal() + return False + + meta["file_path"] = file_path + self._set_query_dirty(page, is_dirty=False) + QUERY_LOGS.append(_("-- Autosaved query to {file_path}").format(file_path=file_path)) + return True def _setup_subscribers(self): self.toggle_panel() @@ -152,6 +678,12 @@ def _setup_subscribers(self): AUTO_APPLY.subscribe(self._on_auto_apply) + # Initialize record toolbar states + self._initialize_record_toolbar_states() + + # Initialize column toolbar states + self._initialize_column_toolbar_states() + def _write_query_log(self, text: str): self.sql_query_logs.AppendText(f"{text}\n") self.sql_query_logs.GotoLine(self.sql_query_logs.GetLineCount() - 1) @@ -280,12 +812,314 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S if self.MainFrameNotebook.GetSelection() < 4: self.MainFrameNotebook.SetSelection(3) + def _get_records_filters(self) -> str: + return (self.sql_query_filters.GetSelectedText() or self.sql_query_filters.GetText()).strip() + + def _build_records_total_key(self, table: SQLTable, filters: str) -> tuple[str, str, str, str]: + schema = str(getattr(table, "schema", "") or "") + return table.database.name, schema, table.name, filters + + def _count_table_records( + self, + table: SQLTable, + filters: str, + context: Optional[Any] = None, + ) -> int: + if context is None: + context = table.database.context + + where = f" WHERE {filters}" if filters else "" + schema = str(getattr(table, "schema", "") or "") + + if schema: + from_clause = context.qualify(schema, table.name) + else: + from_clause = context.qualify(table.database.name, table.name) + + query = f"SELECT COUNT(*) AS total_rows FROM {from_clause}{where}" + context.execute(query) + + row = context.fetchone() or {} + total_rows = None + try: + total_rows = dict(row).get("total_rows") + except Exception as ex: + logger.error(ex) + if total_rows is None and row: + try: + total_rows = next(iter(row.values()), 0) + except Exception: + total_rows = 0 + + return int(total_rows or 0) + + def _count_table_records_worker( + self, + session: Session, + table: SQLTable, + filters: str, + total_key: tuple[str, str, str, str], + request_id: int, + ) -> None: + total_rows = 0 + error = None + context = None + + try: + context = session._get_context_class()(self._build_records_count_connection(session)) + context.connect(skip_before_connect=True, skip_after_connect=True) + context.set_database(table.database) + total_rows = self._count_table_records(table, filters, context) + except Exception as ex: + error = str(ex) + logger.warning("Failed async records count: %s", ex, exc_info=True) + finally: + if context is not None: + try: + context.disconnect() + except Exception: + pass + + wx.CallAfter( + self._on_records_count_complete, + total_key, + request_id, + total_rows, + error, + ) + + def _build_records_count_connection(self, session: Session) -> Connection: + connection = session.connection.copy() + + if not connection.has_enabled_tunnel(): + return connection + + context = getattr(session, "context", None) + configuration = getattr(connection, "configuration", None) + + if context is not None and configuration is not None and hasattr(configuration, "_replace"): + replace_kwargs = {} + + if hasattr(configuration, "hostname") and getattr(context, "host", None): + replace_kwargs["hostname"] = context.host + + if hasattr(configuration, "port") and getattr(context, "port", None) is not None: + replace_kwargs["port"] = int(context.port) + + if replace_kwargs: + connection.configuration = configuration._replace(**replace_kwargs) + + connection.ssh_tunnel = None + return connection + + def _format_records_number(self, value: int) -> str: + locale = wx.GetApp().settings.get_value("language", default="en_US") + try: + return babel.numbers.format_decimal(value, locale=locale) + except Exception: + return str(value) + + def _load_records_limit_from_settings(self) -> int: + settings = wx.GetApp().settings + + max_limit = 1000 + if hasattr(self, "limit_records"): + with contextlib.suppress(Exception): + max_limit = max(1, int(self.limit_records.GetMax())) + + saved_limit = settings.get_value("records", "limit", default=100) + + try: + return min(max(1, int(saved_limit)), max_limit) + except Exception: + return min(100, max_limit) + + def _can_skip_count_query(self, table: SQLTable, filters: str) -> bool: + if filters: + return False + + if table.total_rows is None: + return False + + return int(table.total_rows) <= self._records_limit + + def _get_loaded_records_count(self, table: SQLTable) -> int: + records = getattr(table, "records", None) + if records is None: + return 0 + + if getattr(records, "is_loaded", False): + return len(records) + + return 0 + + def _get_loading_total_text(self, table: SQLTable, filters: str) -> str: + if not filters and table.total_rows is not None: + estimated = self._format_records_number(int(table.total_rows)) + return _("~{estimated} (Loading...)").format(estimated=estimated) + + return _("~ (Loading...)") + + def _refresh_records_total_rows(self, table: SQLTable, filters: str) -> None: + total_key = self._build_records_total_key(table, filters) + if self._records_total_key == total_key: + return + + self._records_total_key = total_key + self._records_total_request_id += 1 + + if self._can_skip_count_query(table, filters): + self._records_total_is_loading = False + self._records_total_rows = max(int(table.total_rows or 0), 0) + self._update_records_label(table) + self._set_records_paging_buttons(table) + return + + self._records_total_is_loading = True + + self._update_records_label(table) + self._set_records_paging_buttons(table) + + session = CURRENT_SESSION.get_value() + if session is None: + self._records_total_is_loading = False + self._update_records_label(table) + self._set_records_paging_buttons(table) + return + + worker = threading.Thread( + target=self._count_table_records_worker, + args=( + session, + table, + filters, + total_key, + self._records_total_request_id, + ), + daemon=True, + ) + worker.start() + + def _on_records_count_complete( + self, + total_key: tuple[str, str, str, str], + request_id: int, + total_rows: int, + error: Optional[str], + ) -> None: + if request_id != self._records_total_request_id: + return + + self._records_total_is_loading = False + + if error: + table = CURRENT_TABLE.get_value() + if table is not None: + self._update_records_label(table) + self._set_records_paging_buttons(table) + return + + table = CURRENT_TABLE.get_value() + if table is None: + return + + filters = self._get_records_filters() + if self._build_records_total_key(table, filters) != total_key: + return + + self._records_total_rows = max(int(total_rows), 0) + last_offset = self._get_records_last_offset(self._records_limit) + + if self._records_offset > last_offset: + self._records_offset = last_offset + try: + self._load_records_page() + except Exception as ex: + logger.error(f"Error reloading records page after count: {ex}", exc_info=True) + return + + try: + self._update_records_label(table) + self._set_records_paging_buttons(table) + except Exception as ex: + logger.error(f"Error updating records label: {ex}", exc_info=True) + + def _get_records_last_offset(self, limit: int) -> int: + total_rows = int(self._records_total_rows or 0) + if total_rows <= 0: + return 0 + + return ((total_rows - 1) // limit) * limit + + def _load_records_page(self): + table = CURRENT_TABLE.get_value() + if table is None: + return + + limit = max(1, self.limit_records.GetValue()) + self._records_limit = limit + + filters = self._get_records_filters() + self._refresh_records_total_rows(table, filters) + + last_offset = self._get_records_last_offset(limit) + + self._records_offset = min(max(self._records_offset, 0), last_offset) + + with Loader.cursor_wait(): + table.load_records(filters=filters, limit=limit, offset=self._records_offset) + self.controller_list_table_records.load_model() + + self._update_records_label(table) + self._set_records_paging_buttons(table) + + def _update_records_label(self, table: SQLTable): + rows_count = self._get_loaded_records_count(table) + from_row = 0 if rows_count == 0 else self._records_offset + 1 + to_row = 0 if rows_count == 0 else self._records_offset + rows_count + + if self._records_total_is_loading: + total_rows_text = self._get_loading_total_text(table, self._get_records_filters()) + else: + total_rows_text = self._format_records_number(int(self._records_total_rows or 0)) + + self.name_database_table.SetLabel( + self._records_label_template.format( + database_name=table.database.name, + table_name=table.name, + total_rows=total_rows_text, + from_row=self._format_records_number(from_row), + to_row=self._format_records_number(to_row), + ) + ) + + def _set_records_paging_buttons(self, table: SQLTable): + if self._records_total_is_loading: + rows_count = self._get_loaded_records_count(table) + at_first_page = self._records_offset <= 0 + has_next_page = rows_count >= self._records_limit + + self.btn_first_records.Enable(not at_first_page) + self.btn_prev_records.Enable(not at_first_page) + self.btn_next_records.Enable(has_next_page) + self.btn_last_records.Enable(False) + return + + total_rows = int(self._records_total_rows or 0) + at_first_page = self._records_offset <= 0 + at_last_page = self._records_offset >= self._get_records_last_offset(self._records_limit) + has_rows = total_rows > 0 + + self.btn_first_records.Enable(has_rows and not at_first_page) + self.btn_prev_records.Enable(has_rows and not at_first_page) + self.btn_next_records.Enable(has_rows and not at_last_page) + self.btn_last_records.Enable(has_rows and not at_last_page) + def on_page_chaged(self, event): if int(event.Selection) == 5: if table := CURRENT_TABLE.get_value(): - table.load_records() - - # self.controller_list_table_records.load_model() + self._records_offset = 0 + self._load_records_page() def _on_current_session(self, session: Session): from structures.session import Session @@ -295,7 +1129,7 @@ def _on_current_session(self, session: Session): if session: wx.CallAfter(self.status_bar.SetStatusText, f"{_('Connection')}: {session.name}", 0) - wx.CallAfter(self.status_bar.SetStatusText, f"{_('Version')}: {session.context.get_server_version()}", 1) + wx.CallAfter(self.status_bar.SetStatusText, f"{_('Version')}: {session.context.server_version}", 1) wx.CallAfter(self.status_bar.SetStatusText, f"{_('Uptime')}: {self._format_server_uptime(session.context.get_server_uptime())}", 2) @@ -322,6 +1156,8 @@ def _on_current_session(self, session: Session): def _on_current_database(self, database: SQLDatabase): self.toggle_panel(database) + self._update_database_action_buttons() + if database: self.table_engine.Enable(len(database.context.ENGINES) > 1) self.table_engine.SetItems(database.context.ENGINES) @@ -329,8 +1165,16 @@ def _on_current_database(self, database: SQLDatabase): self.table_collation.Enable(len(database.context.COLLATIONS.keys()) > 1) self.table_collation.SetItems(list(database.context.COLLATIONS.keys())) + row_formats = database.context.ROW_FORMATS + self.table_row_format.Enable(bool(row_formats)) + self.table_row_format.SetItems(row_formats) + + self.convert_data_collation.Enable(bool(database.context.COLLATIONS)) + if (session := CURRENT_SESSION.get_value()) and session.engine in [ConnectionEngine.SQLITE]: self.table_collation.Enable(False) + self.convert_data_collation.Enable(False) + self.table_row_format.Enable(False) def on_apply_database(self, event: wx.Event): database = CURRENT_DATABASE.get_value() @@ -364,9 +1208,11 @@ def on_cancel_database(self, event: wx.Event): return if wx.MessageDialog( - None, - message=_(f"Do you want discard the change to {database.name}?"), - style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, + None, + message=_("Do you want discard the change to {database_name}?").format( + database_name=database.name + ), + style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, ).ShowModal() != wx.ID_YES: return @@ -461,16 +1307,15 @@ def _on_current_table(self, table: SQLTable): return if table: - # `%(database_name)s`.`%(table_name)s` - self.name_database_table.SetLabel( - self.name_database_table.GetLabel() % { - "database_name": table.database.name, - "table_name": table.name, - "total_rows": table.total_rows - } - ) + self._records_offset = 0 + self._records_limit = max(1, self.limit_records.GetValue()) + self._records_total_rows = 0 + self._records_total_key = None + self._records_total_is_loading = False + self._update_records_label(table) self.toggle_panel(table) + self._set_records_paging_buttons(table) CURRENT_COLUMN.set_value(None) CURRENT_RECORDS.set_value([]) @@ -486,6 +1331,9 @@ def _on_current_table(self, table: SQLTable): table.raw_create() ) + if self.MainFrameNotebook.GetSelection() == 5: + self._load_records_page() + self.btn_clone_table.Enable(table is not None) self.btn_delete_table.Enable(table is not None) @@ -554,7 +1402,9 @@ def do_apply_table(self, event: wx.Event): def on_cancel_table(self, event: wx.Event): if new_table := NEW_TABLE.get_value(): if wx.MessageDialog(None, - message=_(f'Do you want discard the change to {new_table.name}?'), + message=_("Do you want discard the change to {table_name}?").format( + table_name=new_table.name + ), style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION ).ShowModal() == wx.ID_YES: return self.do_cancel_table(event) @@ -578,7 +1428,9 @@ def on_delete_table(self, event): table = CURRENT_TABLE.get_value() dialog = wx.MessageDialog(None, - message=_(f'Do you want delete the table {table.name}?'), + message=_("Do you want delete the table {table_name}?").format( + table_name=table.name + ), caption=_("Delete table"), style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION ) @@ -598,7 +1450,7 @@ def on_clone_table(self, event): if table: new_table = table.copy() new_table.id = -1 - new_table.name = _(f"{new_table.name} (COPY)") + new_table.name = _("{table_name} (COPY)").format(table_name=new_table.name) for column in new_table.columns: column.id = -1 @@ -625,17 +1477,17 @@ def on_clone_table(self, event): def _on_current_column(self, column: SQLColumn): selected = self.controller_list_table_columns.list_ctrl_table_columns.GetSelection() if not selected.IsOk(): - self.btn_delete_column.Enable(False) - self.btn_move_up_column.Enable(False) - self.btn_move_down_column.Enable(False) + self.toolbar_columns.EnableTool(self.tool_remove_column.GetId(), False) + self.toolbar_columns.EnableTool(self.tool_move_up_column.GetId(), False) + self.toolbar_columns.EnableTool(self.tool_move_down_column.GetId(), False) return row = self.controller_list_table_columns.model.GetRow(selected) total_rows = len(self.controller_list_table_columns.model.data) - 1 - self.btn_delete_column.Enable(column is not None) - self.btn_move_up_column.Enable(column is not None and row > 0) - self.btn_move_down_column.Enable(column is not None and row < total_rows) + self.toolbar_columns.EnableTool(self.tool_remove_column.GetId(), column is not None) + self.toolbar_columns.EnableTool(self.tool_move_up_column.GetId(), column is not None and row > 0) + self.toolbar_columns.EnableTool(self.tool_move_down_column.GetId(), column is not None and row < total_rows) def on_insert_column(self, event: wx.Event): self.controller_list_table_columns.on_column_insert(event) @@ -674,8 +1526,29 @@ def on_clear_foreign_key(self, event: wx.Event): # RECORDS def _on_auto_apply(self, value: bool): - self.btn_cancel_record.Enable(not self.chb_auto_apply.GetValue()) - self.btn_apply_record.Enable(not self.chb_auto_apply.GetValue()) + auto_apply_enabled = self.chb_auto_apply.GetValue() + + # Enable/disable apply and cancel tools based on auto-apply state + self.m_toolBar3.EnableTool(self.tool_apply_record.GetId(), not auto_apply_enabled) + self.m_toolBar3.EnableTool(self.tool_cancel_record.GetId(), not auto_apply_enabled) + + def _initialize_record_toolbar_states(self): + """Initialize toolbar states to ensure proper default behavior.""" + # Initially disable duplicate and delete tools (no selection) + self.m_toolBar3.EnableTool(self.tool_duplicate_record.GetId(), False) + self.m_toolBar3.EnableTool(self.tool_delete_record.GetId(), False) + + # Set apply/cancel tools based on auto-apply checkbox state + auto_apply_enabled = self.chb_auto_apply.GetValue() + self.m_toolBar3.EnableTool(self.tool_apply_record.GetId(), not auto_apply_enabled) + self.m_toolBar3.EnableTool(self.tool_cancel_record.GetId(), not auto_apply_enabled) + + def _initialize_column_toolbar_states(self): + """Initialize column toolbar states to ensure proper default behavior.""" + # Initially disable all column tools (no selection) + self.toolbar_columns.EnableTool(self.tool_remove_column.GetId(), False) + self.toolbar_columns.EnableTool(self.tool_move_up_column.GetId(), False) + self.toolbar_columns.EnableTool(self.tool_move_down_column.GetId(), False) def on_auto_apply(self, event): AUTO_APPLY.set_value(self.chb_auto_apply.GetValue()) @@ -685,32 +1558,116 @@ def on_collapsible_pane_changed(self, event): event.Skip() def _on_current_records(self, records: list[SQLRecord]): - self.btn_duplicate_record.Enable(len(records) == 1) - self.btn_delete_record.Enable(len(records) > 0) + # Enable/disable duplicate and delete tools based on record selection + self.m_toolBar3.EnableTool(self.tool_duplicate_record.GetId(), len(records) == 1) + self.m_toolBar3.EnableTool(self.tool_delete_record.GetId(), len(records) > 0) + + def on_apply_record(self, event): + self.controller_list_table_records.do_apply_records() + + def on_cancel_record(self, event): + self.controller_list_table_records.do_cancel_records() def on_insert_record(self, event): self.controller_list_table_records.do_insert_record() + def on_refresh_records(self, event): + self.controller_list_table_records.do_refresh_records() + def on_duplicate_record(self, event): self.controller_list_table_records.do_duplicate_record() def on_delete_record(self, event): dialog = wx.MessageDialog(None, - message=_(f'Do you want delete the records?'), + message=_("Do you want delete the records?"), style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION ) if dialog.ShowModal() == wx.ID_YES: self.controller_list_table_records.do_delete_record() - def on_apply_filters(self, event): - # self.controller_list_table_records.do_apply_filters() + def on_first_records(self, event): + self._records_offset = 0 + self._load_records_page() + + def on_prev_records(self, event): + self._records_offset = max(self._records_offset - self._records_limit, 0) + self._load_records_page() + + def on_next_records(self, event): table = CURRENT_TABLE.get_value() - if table: - filters = (self.sql_query_filters.GetSelectedText() or self.sql_query_filters.GetText()).strip() - table.load_records(filters) + if table is None: + return + + self._records_offset = min( + self._records_offset + self._records_limit, + self._get_records_last_offset(self._records_limit), + ) + self._load_records_page() + + def on_last_records(self, event): + table = CURRENT_TABLE.get_value() + if table is None: + return + + self._records_offset = self._get_records_last_offset(self._records_limit) + self._load_records_page() + + def on_limit_records_changed(self, event): + min_limit = max(1, int(self.limit_records.GetMin())) + max_limit = max(min_limit, int(self.limit_records.GetMax())) + self._records_limit = min(max(int(self.limit_records.GetValue()), min_limit), max_limit) + wx.GetApp().settings.set_value("records", "limit", value=self._records_limit) + self._records_offset = 0 + self._load_records_page() + + def on_apply_filters(self, event): + self._records_offset = 0 + self._load_records_page() + + def on_new_query(self, event): + self._create_new_query_page() + + def on_close_query(self, event): + self._close_active_query_page() + + def on_save(self, event): + page = self.notebook_query_editor.GetCurrentPage() + if page is None: + return + + self._save_query_page(page, force_save_as=False) + + def on_save_as_query(self, event): + page = self.notebook_query_editor.GetCurrentPage() + if page is None: + return - # self.controller_list_table_records.load_model() + self._save_query_page(page, force_save_as=True) + + def on_execute_statement(self, event): + controller = self._get_active_query_controller() + if controller is not None: + self.controller_query_records = controller + controller.execute_current(event) + + def on_execute_statements(self, event): + controller = self._get_active_query_controller() + if controller is not None: + self.controller_query_records = controller + controller.execute_all(event) + + def on_stop_statements(self, event): + controller = self._get_active_query_controller() + if controller is not None: + self.controller_query_records = controller + controller.cancel_execution(event) + + def on_cancel_query_execution(self, event): + controller = self._get_active_query_controller() + if controller is not None: + self.controller_query_records = controller + controller.cancel_execution(event) # def on_clear_record(self, event): # self.controller_list_table_records.on_row_clear() diff --git a/windows/main/tabs/__init__.py b/windows/main/database/__init__.py similarity index 100% rename from windows/main/tabs/__init__.py rename to windows/main/database/__init__.py diff --git a/windows/main/tabs/database.py b/windows/main/database/list.py similarity index 95% rename from windows/main/tabs/database.py rename to windows/main/database/list.py index d9333fc..53fabd5 100644 --- a/windows/main/tabs/database.py +++ b/windows/main/database/list.py @@ -6,16 +6,14 @@ from gettext import gettext as _ from helpers import bytes_to_human -from helpers.dataview import BaseDataViewListModel, ColumnField +from helpers.dataview import BaseObservableDataViewListModel, ColumnField from structures.engines.database import SQLTable, SQLDatabase from windows.main import CURRENT_DATABASE, CURRENT_TABLE, CURRENT_SESSION -# SELECTED_TABLE: Observable[SQLTable] = Observable() - -class ModelDatabaseTable(BaseDataViewListModel): +class ModelDatabaseTable(BaseObservableDataViewListModel): MAP_COLUMN_FIELDS = { 0: ColumnField("name", str), 1: ColumnField("total_rows", str), diff --git a/windows/main/tabs/database_options.py b/windows/main/database/options.py similarity index 74% rename from windows/main/tabs/database_options.py rename to windows/main/database/options.py index 034161e..e3197c5 100644 --- a/windows/main/tabs/database_options.py +++ b/windows/main/database/options.py @@ -3,7 +3,7 @@ import wx from helpers.bindings import AbstractModel -from helpers.observables import Observable, debounce +from helpers.observables import CallbackEvent, Observable, debounce from structures.connection import ConnectionEngine @@ -13,7 +13,6 @@ class EditDatabaseOptionsModel(AbstractModel): def __init__(self): self.database_name = Observable() - self.database_character_set = Observable() self.database_collation = Observable() self.database_encryption = Observable(False) self.database_read_only = Observable(False) @@ -30,7 +29,6 @@ def __init__(self): debounce( self.database_name, - self.database_character_set, self.database_collation, self.database_encryption, self.database_read_only, @@ -78,15 +76,6 @@ def _load_database(self, database) -> None: self._first_attr(database, ["default_collation", "collation", "collation_name"], "") ) - context = database.context if database else None - charset = None - if context and self.database_collation.get_value() and getattr(context, "COLLATIONS", None): - charset = context.COLLATIONS.get(self.database_collation.get_value()) - - self.database_character_set.set_initial( - charset or self._first_attr(database, ["character_set", "charset"], "") - ) - self.database_encryption.set_initial( self._encryption_to_bool(self._first_attr(database, ["encryption"], None)) ) @@ -121,8 +110,6 @@ def _update_database(self, *args) -> None: database.name = self.database_name.get_value() mapping = { - "character_set": self.database_character_set.get_value(), - "charset": self.database_character_set.get_value(), "default_collation": self.database_collation.get_value(), "collation": self.database_collation.get_value(), "collation_name": self.database_collation.get_value(), @@ -145,10 +132,15 @@ def _update_database(self, *args) -> None: if hasattr(database, attr): setattr(database, attr, value) + CURRENT_DATABASE.execute_callback(CallbackEvent.AFTER_CHANGE) + class DatabaseOptionsController: def __init__(self, parent): self.parent = parent + self._is_updating_choices = False + self._is_apply_scheduled = False + self._last_applied_state: Optional[tuple[Optional[ConnectionEngine], Optional[int], Optional[str]]] = None self.model = EditDatabaseOptionsModel() self._panel_by_name = self._build_panel_by_name() self._panels_all = list(self._panel_by_name.values()) @@ -159,7 +151,19 @@ def __init__(self, parent): CURRENT_SESSION.subscribe(self._on_current_session) CURRENT_DATABASE.subscribe(self._on_current_database) - self.apply_for_current_state() + self._schedule_apply_for_current_state() + + def _schedule_apply_for_current_state(self) -> None: + if self._is_apply_scheduled: + return + + self._is_apply_scheduled = True + + def _run(): + self._is_apply_scheduled = False + self.apply_for_current_state() + + wx.CallAfter(_run) @staticmethod def _first_attr(source, names: list[str], default=None): @@ -174,20 +178,34 @@ def _first_attr(source, names: list[str], default=None): return default - def _apply_choice(self, choice: wx.Choice, items: list[str], selected: Optional[str]) -> None: - normalized = [str(item) for item in items if item is not None and str(item)] + @staticmethod + def _safe_text(value) -> str: + if value is None: + return "" - if selected is not None and str(selected) and str(selected) not in normalized: - normalized.append(str(selected)) + if isinstance(value, bytes): + return value.decode("utf-8", errors="replace") - choice.SetItems(normalized) + text = str(value) + return text.encode("utf-8", errors="replace").decode("utf-8", errors="replace") + + def _apply_choice(self, choice: wx.Choice, items: list[str], selected: Optional[str]) -> None: + normalized = [self._safe_text(item) for item in items if item is not None and self._safe_text(item)] + selected_text = self._safe_text(selected) + + if selected_text and selected_text not in normalized: + normalized.append(selected_text) if not normalized: - return + if choice.GetCount() > 0: + choice.Clear() - if selected and choice.SetStringSelection(str(selected)): return + choice.SetItems(normalized) + + if selected_text and choice.SetStringSelection(selected_text): + return choice.SetSelection(0) def _apply_engine(self, engine: Optional[ConnectionEngine]) -> None: @@ -213,7 +231,6 @@ def _batch_show_hide(self, show: list[wx.Window], hide: list[wx.Window]) -> None def _bind_controls(self) -> None: self.model.bind_controls( database_name=self.parent.database_name, - database_character_set=self.parent.database_character_set, database_collation=self.parent.database_collation, database_encryption=self.parent.database_encryption, database_read_only=self.parent.database_read_only, @@ -231,7 +248,6 @@ def _bind_controls(self) -> None: def _build_controls_all(self) -> list[wx.Window]: return [ - self.parent.database_character_set, self.parent.database_collation, self.parent.database_encryption, self.parent.database_read_only, @@ -249,7 +265,6 @@ def _build_controls_all(self) -> list[wx.Window]: def _build_panel_by_name(self) -> dict[str, wx.Window]: return { - "database_character_set_panel": self.parent.database_character_set_panel, "database_collation_panel": self.parent.database_collation_panel, "database_encryption_panel": self.parent.database_encryption_panel, "database_read_only_panel": self.parent.database_read_only_panel, @@ -268,7 +283,6 @@ def _build_panel_by_name(self) -> dict[str, wx.Window]: def _get_panel_names_for_engine(self, engine: Optional[ConnectionEngine]) -> list[str]: if engine in [ConnectionEngine.MYSQL, ConnectionEngine.MARIADB]: return [ - "database_character_set_panel", "database_collation_panel", "database_encryption_panel", ] @@ -300,67 +314,92 @@ def _layout_database_options(self) -> None: parent.Layout() def _on_current_database(self, database) -> None: - self.apply_for_current_state() + self._schedule_apply_for_current_state() def _on_current_session(self, session) -> None: - self.apply_for_current_state() + self._schedule_apply_for_current_state() + + def _populate_choices(self, database, engine: Optional[ConnectionEngine]) -> None: + if database is None: + return - def _populate_choices(self, database) -> None: context = database.context if database else None collations = [] - if context and getattr(context, "COLLATIONS", None): - collations = sorted(context.COLLATIONS.keys()) - - charsets = [] - if context and getattr(context, "COLLATIONS", None): - charsets = sorted(set(context.COLLATIONS.values())) + collations_source = getattr(context, "COLLATIONS", None) if context else None + + if isinstance(collations_source, dict): + collations = sorted( + str(key) + for key in collations_source.keys() + if key is not None and str(key) + ) + + if engine in [ConnectionEngine.MYSQL, ConnectionEngine.MARIADB, ConnectionEngine.POSTGRESQL]: + self._apply_choice( + self.parent.database_collation, + collations, + self.model.database_collation.get_value(), + ) - self._apply_choice( - self.parent.database_character_set, - charsets, - self.model.database_character_set.get_value(), - ) - self._apply_choice( - self.parent.database_collation, - collations, - self.model.database_collation.get_value(), - ) + if engine == ConnectionEngine.POSTGRESQL: + self._apply_choice( + self.parent.database_tablespace, + [self._first_attr(database, ["tablespace", "default_tablespace"])], + self.model.database_tablespace.get_value(), + ) - self._apply_choice( - self.parent.database_tablespace, - [self._first_attr(database, ["tablespace", "default_tablespace"])], - self.model.database_tablespace.get_value(), - ) - self._apply_choice( - self.parent.database_profile, - [self._first_attr(database, ["profile"])], - self.model.database_profile.get_value(), - ) - self._apply_choice( - self.parent.database_default_tablespace, - [self._first_attr(database, ["default_tablespace"])], - self.model.database_default_tablespace.get_value(), - ) - self._apply_choice( - self.parent.database_temporary_tablespace, - [self._first_attr(database, ["temporary_tablespace"])], - self.model.database_temporary_tablespace.get_value(), - ) - self._apply_choice( - self.parent.database_account_status, - [self._first_attr(database, ["account_status"])], - self.model.database_account_status.get_value(), - ) + if engine == ConnectionEngine.ORACLE: + self._apply_choice( + self.parent.database_profile, + [self._first_attr(database, ["profile"])], + self.model.database_profile.get_value(), + ) + self._apply_choice( + self.parent.database_default_tablespace, + [self._first_attr(database, ["default_tablespace"])], + self.model.database_default_tablespace.get_value(), + ) + self._apply_choice( + self.parent.database_temporary_tablespace, + [self._first_attr(database, ["temporary_tablespace"])], + self.model.database_temporary_tablespace.get_value(), + ) + self._apply_choice( + self.parent.database_account_status, + [self._first_attr(database, ["account_status"])], + self.model.database_account_status.get_value(), + ) def _set_controls_enabled(self, enabled: bool) -> None: for control in self._controls_all: control.Enable(enabled) def apply_for_current_state(self) -> None: + if self._is_updating_choices: + return + session = CURRENT_SESSION.get_value() database = CURRENT_DATABASE.get_value() engine = session.engine if session else None - self._populate_choices(database) - self._apply_engine(engine) + if database is None: + self._last_applied_state = None + return + + current_state = ( + engine, + getattr(database, "id", None), + getattr(database, "name", None), + ) + + if current_state == self._last_applied_state: + return + + self._is_updating_choices = True + try: + self._populate_choices(database, engine) + self._apply_engine(engine) + self._last_applied_state = current_state + finally: + self._is_updating_choices = False diff --git a/windows/main/tabs/view.py b/windows/main/database/view.py similarity index 100% rename from windows/main/tabs/view.py rename to windows/main/database/view.py diff --git a/windows/main/tabs/explorer.py b/windows/main/explorer.py similarity index 100% rename from windows/main/tabs/explorer.py rename to windows/main/explorer.py diff --git a/windows/main/query.py b/windows/main/query.py new file mode 100644 index 0000000..0d06e58 --- /dev/null +++ b/windows/main/query.py @@ -0,0 +1,17 @@ +import wx + + +class QueryMixin: + def on_new_query(self, event): + new_panel = wx.Panel(self.notebook_query_editor) + sizer = wx.BoxSizer(wx.VERTICAL) + stc = wx.stc.StyledTextCtrl(new_panel, style=0) + sizer.Add(stc, 1, wx.EXPAND) + new_panel.SetSizer(sizer) + n = self.notebook_query_editor.GetPageCount() + 1 + self.notebook_query_editor.AddPage(new_panel, f"Query #{n}", select=True) + + def on_close_query(self, event): + n = self.notebook_query_editor.GetSelection() + if n != wx.NOT_FOUND: + self.notebook_query_editor.DeletePage(n) \ No newline at end of file diff --git a/windows/main/query/__init__.py b/windows/main/query/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/windows/main/query/controller.py b/windows/main/query/controller.py new file mode 100644 index 0000000..81376dc --- /dev/null +++ b/windows/main/query/controller.py @@ -0,0 +1,274 @@ +from typing import Any, Callable, Optional +from gettext import gettext as _ + +import wx +import wx.stc + +from helpers.logger import logger + +from structures.session import Session + +from windows.main.query.parser import ExecutionMode, SQLStatementParser, StatementSelector +from windows.main.query.executor import ExecutionResult, ExecutionSummary, QueryExecutor +from windows.main.query.renderer import QueryResultsRenderer + + +class QueryEditorController: + def __init__( + self, + stc_editor: wx.stc.StyledTextCtrl, + results_notebook: wx.Notebook, + session_provider: Callable[[], Optional[Session]], + database_provider: Optional[Callable[[], Optional[Any]]] = None, + cancel_button: Optional[wx.Button] = None, + on_new_query: Optional[Callable[[wx.Event], None]] = None, + on_close_query: Optional[Callable[[wx.Event], None]] = None, + on_save_query: Optional[Callable[[wx.Event], None]] = None, + on_save_as_query: Optional[Callable[[wx.Event], None]] = None, + on_stop_state_changed: Optional[Callable[[bool], None]] = None, + on_before_execute: Optional[Callable[[], bool]] = None, + ): + self.editor = stc_editor + self.notebook = results_notebook + self.get_session = session_provider + self.get_database = database_provider or (lambda: None) + self.cancel_button = cancel_button + self.on_new_query = on_new_query + self.on_close_query = on_close_query + self.on_save_query = on_save_query + self.on_save_as_query = on_save_as_query + self.on_stop_state_changed = on_stop_state_changed + self.on_before_execute = on_before_execute + + self.parser: Optional[SQLStatementParser] = None + self.selector = StatementSelector(stc_editor) + self.executor: Optional[QueryExecutor] = None + self.renderer: Optional[QueryResultsRenderer] = None + self._cancel_feedback_pending = False + self._shortcuts = self._load_shortcuts() + + self._bind_shortcuts() + self._set_cancel_button_enabled(False) + + def _load_shortcuts(self) -> dict[str, str]: + settings = wx.GetApp().settings + return { + "execute_current": settings.get_value("ui", "shortcuts", "query", "execute_current", default="Ctrl+Enter"), + "execute_all": settings.get_value("ui", "shortcuts", "query", "execute_all", default="Ctrl+Shift+Enter"), + "stop": settings.get_value("ui", "shortcuts", "query", "stop", default="Esc"), + "new_query": settings.get_value("ui", "shortcuts", "query", "new_query", default="Ctrl+T"), + "close_query": settings.get_value("ui", "shortcuts", "query", "close_query", default="Ctrl+W"), + "save": settings.get_value("ui", "shortcuts", "query", "save", default="Ctrl+S"), + "save_as": settings.get_value("ui", "shortcuts", "query", "save_as", default="Ctrl+Shift+S"), + } + + @staticmethod + def _matches_shortcut_key(key_name: str, key_code: int) -> bool: + if key_name == "enter": + return key_code in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER] + + if key_name == "esc": + return key_code == wx.WXK_ESCAPE + + if len(key_name) == 1: + return key_code == ord(key_name.upper()) + + return False + + def _matches_shortcut(self, event: wx.KeyEvent, shortcut: str) -> bool: + parts = [part.strip().lower() for part in shortcut.split("+") if part.strip()] + if not parts: + return False + + key_name = parts[-1] + modifiers = set(parts[:-1]) + + if event.ControlDown() != ("ctrl" in modifiers): + return False + + if event.ShiftDown() != ("shift" in modifiers): + return False + + if event.AltDown() != ("alt" in modifiers): + return False + + key_code = event.GetKeyCode() + return self._matches_shortcut_key(key_name, key_code) + + def _bind_shortcuts(self) -> None: + self.editor.Bind(wx.EVT_KEY_DOWN, self._on_key_down) + + def _set_cancel_button_enabled(self, enabled: bool) -> None: + if self.cancel_button is not None: + self.cancel_button.Enable(enabled) + + if self.on_stop_state_changed is not None: + self.on_stop_state_changed(enabled) + + def _format_elapsed(self, elapsed_ms: float) -> str: + if elapsed_ms < 1000: + return _("{elapsed_ms:.0f} ms").format(elapsed_ms=elapsed_ms) + + return _("{elapsed_s:.2f} s").format(elapsed_s=elapsed_ms / 1000) + + def _show_cancel_message(self, summary: ExecutionSummary) -> None: + last_statement_label = _("none") + if summary.last_statement is not None: + last_statement_label = str(summary.last_statement.statement_index + 1) + + wx.MessageBox( + _( + "Query execution stopped after {elapsed}.\n" + "Completed statements: {completed}/{total}.\n" + "Successful: {success}.\n" + "Failed: {failed}.\n" + "Last statement: #{last}." + ).format( + elapsed=self._format_elapsed(summary.elapsed_ms), + completed=summary.completed_statements, + total=summary.total_statements, + success=summary.successful_statements, + failed=summary.failed_statements, + last=last_statement_label, + ), + _("Query execution cancelled"), + wx.OK | wx.ICON_INFORMATION, + ) + + def _on_key_down(self, event: wx.KeyEvent) -> None: + if self._matches_shortcut(event, self._shortcuts["execute_current"]): + self.execute_current(event) + return + + if self._matches_shortcut(event, self._shortcuts["execute_all"]): + self.execute_all(event) + return + + if self._matches_shortcut(event, self._shortcuts["stop"]): + self.cancel_execution(event) + return + + if self._matches_shortcut(event, self._shortcuts["new_query"]) and self.on_new_query is not None: + self.on_new_query(event) + return + + if self._matches_shortcut(event, self._shortcuts["close_query"]) and self.on_close_query is not None: + self.on_close_query(event) + return + + if self._matches_shortcut(event, self._shortcuts["save_as"]) and self.on_save_as_query is not None: + self.on_save_as_query(event) + return + + if self._matches_shortcut(event, self._shortcuts["save"]) and self.on_save_query is not None: + self.on_save_query(event) + return + + event.Skip() + + def _execute(self, mode: ExecutionMode) -> None: + if self.on_before_execute is not None and not self.on_before_execute(): + return + + session = self.get_session() + if not session or not session.is_connected: + wx.MessageBox( + _("No active database connection"), + _("Error"), + wx.OK | wx.ICON_ERROR + ) + return + + if not self.parser or self.parser.engine != session.engine: + self.parser = SQLStatementParser(session.engine) + self.executor = QueryExecutor(session) + self.renderer = QueryResultsRenderer(self.notebook, session) + + sql_text = self.editor.GetText() + if not sql_text.strip(): + return + + statements = self.parser.parse(sql_text) + if not statements: + return + + if mode == ExecutionMode.CURRENT or mode == ExecutionMode.SELECTION: + _, statements_to_execute = self.selector.get_execution_scope(statements) + else: + statements_to_execute = statements + + if not statements_to_execute: + return + + self.renderer.clear_all_tabs() + self._cancel_feedback_pending = False + self._set_cancel_button_enabled(True) + + self.executor.execute_statements( + statements=statements_to_execute, + on_statement_complete=self._on_statement_complete, + on_all_complete=self._on_all_complete, + current_database=self.get_database(), + stop_on_error=True + ) + + def _on_statement_complete(self, result: ExecutionResult) -> None: + if result.cancelled: + return + + if self.renderer: + self.renderer.create_result_tab(result) + + def _on_all_complete(self, summary: ExecutionSummary) -> None: + self._set_cancel_button_enabled(False) + + if summary.cancelled and self._cancel_feedback_pending: + self._show_cancel_message(summary) + + self._cancel_feedback_pending = False + logger.info("Query execution completed") + + def get_shortcuts(self) -> dict[str, str]: + return dict(self._shortcuts) + + def execute_all(self, event: wx.Event) -> None: + self._execute(ExecutionMode.ALL) + + def execute_current(self, event: wx.Event) -> None: + self._execute(ExecutionMode.CURRENT) + + def cancel_execution(self, event: wx.Event) -> None: + if self.executor and self.executor.is_running(): + self._cancel_feedback_pending = True + self.executor.cancel() + logger.info("Query execution cancelled") + + +class QueryResultsController(QueryEditorController): + def __init__( + self, + stc_sql_query: wx.stc.StyledTextCtrl, + notebook_sql_results: wx.Notebook, + cancel_button: Optional[wx.Button] = None, + on_new_query: Optional[Callable[[wx.Event], None]] = None, + on_close_query: Optional[Callable[[wx.Event], None]] = None, + on_save_query: Optional[Callable[[wx.Event], None]] = None, + on_save_as_query: Optional[Callable[[wx.Event], None]] = None, + on_stop_state_changed: Optional[Callable[[bool], None]] = None, + on_before_execute: Optional[Callable[[], bool]] = None, + ): + from windows.main import CURRENT_DATABASE, CURRENT_SESSION # Lazy import: unavoidable circular dependency. + + super().__init__( + stc_editor=stc_sql_query, + results_notebook=notebook_sql_results, + session_provider=lambda: CURRENT_SESSION.get_value(), + database_provider=lambda: CURRENT_DATABASE.get_value(), + cancel_button=cancel_button, + on_new_query=on_new_query, + on_close_query=on_close_query, + on_save_query=on_save_query, + on_save_as_query=on_save_as_query, + on_stop_state_changed=on_stop_state_changed, + on_before_execute=on_before_execute, + ) diff --git a/windows/main/query/executor.py b/windows/main/query/executor.py new file mode 100644 index 0000000..dea2698 --- /dev/null +++ b/windows/main/query/executor.py @@ -0,0 +1,259 @@ +import contextlib +import dataclasses +import threading +import time + +from typing import Any, Callable, Optional + +import wx + +from helpers.loader import Loader +from helpers.logger import logger + +from structures.session import Session +from structures.connection import Connection, ConnectionEngine +from structures.engines.datatype import SQLDataType + +from windows.main.query.parser import ParsedStatement + + +@dataclasses.dataclass +class ExecutionResult: + statement: ParsedStatement + success: bool + columns: Optional[list[str]] = None + rows: Optional[list[tuple]] = None + column_datatypes: Optional[list[Optional[SQLDataType]]] = None + affected_rows: Optional[int] = None + elapsed_ms: float = 0.0 + error: Optional[str] = None + cancelled: bool = False + warnings: list[str] = dataclasses.field(default_factory=list) + + +@dataclasses.dataclass +class ExecutionSummary: + total_statements: int = 0 + completed_statements: int = 0 + successful_statements: int = 0 + failed_statements: int = 0 + elapsed_ms: float = 0.0 + cancelled: bool = False + last_statement: Optional[ParsedStatement] = None + + +class QueryExecutor: + def __init__(self, session: Session): + self.session = session + self._cancel_requested = False + self._current_thread: Optional[threading.Thread] = None + self._worker_context: Optional[Any] = None + self._loader_context: Optional[Any] = None + self._lock = threading.Lock() + + def execute_statements( + self, + statements: list[ParsedStatement], + on_statement_complete: Callable[[ExecutionResult], None], + on_all_complete: Callable[[ExecutionSummary], None], + current_database: Optional[Any] = None, + stop_on_error: bool = True + ) -> None: + self._cancel_requested = False + self._loader_context = Loader.cursor_wait() + self._loader_context.__enter__() + + self._current_thread = threading.Thread( + target=self._execute_worker, + args=( + statements, + on_statement_complete, + on_all_complete, + current_database, + stop_on_error, + ), + daemon=True + ) + self._current_thread.start() + + def _dispatch_statement_result( + self, + on_statement_complete: Callable[[ExecutionResult], None], + result: ExecutionResult, + ) -> None: + ui_done_event = threading.Event() + + def _on_ui_thread() -> None: + try: + on_statement_complete(result) + finally: + ui_done_event.set() + + wx.CallAfter(_on_ui_thread) + while not ui_done_event.wait(0.05): + continue + + def _execute_worker( + self, + statements: list[ParsedStatement], + on_statement_complete: Callable[[ExecutionResult], None], + on_all_complete: Callable[[ExecutionSummary], None], + current_database: Optional[Any], + stop_on_error: bool + ) -> None: + time_start = time.perf_counter() + summary = ExecutionSummary(total_statements=len(statements)) + + try: + context = self._create_worker_context(current_database) + self._set_worker_context(context) + + for stmt in statements: + if self._cancel_requested: + summary.cancelled = True + break + + summary.last_statement = stmt + result = self._execute_single(context, stmt) + + if result.success: + summary.completed_statements += 1 + summary.successful_statements += 1 + elif not result.cancelled: + summary.completed_statements += 1 + summary.failed_statements += 1 + + self._dispatch_statement_result(on_statement_complete, result) + + if not result.success and stop_on_error: + break + + except Exception as ex: + logger.error(f"Execution worker error: {ex}", exc_info=True) + finally: + summary.cancelled = summary.cancelled or self._cancel_requested + summary.elapsed_ms = (time.perf_counter() - time_start) * 1000 + + self._clear_worker_context() + + wx.CallAfter(self._stop_loader) + wx.CallAfter(on_all_complete, summary) + + def _execute_single(self, context: Any, statement: ParsedStatement) -> ExecutionResult: + start_time = time.time() + + try: + context.execute(statement.text) + + elapsed_ms = (time.time() - start_time) * 1000 + + cursor = context.cursor + if cursor.description: + columns = [desc[0] for desc in cursor.description] + column_datatypes = context.get_result_column_datatypes(cursor) + rows = context.fetchall() + + return ExecutionResult( + statement=statement, + success=True, + columns=columns, + rows=rows, + column_datatypes=column_datatypes, + affected_rows=len(rows), + elapsed_ms=elapsed_ms + ) + else: + affected = cursor.rowcount if cursor.rowcount >= 0 else 0 + + return ExecutionResult( + statement=statement, + success=True, + affected_rows=affected, + elapsed_ms=elapsed_ms + ) + + except Exception as ex: + elapsed_ms = (time.time() - start_time) * 1000 + is_cancelled = self._cancel_requested + + return ExecutionResult( + statement=statement, + success=False, + error=str(ex), + cancelled=is_cancelled, + elapsed_ms=elapsed_ms + ) + + def _build_worker_connection(self) -> Connection: + connection = self.session.connection.copy() + + if not connection.has_enabled_tunnel(): + return connection + + context = getattr(self.session, "context", None) + configuration = getattr(connection, "configuration", None) + + if context is not None and configuration is not None and hasattr(configuration, "_replace"): + replace_kwargs = {} + + if hasattr(configuration, "hostname") and getattr(context, "host", None): + replace_kwargs["hostname"] = context.host + + if hasattr(configuration, "port") and getattr(context, "port", None) is not None: + replace_kwargs["port"] = int(context.port) + + if replace_kwargs: + connection.configuration = configuration._replace(**replace_kwargs) + + connection.ssh_tunnel = None + return connection + + def _create_worker_context(self, current_database: Optional[Any]) -> Any: + context = self.session._get_context_class()(self._build_worker_connection()) + + if self.session.engine == ConnectionEngine.POSTGRESQL: + connect_kwargs = { + "skip_before_connect": True, + "skip_after_connect": True, + } + + if current_database is not None and hasattr(current_database, "name"): + connect_kwargs["database"] = current_database.name + + context.connect(**connect_kwargs) + return context + + context.connect(skip_before_connect=True, skip_after_connect=True, database=current_database.name if current_database is not None else None) + + # if current_database is not None: + # with contextlib.suppress(Exception): + # context.set_database(current_database) + + return context + + def _set_worker_context(self, context: Any) -> None: + with self._lock: + self._worker_context = context + + def _clear_worker_context(self) -> None: + context = None + + with self._lock: + context = self._worker_context + self._worker_context = None + + if context is not None: + with contextlib.suppress(Exception): + context.disconnect() + + def _stop_loader(self) -> None: + if self._loader_context is not None: + self._loader_context.__exit__(None, None, None) + self._loader_context = None + + def cancel(self) -> None: + self._cancel_requested = True + self._clear_worker_context() + + def is_running(self) -> bool: + return self._current_thread is not None and self._current_thread.is_alive() \ No newline at end of file diff --git a/windows/main/query/parser.py b/windows/main/query/parser.py new file mode 100644 index 0000000..e6c3d50 --- /dev/null +++ b/windows/main/query/parser.py @@ -0,0 +1,83 @@ +import dataclasses +import enum + +from typing import Optional + +import wx.stc + +from structures.connection import ConnectionEngine + +from windows.components.stc.autocomplete.statement_extractor import StatementExtractor + + +@dataclasses.dataclass +class ParsedStatement: + text: str + start_pos: int + end_pos: int + statement_index: int + + +class ExecutionMode(enum.Enum): + ALL = "all" + SELECTION = "selection" + CURRENT = "current" + + +class SQLStatementParser: + def __init__(self, engine: ConnectionEngine): + self.engine = engine + + def parse(self, sql_text: str) -> list[ParsedStatement]: + return [ + ParsedStatement(text=text, start_pos=start, end_pos=end, statement_index=i) + for i, (text, start, end) in enumerate(StatementExtractor.extract_all_statements(sql_text)) + ] + + +class StatementSelector: + def __init__(self, stc_editor: wx.stc.StyledTextCtrl): + self.editor = stc_editor + + def get_execution_scope( + self, + statements: list[ParsedStatement] + ) -> tuple[ExecutionMode, list[ParsedStatement]]: + selection_start = self.editor.GetSelectionStart() + selection_end = self.editor.GetSelectionEnd() + + if selection_start != selection_end: + if selected_text := self.editor.GetSelectedText().strip(): + return (ExecutionMode.SELECTION, [ParsedStatement( + text=selected_text, + start_pos=selection_start, + end_pos=selection_end, + statement_index=0 + )]) + + caret_pos = self.editor.GetCurrentPos() + + if current_stmt := self._find_statement_at_caret(caret_pos, statements): + return (ExecutionMode.CURRENT, [current_stmt]) + + return (ExecutionMode.ALL, statements) + + def _find_statement_at_caret( + self, + caret_pos: int, + statements: list[ParsedStatement] + ) -> Optional[ParsedStatement]: + for stmt in statements: + if stmt.start_pos <= caret_pos <= stmt.end_pos: + return stmt + + # Caret is in whitespace: execute next statement + for stmt in statements: + if caret_pos < stmt.start_pos: + return stmt + + # Caret after all statements: execute last + if statements: + return statements[-1] + + return None diff --git a/windows/main/query/renderer.py b/windows/main/query/renderer.py new file mode 100644 index 0000000..01f41de --- /dev/null +++ b/windows/main/query/renderer.py @@ -0,0 +1,304 @@ +import datetime + +from typing import Any, Optional +from gettext import gettext as _ + +import wx +import wx.dataview + +from helpers.dataview import BaseDataViewListModel + +from structures.session import Session +from structures.engines.datatype import DataTypeCategory, SQLDataType + +from windows.components.popup import PopupCalendar, PopupCalendarTime +from windows.components.renders import AdvancedTextRenderer, FloatRenderer, IntegerRenderer, PopupRenderer, TextRenderer, TimeRenderer +from windows.components.dataview import QueryEditorResultsDataViewCtrl + +from windows.main.query.executor import ExecutionResult + + +class _ReadOnlyPopupRenderer(PopupRenderer): + def ActivateCell(self, rect, model, item, col, mouseEvent): + return False + + +class _ReadOnlyTimeRenderer(TimeRenderer): + def HasEditorCtrl(self): + return False + + +class QueryResultsRenderer: + def __init__(self, notebook: wx.Notebook, session: Session): + self.notebook = notebook + self.session = session + self._models: list[Any] = [] + self._tab_counter = 0 + + def create_result_tab(self, result: ExecutionResult) -> wx.Panel: + self._tab_counter += 1 + + panel = wx.Panel(self.notebook) + sizer = wx.BoxSizer(wx.VERTICAL) + + if result.success and result.columns: + results_dataview = QueryEditorResultsDataViewCtrl(panel) + self._populate_grid(results_dataview, result) + sizer.Add(results_dataview, 1, wx.EXPAND | wx.ALL, 5) + + tab_name = self._generate_tab_name(result) + elif result.success: + msg = wx.StaticText( + panel, + label=_("{affected_rows} rows affected").format( + affected_rows=result.affected_rows or 0 + ), + ) + msg.SetFont(msg.GetFont().MakeBold()) + sizer.Add(msg, 1, wx.ALIGN_CENTER | wx.ALL, 20) + + tab_name = _("Query {query_number}").format(query_number=self._tab_counter) + else: + error_panel = self._create_error_panel(panel, result) + sizer.Add(error_panel, 1, wx.EXPAND | wx.ALL, 5) + + tab_name = _("Query {query_number} (Error)").format( + query_number=self._tab_counter + ) + + footer = self._create_footer(panel, result) + sizer.Add(footer, 0, wx.EXPAND | wx.ALL, 5) + + panel.SetSizer(sizer) + self.notebook.AddPage(panel, tab_name, select=True) + + return panel + + def _generate_tab_name(self, result: ExecutionResult) -> str: + if result.columns and result.rows is not None: + return _("Query {query_number} ({rows_count} rows × {columns_count} cols)").format( + query_number=self._tab_counter, + rows_count=len(result.rows), + columns_count=len(result.columns), + ) + return _("Query {query_number}").format(query_number=self._tab_counter) + + def _get_column_datatype(self, result: ExecutionResult, column_index: int) -> Optional[SQLDataType]: + if not result.column_datatypes: + return None + + if column_index >= len(result.column_datatypes): + return None + + return result.column_datatypes[column_index] + + def _get_column_renderer( + self, + results_dataview: QueryEditorResultsDataViewCtrl, + datatype: Optional[SQLDataType] + ) -> wx.dataview.DataViewRenderer: + if datatype is None: + return TextRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) + + if datatype.name == "BOOLEAN": + return wx.dataview.DataViewToggleRenderer( + mode=wx.dataview.DATAVIEW_CELL_INERT, + align=wx.ALIGN_CENTER, + ) + + if datatype.name == "DATE": + return _ReadOnlyPopupRenderer(PopupCalendar) + + if datatype.name == "TIME": + return _ReadOnlyTimeRenderer() + + if datatype.name in ["DATETIME", "TIMESTAMP"]: + return _ReadOnlyPopupRenderer(PopupCalendarTime) + + if datatype.category == DataTypeCategory.INTEGER: + return IntegerRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) + + if datatype.category == DataTypeCategory.REAL: + return FloatRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) + + if datatype.category == DataTypeCategory.TEXT: + return AdvancedTextRenderer( + mode=wx.dataview.DATAVIEW_CELL_INERT, + dialog_factory=results_dataview.make_advanced_dialog, + ) + + return TextRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) + + def _populate_grid( + self, + results_dataview: QueryEditorResultsDataViewCtrl, + result: ExecutionResult + ) -> None: + if not result.columns: + return + + for i, col_name in enumerate(result.columns): + datatype = self._get_column_datatype(result, i) + renderer = self._get_column_renderer(results_dataview, datatype) + align = wx.ALIGN_CENTER if datatype and datatype.name == "BOOLEAN" else wx.ALIGN_LEFT + + column = wx.dataview.DataViewColumn( + col_name, + renderer, + i, + width=results_dataview.measure_text(col_name), + align=align, + flags=wx.dataview.DATAVIEW_COL_RESIZABLE, + ) + results_dataview.AppendColumn(column) + + model = QueryResultsModel(column_count=len(result.columns)) + model.load(result.rows, result.columns, result.column_datatypes) + self._models.append(model) + results_dataview.AssociateModel(model) + wx.CallAfter(results_dataview.autosize_columns_from_content) + + def _create_footer(self, parent: wx.Panel, result: ExecutionResult) -> wx.StaticText: + parts = [] + + if result.affected_rows is not None: + parts.append(_("{rows_count} rows").format(rows_count=result.affected_rows)) + + parts.append(_("{elapsed_ms:.1f} ms").format(elapsed_ms=result.elapsed_ms)) + + if result.warnings: + parts.append( + _("{warnings_count} warnings").format( + warnings_count=len(result.warnings) + ) + ) + + footer_text = " | ".join(parts) + footer = wx.StaticText(parent, label=footer_text) + footer.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)) + + return footer + + def _create_error_panel(self, parent: wx.Panel, result: ExecutionResult) -> wx.Panel: + error_panel = wx.Panel(parent) + error_sizer = wx.BoxSizer(wx.VERTICAL) + + error_label = wx.StaticText(error_panel, label=_("Error:")) + error_label.SetFont(error_label.GetFont().MakeBold()) + error_sizer.Add(error_label, 0, wx.ALL, 5) + + error_text = wx.TextCtrl( + error_panel, + value=result.error or _("Unknown error"), + style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_WORDWRAP + ) + error_text.SetBackgroundColour(wx.Colour(255, 240, 240)) + error_sizer.Add(error_text, 1, wx.EXPAND | wx.ALL, 5) + + error_panel.SetSizer(error_sizer) + return error_panel + + def clear_all_tabs(self) -> None: + while self.notebook.GetPageCount() > 0: + self.notebook.DeletePage(0) + self._models = [] + self._tab_counter = 0 + + +class QueryResultsModel(BaseDataViewListModel): + def __init__(self, column_count: int): + super().__init__(column_count) + self._columns: list[str] = [] + self._column_datatypes: list[Optional[SQLDataType]] = [] + + def load( + self, + data: list[Any], + columns: list[str], + column_datatypes: Optional[list[Optional[SQLDataType]]] = None, + ): + self._columns = columns + self._column_datatypes = column_datatypes or [None for _ in columns] + BaseDataViewListModel.load(self, data) + + def GetValueByRow(self, row, col): + if row < 0 or row >= len(self.data): + return "" + + if col < 0 or col >= len(self._columns): + return "" + + value = self._get_cell_value(self.data[row], col) + if value is None: + return "" + + datatype = self._get_column_datatype(col) + if datatype is None: + return str(value) + + if datatype.name == "BOOLEAN": + return bool(value) + + if datatype.category == DataTypeCategory.TEMPORAL: + return self._format_temporal_value(value, datatype.name) + + return str(value) + + def SetValueByRow(self, value, row, col): + return False + + def HasValue(self, item, col): + if col < 0 or col >= len(self._columns): + return False + + row = self.GetRow(item) + if row < 0 or row >= len(self.data): + return False + + return self._get_cell_value(self.data[row], col) is not None + + def GetAttr(self, item, col, attr): + datatype = self._get_column_datatype(col) + if datatype is None: + return super().GetAttr(item, col, attr) + + color = datatype.category.value.color + attr.SetColour(wx.Colour(color)) + return super().GetAttr(item, col, attr) + + def _get_cell_value(self, row_data: Any, col: int) -> Any: + if isinstance(row_data, dict): + return row_data.get(self._columns[col]) + + if col < len(row_data): + return row_data[col] + + return None + + def _get_column_datatype(self, col: int) -> Optional[SQLDataType]: + if col < 0 or col >= len(self._column_datatypes): + return None + + return self._column_datatypes[col] + + def _format_temporal_value(self, value: Any, datatype_name: str) -> str: + if isinstance(value, datetime.datetime): + if datatype_name == "DATE": + return value.strftime("%Y-%m-%d") + + if datatype_name == "TIME": + return value.strftime("%H:%M:%S") + + if datatype_name in ["DATETIME", "TIMESTAMP"]: + return value.strftime("%Y-%m-%d %H:%M:%S") + + if datatype_name == "YEAR": + return value.strftime("%Y") + + if isinstance(value, datetime.date) and datatype_name == "DATE": + return value.strftime("%Y-%m-%d") + + if isinstance(value, datetime.time) and datatype_name == "TIME": + return value.strftime("%H:%M:%S") + + return str(value) diff --git a/windows/main/table/__init__.py b/windows/main/table/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/windows/main/tabs/check.py b/windows/main/table/check.py similarity index 95% rename from windows/main/tabs/check.py rename to windows/main/table/check.py index 2928d75..0c6256a 100755 --- a/windows/main/tabs/check.py +++ b/windows/main/table/check.py @@ -3,16 +3,16 @@ import wx import wx.dataview -from helpers.dataview import BaseDataViewListModel, ColumnField +from helpers.dataview import BaseObservableDataViewListModel, ColumnField from structures.helpers import merge_original_current from structures.engines.database import SQLCheck, SQLTable +from windows.state import NEW_TABLE from windows.main import CURRENT_INDEX, CURRENT_TABLE -from windows.main.tabs.column import NEW_TABLE -class TableCheckModel(BaseDataViewListModel): +class TableCheckModel(BaseObservableDataViewListModel): MAP_COLUMN_FIELDS = { 0: ColumnField("name", lambda s, x: wx.dataview.DataViewIconText(s.name or "", wx.NullBitmap)), 1: ColumnField("expression"), diff --git a/windows/main/tabs/column.py b/windows/main/table/column.py similarity index 98% rename from windows/main/tabs/column.py rename to windows/main/table/column.py index c4aca9b..20671c0 100644 --- a/windows/main/tabs/column.py +++ b/windows/main/table/column.py @@ -5,7 +5,7 @@ from helpers.loader import Loader from helpers.logger import logger -from helpers.dataview import BaseDataViewListModel, ColumnField +from helpers.dataview import BaseObservableDataViewListModel, ColumnField from structures.helpers import merge_original_current from structures.session import Session @@ -16,7 +16,7 @@ from windows.state import CURRENT_COLUMN, CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE, NEW_TABLE -class ColumnModel(BaseDataViewListModel): +class ColumnModel(BaseObservableDataViewListModel): MAP_COLUMN_FIELDS: dict[int, ColumnField] def GetColumnCount(self): @@ -247,8 +247,8 @@ def on_column_insert(self, event: wx.Event): default_values['set'] = datatype.default_set new_empty_column = session.context.build_empty_column( - table=table, - datatype=datatype, + table, + datatype, **default_values ) diff --git a/windows/main/table/executor.py b/windows/main/table/executor.py new file mode 100644 index 0000000..1fdd2be --- /dev/null +++ b/windows/main/table/executor.py @@ -0,0 +1,240 @@ +import contextlib +import dataclasses +import threading +import time + +from typing import Any, Callable, Optional + +import wx + +from helpers.loader import Loader +from helpers.logger import logger + +from structures.session import Session +from structures.connection import Connection, ConnectionEngine +from structures.engines.database import SQLTable, SQLRecord + +from windows.main.query.executor import QueryExecutor + + +@dataclasses.dataclass +class RecordsOperationResult: + operation: str + success: bool + records: Optional[list[SQLRecord]] = None + affected_records: Optional[int] = None + elapsed_ms: float = 0.0 + error: Optional[str] = None + cancelled: bool = False + warnings: list[str] = dataclasses.field(default_factory=list) + + +@dataclasses.dataclass +class RecordsOperationSummary: + total_operations: int = 0 + completed_operations: int = 0 + successful_operations: int = 0 + failed_operations: int = 0 + elapsed_ms: float = 0.0 + cancelled: bool = False + last_operation: Optional[str] = None + + +class RecordsExecutor: + def __init__(self, session: Session): + self.session = session + self._cancel_requested = False + self._current_thread: Optional[threading.Thread] = None + self._worker_context: Optional[Any] = None + self._loader_context: Optional[Any] = None + self._lock = threading.Lock() + + def load_records( + self, + table: SQLTable, + on_complete: Callable[[RecordsOperationResult], None], + filters: Optional[str] = None, + limit: int = 1000, + offset: int = 0, + orders: Optional[str] = None + ) -> None: + self._execute_operation( + operation="load_records", + on_complete=on_complete, + table=table, + filters=filters, + limit=limit, + offset=offset, + orders=orders + ) + + def _execute_operation(self, **kwargs) -> None: + self._cancel_requested = False + self._loader_context = Loader.cursor_wait() + self._loader_context.__enter__() + + self._current_thread = threading.Thread( + target=self._execute_worker, + args=(kwargs,), + daemon=True + ) + self._current_thread.start() + + def _dispatch_operation_result( + self, + on_complete: Callable[[RecordsOperationResult], None], + result: RecordsOperationResult, + ) -> None: + ui_done_event = threading.Event() + + def _on_ui_thread() -> None: + try: + on_complete(result) + finally: + ui_done_event.set() + + wx.CallAfter(_on_ui_thread) + while not ui_done_event.wait(0.05): + continue + + def _execute_worker(self, operation_kwargs: dict) -> None: + time_start = time.perf_counter() + operation = operation_kwargs.get("operation", "unknown") + on_complete = operation_kwargs.get("on_complete") + + try: + context = self._create_worker_context() + self._set_worker_context(context) + + result = self._execute_single_operation(context, operation_kwargs) + + self._dispatch_operation_result(on_complete, result) + + except Exception as ex: + logger.error(f"Records executor error: {ex}", exc_info=True) + error_result = RecordsOperationResult( + operation=operation, + success=False, + error=str(ex), + elapsed_ms=(time.perf_counter() - time_start) * 1000 + ) + self._dispatch_operation_result(on_complete, error_result) + finally: + self._clear_worker_context() + wx.CallAfter(self._stop_loader) + + def _execute_single_operation(self, context: Any, operation_kwargs: dict) -> RecordsOperationResult: + start_time = time.time() + operation = operation_kwargs.get("operation", "unknown") + + try: + if operation == "load_records": + return self._load_records_operation(context, operation_kwargs) + else: + raise ValueError(f"Unknown operation: {operation}") + + except Exception as ex: + elapsed_ms = (time.time() - start_time) * 1000 + is_cancelled = self._cancel_requested + + return RecordsOperationResult( + operation=operation, + success=False, + error=str(ex), + cancelled=is_cancelled, + elapsed_ms=elapsed_ms + ) + + def _load_records_operation(self, context: Any, operation_kwargs: dict) -> RecordsOperationResult: + start_time = time.time() + table = operation_kwargs.get("table") + filters = operation_kwargs.get("filters") + limit = operation_kwargs.get("limit", 1000) + offset = operation_kwargs.get("offset", 0) + orders = operation_kwargs.get("orders") + + records = context.get_records( + table=table, + filters=filters, + limit=limit, + offset=offset, + orders=orders + ) + + elapsed_ms = (time.time() - start_time) * 1000 + + return RecordsOperationResult( + operation="load_records", + success=True, + records=records, + affected_records=len(records), + elapsed_ms=elapsed_ms + ) + + + def _build_worker_connection(self) -> Connection: + """Build a worker connection similar to QueryExecutor.""" + connection = self.session.connection.copy() + + if not connection.has_enabled_tunnel(): + return connection + + context = getattr(self.session, "context", None) + configuration = getattr(connection, "configuration", None) + + if context is not None and configuration is not None and hasattr(configuration, "_replace"): + replace_kwargs = {} + + if hasattr(configuration, "hostname") and getattr(context, "host", None): + replace_kwargs["hostname"] = context.host + + if hasattr(configuration, "port") and getattr(context, "port", None) is not None: + replace_kwargs["port"] = int(context.port) + + if replace_kwargs: + connection.configuration = configuration._replace(**replace_kwargs) + + connection.ssh_tunnel = None + return connection + + def _create_worker_context(self) -> Any: + """Create a worker context similar to QueryExecutor.""" + context = self.session._get_context_class()(self._build_worker_connection()) + + # if self.session.engine == ConnectionEngine.POSTGRESQL: + # connect_kwargs = { + # "skip_before_connect": True, + # "skip_after_connect": True, + # } + # context.connect(**connect_kwargs) + # return context + + context.connect(skip_before_connect=True, skip_after_connect=True, database=self.session.database) + return context + + def _set_worker_context(self, context: Any) -> None: + with self._lock: + self._worker_context = context + + def _clear_worker_context(self) -> None: + context = None + + with self._lock: + context = self._worker_context + self._worker_context = None + + if context is not None: + with contextlib.suppress(Exception): + context.disconnect() + + def _stop_loader(self) -> None: + if self._loader_context is not None: + self._loader_context.__exit__(None, None, None) + self._loader_context = None + + def cancel(self) -> None: + self._cancel_requested = True + self._clear_worker_context() + + def is_running(self) -> bool: + return self._current_thread is not None and self._current_thread.is_alive() diff --git a/windows/main/tabs/foreign_key.py b/windows/main/table/foreign_key.py similarity index 97% rename from windows/main/tabs/foreign_key.py rename to windows/main/table/foreign_key.py index 20c2c54..ee53470 100755 --- a/windows/main/tabs/foreign_key.py +++ b/windows/main/table/foreign_key.py @@ -5,7 +5,7 @@ from helpers.loader import Loader from helpers.logger import logger -from helpers.dataview import BaseDataViewListModel +from helpers.dataview import BaseObservableDataViewListModel from structures.helpers import merge_original_current @@ -16,7 +16,7 @@ from structures.engines.database import SQLForeignKey, SQLTable -class TableForeignKeyModel(BaseDataViewListModel): +class TableForeignKeyModel(BaseObservableDataViewListModel): def GetValueByRow(self, row, col): if row >= len(self.data): diff --git a/windows/main/tabs/index.py b/windows/main/table/index.py similarity index 95% rename from windows/main/tabs/index.py rename to windows/main/table/index.py index 00bce6e..175e8e5 100755 --- a/windows/main/tabs/index.py +++ b/windows/main/table/index.py @@ -1,17 +1,17 @@ import wx import wx.dataview -from helpers.dataview import BaseDataViewListModel, ColumnField +from helpers.dataview import BaseObservableDataViewListModel, ColumnField from structures.helpers import merge_original_current -from windows.main import CURRENT_TABLE, CURRENT_INDEX -from windows.main.tabs.column import NEW_TABLE - from structures.engines.database import SQLTable, SQLIndex +from windows.state import NEW_TABLE +from windows.main import CURRENT_TABLE, CURRENT_INDEX + -class TableIndexModel(BaseDataViewListModel): +class TableIndexModel(BaseObservableDataViewListModel): MAP_COLUMN_FIELDS = { 0: ColumnField("name", lambda i, x: wx.dataview.DataViewIconText(i.name, wx.GetApp().icon_registry_16.get_bitmap(i.type.bitmap))), 1: ColumnField("expression", lambda i, x: ", ".join(i.columns)), diff --git a/windows/main/tabs/table.py b/windows/main/table/options.py similarity index 78% rename from windows/main/tabs/table.py rename to windows/main/table/options.py index e53116f..a948aa9 100644 --- a/windows/main/tabs/table.py +++ b/windows/main/table/options.py @@ -14,10 +14,13 @@ def __init__(self): self.auto_increment = Observable() self.collation = Observable() + self.convert_data = Observable() self.engine = Observable() + self.row_format = Observable() debounce( - self.name, self.comment, self.auto_increment, self.collation, self.engine, + self.name, self.comment, self.auto_increment, self.collation, self.convert_data, + self.engine, self.row_format, callback=self.update_table ) @@ -31,7 +34,9 @@ def _load_table(self, table: SQLTable): self.comment.set_initial(table.comment) self.auto_increment.set_initial(table.auto_increment) self.collation.set_initial(table.collation_name) + self.convert_data.set_initial(False) self.engine.set_initial(table.engine) + self.row_format.set_initial(getattr(table, "row_format", None)) def update_table(self, *args): if not any(args): @@ -45,6 +50,11 @@ def update_table(self, *args): table.collation_name = self.collation.get_value() table.engine = self.engine.get_value() + if hasattr(table, "convert_data"): + table.convert_data = bool(self.convert_data.get_value()) + if hasattr(table, "row_format"): + table.row_format = self.row_format.get_value() or None + if not table.is_new: original_table = next((t for t in CURRENT_DATABASE.get_value().tables if t.id == table.id), None) diff --git a/windows/main/tabs/records.py b/windows/main/table/records.py similarity index 59% rename from windows/main/tabs/records.py rename to windows/main/table/records.py index 4f15fd9..fe1d747 100644 --- a/windows/main/tabs/records.py +++ b/windows/main/table/records.py @@ -1,11 +1,13 @@ import datetime +from gettext import gettext as _ from typing import Optional +import wx import wx.dataview import wx.stc -from helpers.dataview import BaseDataViewListModel +from helpers.dataview import BaseObservableDataViewListModel from helpers.logger import logger from helpers.observables import ObservableList @@ -15,19 +17,30 @@ from windows.views import TableRecordsDataViewCtrl -from windows.dialogs.advanced_cell_editor import AdvancedCellEditorController +from windows.dialogs.column_content import ColumnContentDialogController from windows.main import CURRENT_TABLE, CURRENT_SESSION, CURRENT_DATABASE, AUTO_APPLY, CURRENT_RECORDS +from windows.main.table.executor import RecordsExecutor, RecordsOperationResult NEW_RECORDS: ObservableList[SQLRecord] = ObservableList() +NULL_DISPLAY = "NULL" -class RecordsModel(BaseDataViewListModel): + +class RecordsModel(BaseObservableDataViewListModel): def __init__(self, table: SQLTable, column_count: Optional[int] = None): super().__init__(column_count) self.table: SQLTable = table + def _load(self, data): + super()._load([record.copy() for record in data]) + + def _is_null(self, row, col): + column = self.table.columns[col] + record: SQLRecord = self.data[row] + return record.values.get(column.name) is None + def GetValueByRow(self, row, col): if not len(self.data): return None @@ -36,10 +49,12 @@ def GetValueByRow(self, row, col): record: SQLRecord = self.data[row] - value = record.values.get(column.name, "") + value = record.values.get(column.name) if value is None: - return '' + if column.datatype.name == "BOOLEAN": + return False + return NULL_DISPLAY if not str(value).strip(): return '' @@ -65,44 +80,43 @@ def GetValueByRow(self, row, col): return str(value) def SetValueByRow(self, value, row, col): - item = self.GetItem(row) - column: SQLColumn = self.table.columns[col] - self.data[row].values[column.name] = value + if value == NULL_DISPLAY or (isinstance(value, str) and not value.strip()): + value = None - self.ValueChanged(item, col) + self.data[row].values[column.name] = value return True def GetAttr(self, item, col, attr): - column: SQLColumn = self.table.columns[col] - - color = column.datatype.category.value.color + try: + column: SQLColumn = self.table.columns[col] + except Exception: + return False + + row = self.GetRow(item) + if 0 <= row < len(self.data) and self._is_null(row, col): + attr.SetItalic(True) + attr.SetColour(wx.Colour(180, 180, 120)) + else: + color = column.datatype.category.value.color + attr.SetColour(wx.Colour(color)) + + if column.is_primary_key: + attr.SetBold(True) - attr.SetColour(wx.Colour(color)) + return True - if self.table.columns[col].is_primary_key: - attr.SetBold(True) + def HasValue(self, item, col): + return bool(self.data) - return super().GetAttr(item, col, attr) def add_row(self, data: SQLRecord) -> wx.dataview.DataViewItem: self.data.append(data) self.RowAppended() return self.GetItem(len(self.data) - 1) - # - # def del_row(self, item: wx.dataview.DataViewItem): - # row = self.GetRow(item) - # del self.data[row] - # self.RowDeleted(row) - # - # def clear(self): - # self.data = [] - # self.Reset(0) - # self.Cleared() - class TableRecordsController: app = wx.GetApp() @@ -117,9 +131,12 @@ def __init__(self, list_ctrl_records: TableRecordsDataViewCtrl): CURRENT_SESSION.subscribe(self._load_session) CURRENT_DATABASE.subscribe(self._load_database) CURRENT_TABLE.subscribe(self._load_table) + + self.executor: Optional[RecordsExecutor] = None def _load_session(self, session: Session): self.session = session + self.executor = RecordsExecutor(session) if session else None def _load_database(self, database: SQLDatabase): self.database = database @@ -127,6 +144,40 @@ def _load_database(self, database: SQLDatabase): def _load_table(self, table: SQLTable): if table is not None: self.table = table + self.load_records_async() + + def _on_auto_apply_changed(self, auto_apply_enabled: bool): + """Handle auto-apply setting change and update toolbar states.""" + selected_records = self.get_selected_records() + self._update_toolbar_states(selected_records) + + def load_records_async(self, filters: Optional[str] = None, limit: int = 1000, offset: int = 0, orders: Optional[str] = None): + """Load records asynchronously using RecordsExecutor.""" + if not self.executor or not self.table: + return + + self.executor.load_records( + table=self.table, + on_complete=self._on_records_loaded, + filters=filters, + limit=limit, + offset=offset, + orders=orders + ) + + def _on_records_loaded(self, result: RecordsOperationResult): + """Handle completion of records loading.""" + if result.success and result.records is not None: + self.table.records.set_value(result.records) + self.load_model() + else: + logger.error(f"Failed to load records: {result.error}") + # Fallback to synchronous loading + try: + self.table.load_records() + self.load_model() + except Exception as ex: + logger.error(f"Fallback loading also failed: {ex}", exc_info=True) def load_model(self): self.model = RecordsModel(self.table, len(self.table.columns)) @@ -156,11 +207,9 @@ def _on_item_value_changed(self, event: wx.dataview.DataViewEvent): current_record.save() except Exception as ex: logger.error(f"Error saving record: {ex}", exc_info=True) - else: - records = list(self.session.context.get_records(table=self.table)) - - self.table.records.set_value(records) + # Refresh records after successful save + self.load_records_async() else: NEW_RECORDS.append(current_record, replace_existing=True) @@ -168,11 +217,54 @@ def _on_item_value_changed(self, event: wx.dataview.DataViewEvent): def _on_selection_changed(self, event: wx.dataview.DataViewEvent): logger.debug(f"{'#' * 10} ON SELECTION CHANGED {'#' * 10}") - CURRENT_RECORDS.set_value(self.get_selected_records()) + selected_records = self.get_selected_records() + CURRENT_RECORDS.set_value(selected_records) + + # Update toolbar states based on selection + self._update_toolbar_states(selected_records) + event.Skip() + + def _update_toolbar_states(self, selected_records: list): + """Update toolbar tool states based on record selection and auto-apply setting.""" + # This method provides the logic for toolbar state management + # The actual toolbar updates will be handled by the main controller + # through the CURRENT_RECORDS observable subscription + + # Calculate toolbar states + has_selection = len(selected_records) > 0 + has_single_selection = len(selected_records) == 1 + auto_apply_enabled = AUTO_APPLY.get_value() + + # Store states for the main controller to use + self._toolbar_states = { + 'duplicate_enabled': has_single_selection, + 'delete_enabled': has_selection, + 'apply_enabled': not auto_apply_enabled, + 'cancel_enabled': not auto_apply_enabled + } + + def get_selection_state(self): + """Return the current selection state for toolbar management.""" + selected_records = self.get_selected_records() + return { + 'has_selection': len(selected_records) > 0, + 'has_single_selection': len(selected_records) == 1, + 'auto_apply_enabled': AUTO_APPLY.get_value() + } + + def get_toolbar_states(self): + """Return the current toolbar states for the main controller.""" + return getattr(self, '_toolbar_states', { + 'duplicate_enabled': False, + 'delete_enabled': False, + 'apply_enabled': not AUTO_APPLY.get_value(), + 'cancel_enabled': not AUTO_APPLY.get_value() + }) + def make_advanced_dialog(self, parent, value: str): - dialog = AdvancedCellEditorController(parent, value) + dialog = ColumnContentDialogController(parent, value) return dialog @@ -227,6 +319,42 @@ def _do_new_empty_record(self, index: int, copy_from_selected: bool = False, use self._do_edit(new_empty_item, 1) + def do_apply_records(self): + """Save all pending records from NEW_RECORDS.""" + records = list(NEW_RECORDS) + if not records: + return + + errors = [] + for record in records: + try: + record.save() + except Exception as ex: + logger.error(f"Error saving record: {ex}", exc_info=True) + errors.append(str(ex)) + + NEW_RECORDS.clear() + + if errors: + wx.MessageBox( + "\n".join(errors), + _("Error saving records"), + wx.OK | wx.ICON_ERROR, + ) + + self.load_records_async() + + def do_cancel_records(self): + """Discard all pending changes in NEW_RECORDS.""" + NEW_RECORDS.clear() + if self.table: + self.load_model() + + def do_refresh_records(self): + """Refresh records from database.""" + if self.table: + self.load_records_async() + def do_insert_record(self): session = CURRENT_SESSION.get_value() table = CURRENT_TABLE.get_value() @@ -261,9 +389,19 @@ def do_delete_record(self): records = CURRENT_RECORDS.get_value() - SQLRecord.delete_many(table, records) - - CURRENT_RECORDS.set_value([]) + if records: + try: + SQLRecord.delete_many(table, records) + CURRENT_RECORDS.set_value([]) + # Refresh records after successful deletion + self.load_records_async() + except Exception as ex: + logger.error(f"Error deleting records: {ex}", exc_info=True) + wx.MessageBox( + f"Failed to delete records: {ex}", + "Error", + wx.OK | wx.ICON_ERROR + ) # def update_record(self, row, record): # if row < 0 or row >= len(self.list_ctrl_records.GetModel().records): diff --git a/windows/main/tabs/query.py b/windows/main/tabs/query.py deleted file mode 100644 index bc35b70..0000000 --- a/windows/main/tabs/query.py +++ /dev/null @@ -1,501 +0,0 @@ -import dataclasses -import enum -import threading -import time - -from typing import Any, Callable, Optional -from gettext import gettext as _ - -import wx -import wx.dataview - -from helpers.logger import logger - -from structures.session import Session -from structures.connection import ConnectionEngine - -from windows.components.dataview import QueryEditorResultsDataViewCtrl - - -@dataclasses.dataclass -class ParsedStatement: - text: str - start_pos: int - end_pos: int - statement_index: int - - -@dataclasses.dataclass -class ExecutionResult: - statement: ParsedStatement - success: bool - columns: Optional[list[str]] = None - rows: Optional[list[tuple]] = None - affected_rows: Optional[int] = None - elapsed_ms: float = 0.0 - error: Optional[str] = None - warnings: list[str] = dataclasses.field(default_factory=list) - - -class ExecutionMode(enum.Enum): - ALL = "all" - SELECTION = "selection" - CURRENT = "current" - - -class SQLStatementParser: - def __init__(self, engine: ConnectionEngine): - self.engine = engine - - def parse(self, sql_text: str) -> list[ParsedStatement]: - if not sql_text.strip(): - return [] - - statements = [] - statement_index = 0 - current_start = 0 - i = 0 - length = len(sql_text) - - in_single_quote = False - in_double_quote = False - in_line_comment = False - in_block_comment = False - - while i < length: - char = sql_text[i] - - if in_line_comment: - if char == '\n': - in_line_comment = False - i += 1 - continue - - if in_block_comment: - if i + 1 < length and sql_text[i:i+2] == '*/': - in_block_comment = False - i += 2 - continue - i += 1 - continue - - if not in_single_quote and not in_double_quote: - if self._is_line_comment_start(sql_text, i): - in_line_comment = True - i += 2 - continue - - if self._is_block_comment_start(sql_text, i): - in_block_comment = True - i += 2 - continue - - if char == "'" and not in_double_quote: - if i + 1 < length and sql_text[i+1] == "'": - i += 2 - continue - in_single_quote = not in_single_quote - - elif char == '"' and not in_single_quote: - if i + 1 < length and sql_text[i+1] == '"': - i += 2 - continue - in_double_quote = not in_double_quote - - elif char == ';' and not in_single_quote and not in_double_quote: - statement_text = sql_text[current_start:i].strip() - if statement_text: - statements.append(ParsedStatement( - text=statement_text, - start_pos=current_start, - end_pos=i, - statement_index=statement_index - )) - statement_index += 1 - current_start = i + 1 - - i += 1 - - final_statement = sql_text[current_start:].strip() - if final_statement: - statements.append(ParsedStatement( - text=final_statement, - start_pos=current_start, - end_pos=length, - statement_index=statement_index - )) - - return statements - - def _is_line_comment_start(self, text: str, pos: int) -> bool: - if pos + 1 >= len(text): - return False - return text[pos:pos+2] in ('--', '# ') - - def _is_block_comment_start(self, text: str, pos: int) -> bool: - if pos + 1 >= len(text): - return False - return text[pos:pos+2] == '/*' - - -class StatementSelector: - def __init__(self, stc_editor: wx.stc.StyledTextCtrl): - self.editor = stc_editor - - def get_execution_scope( - self, - statements: list[ParsedStatement] - ) -> tuple[ExecutionMode, list[ParsedStatement]]: - selection_start = self.editor.GetSelectionStart() - selection_end = self.editor.GetSelectionEnd() - - if selection_start != selection_end: - selected_text = self.editor.GetSelectedText().strip() - if selected_text: - return (ExecutionMode.SELECTION, [ParsedStatement( - text=selected_text, - start_pos=selection_start, - end_pos=selection_end, - statement_index=0 - )]) - - caret_pos = self.editor.GetCurrentPos() - current_stmt = self._find_statement_at_caret(caret_pos, statements) - - if current_stmt: - return (ExecutionMode.CURRENT, [current_stmt]) - - return (ExecutionMode.ALL, statements) - - def _find_statement_at_caret( - self, - caret_pos: int, - statements: list[ParsedStatement] - ) -> Optional[ParsedStatement]: - for stmt in statements: - if stmt.start_pos <= caret_pos <= stmt.end_pos: - return stmt - - # Caret is in whitespace: execute next statement - for stmt in statements: - if caret_pos < stmt.start_pos: - return stmt - - # Caret after all statements: execute last - if statements: - return statements[-1] - - return None - - -class QueryExecutor: - def __init__(self, session: Session): - self.session = session - self._cancel_requested = False - self._current_thread: Optional[threading.Thread] = None - self._lock = threading.Lock() - - def execute_statements( - self, - statements: list[ParsedStatement], - on_statement_complete: Callable[[ExecutionResult], None], - on_all_complete: Callable[[], None], - stop_on_error: bool = True - ) -> None: - self._cancel_requested = False - - self._current_thread = threading.Thread( - target=self._execute_worker, - args=(statements, on_statement_complete, on_all_complete, stop_on_error), - daemon=True - ) - self._current_thread.start() - - def _execute_worker( - self, - statements: list[ParsedStatement], - on_statement_complete: Callable[[ExecutionResult], None], - on_all_complete: Callable[[], None], - stop_on_error: bool - ) -> None: - try: - for stmt in statements: - if self._cancel_requested: - break - - result = self._execute_single(stmt) - # Thread-safe UI update - wx.CallAfter(on_statement_complete, result) - - if not result.success and stop_on_error: - break - - except Exception as ex: - logger.error(f"Execution worker error: {ex}", exc_info=True) - finally: - wx.CallAfter(on_all_complete) - - def _execute_single(self, statement: ParsedStatement) -> ExecutionResult: - start_time = time.time() - - try: - self.session.context.execute(statement.text) - - elapsed_ms = (time.time() - start_time) * 1000 - - cursor = self.session.context.cursor - if cursor.description: - columns = [desc[0] for desc in cursor.description] - rows = self.session.context.fetchall() - - return ExecutionResult( - statement=statement, - success=True, - columns=columns, - rows=rows, - affected_rows=len(rows), - elapsed_ms=elapsed_ms - ) - else: - affected = cursor.rowcount if cursor.rowcount >= 0 else 0 - - return ExecutionResult( - statement=statement, - success=True, - affected_rows=affected, - elapsed_ms=elapsed_ms - ) - - except Exception as ex: - elapsed_ms = (time.time() - start_time) * 1000 - - return ExecutionResult( - statement=statement, - success=False, - error=str(ex), - elapsed_ms=elapsed_ms - ) - - def cancel(self) -> None: - self._cancel_requested = True - - def is_running(self) -> bool: - return self._current_thread is not None and self._current_thread.is_alive() - - -class QueryResultsRenderer: - def __init__(self, notebook: wx.Notebook, session: Session): - self.notebook = notebook - self.session = session - self._tab_counter = 0 - - def create_result_tab(self, result: ExecutionResult) -> wx.Panel: - self._tab_counter += 1 - - panel = wx.Panel(self.notebook) - sizer = wx.BoxSizer(wx.VERTICAL) - - if result.success and result.columns: - grid = QueryEditorResultsDataViewCtrl(panel) - self._populate_grid(grid, result) - sizer.Add(grid, 1, wx.EXPAND | wx.ALL, 5) - - tab_name = self._generate_tab_name(result) - elif result.success: - msg = wx.StaticText(panel, label=_("{} rows affected").format(result.affected_rows or 0)) - msg.SetFont(msg.GetFont().MakeBold()) - sizer.Add(msg, 1, wx.ALIGN_CENTER | wx.ALL, 20) - - tab_name = _("Query {}").format(self._tab_counter) - else: - error_panel = self._create_error_panel(panel, result) - sizer.Add(error_panel, 1, wx.EXPAND | wx.ALL, 5) - - tab_name = _("Query {} (Error)").format(self._tab_counter) - - footer = self._create_footer(panel, result) - sizer.Add(footer, 0, wx.EXPAND | wx.ALL, 5) - - panel.SetSizer(sizer) - self.notebook.AddPage(panel, tab_name, select=True) - - return panel - - def _generate_tab_name(self, result: ExecutionResult) -> str: - if result.columns and result.rows is not None: - return _("Query {} ({} rows × {} cols)").format( - self._tab_counter, - len(result.rows), - len(result.columns) - ) - return _("Query {}").format(self._tab_counter) - - def _populate_grid( - self, - grid: QueryEditorResultsDataViewCtrl, - result: ExecutionResult - ) -> None: - if not result.columns or not result.rows: - return - - for col_name in result.columns: - grid.AppendTextColumn(col_name, wx.dataview.DATAVIEW_CELL_INERT) - - model = grid.GetModel() - if hasattr(model, 'data'): - model.data = list(result.rows) - model.Reset(len(result.rows)) - - def _create_footer(self, parent: wx.Panel, result: ExecutionResult) -> wx.StaticText: - parts = [] - - if result.affected_rows is not None: - parts.append(_("{} rows").format(result.affected_rows)) - - parts.append(_("{:.1f} ms").format(result.elapsed_ms)) - - if result.warnings: - parts.append(_("{} warnings").format(len(result.warnings))) - - footer_text = " | ".join(parts) - footer = wx.StaticText(parent, label=footer_text) - footer.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)) - - return footer - - def _create_error_panel(self, parent: wx.Panel, result: ExecutionResult) -> wx.Panel: - error_panel = wx.Panel(parent) - error_sizer = wx.BoxSizer(wx.VERTICAL) - - error_label = wx.StaticText(error_panel, label=_("Error:")) - error_label.SetFont(error_label.GetFont().MakeBold()) - error_sizer.Add(error_label, 0, wx.ALL, 5) - - error_text = wx.TextCtrl( - error_panel, - value=result.error or _("Unknown error"), - style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_WORDWRAP - ) - error_text.SetBackgroundColour(wx.Colour(255, 240, 240)) - error_sizer.Add(error_text, 1, wx.EXPAND | wx.ALL, 5) - - error_panel.SetSizer(error_sizer) - return error_panel - - def clear_all_tabs(self) -> None: - while self.notebook.GetPageCount() > 0: - self.notebook.DeletePage(0) - self._tab_counter = 0 - - -class QueryEditorController: - def __init__( - self, - stc_editor: wx.stc.StyledTextCtrl, - results_notebook: wx.Notebook, - session_provider: Callable[[], Optional[Session]] - ): - self.editor = stc_editor - self.notebook = results_notebook - self.get_session = session_provider - - self.parser: Optional[SQLStatementParser] = None - self.selector = StatementSelector(stc_editor) - self.executor: Optional[QueryExecutor] = None - self.renderer: Optional[QueryResultsRenderer] = None - - self._bind_shortcuts() - - def _bind_shortcuts(self) -> None: - self.editor.Bind(wx.EVT_KEY_DOWN, self._on_key_down) - - def _on_key_down(self, event: wx.KeyEvent) -> None: - key_code = event.GetKeyCode() - ctrl_down = event.ControlDown() - shift_down = event.ShiftDown() - - if key_code == wx.WXK_F5: - if shift_down: - self.cancel_execution(event) - else: - self.execute_all(event) - return - - if ctrl_down and key_code == wx.WXK_RETURN: - self.execute_current(event) - return - - if ctrl_down and shift_down and key_code == ord('C'): - self.cancel_execution(event) - return - - event.Skip() - - def execute_all(self, event: wx.Event) -> None: - self._execute(ExecutionMode.ALL) - - def execute_current(self, event: wx.Event) -> None: - self._execute(ExecutionMode.CURRENT) - - def cancel_execution(self, event: wx.Event) -> None: - if self.executor and self.executor.is_running(): - self.executor.cancel() - logger.info("Query execution cancelled") - - def _execute(self, mode: ExecutionMode) -> None: - session = self.get_session() - if not session or not session.is_connected: - wx.MessageBox( - _("No active database connection"), - _("Error"), - wx.OK | wx.ICON_ERROR - ) - return - - if not self.parser or self.parser.engine != session.engine: - self.parser = SQLStatementParser(session.engine) - self.executor = QueryExecutor(session) - self.renderer = QueryResultsRenderer(self.notebook, session) - - sql_text = self.editor.GetText() - if not sql_text.strip(): - return - - statements = self.parser.parse(sql_text) - if not statements: - return - - if mode == ExecutionMode.CURRENT or mode == ExecutionMode.SELECTION: - _, statements_to_execute = self.selector.get_execution_scope(statements) - else: - statements_to_execute = statements - - if not statements_to_execute: - return - - self.renderer.clear_all_tabs() - - self.executor.execute_statements( - statements=statements_to_execute, - on_statement_complete=self._on_statement_complete, - on_all_complete=self._on_all_complete, - stop_on_error=True - ) - - def _on_statement_complete(self, result: ExecutionResult) -> None: - if self.renderer: - self.renderer.create_result_tab(result) - - def _on_all_complete(self) -> None: - logger.info("Query execution completed") - - -class QueryResultsController(QueryEditorController): - def __init__(self, stc_sql_query: wx.stc.StyledTextCtrl, notebook_sql_results: wx.Notebook): - from windows.main import CURRENT_SESSION - - super().__init__( - stc_editor=stc_sql_query, - results_notebook=notebook_sql_results, - session_provider=lambda: CURRENT_SESSION.get_value() - ) diff --git a/windows/views.py b/windows/views.py index eb525bd..acf2939 100755 --- a/windows/views.py +++ b/windows/views.py @@ -18,7 +18,7 @@ import wx.dataview import wx.stc import wx.lib.agw.hypertreelist -import wx.aui +import wx.adv import gettext _ = gettext.gettext @@ -30,7 +30,7 @@ class ConnectionsDialog ( wx.Dialog ): def __init__( self, parent ): - wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Connection"), pos = wx.DefaultPosition, size = wx.Size( 800,600 ), style = wx.DEFAULT_DIALOG_STYLE|wx.DIALOG_NO_PARENT|wx.RESIZE_BORDER ) + wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Connection"), pos = wx.DefaultPosition, size = wx.Size( 900,768 ), style = wx.DEFAULT_DIALOG_STYLE|wx.DIALOG_NO_PARENT|wx.RESIZE_BORDER ) self.SetSizeHints( wx.Size( -1,-1 ), wx.DefaultSize ) @@ -169,22 +169,53 @@ def __init__( self, parent ): bSizer103.Add( bSizer1221, 0, wx.EXPAND, 5 ) + bSizer159 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText84 = wx.StaticText( self.panel_credentials, wx.ID_ANY, _(u"Connection timeout"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText84.Wrap( -1 ) + + self.m_staticText84.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer159.Add( self.m_staticText84, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) + + self.connection_timeout = wx.SpinCtrl( self.panel_credentials, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_ARROW_KEYS, 0, 60, 10 ) + bSizer159.Add( self.connection_timeout, 1, wx.ALL, 5 ) + + + bSizer103.Add( bSizer159, 1, wx.EXPAND, 5 ) + bSizer116 = wx.BoxSizer( wx.HORIZONTAL ) bSizer116.Add( ( 156, 0), 0, wx.EXPAND, 5 ) - self.use_tls_enabled = wx.CheckBox( self.panel_credentials, wx.ID_ANY, _(u"Use TLS"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer116.Add( self.use_tls_enabled, 0, wx.ALL, 5 ) + self.use_tls = wx.CheckBox( self.panel_credentials, wx.ID_ANY, _(u"Use TLS"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer116.Add( self.use_tls, 0, wx.ALL, 5 ) + + + bSizer103.Add( bSizer116, 0, wx.EXPAND, 5 ) + + bSizer163 = wx.BoxSizer( wx.HORIZONTAL ) + + + bSizer163.Add( ( 156, 0), 0, wx.EXPAND, 5 ) self.ssh_tunnel_enabled = wx.CheckBox( self.panel_credentials, wx.ID_ANY, _(u"Use SSH tunnel"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer116.Add( self.ssh_tunnel_enabled, 0, wx.ALL, 5 ) + bSizer163.Add( self.ssh_tunnel_enabled, 0, wx.ALL, 5 ) - bSizer103.Add( bSizer116, 0, wx.EXPAND, 5 ) + bSizer103.Add( bSizer163, 0, wx.EXPAND, 5 ) + + bSizer164 = wx.BoxSizer( wx.HORIZONTAL ) - self.m_staticline5 = wx.StaticLine( self.panel_credentials, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) - bSizer103.Add( self.m_staticline5, 0, wx.EXPAND | wx.ALL, 5 ) + + bSizer164.Add( ( 156, 0), 0, wx.EXPAND, 5 ) + + self.compressed_protocol = wx.CheckBox( self.panel_credentials, wx.ID_ANY, _(u"Compressed client/server protocol"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer164.Add( self.compressed_protocol, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + + bSizer103.Add( bSizer164, 0, wx.EXPAND, 5 ) self.panel_credentials.SetSizer( bSizer103 ) @@ -216,6 +247,9 @@ def __init__( self, parent ): bSizer105.Fit( self.panel_source ) bSizer12.Add( self.panel_source, 0, wx.EXPAND | wx.ALL, 0 ) + self.m_staticline5 = wx.StaticLine( self.panel_connection, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) + bSizer12.Add( self.m_staticline5, 0, wx.EXPAND | wx.ALL, 5 ) + bSizer122111 = wx.BoxSizer( wx.HORIZONTAL ) self.m_staticText22111 = wx.StaticText( self.panel_connection, wx.ID_ANY, _(u"Comments"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) @@ -351,6 +385,19 @@ def __init__( self, parent ): bSizer102.Add( bSizer121311, 0, wx.EXPAND, 5 ) + bSizer121322 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText21322 = wx.StaticText( self.panel_ssh_tunnel, wx.ID_ANY, _(u"SSH extra args"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) + self.m_staticText21322.Wrap( -1 ) + + bSizer121322.Add( self.m_staticText21322, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + self.ssh_tunnel_extra_args = wx.TextCtrl( self.panel_ssh_tunnel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer121322.Add( self.ssh_tunnel_extra_args, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) + + + bSizer102.Add( bSizer121322, 0, wx.EXPAND, 5 ) + self.panel_ssh_tunnel.SetSizer( bSizer102 ) self.panel_ssh_tunnel.Layout() @@ -480,7 +527,7 @@ def __init__( self, parent ): bSizer37111211 = wx.BoxSizer( wx.HORIZONTAL ) - self.m_staticText15111211 = wx.StaticText( self.panel_statistics, wx.ID_ANY, _(u" Average connection time (ms)"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText15111211 = wx.StaticText( self.panel_statistics, wx.ID_ANY, _(u"Average connection time (ms)"), wx.DefaultPosition, wx.DefaultSize, 0 ) self.m_staticText15111211.Wrap( -1 ) self.m_staticText15111211.SetMinSize( wx.Size( 200,-1 ) ) @@ -497,7 +544,7 @@ def __init__( self, parent ): bSizer371112111 = wx.BoxSizer( wx.HORIZONTAL ) - self.m_staticText151112111 = wx.StaticText( self.panel_statistics, wx.ID_ANY, _(u" Most recent connection duration"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText151112111 = wx.StaticText( self.panel_statistics, wx.ID_ANY, _(u"Most recent connection duration"), wx.DefaultPosition, wx.DefaultSize, 0 ) self.m_staticText151112111.Wrap( -1 ) self.m_staticText151112111.SetMinSize( wx.Size( 200,-1 ) ) @@ -724,13 +771,13 @@ def __del__( self ): ########################################################################### -## Class AdvancedCellEditorDialog +## Class ColumnContentDialog ########################################################################### -class AdvancedCellEditorDialog ( wx.Dialog ): +class ColumnContentDialog ( wx.Dialog ): def __init__( self, parent ): - wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Edit Value"), pos = wx.DefaultPosition, size = wx.Size( 900,550 ), style = wx.DEFAULT_DIALOG_STYLE ) + wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Column content"), pos = wx.DefaultPosition, size = wx.Size( 900,550 ), style = wx.DEFAULT_DIALOG_STYLE ) self.SetSizeHints( wx.Size( 640,480 ), wx.DefaultSize ) @@ -850,10 +897,10 @@ def __init__( self, parent ): self.m_toolBar1 = self.CreateToolBar( wx.TB_HORIZONTAL, wx.ID_ANY ) self.m_tool5 = self.m_toolBar1.AddTool( wx.ID_ANY, _(u"Open connection manager"), wx.Bitmap( u"icons/16x16/server_connect.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - self.m_tool4 = self.m_toolBar1.AddTool( wx.ID_ANY, _(u"Disconnect from server"), wx.Bitmap( u"icons/16x16/disconnect.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - self.m_toolBar1.AddSeparator() + self.m_tool4 = self.m_toolBar1.AddTool( wx.ID_ANY, _(u"Disconnect from server"), wx.Bitmap( u"icons/16x16/disconnect.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + self.database_refresh = self.m_toolBar1.AddTool( wx.ID_ANY, _(u"tool"), wx.Bitmap( u"icons/16x16/database_refresh.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Refresh"), _(u"Refresh"), None ) self.m_toolBar1.AddSeparator() @@ -948,6 +995,9 @@ def __init__( self, parent ): bSizer80 = wx.BoxSizer( wx.VERTICAL ) self.m_splitter7 = wx.SplitterWindow( self.m_panel30, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_3D ) + self.m_splitter7.Bind( wx.EVT_IDLE, self.m_splitter7OnIdle ) + self.m_splitter7.SetMinimumPaneSize( 200 ) + self.m_panel54 = wx.Panel( self.m_splitter7, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer158 = wx.BoxSizer( wx.VERTICAL ) @@ -973,27 +1023,6 @@ def __init__( self, parent ): bSizer142 = wx.BoxSizer( wx.HORIZONTAL ) - self.database_character_set_panel = wx.Panel( self.m_panel54, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer139 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText70 = wx.StaticText( self.database_character_set_panel, wx.ID_ANY, _(u"Character set"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText70.Wrap( -1 ) - - self.m_staticText70.SetMinSize( wx.Size( 150,-1 ) ) - - bSizer139.Add( self.m_staticText70, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - database_character_setChoices = [] - self.database_character_set = wx.Choice( self.database_character_set_panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, database_character_setChoices, 0 ) - self.database_character_set.SetSelection( 0 ) - bSizer139.Add( self.database_character_set, 1, wx.ALL, 5 ) - - - self.database_character_set_panel.SetSizer( bSizer139 ) - self.database_character_set_panel.Layout() - bSizer139.Fit( self.database_character_set_panel ) - bSizer142.Add( self.database_character_set_panel, 1, wx.ALIGN_CENTER, 5 ) - self.database_collation_panel = wx.Panel( self.m_panel54, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer1392 = wx.BoxSizer( wx.HORIZONTAL ) @@ -1016,6 +1045,9 @@ def __init__( self, parent ): bSizer142.Add( self.database_collation_panel, 1, wx.ALIGN_CENTER, 5 ) + bSizer142.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + + bSizer158.Add( bSizer142, 0, wx.EXPAND, 5 ) bSizer13911 = wx.BoxSizer( wx.HORIZONTAL ) @@ -1320,18 +1352,24 @@ def __init__( self, parent ): self.m_panel55.SetSizer( bSizer154 ) self.m_panel55.Layout() bSizer154.Fit( self.m_panel55 ) - self.m_splitter7.SplitHorizontally( self.m_panel54, self.m_panel55, -1 ) + self.m_splitter7.SplitHorizontally( self.m_panel54, self.m_panel55, 200 ) bSizer80.Add( self.m_splitter7, 1, wx.EXPAND, 5 ) bSizer138 = wx.BoxSizer( wx.HORIZONTAL ) self.btn_cancel_database = wx.Button( self.m_panel30, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btn_cancel_database.Enable( False ) + bSizer138.Add( self.btn_cancel_database, 0, wx.ALL, 5 ) self.btn_delete_database = wx.Button( self.m_panel30, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btn_delete_database.Enable( False ) + bSizer138.Add( self.btn_delete_database, 0, wx.ALL, 5 ) self.btn_apply_database = wx.Button( self.m_panel30, wx.ID_ANY, _(u"Apply"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btn_apply_database.Enable( False ) + bSizer138.Add( self.btn_apply_database, 0, wx.ALL, 5 ) @@ -1470,9 +1508,9 @@ def __init__( self, parent ): bSizer2712.Add( self.m_staticText812, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - table_engineChoices = [ wx.EmptyString ] + table_engineChoices = [] self.table_engine = wx.Choice( self.PanelTableOptions, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, table_engineChoices, 0 ) - self.table_engine.SetSelection( 1 ) + self.table_engine.SetSelection( 0 ) bSizer2712.Add( self.table_engine, 1, wx.ALL|wx.EXPAND, 5 ) @@ -1490,9 +1528,29 @@ def __init__( self, parent ): self.table_collation.SetSelection( 0 ) bSizer2721.Add( self.table_collation, 1, wx.ALL, 5 ) + self.convert_data_collation = wx.CheckBox( self.PanelTableOptions, wx.ID_ANY, _(u"Convert data"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer2721.Add( self.convert_data_collation, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + gSizer11.Add( bSizer2721, 0, wx.EXPAND, 5 ) + bSizer145 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText71 = wx.StaticText( self.PanelTableOptions, wx.ID_ANY, _(u"Row format"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText71.Wrap( -1 ) + + self.m_staticText71.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer145.Add( self.m_staticText71, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + table_row_formatChoices = [] + self.table_row_format = wx.Choice( self.PanelTableOptions, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, table_row_formatChoices, 0 ) + self.table_row_format.SetSelection( 0 ) + bSizer145.Add( self.table_row_format, 1, wx.ALL, 5 ) + + + gSizer11.Add( bSizer145, 1, wx.EXPAND, 5 ) + bSizer261.Add( gSizer11, 0, wx.EXPAND, 5 ) @@ -1688,47 +1746,24 @@ def __init__( self, parent ): bSizer54 = wx.BoxSizer( wx.VERTICAL ) - bSizer53 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText39 = wx.StaticText( self.panel_table_columns, wx.ID_ANY, _(u"Columns:"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.toolbar_columns = wx.ToolBar( self.panel_table_columns, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL|wx.TB_HORZ_TEXT ) + self.m_staticText39 = wx.StaticText( self.toolbar_columns, wx.ID_ANY, _(u"Columns:"), wx.DefaultPosition, wx.DefaultSize, 0 ) self.m_staticText39.Wrap( -1 ) - bSizer53.Add( self.m_staticText39, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - + self.toolbar_columns.AddControl( self.m_staticText39 ) + self.tool_add_column = self.toolbar_columns.AddTool( wx.ID_ANY, _(u"Add"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - bSizer53.Add( ( 100, 0), 0, wx.EXPAND, 5 ) - - self.btn_insert_column = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Insert"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_insert_column.SetBitmap( wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ) ) - bSizer53.Add( self.btn_insert_column, 0, wx.LEFT|wx.RIGHT, 2 ) + self.tool_remove_column = self.toolbar_columns.AddTool( wx.ID_ANY, _(u"Remove"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - self.btn_delete_column = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_delete_column.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_column.Enable( False ) - - bSizer53.Add( self.btn_delete_column, 0, wx.LEFT|wx.RIGHT, 2 ) - - self.btn_move_up_column = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Up"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_move_up_column.SetBitmap( wx.Bitmap( u"icons/16x16/arrow_up.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_move_up_column.Enable( False ) - - bSizer53.Add( self.btn_move_up_column, 0, wx.LEFT|wx.RIGHT, 2 ) - - self.btn_move_down_column = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Down"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_move_down_column.SetBitmap( wx.Bitmap( u"icons/16x16/arrow_down.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_move_down_column.Enable( False ) - - bSizer53.Add( self.btn_move_down_column, 0, wx.LEFT|wx.RIGHT, 2 ) + self.toolbar_columns.AddSeparator() + self.tool_move_up_column = self.toolbar_columns.AddTool( wx.ID_ANY, _(u"Move Up"), wx.Bitmap( u"icons/16x16/arrow_up.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - bSizer53.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + self.tool_move_down_column = self.toolbar_columns.AddTool( wx.ID_ANY, _(u"Move Down"), wx.Bitmap( u"icons/16x16/arrow_down.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + self.toolbar_columns.Realize() - bSizer54.Add( bSizer53, 0, wx.ALL|wx.EXPAND, 5 ) + bSizer54.Add( self.toolbar_columns, 0, wx.EXPAND, 5 ) self.list_ctrl_table_columns = TableColumnsDataViewCtrl( self.panel_table_columns, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) bSizer54.Add( self.list_ctrl_table_columns, 1, wx.ALL|wx.EXPAND, 5 ) @@ -1777,7 +1812,7 @@ def __init__( self, parent ): self.panel_table.SetSizer( bSizer251 ) self.panel_table.Layout() bSizer251.Fit( self.panel_table ) - self.MainFrameNotebook.AddPage( self.panel_table, _(u"Table"), True ) + self.MainFrameNotebook.AddPage( self.panel_table, _(u"Table"), False ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/table.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2034,74 +2069,72 @@ def __init__( self, parent ): self.panel_records = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer61 = wx.BoxSizer( wx.VERTICAL ) - bSizer94 = wx.BoxSizer( wx.HORIZONTAL ) - - self.name_database_table = wx.StaticText( self.panel_records, wx.ID_ANY, _(u"Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.name_database_table.Wrap( -1 ) - - bSizer94.Add( self.name_database_table, 0, wx.ALL, 5 ) + self.m_toolBar3 = wx.ToolBar( self.panel_records, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL|wx.TB_HORZ_TEXT ) + self.tool_refresh_records = self.m_toolBar3.AddTool( wx.ID_ANY, _(u"Refrsh"), wx.Bitmap( u"icons/16x16/arrow_refresh.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + self.m_toolBar3.AddSeparator() - bSizer61.Add( bSizer94, 0, wx.EXPAND, 5 ) + self.tool_insert_record = self.m_toolBar3.AddTool( wx.ID_ANY, _(u"Add"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - bSizer83 = wx.BoxSizer( wx.HORIZONTAL ) + self.tool_duplicate_record = self.m_toolBar3.AddTool( wx.ID_ANY, _(u"Duplicate"), wx.Bitmap( u"icons/16x16/page_copy_columns.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - self.btn_insert_record = wx.Button( self.panel_records, wx.ID_ANY, _(u"Insert record"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.tool_delete_record = self.m_toolBar3.AddTool( wx.ID_ANY, _(u"Remove"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - self.btn_insert_record.SetBitmap( wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ) ) - bSizer83.Add( self.btn_insert_record, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + self.m_toolBar3.AddSeparator() - self.btn_duplicate_record = wx.Button( self.panel_records, wx.ID_ANY, _(u"Duplicate record"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.chb_auto_apply = wx.CheckBox( self.m_toolBar3, wx.ID_ANY, _(u"Apply changes automatically"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.chb_auto_apply.SetValue(True) + self.chb_auto_apply.SetToolTip( _(u"If enabled, table edits are applied immediately without pressing Apply or Cancel") ) + self.chb_auto_apply.SetHelpText( _(u"If enabled, table edits are applied immediately without pressing Apply or Cancel") ) - self.btn_duplicate_record.SetBitmap( wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_duplicate_record.Enable( False ) + self.m_toolBar3.AddControl( self.chb_auto_apply ) + self.tool_apply_record = self.m_toolBar3.AddTool( wx.ID_ANY, _(u"Apply"), wx.Bitmap( u"icons/16x16/tick.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - bSizer83.Add( self.btn_duplicate_record, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + self.tool_cancel_record = self.m_toolBar3.AddTool( wx.ID_ANY, _(u"Cancel"), wx.Bitmap( u"icons/16x16/cross.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - self.btn_delete_record = wx.Button( self.panel_records, wx.ID_ANY, _(u"Delete record"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.m_toolBar3.Realize() - self.btn_delete_record.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_record.Enable( False ) + bSizer61.Add( self.m_toolBar3, 0, wx.EXPAND, 5 ) - bSizer83.Add( self.btn_delete_record, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + bSizer94 = wx.BoxSizer( wx.HORIZONTAL ) - self.m_staticline3 = wx.StaticLine( self.panel_records, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_VERTICAL ) - bSizer83.Add( self.m_staticline3, 0, wx.EXPAND | wx.ALL, 5 ) + self.name_database_table = wx.StaticText( self.panel_records, wx.ID_ANY, _(u"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.name_database_table.Wrap( -1 ) - self.chb_auto_apply = wx.CheckBox( self.panel_records, wx.ID_ANY, _(u"Apply changes automatically"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.chb_auto_apply.SetValue(True) - self.chb_auto_apply.SetToolTip( _(u"If enabled, table edits are applied immediately without pressing Apply or Cancel") ) - self.chb_auto_apply.SetHelpText( _(u"If enabled, table edits are applied immediately without pressing Apply or Cancel") ) + bSizer94.Add( self.name_database_table, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - bSizer83.Add( self.chb_auto_apply, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - self.btn_cancel_record = wx.Button( self.panel_records, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + bSizer94.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - self.btn_cancel_record.SetBitmap( wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_cancel_record.Enable( False ) + self.btn_first_records = wx.Button( self.panel_records, wx.ID_ANY, _(u"First"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - bSizer83.Add( self.btn_cancel_record, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + self.btn_first_records.SetBitmap( wx.Bitmap( u"icons/16x16/resultset_first.png", wx.BITMAP_TYPE_ANY ) ) + bSizer94.Add( self.btn_first_records, 0, wx.ALL|wx.EXPAND, 5 ) - self.btn_apply_record = wx.Button( self.panel_records, wx.ID_ANY, _(u"Apply"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.btn_prev_records = wx.Button( self.panel_records, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE|wx.BU_EXACTFIT ) - self.btn_apply_record.SetBitmap( wx.Bitmap( u"icons/16x16/disk.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_apply_record.Enable( False ) + self.btn_prev_records.SetBitmap( wx.Bitmap( u"icons/16x16/arrow_left.png", wx.BITMAP_TYPE_ANY ) ) + bSizer94.Add( self.btn_prev_records, 0, wx.ALL|wx.EXPAND, 5 ) - bSizer83.Add( self.btn_apply_record, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + self.limit_records = wx.SpinCtrl( self.panel_records, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_ARROW_KEYS, 0, 1000, 100 ) + bSizer94.Add( self.limit_records, 0, wx.ALL, 5 ) + self.btn_next_records = wx.Button( self.panel_records, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE|wx.BU_EXACTFIT|wx.BU_NOTEXT ) - bSizer83.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + self.btn_next_records.SetBitmap( wx.Bitmap( u"icons/16x16/resultset_next.png", wx.BITMAP_TYPE_ANY ) ) + bSizer94.Add( self.btn_next_records, 0, wx.ALL|wx.EXPAND, 5 ) - self.m_button40 = wx.Button( self.panel_records, wx.ID_ANY, _(u"Next"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.btn_last_records = wx.Button( self.panel_records, wx.ID_ANY, _(u"Last"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - self.m_button40.SetBitmap( wx.Bitmap( u"icons/16x16/resultset_next.png", wx.BITMAP_TYPE_ANY ) ) - bSizer83.Add( self.m_button40, 0, wx.ALL, 5 ) + self.btn_last_records.SetBitmap( wx.Bitmap( u"icons/16x16/resultset_last.png", wx.BITMAP_TYPE_ANY ) ) + self.btn_last_records.SetBitmapPosition( wx.RIGHT ) + bSizer94.Add( self.btn_last_records, 0, wx.ALL|wx.EXPAND, 5 ) - bSizer61.Add( bSizer83, 0, wx.EXPAND, 5 ) + bSizer61.Add( bSizer94, 0, wx.EXPAND, 5 ) self.m_collapsiblePane1 = wx.CollapsiblePane( self.panel_records, wx.ID_ANY, _(u"Filters"), wx.DefaultPosition, wx.DefaultSize, wx.CP_DEFAULT_STYLE|wx.CP_NO_TLW_RESIZE|wx.FULL_REPAINT_ON_RESIZE ) - self.m_collapsiblePane1.Collapse( False ) + self.m_collapsiblePane1.Collapse( True ) bSizer831 = wx.BoxSizer( wx.VERTICAL ) @@ -2168,7 +2201,7 @@ def __init__( self, parent ): self.panel_records.Bind( wx.EVT_RIGHT_DOWN, self.panel_recordsOnContextMenu ) - self.MainFrameNotebook.AddPage( self.panel_records, _(u"Data"), False ) + self.MainFrameNotebook.AddPage( self.panel_records, _(u"Data"), True ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/text_columns.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2184,7 +2217,32 @@ def __init__( self, parent ): self.m_panel52 = wx.Panel( self.m_splitter6, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer125 = wx.BoxSizer( wx.VERTICAL ) - self.sql_query_editor = wx.stc.StyledTextCtrl( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0) + self.m_toolBar2 = wx.ToolBar( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL|wx.TB_HORZ_TEXT ) + self.new_query = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Add"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"New query"), wx.EmptyString, None ) + + self.close_query = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Close"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Close query"), wx.EmptyString, None ) + + self.m_toolBar2.AddSeparator() + + self.execute_statement = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Run"), wx.Bitmap( u"icons/16x16/arrow_right.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Execute"), wx.EmptyString, None ) + + self.execute_all_statements = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Run all"), wx.Bitmap( u"icons/16x16/arrows_lefttoright.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Execute all statements"), wx.EmptyString, None ) + + self.stop_statements = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Stop"), wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Stop"), wx.EmptyString, None ) + + self.m_toolBar2.AddSeparator() + + self.save = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Save"), wx.Bitmap( u"icons/16x16/disk.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + + self.m_toolBar2.Realize() + + bSizer125.Add( self.m_toolBar2, 0, wx.EXPAND, 5 ) + + self.notebook_query_editor = wx.Notebook( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_panel63 = wx.Panel( self.notebook_query_editor, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer146 = wx.BoxSizer( wx.VERTICAL ) + + self.sql_query_editor = wx.stc.StyledTextCtrl( self.m_panel63, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0) self.sql_query_editor.SetUseTabs ( True ) self.sql_query_editor.SetTabWidth ( 4 ) self.sql_query_editor.SetIndent ( 4 ) @@ -2220,10 +2278,15 @@ def __init__( self, parent ): self.sql_query_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY ) self.sql_query_editor.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) ) self.sql_query_editor.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) ) - bSizer125.Add( self.sql_query_editor, 1, wx.EXPAND | wx.ALL, 5 ) + bSizer146.Add( self.sql_query_editor, 1, wx.EXPAND | wx.ALL, 5 ) + - self.m_button12 = wx.Button( self.m_panel52, wx.ID_ANY, _(u"New"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer125.Add( self.m_button12, 0, wx.ALIGN_RIGHT|wx.ALL, 5 ) + self.m_panel63.SetSizer( bSizer146 ) + self.m_panel63.Layout() + bSizer146.Fit( self.m_panel63 ) + self.notebook_query_editor.AddPage( self.m_panel63, _(u"a page"), False ) + + bSizer125.Add( self.notebook_query_editor, 1, wx.EXPAND | wx.ALL, 5 ) self.m_panel52.SetSizer( bSizer125 ) @@ -2234,9 +2297,9 @@ def __init__( self, parent ): bSizer1261 = wx.BoxSizer( wx.VERTICAL ) - self.notebook_sql_results = FlatNotebook( self.m_panel53, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.notebook_query_results = FlatNotebook( self.m_panel53, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer1261.Add( self.notebook_sql_results, 1, wx.EXPAND | wx.ALL, 5 ) + bSizer1261.Add( self.notebook_query_results, 1, wx.EXPAND | wx.ALL, 5 ) self.m_panel53.SetSizer( bSizer1261 ) @@ -2256,33 +2319,6 @@ def __init__( self, parent ): self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex ) MainFrameNotebookIndex += 1 - self.QueryPanelTpl = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - self.QueryPanelTpl.Hide() - - bSizer263 = wx.BoxSizer( wx.VERTICAL ) - - self.m_textCtrl101 = wx.TextCtrl( self.QueryPanelTpl, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE|wx.TE_RICH|wx.TE_RICH2 ) - bSizer263.Add( self.m_textCtrl101, 1, wx.ALL|wx.EXPAND, 5 ) - - bSizer49 = wx.BoxSizer( wx.HORIZONTAL ) - - - bSizer49.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.m_button17 = wx.Button( self.QueryPanelTpl, wx.ID_ANY, _(u"Close"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer49.Add( self.m_button17, 0, wx.ALL, 5 ) - - self.m_button121 = wx.Button( self.QueryPanelTpl, wx.ID_ANY, _(u"New"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer49.Add( self.m_button121, 0, wx.ALL, 5 ) - - - bSizer263.Add( bSizer49, 0, wx.EXPAND, 5 ) - - - self.QueryPanelTpl.SetSizer( bSizer263 ) - self.QueryPanelTpl.Layout() - bSizer263.Fit( self.QueryPanelTpl ) - self.MainFrameNotebook.AddPage( self.QueryPanelTpl, _(u"Query #2"), False ) bSizer25.Add( self.MainFrameNotebook, 1, wx.ALL|wx.EXPAND, 5 ) @@ -2358,7 +2394,8 @@ def __init__( self, parent ): self.Bind( wx.EVT_MENU, self.on_settings, id = self.m_menuItem22.GetId() ) self.Bind( wx.EVT_MENU, self.on_menu_about, id = self.m_menuItem15.GetId() ) self.Bind( wx.EVT_TOOL, self.do_open_connection_manager, id = self.m_tool5.GetId() ) - self.Bind( wx.EVT_TOOL, self.do_disconnect, id = self.m_tool4.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_database_disconnect, id = self.m_tool4.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_database_refresh, id = self.database_refresh.GetId() ) self.MainFrameNotebook.Bind( wx.EVT_NOTEBOOK_PAGE_CHANGED, self.on_page_chaged ) self.btn_insert_table.Bind( wx.EVT_BUTTON, self.on_insert_table ) self.btn_clone_table.Bind( wx.EVT_BUTTON, self.on_clone_table ) @@ -2374,20 +2411,32 @@ def __init__( self, parent ): self.btn_insert_check.Bind( wx.EVT_BUTTON, self.on_insert_foreign_key ) self.btn_delete_check.Bind( wx.EVT_BUTTON, self.on_delete_foreign_key ) self.btn_clear_check.Bind( wx.EVT_BUTTON, self.on_clear_foreign_key ) - self.btn_insert_column.Bind( wx.EVT_BUTTON, self.on_insert_column ) - self.btn_delete_column.Bind( wx.EVT_BUTTON, self.on_delete_column ) - self.btn_move_up_column.Bind( wx.EVT_BUTTON, self.on_move_up_column ) - self.btn_move_down_column.Bind( wx.EVT_BUTTON, self.on_move_down_column ) + self.Bind( wx.EVT_TOOL, self.on_insert_column, id = self.tool_add_column.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_column, id = self.tool_remove_column.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_move_up_column, id = self.tool_move_up_column.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_move_down_column, id = self.tool_move_down_column.GetId() ) self.btn_delete_table.Bind( wx.EVT_BUTTON, self.on_delete_table ) self.btn_cancel_table.Bind( wx.EVT_BUTTON, self.on_cancel_table ) self.btn_apply_table.Bind( wx.EVT_BUTTON, self.do_apply_table ) - self.btn_insert_record.Bind( wx.EVT_BUTTON, self.on_insert_record ) - self.btn_duplicate_record.Bind( wx.EVT_BUTTON, self.on_duplicate_record ) - self.btn_delete_record.Bind( wx.EVT_BUTTON, self.on_delete_record ) + self.Bind( wx.EVT_TOOL, self.on_refresh_records, id = self.tool_refresh_records.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_insert_record, id = self.tool_insert_record.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_duplicate_record, id = self.tool_duplicate_record.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_record, id = self.tool_delete_record.GetId() ) self.chb_auto_apply.Bind( wx.EVT_CHECKBOX, self.on_auto_apply ) - self.m_button40.Bind( wx.EVT_BUTTON, self.on_next_records ) + self.Bind( wx.EVT_TOOL, self.on_apply_record, id = self.tool_apply_record.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_cancel_record, id = self.tool_cancel_record.GetId() ) + self.btn_first_records.Bind( wx.EVT_BUTTON, self.on_first_records ) + self.btn_prev_records.Bind( wx.EVT_BUTTON, self.on_prev_records ) + self.btn_next_records.Bind( wx.EVT_BUTTON, self.on_next_records ) + self.btn_last_records.Bind( wx.EVT_BUTTON, self.on_last_records ) self.m_collapsiblePane1.Bind( wx.EVT_COLLAPSIBLEPANE_CHANGED, self.on_collapsible_pane_changed ) self.m_button41.Bind( wx.EVT_BUTTON, self.on_apply_filters ) + self.Bind( wx.EVT_TOOL, self.on_new_query, id = self.new_query.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_close_query, id = self.close_query.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_execute_statement, id = self.execute_statement.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_execute_statements, id = self.execute_all_statements.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_stop_statements, id = self.stop_statements.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_save, id = self.save.GetId() ) def __del__( self ): pass @@ -2406,7 +2455,10 @@ def on_menu_about( self, event ): def do_open_connection_manager( self, event ): event.Skip() - def do_disconnect( self, event ): + def on_database_disconnect( self, event ): + event.Skip() + + def on_database_refresh( self, event ): event.Skip() def on_page_chaged( self, event ): @@ -2467,6 +2519,9 @@ def on_cancel_table( self, event ): def do_apply_table( self, event ): event.Skip() + def on_refresh_records( self, event ): + event.Skip() + def on_insert_record( self, event ): event.Skip() @@ -2479,15 +2534,48 @@ def on_delete_record( self, event ): def on_auto_apply( self, event ): event.Skip() + def on_apply_record( self, event ): + event.Skip() + + def on_cancel_record( self, event ): + event.Skip() + + def on_first_records( self, event ): + event.Skip() + + def on_prev_records( self, event ): + event.Skip() + def on_next_records( self, event ): event.Skip() + def on_last_records( self, event ): + event.Skip() + def on_collapsible_pane_changed( self, event ): event.Skip() def on_apply_filters( self, event ): event.Skip() + def on_new_query( self, event ): + event.Skip() + + def on_close_query( self, event ): + event.Skip() + + def on_execute_statement( self, event ): + event.Skip() + + def on_execute_statements( self, event ): + event.Skip() + + def on_stop_statements( self, event ): + event.Skip() + + def on_save( self, event ): + event.Skip() + def m_splitter51OnIdle( self, event ): self.m_splitter51.SetSashPosition( -150 ) self.m_splitter51.Unbind( wx.EVT_IDLE ) @@ -2499,6 +2587,10 @@ def m_splitter4OnIdle( self, event ): def m_panel14OnContextMenu( self, event ): self.m_panel14.PopupMenu( self.m_menu5, event.GetPosition() ) + def m_splitter7OnIdle( self, event ): + self.m_splitter7.SetSashPosition( 200 ) + self.m_splitter7.Unbind( wx.EVT_IDLE ) + def panel_databaseOnContextMenu( self, event ): self.panel_database.PopupMenu( self.m_menu15, event.GetPosition() ) @@ -2526,584 +2618,176 @@ class Trash ( wx.Panel ): def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.Size( 500,300 ), style = wx.TAB_TRAVERSAL, name = wx.EmptyString ): wx.Panel.__init__ ( self, parent, id = id, pos = pos, size = size, style = style, name = name ) - bSizer90 = wx.BoxSizer( wx.VERTICAL ) - - self.m_textCtrl221 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.m_textCtrl221, 1, wx.ALL|wx.EXPAND, 5 ) - - bSizer93 = wx.BoxSizer( wx.VERTICAL ) - - self.tree_ctrl_explorer____ = wx.dataview.TreeListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.TL_DEFAULT_STYLE ) - self.tree_ctrl_explorer____.AppendColumn( _(u"Column5"), wx.COL_WIDTH_DEFAULT, wx.ALIGN_LEFT, wx.COL_RESIZABLE ) - - bSizer93.Add( self.tree_ctrl_explorer____, 1, wx.EXPAND | wx.ALL, 5 ) - - bSizer129 = wx.BoxSizer( wx.VERTICAL ) - - self.m_radioBtn11 = wx.RadioButton( self, wx.ID_ANY, _(u"UNDEFINED"), wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP ) - bSizer129.Add( self.m_radioBtn11, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_radioBtn21 = wx.RadioButton( self, wx.ID_ANY, _(u"MERGE"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_menu13 = wx.Menu() - self.m_menuItem10 = wx.MenuItem( self.m_menu13, wx.ID_ANY, _(u"Import"), wx.EmptyString, wx.ITEM_NORMAL ) - self.m_menu13.Append( self.m_menuItem10 ) - - self.m_radioBtn21.Bind( wx.EVT_RIGHT_DOWN, self.m_radioBtn21OnContextMenu ) - - bSizer129.Add( self.m_radioBtn21, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_radioBtn31 = wx.RadioButton( self, wx.ID_ANY, _(u"TEMPTABLE"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer129.Add( self.m_radioBtn31, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_staticText4011 = wx.StaticText( self, wx.ID_ANY, _(u"Algorithm"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText4011.Wrap( -1 ) - - bSizer129.Add( self.m_staticText4011, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - fgSizer1 = wx.FlexGridSizer( 3, 2, 0, 0 ) - fgSizer1.SetFlexibleDirection( wx.BOTH ) - fgSizer1.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_NONE ) - - - fgSizer1.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - - bSizer129.Add( fgSizer1, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_checkBox7 = wx.CheckBox( self, wx.ID_ANY, _(u"Read only"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer129.Add( self.m_checkBox7, 0, wx.ALL, 5 ) - - rad_view_algorithmChoices = [ _(u"UNDEFINED"), _(u"MERGE"), _(u"TEMPTABLE") ] - self.rad_view_algorithm = wx.RadioBox( self, wx.ID_ANY, _(u"Algorithm"), wx.DefaultPosition, wx.DefaultSize, rad_view_algorithmChoices, 1, wx.RA_SPECIFY_COLS ) - self.rad_view_algorithm.SetSelection( 0 ) - bSizer129.Add( self.rad_view_algorithm, 0, wx.ALL|wx.EXPAND, 5 ) - - rad_view_constraintChoices = [ _(u"None"), _(u"LOCAL"), _(u"CASCADED"), _(u"CHECK OPTION"), _(u"READ ONLY") ] - self.rad_view_constraint = wx.RadioBox( self, wx.ID_ANY, _(u"View constraint"), wx.DefaultPosition, wx.DefaultSize, rad_view_constraintChoices, 1, wx.RA_SPECIFY_COLS ) - self.rad_view_constraint.SetSelection( 0 ) - self.m_menu15 = wx.Menu() - self.rad_view_constraint.Bind( wx.EVT_RIGHT_DOWN, self.rad_view_constraintOnContextMenu ) - - bSizer129.Add( self.rad_view_constraint, 0, wx.ALL|wx.EXPAND, 5 ) - - self.m_textCtrl10 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE|wx.TE_RICH|wx.TE_RICH2 ) - bSizer129.Add( self.m_textCtrl10, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_textCtrl361 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_PASSWORD ) - bSizer129.Add( self.m_textCtrl361, 1, wx.ALL, 5 ) - - - bSizer93.Add( bSizer129, 1, wx.EXPAND, 5 ) - - self.notebook_sql_results = wx.aui.AuiNotebook( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.aui.AUI_NB_DEFAULT_STYLE|wx.aui.AUI_NB_MIDDLE_CLICK_CLOSE ) - - bSizer93.Add( self.notebook_sql_results, 1, wx.EXPAND | wx.ALL, 5 ) - - self.ssh_tunnel_password1 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_PASSWORD ) - bSizer93.Add( self.ssh_tunnel_password1, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) - - database_encryption_oldChoices = [] - self.database_encryption_old = wx.Choice( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, database_encryption_oldChoices, 0 ) - self.database_encryption_old.SetSelection( 0 ) - bSizer93.Add( self.database_encryption_old, 1, wx.ALL, 5 ) - - - bSizer90.Add( bSizer93, 1, wx.EXPAND, 5 ) - - self.m_collapsiblePane2 = wx.CollapsiblePane( self, wx.ID_ANY, _(u"collapsible"), wx.DefaultPosition, wx.DefaultSize, wx.CP_DEFAULT_STYLE ) - self.m_collapsiblePane2.Collapse( False ) - - bSizer90.Add( self.m_collapsiblePane2, 1, wx.EXPAND | wx.ALL, 5 ) - - self.tree_ctrl_sessions = wx.TreeCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TR_DEFAULT_STYLE|wx.TR_FULL_ROW_HIGHLIGHT|wx.TR_HAS_BUTTONS|wx.TR_HIDE_ROOT|wx.TR_TWIST_BUTTONS ) - self.m_menu12 = wx.Menu() - self.tree_ctrl_sessions.Bind( wx.EVT_RIGHT_DOWN, self.tree_ctrl_sessionsOnContextMenu ) - - bSizer90.Add( self.tree_ctrl_sessions, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_treeListCtrl3 = wx.dataview.TreeListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.TL_DEFAULT_STYLE ) - - bSizer90.Add( self.m_treeListCtrl3, 1, wx.EXPAND | wx.ALL, 5 ) - - self.tree_ctrl_sessions1 = wx.dataview.TreeListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.TL_DEFAULT_STYLE ) - self.tree_ctrl_sessions1.AppendColumn( _(u"Column3"), wx.COL_WIDTH_DEFAULT, wx.ALIGN_LEFT, wx.COL_RESIZABLE ) - self.tree_ctrl_sessions1.AppendColumn( _(u"Column4"), wx.COL_WIDTH_DEFAULT, wx.ALIGN_LEFT, wx.COL_RESIZABLE ) - - bSizer90.Add( self.tree_ctrl_sessions1, 1, wx.EXPAND | wx.ALL, 5 ) - - self.table_collationdd = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.table_collationdd, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_textCtrl21 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE ) - bSizer90.Add( self.m_textCtrl21, 1, wx.ALL|wx.EXPAND, 5 ) - - bSizer51 = wx.BoxSizer( wx.VERTICAL ) - - self.panel_credentials = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer48 = wx.BoxSizer( wx.VERTICAL ) - - self.m_notebook8 = wx.Notebook( self.panel_credentials, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - - bSizer48.Add( self.m_notebook8, 1, wx.EXPAND | wx.ALL, 5 ) - - - self.panel_credentials.SetSizer( bSizer48 ) - self.panel_credentials.Layout() - bSizer48.Fit( self.panel_credentials ) - self.m_menu3 = wx.Menu() - self.m_menuItem3 = wx.MenuItem( self.m_menu3, wx.ID_ANY, _(u"MyMenuItem"), wx.EmptyString, wx.ITEM_NORMAL ) - self.m_menu3.Append( self.m_menuItem3 ) - - self.panel_credentials.Bind( wx.EVT_RIGHT_DOWN, self.panel_credentialsOnContextMenu ) - - bSizer51.Add( self.panel_credentials, 0, wx.EXPAND | wx.ALL, 0 ) - - self.panel_source = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - self.panel_source.Hide() - - bSizer52 = wx.BoxSizer( wx.VERTICAL ) - - bSizer1212 = wx.BoxSizer( wx.HORIZONTAL ) + bSizer144 = wx.BoxSizer( wx.VERTICAL ) - self.m_staticText212 = wx.StaticText( self.panel_source, wx.ID_ANY, _(u"Filename"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) - self.m_staticText212.Wrap( -1 ) - - bSizer1212.Add( self.m_staticText212, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.filename = wx.FilePickerCtrl( self.panel_source, wx.ID_ANY, wx.EmptyString, _(u"Select a file"), _(u"Database (*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3"), wx.DefaultPosition, wx.DefaultSize, wx.FLP_CHANGE_DIR|wx.FLP_USE_TEXTCTRL ) - bSizer1212.Add( self.filename, 1, wx.ALL, 5 ) - - - bSizer52.Add( bSizer1212, 0, wx.EXPAND, 0 ) - - - self.panel_source.SetSizer( bSizer52 ) - self.panel_source.Layout() - bSizer52.Fit( self.panel_source ) - bSizer51.Add( self.panel_source, 0, wx.EXPAND | wx.ALL, 0 ) - - self.m_staticText2211 = wx.StaticText( self, wx.ID_ANY, _(u"Port"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) - self.m_staticText2211.Wrap( -1 ) - - bSizer51.Add( self.m_staticText2211, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - - bSizer90.Add( bSizer51, 0, wx.EXPAND, 0 ) - - self.ssh_tunnel_port = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.ssh_tunnel_port, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.ssh_tunnel_local_port = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.ssh_tunnel_local_port, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.tree_ctrl_sessions2 = wx.TreeCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TR_DEFAULT_STYLE ) - self.tree_ctrl_sessions2.Hide() - - bSizer90.Add( self.tree_ctrl_sessions2, 1, wx.ALL|wx.EXPAND, 5 ) - - self.tree_ctrl_sessions_bkp3 = wx.dataview.TreeListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.TL_DEFAULT_STYLE|wx.dataview.TL_SINGLE ) - self.tree_ctrl_sessions_bkp3.Hide() - - self.tree_ctrl_sessions_bkp3.AppendColumn( _(u"Name"), wx.COL_WIDTH_DEFAULT, wx.ALIGN_LEFT, wx.COL_RESIZABLE ) - self.tree_ctrl_sessions_bkp3.AppendColumn( _(u"Usage"), wx.COL_WIDTH_DEFAULT, wx.ALIGN_LEFT, wx.COL_RESIZABLE ) - - bSizer90.Add( self.tree_ctrl_sessions_bkp3, 1, wx.EXPAND | wx.ALL, 5 ) - - self.tree_ctrl_sessions_bkp = wx.dataview.DataViewCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.DV_SINGLE ) - self.tree_ctrl_sessions_bkp.Hide() - - self.m_dataViewColumn1 = self.tree_ctrl_sessions_bkp.AppendIconTextColumn( _(u"Database"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn3 = self.tree_ctrl_sessions_bkp.AppendProgressColumn( _(u"Size"), 1, wx.dataview.DATAVIEW_CELL_INERT, 50, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - bSizer90.Add( self.tree_ctrl_sessions_bkp, 1, wx.ALL|wx.EXPAND, 5 ) + self.database_character_set_panel = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer139 = wx.BoxSizer( wx.HORIZONTAL ) - self.rows_database_table = wx.StaticText( self, wx.ID_ANY, _(u"%(total_rows)s"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.rows_database_table.Wrap( -1 ) + self.m_staticText70 = wx.StaticText( self.database_character_set_panel, wx.ID_ANY, _(u"Character set"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText70.Wrap( -1 ) - bSizer90.Add( self.rows_database_table, 0, wx.ALL, 5 ) + self.m_staticText70.SetMinSize( wx.Size( 150,-1 ) ) - self.m_staticText44 = wx.StaticText( self, wx.ID_ANY, _(u"rows total"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText44.Wrap( -1 ) + bSizer139.Add( self.m_staticText70, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - bSizer90.Add( self.m_staticText44, 0, wx.ALL, 5 ) + database_character_setChoices = [] + self.database_character_set = wx.Choice( self.database_character_set_panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, database_character_setChoices, 0 ) + self.database_character_set.SetSelection( 0 ) + bSizer139.Add( self.database_character_set, 1, wx.ALL, 5 ) - self.____list_ctrl_database_tables = wx.dataview.DataViewCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_dataViewColumn5 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn6 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn7 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn8 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn9 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn10 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn11 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn20 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn21 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - bSizer90.Add( self.____list_ctrl_database_tables, 0, wx.ALL, 5 ) - self.___list_ctrl_database_tables = wx.dataview.DataViewListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_dataViewListColumn6 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Name"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewListColumn7 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Lines"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewListColumn8 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Size"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewListColumn9 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Created at"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewListColumn10 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Updated at"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewListColumn11 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Engine"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewListColumn12 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Comments"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - bSizer90.Add( self.___list_ctrl_database_tables, 1, wx.ALL|wx.EXPAND, 5 ) + self.database_character_set_panel.SetSizer( bSizer139 ) + self.database_character_set_panel.Layout() + bSizer139.Fit( self.database_character_set_panel ) + bSizer144.Add( self.database_character_set_panel, 1, wx.ALIGN_CENTER, 5 ) - self.m_gauge1 = wx.Gauge( self, wx.ID_ANY, 100, wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_gauge1.SetValue( 0 ) - bSizer90.Add( self.m_gauge1, 0, wx.ALL|wx.EXPAND, 5 ) + self.m_button12 = wx.Button( self, wx.ID_ANY, _(u"New"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer144.Add( self.m_button12, 0, wx.ALIGN_RIGHT|wx.ALL, 5 ) + self.QueryPanelTpl = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + self.QueryPanelTpl.Hide() - bSizer90.Add( ( 150, 0), 0, wx.EXPAND, 5 ) + bSizer263 = wx.BoxSizer( wx.VERTICAL ) + self.m_textCtrl101 = wx.TextCtrl( self.QueryPanelTpl, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE|wx.TE_RICH|wx.TE_RICH2 ) + bSizer263.Add( self.m_textCtrl101, 1, wx.ALL|wx.EXPAND, 5 ) - bSizer90.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + bSizer49 = wx.BoxSizer( wx.HORIZONTAL ) - self.tree_ctrl_explorer__ = wx.dataview.DataViewCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - self.tree_ctrl_explorer__.Hide() - bSizer90.Add( self.tree_ctrl_explorer__, 1, wx.ALL|wx.EXPAND, 5 ) + bSizer49.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - self.m_vlistBox1 = wx.VListBox( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.m_vlistBox1, 0, wx.ALL, 5 ) + self.m_button17 = wx.Button( self.QueryPanelTpl, wx.ID_ANY, _(u"Close"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer49.Add( self.m_button17, 0, wx.ALL, 5 ) - m_listBox1Choices = [] - self.m_listBox1 = wx.ListBox( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_listBox1Choices, 0 ) - bSizer90.Add( self.m_listBox1, 0, wx.ALL, 5 ) + self.m_button121 = wx.Button( self.QueryPanelTpl, wx.ID_ANY, _(u"New"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer49.Add( self.m_button121, 0, wx.ALL, 5 ) - bSizer871 = wx.BoxSizer( wx.HORIZONTAL ) - self.m_staticText401 = wx.StaticText( self, wx.ID_ANY, _(u"Temporary"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText401.Wrap( -1 ) + bSizer263.Add( bSizer49, 0, wx.EXPAND, 5 ) - bSizer871.Add( self.m_staticText401, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - self.m_checkBox5 = wx.CheckBox( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer871.Add( self.m_checkBox5, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + self.QueryPanelTpl.SetSizer( bSizer263 ) + self.QueryPanelTpl.Layout() + bSizer263.Fit( self.QueryPanelTpl ) + bSizer144.Add( self.QueryPanelTpl, 1, wx.EXPAND | wx.ALL, 5 ) + bSizer83 = wx.BoxSizer( wx.HORIZONTAL ) - bSizer90.Add( bSizer871, 1, wx.EXPAND, 5 ) + self.m_staticline3 = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_VERTICAL ) + bSizer83.Add( self.m_staticline3, 0, wx.EXPAND | wx.ALL, 5 ) - self.m_collapsiblePane3 = wx.CollapsiblePane( self, wx.ID_ANY, _(u"Engine options"), wx.DefaultPosition, wx.DefaultSize, wx.CP_DEFAULT_STYLE ) - self.m_collapsiblePane3.Collapse( False ) - bSizer115 = wx.BoxSizer( wx.VERTICAL ) + bSizer144.Add( bSizer83, 0, wx.EXPAND, 5 ) - self.m_panel41 = wx.Panel( self.m_collapsiblePane3.GetPane(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer115.Add( self.m_panel41, 1, wx.EXPAND | wx.ALL, 5 ) + self.btn_insert_record = wx.Button( self, wx.ID_ANY, _(u"Insert record"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - self.m_panel42 = wx.Panel( self.m_collapsiblePane3.GetPane(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer115.Add( self.m_panel42, 1, wx.EXPAND | wx.ALL, 5 ) + self.btn_insert_record.SetBitmap( wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ) ) + bSizer144.Add( self.btn_insert_record, 0, wx.ALL, 5 ) - self.m_panel43 = wx.Panel( self.m_collapsiblePane3.GetPane(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer115.Add( self.m_panel43, 1, wx.EXPAND | wx.ALL, 5 ) + self.btn_duplicate_record = wx.Button( self, wx.ID_ANY, _(u"Duplicate record"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.btn_duplicate_record.SetBitmap( wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ) ) + self.btn_duplicate_record.Enable( False ) - self.m_collapsiblePane3.GetPane().SetSizer( bSizer115 ) - self.m_collapsiblePane3.GetPane().Layout() - bSizer115.Fit( self.m_collapsiblePane3.GetPane() ) - bSizer90.Add( self.m_collapsiblePane3, 1, wx.EXPAND | wx.ALL, 5 ) + bSizer144.Add( self.btn_duplicate_record, 0, wx.ALL, 5 ) - self.m_textCtrl2211 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.m_textCtrl2211, 1, wx.ALL|wx.EXPAND, 5 ) + self.btn_delete_record = wx.Button( self, wx.ID_ANY, _(u"Delete record"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - self.m_textCtrl2212 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.m_textCtrl2212, 1, wx.ALL|wx.EXPAND, 5 ) + self.btn_delete_record.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) + self.btn_delete_record.Enable( False ) - m_comboBox11Choices = [] - self.m_comboBox11 = wx.ComboBox( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, m_comboBox11Choices, 0 ) - bSizer90.Add( self.m_comboBox11, 1, wx.ALL|wx.EXPAND, 5 ) + bSizer144.Add( self.btn_delete_record, 0, wx.ALL, 5 ) - gSizer3 = wx.GridSizer( 0, 2, 0, 0 ) + self.btn_cancel_record = wx.Button( self, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - bSizer8712 = wx.BoxSizer( wx.HORIZONTAL ) + self.btn_cancel_record.SetBitmap( wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ) ) + self.btn_cancel_record.Enable( False ) - self.m_staticText4012 = wx.StaticText( self, wx.ID_ANY, _(u"Algorithm"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText4012.Wrap( -1 ) + bSizer144.Add( self.btn_cancel_record, 0, wx.ALL, 5 ) - bSizer8712.Add( self.m_staticText4012, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + self.btn_apply_record = wx.Button( self, wx.ID_ANY, _(u"Apply"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - self.m_radioBtn1 = wx.RadioButton( self, wx.ID_ANY, _(u"UNDEFINED"), wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP ) - bSizer8712.Add( self.m_radioBtn1, 1, wx.ALL|wx.EXPAND, 5 ) + self.btn_apply_record.SetBitmap( wx.Bitmap( u"icons/16x16/disk.png", wx.BITMAP_TYPE_ANY ) ) + self.btn_apply_record.Enable( False ) - self.m_radioBtn2 = wx.RadioButton( self, wx.ID_ANY, _(u"MERGE"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer8712.Add( self.m_radioBtn2, 1, wx.ALL|wx.EXPAND, 5 ) + bSizer144.Add( self.btn_apply_record, 0, wx.ALL, 5 ) - self.m_radioBtn3 = wx.RadioButton( self, wx.ID_ANY, _(u"TEMPTABLE"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer8712.Add( self.m_radioBtn3, 1, wx.ALL|wx.EXPAND, 5 ) + bSizer53 = wx.BoxSizer( wx.HORIZONTAL ) - gSizer3.Add( bSizer8712, 1, wx.EXPAND, 5 ) + bSizer53.Add( ( 100, 0), 0, wx.EXPAND, 5 ) - bSizer12211 = wx.BoxSizer( wx.HORIZONTAL ) + self.btn_insert_column = wx.Button( self, wx.ID_ANY, _(u"Insert"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.btn_insert_column.SetBitmap( wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ) ) + bSizer53.Add( self.btn_insert_column, 0, wx.LEFT|wx.RIGHT, 2 ) - gSizer3.Add( bSizer12211, 0, wx.EXPAND, 5 ) + self.btn_delete_column = wx.Button( self, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.btn_delete_column.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) + self.btn_delete_column.Enable( False ) - bSizer90.Add( gSizer3, 1, wx.EXPAND, 5 ) + bSizer53.Add( self.btn_delete_column, 0, wx.LEFT|wx.RIGHT, 2 ) - self.m_radioBtn10 = wx.RadioButton( self, wx.ID_ANY, _(u"RadioBtn"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.m_radioBtn10, 0, wx.ALL, 5 ) + self.btn_move_up_column = wx.Button( self, wx.ID_ANY, _(u"Up"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - bSizer86 = wx.BoxSizer( wx.HORIZONTAL ) + self.btn_move_up_column.SetBitmap( wx.Bitmap( u"icons/16x16/arrow_up.png", wx.BITMAP_TYPE_ANY ) ) + self.btn_move_up_column.Enable( False ) - self.m_panel44 = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer86.Add( self.m_panel44, 1, wx.EXPAND | wx.ALL, 5 ) + bSizer53.Add( self.btn_move_up_column, 0, wx.LEFT|wx.RIGHT, 2 ) + self.btn_move_down_column = wx.Button( self, wx.ID_ANY, _(u"Down"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - bSizer90.Add( bSizer86, 0, wx.EXPAND, 5 ) + self.btn_move_down_column.SetBitmap( wx.Bitmap( u"icons/16x16/arrow_down.png", wx.BITMAP_TYPE_ANY ) ) + self.btn_move_down_column.Enable( False ) - self.filename1 = wx.FilePickerCtrl( self, wx.ID_ANY, wx.EmptyString, _(u"Select a file"), _(u"*.*"), wx.DefaultPosition, wx.DefaultSize, wx.FLP_CHANGE_DIR|wx.FLP_DEFAULT_STYLE|wx.FLP_FILE_MUST_EXIST ) - bSizer90.Add( self.filename1, 0, wx.ALL, 5 ) + bSizer53.Add( self.btn_move_down_column, 0, wx.LEFT|wx.RIGHT, 2 ) - self.m_textCtrl351 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.m_textCtrl351, 0, wx.ALL, 5 ) - self.m_staticText701 = wx.StaticText( self, wx.ID_ANY, _(u"Encryption"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText701.Wrap( -1 ) + bSizer53.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - self.m_staticText701.SetMinSize( wx.Size( 150,-1 ) ) - bSizer90.Add( self.m_staticText701, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + bSizer144.Add( bSizer53, 0, wx.ALL|wx.EXPAND, 5 ) - self.SetSizer( bSizer90 ) + self.SetSizer( bSizer144 ) self.Layout() - self.m_menu11 = wx.Menu() - self.Bind( wx.EVT_RIGHT_DOWN, self.TrashOnContextMenu ) - # Connect Events - self.Bind( wx.EVT_MENU, self.on_import, id = self.m_menuItem10.GetId() ) - self.tree_ctrl_sessions.Bind( wx.EVT_TREE_ITEM_RIGHT_CLICK, self.show_tree_ctrl_menu ) + self.btn_insert_record.Bind( wx.EVT_BUTTON, self.on_insert_record ) + self.btn_duplicate_record.Bind( wx.EVT_BUTTON, self.on_duplicate_record ) + self.btn_delete_record.Bind( wx.EVT_BUTTON, self.on_delete_record ) + self.btn_insert_column.Bind( wx.EVT_BUTTON, self.on_insert_column ) + self.btn_delete_column.Bind( wx.EVT_BUTTON, self.on_delete_column ) + self.btn_move_up_column.Bind( wx.EVT_BUTTON, self.on_move_up_column ) + self.btn_move_down_column.Bind( wx.EVT_BUTTON, self.on_move_down_column ) def __del__( self ): pass # Virtual event handlers, override them in your derived class - def on_import( self, event ): + def on_insert_record( self, event ): event.Skip() - def show_tree_ctrl_menu( self, event ): + def on_duplicate_record( self, event ): event.Skip() - def m_radioBtn21OnContextMenu( self, event ): - self.m_radioBtn21.PopupMenu( self.m_menu13, event.GetPosition() ) - - def rad_view_constraintOnContextMenu( self, event ): - self.rad_view_constraint.PopupMenu( self.m_menu15, event.GetPosition() ) - - def tree_ctrl_sessionsOnContextMenu( self, event ): - self.tree_ctrl_sessions.PopupMenu( self.m_menu12, event.GetPosition() ) - - def panel_credentialsOnContextMenu( self, event ): - self.panel_credentials.PopupMenu( self.m_menu3, event.GetPosition() ) - - def TrashOnContextMenu( self, event ): - self.PopupMenu( self.m_menu11, event.GetPosition() ) - - -########################################################################### -## Class MyPanel1 -########################################################################### - -class MyPanel1 ( wx.Panel ): - - def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.Size( 500,300 ), style = wx.TAB_TRAVERSAL, name = wx.EmptyString ): - wx.Panel.__init__ ( self, parent, id = id, pos = pos, size = size, style = style, name = name ) - - - def __del__( self ): - pass - - -########################################################################### -## Class EditColumnView -########################################################################### - -class EditColumnView ( wx.Dialog ): - - def __init__( self, parent ): - wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Edit Column"), pos = wx.DefaultPosition, size = wx.Size( 600,600 ), style = wx.DEFAULT_DIALOG_STYLE|wx.STAY_ON_TOP ) - - self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) - - bSizer98 = wx.BoxSizer( wx.VERTICAL ) - - bSizer52 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText26 = wx.StaticText( self, wx.ID_ANY, _(u"Name"), wx.DefaultPosition, wx.Size( 100,-1 ), wx.ST_NO_AUTORESIZE ) - self.m_staticText26.Wrap( -1 ) - - bSizer52.Add( self.m_staticText26, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - self.column_name = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer52.Add( self.column_name, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - self.m_staticText261 = wx.StaticText( self, wx.ID_ANY, _(u"Datatype"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText261.Wrap( -1 ) - - bSizer52.Add( self.m_staticText261, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - column_datatypeChoices = [] - self.column_datatype = wx.Choice( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, column_datatypeChoices, 0 ) - self.column_datatype.SetSelection( 0 ) - bSizer52.Add( self.column_datatype, 1, wx.ALL, 5 ) - - - bSizer98.Add( bSizer52, 0, wx.EXPAND, 5 ) - - bSizer5211 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText2611 = wx.StaticText( self, wx.ID_ANY, _(u"Length/Set"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText2611.Wrap( -1 ) - - bSizer5211.Add( self.m_staticText2611, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - bSizer60 = wx.BoxSizer( wx.HORIZONTAL ) - - self.column_set = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer60.Add( self.column_set, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.column_length = wx.SpinCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_ARROW_KEYS, 0, 65536, 0 ) - bSizer60.Add( self.column_length, 1, wx.ALL, 5 ) - - self.column_scale = wx.SpinCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_WRAP, 0, 65536, 0 ) - self.column_scale.Enable( False ) - - bSizer60.Add( self.column_scale, 1, wx.ALL, 5 ) - - - bSizer5211.Add( bSizer60, 1, wx.EXPAND, 5 ) - - self.m_staticText261111112 = wx.StaticText( self, wx.ID_ANY, _(u"Collation"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText261111112.Wrap( -1 ) - - bSizer5211.Add( self.m_staticText261111112, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - column_collationChoices = [] - self.column_collation = wx.Choice( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, column_collationChoices, 0 ) - self.column_collation.SetSelection( 0 ) - bSizer5211.Add( self.column_collation, 1, wx.ALL, 5 ) - - - bSizer98.Add( bSizer5211, 0, wx.EXPAND, 5 ) - - bSizer52111 = wx.BoxSizer( wx.HORIZONTAL ) - - - bSizer52111.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.column_unsigned = wx.CheckBox( self, wx.ID_ANY, _(u"Unsigned"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer52111.Add( self.column_unsigned, 1, wx.ALL, 5 ) - - - bSizer52111.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.column_allow_null = wx.CheckBox( self, wx.ID_ANY, _(u"Allow NULL"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer52111.Add( self.column_allow_null, 1, wx.ALL, 5 ) - - - bSizer52111.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.column_zero_fill = wx.CheckBox( self, wx.ID_ANY, _(u"Zero Fill"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer52111.Add( self.column_zero_fill, 1, wx.ALL, 5 ) - - - bSizer52111.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - - bSizer98.Add( bSizer52111, 0, wx.EXPAND, 5 ) - - bSizer53 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText26111111 = wx.StaticText( self, wx.ID_ANY, _(u"Default"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText26111111.Wrap( -1 ) - - bSizer53.Add( self.m_staticText26111111, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - self.column_default = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer53.Add( self.column_default, 1, wx.ALL, 5 ) - - - bSizer98.Add( bSizer53, 0, wx.EXPAND, 5 ) - - bSizer531 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText261111111 = wx.StaticText( self, wx.ID_ANY, _(u"Comments"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText261111111.Wrap( -1 ) - - bSizer531.Add( self.m_staticText261111111, 0, wx.ALL, 5 ) - - self.column_comments = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size( -1,100 ), wx.TE_MULTILINE ) - bSizer531.Add( self.column_comments, 1, wx.ALL, 5 ) - - - bSizer98.Add( bSizer531, 0, wx.EXPAND, 5 ) - - bSizer532 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText261111113 = wx.StaticText( self, wx.ID_ANY, _(u"Virtuality"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText261111113.Wrap( -1 ) - - bSizer532.Add( self.m_staticText261111113, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - column_virtualityChoices = [] - self.column_virtuality = wx.Choice( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, column_virtualityChoices, 0 ) - self.column_virtuality.SetSelection( 0 ) - bSizer532.Add( self.column_virtuality, 1, wx.ALL, 5 ) - - - bSizer98.Add( bSizer532, 0, wx.EXPAND, 5 ) - - bSizer5311 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText2611111111 = wx.StaticText( self, wx.ID_ANY, _(u"Expression"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText2611111111.Wrap( -1 ) - - bSizer5311.Add( self.m_staticText2611111111, 0, wx.ALL, 5 ) - - self.column_expression = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size( -1,100 ), wx.TE_MULTILINE ) - bSizer5311.Add( self.column_expression, 1, wx.ALL, 5 ) - - - bSizer98.Add( bSizer5311, 0, wx.EXPAND, 5 ) - - - bSizer98.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.m_staticline2 = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) - bSizer98.Add( self.m_staticline2, 0, wx.EXPAND | wx.ALL, 5 ) - - bSizer64 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_button16 = wx.Button( self, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) - - self.m_button16.SetDefault() - - self.m_button16.SetBitmap( wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ) ) - bSizer64.Add( self.m_button16, 0, wx.ALL, 5 ) - - - bSizer64.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.m_button15 = wx.Button( self, wx.ID_ANY, _(u"Save"), wx.DefaultPosition, wx.DefaultSize, 0 ) - - self.m_button15.SetBitmap( wx.Bitmap( u"icons/16x16/disk.png", wx.BITMAP_TYPE_ANY ) ) - bSizer64.Add( self.m_button15, 0, wx.ALL, 5 ) - - - bSizer98.Add( bSizer64, 0, wx.EXPAND, 5 ) + def on_delete_record( self, event ): + event.Skip() + def on_insert_column( self, event ): + event.Skip() - self.SetSizer( bSizer98 ) - self.Layout() + def on_delete_column( self, event ): + event.Skip() - self.Centre( wx.BOTH ) + def on_move_up_column( self, event ): + event.Skip() - def __del__( self ): - pass + def on_move_down_column( self, event ): + event.Skip() ########################################################################### @@ -3387,3 +3071,81 @@ def panel_table_columnsOnContextMenu( self, event ): self.panel_table_columns.PopupMenu( self.menu_table_columns, event.GetPosition() ) +########################################################################### +## Class MyWizard1 +########################################################################### + +class MyWizard1 ( wx.adv.Wizard ): + + def __init__( self, parent ): + wx.adv.Wizard.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, bitmap = wx.NullBitmap, pos = wx.DefaultPosition, style = wx.DEFAULT_DIALOG_STYLE ) + + self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) + self.m_pages = [] + + self.Centre( wx.BOTH ) + + + def __del__( self ): + pass + + +########################################################################### +## Class SaveStatments +########################################################################### + +class SaveStatments ( wx.Dialog ): + + def __init__( self, parent ): + wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Save Starments"), pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.DEFAULT_DIALOG_STYLE ) + + self.SetSizeHints( wx.Size( 320,200 ), wx.DefaultSize ) + + bSizer163 = wx.BoxSizer( wx.VERTICAL ) + + bSizer164 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText86 = wx.StaticText( self, wx.ID_ANY, _(u"Location"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText86.Wrap( -1 ) + + self.m_staticText86.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer164.Add( self.m_staticText86, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + self.m_filePicker5 = wx.FilePickerCtrl( self, wx.ID_ANY, wx.EmptyString, _(u"Select a file"), _(u"*.sql"), wx.DefaultPosition, wx.DefaultSize, wx.FLP_DEFAULT_STYLE|wx.FLP_SAVE|wx.FLP_SMALL|wx.FLP_USE_TEXTCTRL ) + bSizer164.Add( self.m_filePicker5, 1, wx.ALL, 5 ) + + + bSizer163.Add( bSizer164, 0, wx.EXPAND, 5 ) + + + bSizer163.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + + self.m_staticline7 = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) + bSizer163.Add( self.m_staticline7, 0, wx.EXPAND | wx.ALL, 5 ) + + bSizer165 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_button57 = wx.Button( self, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer165.Add( self.m_button57, 0, wx.ALL, 5 ) + + + bSizer165.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + + self.m_button58 = wx.Button( self, wx.ID_ANY, _(u"Save"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer165.Add( self.m_button58, 0, wx.ALL, 5 ) + + + bSizer163.Add( bSizer165, 0, wx.EXPAND, 5 ) + + + self.SetSizer( bSizer163 ) + self.Layout() + bSizer163.Fit( self ) + + self.Centre( wx.BOTH ) + + def __del__( self ): + pass + +