diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd4e8cc..881a376 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: fi - name: Run tests (--all) - run: xvfb-run -a uv run ./scripts/runtest.py --all + run: xvfb-run -a uv run ./scripts/runtest.py --suite all update: if: github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]') @@ -236,7 +236,7 @@ jobs: fi - name: Run tests and update README (--update) - run: xvfb-run -a uv run ./scripts/runtest.py --update + run: xvfb-run -a uv run ./scripts/runtest.py --suite all --update - name: Commit and push updated README run: | diff --git a/.gitignore b/.gitignore index 55cfb84..e632bc8 100755 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ PeterSQL.png # Unready build scripts scripts/build/nix/ +assets/ +screenshot/ diff --git a/ENGINES.md b/ENGINES.md index e499f93..d521690 100644 --- a/ENGINES.md +++ b/ENGINES.md @@ -7,6 +7,7 @@ This project stores SQL autocomplete vocabulary in normalized engine specificati The specification model uses a **base + delta** strategy: - `common.functions` and `common.keywords` contain the shared baseline for that engine. +- Added support for stored functions in MySQL and MariaDB contexts, including deterministic flag handling. - `versions..functions_remove` and `versions..keywords_remove` remove entries that are not valid for an older major version. We intentionally keep newer capabilities in `common` and apply only removals for older majors. @@ -19,3 +20,33 @@ At runtime, vocabulary resolution uses: 2. otherwise, the highest configured major version `<=` the server major. Example: if PostgreSQL server major is `19` and the highest configured major is `18`, version `18` is used. + +## Inter-Engine API Differences and Feature Parity + +The engine layer exposes a common base API (`SQLTable`, `SQLColumn`, `SQLIndex`, `SQLForeignKey`, `SQLRecord`, `SQLView`, `SQLTrigger`, `SQLCheck`, `SQLFunction`, `SQLProcedure`, `SQLDatabase`). Engines implement this API with the following known variations: + +### SQLite + +- File-based storage: `SQLiteDatabase.create()`, `alter()`, and `drop()` raise `NotImplementedError` because database lifecycle is managed at the filesystem level, not via SQL DDL. +- Most mature engine path with complete CRUD for tables, columns, indexes, foreign keys, records, views, and triggers. +- Check constraints are **partial**: read/create/delete are implemented; update depends on a recreate strategy. +- Functions and procedures are **not applicable** (N/A). + +### MySQL / MariaDB + +- Strong parity for tables, columns, indexes, foreign keys, records, views, triggers, and functions. +- Check constraints and procedures are **partial**: engine objects exist (`MySQLCheck`, `MySQLProcedure`, `MariaDBCheck`, `MariaDBProcedure`), but cross-version validation is ongoing. +- Database create/drop lifecycle methods exist at the engine level, but context/UI wiring remains read/list-oriented. + +### PostgreSQL + +- Core CRUD available for tables, columns, indexes, foreign keys, records, views, and triggers. +- Functions and procedures are **partial**: `PostgreSQLFunction` and `PostgreSQLProcedure` are implemented but still under broader validation. +- Check constraints are **partial**: `PostgreSQLCheck` and `get_checks()` exist; cross-version validation ongoing. +- Database create/drop lifecycle methods exist at the engine level, but context/UI wiring remains read/list-oriented. +- Schema and sequence objects have basic visibility but no CRUD layer yet. + +### Shared Base Contracts + +- `SQLColumn` and `SQLIndex` are enforced as abstract base classes (`abc.ABC`) with `@abstractmethod` decorators on public methods (`add`, `drop`, `rename`, `modify`, `create`, `raw_create`). Missing implementations are caught at class definition time. +- Equality comparisons (`__eq__`) on `SQLDatabase` and `SQLForeignKey` use identity-based field matching rather than inverted logic. diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 339790a..595b1b4 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # PeterSQL — Project Status -> **Last Updated:** 2026-04-27 +> **Last Updated:** 2026-06-05 > **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. @@ -127,8 +127,8 @@ - **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/` + - **Scope:** persistent connection statistics, empty DB password support, TLS auto-retry (MySQL/MariaDB), keyring-backed password storage. + - **Files:** `structures/connection.py`, `windows/dialogs/connections/`, `structures/secrets.py` - **Next:** SSH testcontainers integration validation (currently skipped) + long-run behavioral validation. - [x] **SQL dump/backup object-driven flow** (PARTIAL) @@ -172,8 +172,9 @@ ## 5. Progress Snapshot -- **P0 implemented (partial):** 5/5 -- **P1 gaps closed:** 2/3 +- **P0 implemented (partial):** 5/5 — all resolved +- **P1 gaps closed:** 3/3 — all resolved +- **P2 technical audit items:** 2/2 — resolved (PostgreSQL import/type hints, ABC enforcement) - **P2 UI tasks complete:** 4/8 - **P3 advanced tasks complete:** 0/6 @@ -181,6 +182,8 @@ ## 6. Recently Added +- Connection passwords are now stored in the system keyring (`keyring`), removing plaintext passwords from `connections.yml`. +- Audit fixes completed: PostgreSQL alter diff handling, equality comparisons, SQLite column/drop/modify signatures, SQLite database lifecycle errors, SQLite record exception safety, VERSION sync, PostgreSQL import/type hints, and ABC enforcement for `SQLColumn`/`SQLIndex`. - 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. diff --git a/PeterSQL.fbp b/PeterSQL.fbp index 924f4ae..4b75db3 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -31,6 +31,166 @@ 1 0 0 + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 0 + 1 + impl_virtual + + + + 0 + wxID_ANY + + + SplashScreen + + 640,480 + wxFRAME_NO_TASKBAR|wxSTAY_ON_TOP + ; ; forward_declare + + + 0 + + + wxTAB_TRAVERSAL + 1 + + + bSizer161 + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + Load From File; petersql_large.png + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_bitmap3 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 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 + m_gauge1 + 1 + + + protected + 1 + + 100 + Resizable + 1 + + wxGA_HORIZONTAL + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 0 wxAUI_MGR_DEFAULT @@ -1658,6 +1818,92 @@ + + 5 + wxEXPAND + 0 + + + bSizer1631 + wxHORIZONTAL + none + + 5 + wxEXPAND + 0 + + 0 + protected + 156 + + + + 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 + Mark read only + + 0 + + + 0 + + 1 + read_only + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 wxEXPAND @@ -2267,7 +2513,7 @@ 1 0 - 1 + 0 wxID_ANY 0 @@ -5652,7 +5898,7 @@ - + 0 wxAUI_MGR_DEFAULT @@ -6401,7 +6647,7 @@ wxTAB_TRAVERSAL 1 do_close - + 1 @@ -6423,7 +6669,7 @@ - + File m_menu2 protected @@ -6442,7 +6688,7 @@ on_settings - + Help m_menu4 protected @@ -6552,11 +6798,11 @@ wxID_ANY wxITEM_NORMAL tool - database_refresh + tool_refresh_database protected Refresh Refresh - on_database_refresh + on_refresh_database protected @@ -6567,10 +6813,11 @@ wxID_ANY wxITEM_NORMAL Add - database_add + tool_add_database protected + on_add_database Load From File; icons/16x16/database_delete.png @@ -6583,6 +6830,82 @@ + + protected + + + 0 + protected + 0 + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + Load From File; icons/16x16/lock.png + + 1 + 0 + 1 + + 1 + + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + {mode} + + 0 + + 0 + + + 0 + + 1 + m_toggleBtn1 + 1 + + + protected + 1 + + + Load From File; icons/16x16/bullet_green.png + Resizable + 1 + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + on_toggle_read_only + @@ -6700,7 +7023,7 @@ Resizable 1 - -150 + -200 -1 1 @@ -6884,7 +7207,7 @@ - wxTAB_TRAVERSAL + wxFULL_REPAINT_ON_RESIZE|wxTAB_TRAVERSAL bSizer24 @@ -6959,7 +7282,7 @@ - + wxFULL_REPAINT_ON_RESIZE @@ -7064,7 +7387,7 @@ 5 wxALL|wxEXPAND 1 - + 1 1 1 @@ -7328,11 +7651,11 @@ - + Load From File; icons/16x16/database.png Database - 0 - + 1 + 1 1 1 @@ -7384,16 +7707,16 @@ wxTAB_TRAVERSAL - + bSizer27 wxVERTICAL none - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -7447,11 +7770,11 @@ - + Options 0 - + 1 1 1 @@ -7503,16 +7826,16 @@ wxTAB_TRAVERSAL - + bSizer80 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -7570,8 +7893,8 @@ - - + + 1 1 1 @@ -7623,7 +7946,7 @@ wxTAB_TRAVERSAL - + bSizer158 wxVERTICAL @@ -7777,20 +8100,20 @@ none - + 5 wxEXPAND 0 - + bSizer142 wxHORIZONTAL none - + 5 wxALIGN_CENTER 1 - + 1 1 1 @@ -7977,11 +8300,11 @@ - + 5 wxEXPAND 1 - + 0 protected 0 @@ -7989,11 +8312,11 @@ - + 5 wxEXPAND 0 - + bSizer13911 wxHORIZONTAL @@ -10137,7 +10460,7 @@ 0 1 - m_panel55 + m_panel651 1 @@ -10155,95 +10478,72 @@ wxTAB_TRAVERSAL - bSizer154 + bSizer149 wxVERTICAL none 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 + 0 + wxID_ANY + + 0 + + + 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 + m_notebook10 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + + + Tables + 1 + 1 1 1 @@ -10252,20 +10552,15 @@ 0 0 - 0 - Load From File; icons/16x16/add.png 1 0 1 1 - - 0 0 - Dock 0 Left @@ -10273,14 +10568,10 @@ 1 1 - 0 0 wxID_ANY - Insert - - 0 0 @@ -10288,37 +10579,258 @@ 0 1 - btn_insert_table + m_panel55 1 protected 1 - - Resizable 1 - wxBORDER_NONE ; ; forward_declare 0 - - wxFILTER_NONE - wxDefaultValidator - - - on_insert_table + wxTAB_TRAVERSAL + + + bSizer154 + 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_toolBar51 + 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 table + tool_insert_table + protected + Add new table + Add new table + on_insert_table + + + Load From File; icons/16x16/page_copy.png + 0 + wxID_ANY + wxITEM_NORMAL + Clone table + tool_clone_table + protected + Clone table + Clone table + on_clone_table + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Delete table + tool_delete_table + protected + Delete table + Delete table + on_delete_table + + + + + 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 - wxALL|wxEXPAND - 0 - + + + Views + 0 + 1 1 1 @@ -10327,35 +10839,26 @@ 0 0 - 0 - Load From File; icons/16x16/table_multiple.png 1 0 1 1 - - 0 0 - Dock 0 Left 0 - 0 + 1 1 - 0 0 wxID_ANY - Clone - - 0 0 @@ -10363,74 +10866,214 @@ 0 1 - btn_clone_table + m_panel65 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 - + wxTAB_TRAVERSAL + + + bSizer1482 + 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_toolBar5 + 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 view + tool_insert_view + protected + Add new view + Add new view + on_insert_view + + + Load From File; icons/16x16/page_copy.png + 0 + wxID_ANY + wxITEM_NORMAL + Clone view + tool_clone_view + protected + Clone view + Clone view + on_clone_view + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Delete view + tool_delete_view + protected + Delete view + Delete view + on_delete_view + + + + + 5 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + list_ctrl_database_views + protected + + + + ; ; forward_declare + + + + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Name + wxDATAVIEW_CELL_INERT + 0 + m_dataViewColumn121 + protected + Text + -1 + + + + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Definition + wxDATAVIEW_CELL_INERT + 1 + m_dataViewColumn131 + protected + Text + -1 + + + + + + + + + Procedures + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + - Load From File; icons/16x16/delete.png 1 0 1 1 - - 0 0 - Dock 0 Left 0 - 0 + 1 1 - 0 0 wxID_ANY - Delete - - 0 0 @@ -10438,208 +11081,857 @@ 0 1 - btn_delete_table1 + m_panel652 1 protected 1 - - Resizable 1 - wxBORDER_NONE ; ; forward_declare 0 - - wxFILTER_NONE - wxDefaultValidator - - - on_delete_table - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 + wxTAB_TRAVERSAL + + + bSizer14821 + 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_toolBar52 + 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 procedure + tool_insert_procedure + protected + Add new procedure + Add new procedure + on_insert_procedure + + + Load From File; icons/16x16/page_copy.png + 0 + wxID_ANY + wxITEM_NORMAL + Clone procedure + tool_clone_procedure + protected + Clone procedure + Clone procedure + on_clone_procedure + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Delete procedure + tool_delete_procedure + protected + Delete procedure + Delete procedure + on_delete_procedure + + + + + 5 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + list_ctrl_database_procedure + protected + + + + ; ; forward_declare + + + + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Name + wxDATAVIEW_CELL_INERT + 0 + m_dataViewColumn1211 + protected + Text + -1 + + + + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Definition + wxDATAVIEW_CELL_INERT + 1 + m_dataViewColumn1311 + protected + Text + -1 + + + + - - - - 5 - wxEXPAND - 1 - - - bSizer152 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - + + + Functions + 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 - list_ctrl_database_tables + 1 + m_panel6521 + 1 + + protected + 1 + Resizable + 1 - ; ; forward_declare + 0 - - - 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 + wxTAB_TRAVERSAL + + + bSizer148211 + 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_toolBar521 + 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 function + tool_insert_function + protected + Add new function + Add new function + on_insert_function + + + Load From File; icons/16x16/page_copy.png + 0 + wxID_ANY + wxITEM_NORMAL + Clone function + tool_clone_function + protected + Clone function + Clone function + on_clone_function + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Delete function + tool_delete_function + protected + Delete function + Delete function + on_delete_function + + + + + 5 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + list_ctrl_database_function + protected + + + + ; ; forward_declare + + + + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Name + wxDATAVIEW_CELL_INERT + 0 + m_dataViewColumn12111 + protected + Text + -1 + + + + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Definition + wxDATAVIEW_CELL_INERT + 1 + m_dataViewColumn13111 + protected + Text + -1 + + + - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Comments - wxDATAVIEW_CELL_INERT - 7 - m_dataViewColumn18 - protected - Text - -1 + + + + + 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 + m_panel65211 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer1482111 + 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_toolBar5211 + 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 trigger + tool_insert_trigger + protected + Add new trigger + Add new trigger + on_insert_view + + + Load From File; icons/16x16/page_copy.png + 0 + wxID_ANY + wxITEM_NORMAL + Clone trigger + tool_clone_trigger + protected + Clone trigger + Clone trigger + on_clone_view + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Delete trigger + tool_delete_trigger + protected + Delete trigger + Delete trigger + on_delete_view + + + + + 5 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + list_ctrl_database_trigger + protected + + + + ; ; forward_declare + + + + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Name + wxDATAVIEW_CELL_INERT + 0 + m_dataViewColumn121111 + protected + Text + -1 + + + + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Definition + wxDATAVIEW_CELL_INERT + 1 + m_dataViewColumn131111 + protected + Text + -1 + + + - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer138 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - + + + Events + 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_panel652111 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer14821111 + 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_toolBar52111 + 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 event + tool_insert_event + protected + Add new event + Add new event + on_insert_view + + + Load From File; icons/16x16/page_copy.png + 0 + wxID_ANY + wxITEM_NORMAL + Clone event + tool_clone_event + protected + Clone event + Clone event + on_clone_view + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Delete event + tool_delete_event + protected + Delete event + Delete event + on_delete_view + + + + + 5 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + list_ctrl_database_event + protected + + + + ; ; forward_declare + + + + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Name + wxDATAVIEW_CELL_INERT + 0 + m_dataViewColumn1211111 + protected + Text + -1 + + + + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Definition + wxDATAVIEW_CELL_INERT + 1 + m_dataViewColumn1311111 + protected + Text + -1 + + + + + + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer138 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + @@ -11115,11 +12407,11 @@ - + Load From File; icons/16x16/table.png Table 0 - + 1 1 1 @@ -11171,16 +12463,16 @@ wxTAB_TRAVERSAL - + bSizer251 wxVERTICAL none - + 0 wxEXPAND 1 - + 1 1 1 @@ -11225,7 +12517,7 @@ 1 Resizable - 0.0 + 0.5 200 -1 1 @@ -11238,8 +12530,8 @@ - - + + 1 1 1 @@ -11291,16 +12583,16 @@ wxTAB_TRAVERSAL - + bSizer55 wxVERTICAL none - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -11694,11 +12986,11 @@ - + Load From File; icons/16x16/wrench.png Options 0 - + 1 1 1 @@ -11750,16 +13042,16 @@ wxTAB_TRAVERSAL - + bSizer261 wxVERTICAL none - + 5 wxEXPAND 0 - + 2 0 @@ -11905,11 +13197,11 @@ - + 5 wxEXPAND 0 - + bSizer2712 wxHORIZONTAL @@ -12043,11 +13335,11 @@ - + 5 wxEXPAND 0 - + bSizer2721 wxHORIZONTAL @@ -12246,20 +13538,20 @@ - + 5 wxEXPAND 1 - + bSizer145 wxHORIZONTAL none - + 5 wxALIGN_CENTER|wxALL 0 - + 1 1 1 @@ -12317,11 +13609,11 @@ -1 - + 5 wxALL 1 - + 1 1 1 @@ -12452,162 +13744,88 @@ none 5 - wxALIGN_CENTER + 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 - 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 - + 1 + m_toolBar12 + 1 + 1 + + + protected + 1 + + Resizable + 5 + 1 + + wxTB_HORZ_TEXT|wxTB_TEXT|wxTB_VERTICAL + ; ; forward_declare + 0 + + + + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Remove + m_tool43 + protected + + + 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 - + + Load From File; icons/16x16/cross.png + 0 + wxID_ANY + wxITEM_NORMAL + Clear + m_tool44 + protected + + + on_clear_index @@ -12701,281 +13919,133 @@ bSizer77 - wxVERTICAL + wxHORIZONTAL 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_toolBar121 + 1 + 1 + + + protected + 1 + + Resizable + 5 + 1 + + wxTB_HORZ_TEXT|wxTB_TEXT|wxTB_VERTICAL + ; ; forward_declare + 0 + + + + + + Load From File; icons/16x16/add.png + 0 + wxID_ANY + wxITEM_NORMAL + Insert + m_tool49 + protected + + + on_insert_foreign_key + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Remove + m_tool431 + protected + + + on_delete_foreign_key + + + Load From File; icons/16x16/cross.png + 0 + wxID_ANY + wxITEM_NORMAL + Clear + m_tool441 + protected + + + on_clear_foreign_key + + + + + 0 + wxALL|wxEXPAND 1 - + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + - 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 - - - - - - + dv_table_foreign_keys + protected + + + + TableForeignKeysDataViewCtrl; .components.dataview; forward_declare + + + + @@ -13040,281 +14110,133 @@ bSizer771 - wxVERTICAL + wxHORIZONTAL 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_toolBar1211 + 1 + 1 + + + protected + 1 + + Resizable + 5 + 1 + + wxTB_HORZ_TEXT|wxTB_TEXT|wxTB_VERTICAL + ; ; forward_declare + 0 + + + + + + Load From File; icons/16x16/add.png + 0 + wxID_ANY + wxITEM_NORMAL + Insert + m_tool491 + protected + + + on_insert_check + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Remove + m_tool4311 + protected + + + on_delete_check + + + Load From File; icons/16x16/cross.png + 0 + wxID_ANY + wxITEM_NORMAL + Clear + m_tool4411 + protected + + + on_clear_check + + + + + 0 + wxALL|wxEXPAND 1 - + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + - 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 - - - - - - + dv_table_checks + protected + + + + TableCheckDataViewCtrl; .components.dataview; forward_declare + + + + @@ -13457,8 +14379,8 @@ - - + + 1 1 1 @@ -13510,16 +14432,16 @@ wxTAB_TRAVERSAL - + bSizer54 wxVERTICAL none - + 5 wxEXPAND 0 - + 1 1 1 @@ -13633,7 +14555,7 @@ -1 - + Load From File; icons/16x16/add.png 0 wxID_ANY @@ -13645,7 +14567,7 @@ on_insert_column - + Load From File; icons/16x16/delete.png 0 wxID_ANY @@ -13657,10 +14579,10 @@ on_delete_column - + protected - + Load From File; icons/16x16/arrow_up.png 0 wxID_ANY @@ -13672,7 +14594,7 @@ on_move_up_column - + Load From File; icons/16x16/arrow_down.png 0 wxID_ANY @@ -14008,9 +14930,9 @@ - + Load From File; icons/16x16/view.png - Views + View 0 1 @@ -14071,9 +14993,9 @@ none 5 - wxEXPAND | wxALL - 0 - + wxEXPAND + 1 + 1 1 1 @@ -14084,7 +15006,6 @@ 0 - 1 0 @@ -14106,11 +15027,12 @@ 0 + 0 0 1 - m_notebook7 + m_splitter11 1 @@ -14118,19 +15040,20 @@ 1 Resizable + 0.0 + 0 + -1 1 - + wxSPLIT_HORIZONTAL + wxSP_3D ; ; forward_declare 0 - - - Options - 0 + 1 1 @@ -14167,7 +15090,7 @@ 0 1 - pnl_view_editor_root + m_panel79 1 @@ -14185,85 +15108,72 @@ wxTAB_TRAVERSAL - bSizer85 + bSizer170 wxVERTICAL none 5 - wxALL|wxEXPAND + wxEXPAND | wxALL 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 + + 1 + m_notebook7 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + + + General + 0 + 1 1 1 @@ -14295,12 +15205,11 @@ 0 - 0 0 1 - txt_view_name + pnl_view_editor_root 1 @@ -14310,99 +15219,24 @@ 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 + wxTAB_TRAVERSAL + + + bSizer85 + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 0 - szr_view_definer + bSizer87 wxHORIZONTAL none @@ -14438,7 +15272,7 @@ 0 0 wxID_ANY - Definer + Name 0 0 @@ -14447,7 +15281,7 @@ 0 150,-1 1 - lbl_view_definer + m_staticText40 1 @@ -14471,7 +15305,7 @@ 5 wxALIGN_CENTER|wxALL 1 - + 1 1 1 @@ -14485,7 +15319,6 @@ 1 0 - 1 1 @@ -14504,11 +15337,12 @@ 0 + 0 0 1 - cmb_view_definer + txt_view_name 1 @@ -14516,7 +15350,6 @@ 1 Resizable - -1 1 @@ -14535,193 +15368,215 @@ - - - 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 + + 5 + wxEXPAND + 0 - szr_view_schema + bSizer89 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 + 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 - cho_view_schema - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - + 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_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 + + + + + + + + + @@ -14729,1113 +15584,1422 @@ - - 5 - wxEXPAND - 1 - + + + Behavior + 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 - 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 + 1 + m_panel76 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer1661 + wxVERTICAL + none + + 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 + wxHORIZONTAL + 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 - 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 + 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_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|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 - 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 + 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 - TEMPTABLE - - 0 - - - 0 - - 1 - rad_view_algorithm_temptable - 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 - 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 + + + + + Security + 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_panel75 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer165 + 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 - 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 - - - + 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 - 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 + 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 - 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_security_barrier - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - + + 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 - 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 - - - - + 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 + + + + + @@ -15848,90 +17012,8 @@ - - - - 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 @@ -15940,35 +17022,26 @@ 0 0 - 0 - 1 0 1 1 - - 0 0 - Dock 0 Left 0 - 0 + 1 1 - 0 0 wxID_ANY - Delete - - 0 0 @@ -15976,41 +17049,193 @@ 0 1 - btn_delete_view + m_panel80 1 protected 1 - - Resizable 1 - ; ; forward_declare 0 - - wxFILTER_NONE - wxDefaultValidator - - + wxTAB_TRAVERSAL + + + bSizer168 + 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,200 + 1 + stc_view_select + 1 + + + protected + 1 + + 0 + Resizable + 1 + -1,-1 + ; ; forward_declare + 1 + 4 + 0 + + 1 + 0 + 0 + + + + + + - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 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 @@ -16155,8 +17380,8 @@ - Load From File; icons/16x16/cog.png - Triggers + Load From File; icons/16x16/code-folding.png + Routine 0 1 @@ -16194,65 +17419,7 @@ 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 + panel_routine 1 @@ -16270,14 +17437,14 @@ wxTAB_TRAVERSAL - bSizer61 + bSizer160 wxVERTICAL none 5 wxEXPAND - 0 - + 1 + 1 1 1 @@ -16288,7 +17455,6 @@ 0 - 1 0 @@ -16307,16 +17473,15 @@ 0 0 wxID_ANY - 0 + 0 0 1 - m_toolBar3 - 1 + m_splitter9 1 @@ -16324,318 +17489,21 @@ 1 Resizable - 5 + 0 + 0 + -1 1 - wxTB_HORIZONTAL|wxTB_HORZ_TEXT + wxSPLIT_HORIZONTAL + wxSP_3D ; ; 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 @@ -16644,20 +17512,15 @@ 0 0 - 0 - Load From File; icons/16x16/arrow_left.png 1 0 1 1 - - 0 0 - Dock 0 Left @@ -16665,14 +17528,10 @@ 1 1 - 0 0 wxID_ANY - - - 0 0 @@ -16680,5021 +17539,5274 @@ 0 1 - btn_prev_records + m_panel73 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 - 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_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 - - - - - - - - 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 - 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 - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer83 - wxHORIZONTAL - 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_staticline3 - 1 - - - protected - 1 - - Resizable - 1 - - wxLI_VERTICAL - ; ; forward_declare - 0 - - - - - - - - - - 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 - 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 - 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 - 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 - wxALL|wxEXPAND - 0 - - - bSizer53 - wxHORIZONTAL - none - - 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 - - - - - - - - 0 - wxAUI_MGR_DEFAULT - - - 1 - 0 - 1 - impl_virtual - - - 0 - wxID_ANY - - - TablePanel - - 640,480 - ; ; forward_declare - - 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|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_collation - 1 - - - protected - 1 - - Resizable - 1 - - - - 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 - - - - - - - - - - - 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_column_insert - - - - 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_column_delete - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_column_delete - - - - 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_column_move_up - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_column_move_up - - - - 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_column_move_down - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_column_move_down - - - - 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_table_delete - 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_table_cancel - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - do_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 - Save - - 0 - - 0 - - - 0 - - 1 - btn_table_save - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - do_save_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 - - - - - - - - - - - - - - - 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 - - - - + wxTAB_TRAVERSAL + + + bSizer166 + 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_notebook11 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + + + General + 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_panel81 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer1701 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + + 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 + Name + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText401 + 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 + routine_name + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxEXPAND + 0 + + + szr_view_schema1 + 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_schema1 + 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 + routine_schema + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer181 + wxHORIZONTAL + none + + 5 + wxEXPAND + 1 + + + bSizer891 + 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 + Type + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText77 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + "Procedure (doesn't return a result)" "Function (return a result)" + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + routine_type + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + 5 + wxEXPAND + 1 + + + bSizer1161 + 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 + Return type + 0 + + 0 + + + 0 + + 1 + m_staticText78 + 1 + + + protected + 1 + + Resizable + 1 + 150,-1 + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 0 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + routine_return_type + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + 5 + wxEXPAND + 1 + + + bSizer182 + 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 + Comment + 0 + + 0 + + + 0 + + 1 + m_staticText79 + 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 + + 0 + + 0 + + 0 + + 1 + routine_comment + 1 + + + protected + 1 + + Resizable + 1 + + wxTE_MULTILINE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + + + Parameters + 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_panel82 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer178 + wxHORIZONTAL + none + + 5 + wxEXPAND + 1 + + + bSizer185 + 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_toolBar11 + 1 + 1 + + + protected + 1 + + Resizable + 5 + 1 + + wxTB_HORZ_TEXT|wxTB_RIGHT|wxTB_TEXT|wxTB_VERTICAL + ; ; forward_declare + 0 + + + + + + Load From File; icons/16x16/add.png + 0 + wxID_ANY + wxITEM_NORMAL + Insert + m_tool40 + protected + + Insert + on_routine_parameters_insert + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Remove + m_tool41 + protected + + + on_routine_parameters_delete + + + Load From File; icons/16x16/cross.png + 0 + wxID_ANY + wxITEM_NORMAL + Clear + m_tool42 + protected + + + on_routine_parameters_clear + + + + + 5 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + routine_parameters + protected + + + + ; ; forward_declare + + + + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE + # + wxDATAVIEW_CELL_INERT + 0 + m_dataViewColumn27 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE + Name + wxDATAVIEW_CELL_INERT + 0 + m_dataViewColumn28 + protected + Text + 600 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE + Datatype + wxDATAVIEW_CELL_INERT + 0 + m_dataViewColumn29 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE + Context + wxDATAVIEW_CELL_INERT + 0 + m_dataViewColumn30 + protected + Text + -1 + + + + + + + + + Behavior + 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_panel86 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer183 + 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 + panel_behavior_mysql_mariadb + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer184 + wxHORIZONTAL + none + + 5 + wxEXPAND + 1 + + + bSizer186 + 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 + Data access + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText80 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + "CONTAINS SQL" "NO SQL" "READS SQL DATA" "MODIFIES SQL DATA" "" "" + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + behavior_data_access + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + 5 + wxEXPAND + 1 + + + bSizer1851 + wxVERTICAL + none + + 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 + Deterministic + + 0 + + + 0 + + 1 + behavior_deterministic + 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 + panel_behavior_postgresql + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer187 + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + + bSizer188 + 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 + Language + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText81 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + "SQL" "PLPGSQL" + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + behavior_postgresql_language + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + 5 + wxEXPAND + 1 + + + bSizer1881 + 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 + Volatility + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText811 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + "VOLATILE" "STABLE" "IMMUTABLE" "" + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + behavior_postgresql_volatility + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + 5 + wxEXPAND + 1 + + + bSizer18811 + 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 + Parallel + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText8112 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + "UNSAFE" "RESTRICTED" "SAFE" + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + behavior_postgresql_parallel + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + 5 + wxEXPAND + 1 + + + bSizer192 + wxHORIZONTAL + none + + 5 + wxEXPAND + 1 + + + bSizer196 + 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 + Cost + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText87 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 1 + 0 + 100 + + 0 + + 0 + + 0 + + 1 + behavior_postgresql_cost + 1 + + + protected + 1 + + Resizable + 1 + + wxSP_ARROW_KEYS + ; ; forward_declare + 0 + + + + + + + + + + + 5 + wxEXPAND + 1 + + + bSizer193 + 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 + Rows + 0 + + 0 + + + 0 + + 1 + m_staticText85 + 1 + + + protected + 1 + + Resizable + 1 + 150,-1 + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 0 + + 1 + + 0 + 0 + wxID_ANY + 0 + 10 + + 0 + + 0 + + 0 + + 1 + behavior_postgresql_rows + 1 + + + protected + 1 + + Resizable + 1 + + wxSP_ARROW_KEYS + ; ; forward_declare + 0 + + + + + + + + + + + + + + + + + + + + Security + 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_panel83 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + Security + 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 + routine_definer_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + szr_view_definer1 + 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_definer1 + 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 + routine_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 + routine_security_panel + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + szr_view_sql_security1 + 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_security1 + 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 + routine_security_sql + 1 + + + protected + 1 + + Resizable + 0 + 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_panel74 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer161 + 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,200 + 1 + routine_stc + 1 + + + protected + 1 + + 0 + Resizable + 1 + -1,-1 + ; ; forward_declare + 1 + 4 + 0 + + 1 + 0 + 0 + + + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer911 + 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_routine_delete + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_routine_delete + + + + 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_routine_cancel + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_routine_cancel + + + + 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_routine_save + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_routine_save + + + + + + + + + Load From File; icons/16x16/cog.png + Trigger + 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 + + 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 + Refresh + 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 + wxEXPAND + 0 + + + bSizer1591 + wxHORIZONTAL + none + + 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 + Apply filters in data CTRL+ENTER + + wxFILTER_NONE + wxDefaultValidator + + + + + on_apply_filters + + + + 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 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Clear + + 0 + + 0 + + + 0 + + 1 + m_button56 + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_clear_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 + menu_table_records + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Insert row + m_menuItem13 + none + Ins + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + MyMenuItem + m_menuItem14 + none + + + + + m_separator2 + none + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + MyMenuItem + m_menuItem20 + none + + + + + + MyMenu + m_menu41 + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + NULL + m_menuItem21 + 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 + 1 + + + bSizer150 + wxHORIZONTAL + 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_splitter8 + 1 + + + protected + 1 + + Resizable + 1 + -480 + -1 + 1 + + wxSPLIT_VERTICAL + 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_panel70 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer157 + 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_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 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel71 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer1581 + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + 200,-1 + tree_ctrl_query_history + protected + + + wxDV_NO_HEADER|wxDV_ROW_LINES + ; ; forward_declare + + + + + + + + + + + + + + + + + + + 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 + + wxBOTH + + 1 + 0 + 1 + impl_virtual + + + + 0 + wxID_ANY + + + Trash + + 500,300 + wxDEFAULT_FRAME_STYLE + ; ; forward_declare + + + 0 + + + wxTAB_TRAVERSAL + 1 + + + bSizer147 + wxVERTICAL + none 5 wxEXPAND - 0 + 1 - bSizer165 - wxHORIZONTAL + bSizer152 + wxVERTICAL 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 b86e67f..8ef5837 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ ![status: unstable](https://img.shields.io/badge/status-unstable-orange) -![Coverage](https://img.shields.io/badge/coverage-54%25-brightgreen) -![Tests](https://img.shields.io/badge/tests-3114-blue) +![Coverage](https://img.shields.io/badge/coverage-46%25-brightgreen) +![Tests](https://img.shields.io/badge/tests-2495-blue) -![SQLite](https://img.shields.io/badge/SQLite-3.45.1-green) +![SQLite](https://img.shields.io/badge/SQLite-3.50.4-green) ![MySQL](https://img.shields.io/badge/MySQL-8%20%7C%209-green) ![MariaDB](https://img.shields.io/badge/MariaDB-5%20%7C%2010%20%7C%2011%20%7C%2012-green) -![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15%20%7C%2016%20%7C%2017%20%7C%2018-lightgrey) +![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15%20%7C%2016%20%7C%2017%20%7C%2018-green) # PeterSQL @@ -37,13 +37,14 @@ For a detailed status snapshot, see: - [PROJECT_STATUS.md](PROJECT_STATUS.md) ### Recent updates - -- 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. - + + - Connection passwords are now stored securely via system keyring using per-connection UUIDs instead of plaintext YAML. + - 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. + - Added stored function support with deterministic flag handling for MySQL and MariaDB engines. --- ## 🧭 Why PeterSQL? @@ -125,12 +126,12 @@ For detailed test coverage matrix, statistics, and architecture, see **[tests/RE | Suite | Passed | Skipped | |-------|--------|---------| | autocomplete | ![passed](https://img.shields.io/badge/passed-2540-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-0-lightgrey) | -| core | ![passed](https://img.shields.io/badge/passed-122-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-0-lightgrey) | -| ui | ![passed](https://img.shields.io/badge/passed-57-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-0-lightgrey) | -| mysql | ![passed](https://img.shields.io/badge/passed-63-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-1-lightgrey) | -| mariadb | ![passed](https://img.shields.io/badge/passed-123-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-3-lightgrey) | -| postgresql | ![passed](https://img.shields.io/badge/passed-140-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-0-lightgrey) | -| sqlite | ![passed](https://img.shields.io/badge/passed-24-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-5-lightgrey) | +| core | ![passed](https://img.shields.io/badge/passed-143-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-0-lightgrey) | +| ui | ![passed](https://img.shields.io/badge/passed-0-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-0-lightgrey) | +| mysql | ![passed](https://img.shields.io/badge/passed-119-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-1-lightgrey) | +| mariadb | ![passed](https://img.shields.io/badge/passed-237-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-3-lightgrey) | +| postgresql | ![passed](https://img.shields.io/badge/passed-244-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-0-lightgrey) | +| sqlite | ![passed](https://img.shields.io/badge/passed-54-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-5-lightgrey) | diff --git a/VERSION b/VERSION index 8a9ecc2..6c6aa7c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.1 \ No newline at end of file +0.1.0 \ No newline at end of file diff --git a/assets/database_options_matrix.md b/assets/database_options_matrix.md new file mode 100644 index 0000000..a87d7b1 --- /dev/null +++ b/assets/database_options_matrix.md @@ -0,0 +1,57 @@ +# Database Options Matrix (Compact) + +## Legend + +- ✅ supported +- ⚠️ partial / indirect +- ❌ not supported + +------------------------------------------------------------------------ + +## Matrix + + Option MySQL MariaDB PostgreSQL SQLite + ------------------- ------- --------- ------------ -------- + Charset ✅ ✅ ❌ ❌ + Collation ✅ ✅ ⚠️ ❌ + Encryption ✅ ❌ ❌ ❌ + Tablespace ❌ ❌ ✅ ❌ + Connection limit ❌ ❌ ✅ ❌ + +------------------------------------------------------------------------ + +## Notes + +### MySQL + +- Focus on: + - charset + - collation + - encryption (Y/N) + +### MariaDB + +- Focus on: + - charset + - collation +- Encryption NOT supported (MySQL-only syntax) + +### PostgreSQL + +- Different model: + - tablespace + - connection limit +- Collation: shown in UI but `PostgreSQLDatabase` has no `default_collation` field — not functional yet + +### SQLite + +- No real database-level configuration +- Database = file + +------------------------------------------------------------------------ + +## Important Caveat + +Collation in PostgreSQL is NOT equivalent to MySQL: +- Derived from locale (LC_COLLATE) — not freely alterable like in MySQL/MariaDB +- Currently not implemented in the app model diff --git a/helpers/bindings.py b/helpers/bindings.py index 9a0a417..87cbe38 100644 --- a/helpers/bindings.py +++ b/helpers/bindings.py @@ -6,6 +6,7 @@ import wx import wx.stc +from helpers.logger import logger from helpers.observables import Observable, CallbackEvent CONTROL_BIND_LABEL: TypeAlias = wx.StaticText @@ -228,25 +229,43 @@ def __init__(cls, name, bases, attrs): class AbstractModel(metaclass=AbstractMetaModel): observables: list[Observable] = [] + def __init__(self): + self.observables = [] + self._bindings: list[AbstractBindControl] = [] + + def _ensure_internal_state(self) -> None: + if not hasattr(self, "observables"): + self.observables = [] + + if not hasattr(self, "_bindings"): + self._bindings = [] + def bind_control(self, control: CONTROLS, observable: Observable): + self._ensure_internal_state() + binding: Optional[AbstractBindControl] = None + if isinstance(control, wx.StaticText): - BindLabelControl(control, observable) + binding = BindLabelControl(control, observable) elif isinstance(control, (wx.TextCtrl, wx.SpinCtrl, wx.CheckBox)): - BindValueControl(control, observable) + binding = BindValueControl(control, observable) elif isinstance(control, (wx.FilePickerCtrl, wx.DirPickerCtrl)): - BindPathControl(control, observable) + binding = BindPathControl(control, observable) elif isinstance(control, wx.Choice): - BindSelectionControl(control, observable) + binding = BindSelectionControl(control, observable) elif isinstance(control, wx.ComboBox): - BindComboControl(control, observable) + binding = BindComboControl(control, observable) elif isinstance(control, wx.stc.StyledTextCtrl): - BindStyledTextControl(control, observable) + binding = BindStyledTextControl(control, observable) elif isinstance(control, list) and control and isinstance(control[0], wx.RadioButton): - BindRadioGroupControl(control, observable) + binding = BindRadioGroupControl(control, observable) + + if binding is not None: + self._bindings.append(binding) self.observables.append(observable) def bind_controls(self, **controls: Union[CONTROLS, tuple[CONTROLS, dict]]): + self._ensure_internal_state() for name, ctrl in controls.items(): if hasattr(self, name) and isinstance(getattr(self, name), Observable): observable = getattr(self, name) @@ -256,9 +275,25 @@ def bind_controls(self, **controls: Union[CONTROLS, tuple[CONTROLS, dict]]): self.bind_control(ctrl, observable) def subscribe(self, callback: Callable): + self._ensure_internal_state() for observable in self.observables: observable.subscribe(callback) + def sync(self) -> None: + self._ensure_internal_state() + for binding in self._bindings: + value = binding.get() + current_value = binding.observable.get_value() + if value != current_value: + logger.debug( + "ui trace: model.sync changed observable=%s control=%s old=%r new=%r", + getattr(binding.observable, "name", None), + type(binding.control).__name__, + current_value, + value, + ) + binding.observable.set_value(value) + def wx_call_after_debounce(*observables: Observable, callback: Callable, wait_time: float = 0.4): waiting = False diff --git a/helpers/logger.py b/helpers/logger.py index 1777585..d731803 100755 --- a/helpers/logger.py +++ b/helpers/logger.py @@ -1,9 +1,99 @@ -import logging.config +import faulthandler +import logging +import sys +import threading + +from logging.handlers import RotatingFileHandler +from pathlib import Path +from types import TracebackType +from typing import Optional, TextIO LOG_FMT = "%(asctime)s %(process)s %(levelname)s %(name)s: %(message)s" DATE_FMT = "%Y-%m-%d %H:%M:%S" -logging.basicConfig(format=LOG_FMT, datefmt=DATE_FMT) +_fault_log_stream: Optional[TextIO] = None + + +def _log_unhandled_exception( + source: str, + exc_type: type[BaseException], + exc_value: BaseException, + exc_traceback: Optional[TracebackType], +) -> None: + logger.critical( + f"Unhandled exception from {source}", + exc_info=(exc_type, exc_value, exc_traceback), + ) + + +def configure_logging(log_file_path: Path) -> None: + log_file_path.parent.mkdir(parents=True, exist_ok=True) + + formatter = logging.Formatter(fmt=LOG_FMT, datefmt=DATE_FMT) + + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + + if not any(isinstance(handler, logging.StreamHandler) for handler in root_logger.handlers): + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(formatter) + root_logger.addHandler(stream_handler) + + if not any( + isinstance(handler, RotatingFileHandler) + and Path(handler.baseFilename) == log_file_path + for handler in root_logger.handlers + ): + file_handler = RotatingFileHandler( + filename=log_file_path, + mode="a", + maxBytes=10_000_000, + backupCount=5, + encoding="utf-8", + ) + file_handler.setFormatter(formatter) + root_logger.addHandler(file_handler) + + +def enable_fault_handler(fault_log_path: Path) -> None: + global _fault_log_stream + + fault_log_path.parent.mkdir(parents=True, exist_ok=True) + _fault_log_stream = fault_log_path.open(mode="a", encoding="utf-8") + faulthandler.enable(file=_fault_log_stream, all_threads=True) + + +def install_global_exception_hooks() -> None: + def _main_excepthook( + exc_type: type[BaseException], + exc_value: BaseException, + exc_traceback: Optional[TracebackType], + ) -> None: + _log_unhandled_exception("sys.excepthook", exc_type, exc_value, exc_traceback) + + def _thread_excepthook(args: threading.ExceptHookArgs) -> None: + _log_unhandled_exception( + f"threading.excepthook thread={args.thread.name}", + args.exc_type, + args.exc_value, + args.exc_traceback, + ) + + def _unraisablehook(args: sys.UnraisableHookArgs) -> None: + exc_value = args.exc_value + if exc_value is None: + exc_value = RuntimeError("unraisable exception without exc_value") + + _log_unhandled_exception( + "sys.unraisablehook", + type(exc_value), + exc_value, + args.exc_traceback, + ) + + sys.excepthook = _main_excepthook + threading.excepthook = _thread_excepthook + sys.unraisablehook = _unraisablehook logger = logging.getLogger("PeterSQL") logger.setLevel(logging.DEBUG) diff --git a/icons/16x16/code-folding.png b/icons/16x16/code-folding.png index ca97e23..d912838 100644 Binary files a/icons/16x16/code-folding.png and b/icons/16x16/code-folding.png differ diff --git a/icons/16x16/js-96.png b/icons/16x16/js-96.png index ac67e6e..11b3e69 100644 Binary files a/icons/16x16/js-96.png and b/icons/16x16/js-96.png differ diff --git a/icons/16x16/markdown-96.png b/icons/16x16/markdown-96.png index 4de6697..bf4da2f 100644 Binary files a/icons/16x16/markdown-96.png and b/icons/16x16/markdown-96.png differ diff --git a/icons/16x16/php-100.png b/icons/16x16/php-100.png index 63d7783..bf21252 100644 Binary files a/icons/16x16/php-100.png and b/icons/16x16/php-100.png differ diff --git a/icons/16x16/server-firebird.png b/icons/16x16/server-firebird.png index ef6fb71..2220dff 100644 Binary files a/icons/16x16/server-firebird.png and b/icons/16x16/server-firebird.png differ diff --git a/icons/16x16/server-interbase.png b/icons/16x16/server-interbase.png index 1f4ce62..8f48202 100644 Binary files a/icons/16x16/server-interbase.png and b/icons/16x16/server-interbase.png differ diff --git a/icons/16x16/server-memsql.png b/icons/16x16/server-memsql.png index d9df637..d92e23b 100644 Binary files a/icons/16x16/server-memsql.png and b/icons/16x16/server-memsql.png differ diff --git a/icons/16x16/server-oracle.png b/icons/16x16/server-oracle.png index 4a1aea9..0e61115 100644 Binary files a/icons/16x16/server-oracle.png and b/icons/16x16/server-oracle.png differ diff --git a/icons/16x16/server-proxysqladmin.png b/icons/16x16/server-proxysqladmin.png index 7bdbbfa..afe17c1 100644 Binary files a/icons/16x16/server-proxysqladmin.png and b/icons/16x16/server-proxysqladmin.png differ diff --git a/icons/16x16/server-rds-mysql.png b/icons/16x16/server-rds-mysql.png index 6040183..7e35a78 100644 Binary files a/icons/16x16/server-rds-mysql.png and b/icons/16x16/server-rds-mysql.png differ diff --git a/icons/16x16/sql-96.png b/icons/16x16/sql-96.png index 6acb7a6..1a68db8 100644 Binary files a/icons/16x16/sql-96.png and b/icons/16x16/sql-96.png differ diff --git a/icons/16x16/textile-lang.png b/icons/16x16/textile-lang.png index 17cd458..eb12163 100644 Binary files a/icons/16x16/textile-lang.png and b/icons/16x16/textile-lang.png differ diff --git a/locale/de_DE/LC_MESSAGES/petersql.mo b/locale/de_DE/LC_MESSAGES/petersql.mo index a5c6beb..7823f6f 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 22cec8b..d32b06a 100644 --- a/locale/de_DE/LC_MESSAGES/petersql.po +++ b/locale/de_DE/LC_MESSAGES/petersql.po @@ -2,12 +2,11 @@ # Copyright (C) 2026 ORGANIZATION # This file is distributed under the same license as the PeterSQL project. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-23 10:07+0100\n" +"POT-Creation-Date: 2026-06-06 11:23+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: de_DE\n" @@ -47,649 +46,846 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "OpenSSH-Client nicht gefunden." -#: structures/engines/mariadb/context.py:611 -#: structures/engines/mysql/context.py:622 -#: structures/engines/postgresql/context.py:645 -#: structures/engines/sqlite/context.py:524 +#: structures/engines/context.py:567 +msgid "This connection is read-only." +msgstr "Diese Verbindung ist schreibgeschützt." + +#: structures/engines/context.py:578 +#, fuzzy, python-brace-format +msgid "Database connection lost: {error}" +msgstr "" +"Verbindungsfehler:\n" +"{error}" + +#: structures/engines/mariadb/context.py:658 +#: structures/engines/mysql/context.py:670 +#: structures/engines/postgresql/context.py:701 +#: structures/engines/sqlite/context.py:564 #, python-brace-format msgid "Table{table_index:03}" -msgstr "" +msgstr "Table{table_index:03}" -#: structures/engines/mariadb/context.py:639 -#: structures/engines/mysql/context.py:650 -#: structures/engines/postgresql/context.py:670 -#: structures/engines/sqlite/context.py:548 +#: structures/engines/mariadb/context.py:686 +#: structures/engines/mysql/context.py:698 +#: structures/engines/postgresql/context.py:726 +#: structures/engines/sqlite/context.py:588 #, python-brace-format msgid "Column{column_index:03}" -msgstr "" +msgstr "Column{column_index:03}" -#: structures/engines/mariadb/context.py:657 -#: structures/engines/mysql/context.py:668 -#: structures/engines/postgresql/context.py:688 -#: structures/engines/sqlite/context.py:566 +#: structures/engines/mariadb/context.py:704 +#: structures/engines/mysql/context.py:716 +#: structures/engines/postgresql/context.py:744 +#: structures/engines/sqlite/context.py:606 #, python-brace-format msgid "Index{index_number:03}" -msgstr "" +msgstr "Index{index_number:03}" -#: structures/engines/mariadb/context.py:697 -#: structures/engines/mysql/context.py:706 -#: structures/engines/postgresql/context.py:728 -#: structures/engines/sqlite/context.py:608 +#: structures/engines/mariadb/context.py:744 +#: structures/engines/mysql/context.py:754 +#: structures/engines/postgresql/context.py:784 +#: structures/engines/sqlite/context.py:646 #, python-brace-format msgid "ForeignKey{foreign_key_number:03}" -msgstr "" +msgstr "ForeignKey{foreign_key_number:03}" -#: structures/engines/mariadb/context.py:730 -#: structures/engines/mysql/context.py:737 -#: structures/engines/postgresql/context.py:758 -#: structures/engines/sqlite/context.py:636 +#: structures/engines/mariadb/context.py:777 +#: structures/engines/mysql/context.py:785 +#: structures/engines/postgresql/context.py:814 +#: structures/engines/sqlite/context.py:674 #, python-brace-format msgid "View{view_index:03}" -msgstr "" +msgstr "View{view_index:03}" -#: structures/engines/mariadb/context.py:781 +#: structures/engines/mariadb/context.py:830 #, python-brace-format msgid "Trigger{trigger_index:03}" -msgstr "" +msgstr "Trigger{trigger_index:03}" -#: windows/dialogs/connections/view.py:415 -#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 -#: windows/views.py:33 +#: windows/dialogs/connections/view.py:417 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1303 +#: windows/views.py:62 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:1006 -#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 -#: windows/views.py:2821 +#: windows/components/dataview.py:115 windows/components/dataview.py:240 +#: windows/components/dataview.py:253 windows/components/dataview.py:268 +#: windows/views.py:76 windows/views.py:126 windows/views.py:1053 +#: windows/views.py:1365 windows/views.py:1398 windows/views.py:1422 +#: windows/views.py:1446 windows/views.py:1470 windows/views.py:1494 +#: windows/views.py:1611 windows/views.py:1957 windows/views.py:2228 +#: windows/views.py:2336 msgid "Name" msgstr "Name" -#: windows/views.py:48 windows/views.py:428 +#: windows/views.py:77 windows/views.py:467 msgid "Last connection" msgstr "Letzte Verbindung" -#: windows/dialogs/connections/view.py:653 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:90 msgid "New directory" msgstr "Neues Verzeichnis" -#: windows/dialogs/connections/model.py:206 -#: windows/dialogs/connections/view.py:613 windows/views.py:65 +#: windows/dialogs/connections/model.py:212 +#: windows/dialogs/connections/view.py:615 windows/views.py:94 msgid "New connection" msgstr "Neue Verbindung" -#: windows/views.py:71 -#, fuzzy +#: windows/views.py:100 msgid "Rename" -msgstr "Name" +msgstr "Umbenennen" -#: windows/views.py:76 -#, fuzzy +#: windows/views.py:105 msgid "Clone connection" -msgstr "Neue Verbindung" +msgstr "Verbindung klonen" -#: 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 +#: windows/views.py:110 windows/views.py:642 windows/views.py:1525 +#: windows/views.py:1893 windows/views.py:2183 windows/views.py:2587 msgid "Delete" msgstr "Löschen" -#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 -#: windows/views.py:2876 +#: windows/views.py:140 windows/views.py:1370 windows/views.py:1666 msgid "Engine" msgstr "Engine" -#: windows/views.py:132 +#: windows/views.py:161 msgid "Host + port" msgstr "Host + Port" -#: windows/views.py:148 +#: windows/views.py:177 msgid "Username" msgstr "Benutzername" -#: windows/views.py:161 windows/views.py:1132 +#: windows/views.py:190 windows/views.py:1179 msgid "Password" msgstr "Passwort" -#: windows/views.py:174 -#, fuzzy +#: windows/views.py:203 msgid "Connection timeout" -msgstr "Verbindung verloren" +msgstr "Verbindungs-Timeout" -#: windows/views.py:192 +#: windows/views.py:221 msgid "Use TLS" msgstr "TLS verwenden" -#: windows/views.py:203 +#: windows/views.py:232 +msgid "Mark read only" +msgstr "Als schreibgeschützt markieren" + +#: windows/views.py:243 msgid "Use SSH tunnel" msgstr "SSH-Tunnel verwenden" -#: windows/views.py:214 +#: windows/views.py:254 msgid "Compressed client/server protocol" -msgstr "" +msgstr "Komprimiertes Client/Server-Protokoll" -#: windows/views.py:233 +#: windows/views.py:273 msgid "Filename" msgstr "Dateiname" -#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 +#: windows/views.py:278 windows/views.py:397 msgid "Select a file" msgstr "Datei auswählen" -#: windows/views.py:238 windows/views.py:358 -#, fuzzy +#: windows/views.py:278 windows/views.py:397 msgid "*.*" -msgstr "*. *" +msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 -#: windows/views.py:2834 +#: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 msgid "Comments" msgstr "Kommentare" -#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 -#: windows/views.py:884 +#: windows/main/controller.py:772 windows/views.py:309 windows/views.py:769 +#: windows/views.py:923 msgid "Settings" msgstr "Einstellungen" -#: windows/views.py:278 +#: windows/views.py:317 msgid "SSH executable" msgstr "SSH-Executable" -#: windows/views.py:283 +#: windows/views.py:322 msgid "ssh" msgstr "ssh" -#: windows/views.py:291 +#: windows/views.py:330 msgid "SSH host + port" msgstr "SSH-Host + Port" -#: windows/views.py:303 +#: windows/views.py:342 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:312 +#: windows/views.py:351 msgid "SSH username" msgstr "SSH-Benutzername" -#: windows/views.py:325 +#: windows/views.py:364 msgid "SSH password" msgstr "SSH-Passwort" -#: windows/views.py:338 +#: windows/views.py:377 msgid "Local port" msgstr "Lokaler Port" -#: windows/views.py:344 +#: windows/views.py:383 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:353 +#: windows/views.py:392 msgid "Identity file" msgstr "Identitätsdatei" -#: windows/views.py:369 -#, fuzzy +#: windows/views.py:408 msgid "Remote host + port" -msgstr "Host + Port" +msgstr "Remote-Host + Port" -#: windows/views.py:381 +#: windows/views.py:420 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:390 +#: windows/views.py:429 msgid "SSH extra args" -msgstr "" +msgstr "Zusätzliche SSH-Argumente" -#: windows/views.py:405 +#: windows/views.py:444 msgid "SSH Tunnel" msgstr "SSH-Tunnel" -#: windows/views.py:411 windows/views.py:1341 +#: windows/views.py:450 windows/views.py:1368 msgid "Created at" msgstr "Erstellt am" -#: windows/views.py:445 +#: windows/views.py:484 msgid "Successful connections" msgstr "Erfolgreiche Verbindungen" -#: windows/views.py:462 -#, fuzzy +#: windows/views.py:501 msgid "Last successful connection" -msgstr "Erfolgreiche Verbindungen" +msgstr "Letzte erfolgreiche Verbindung" -#: windows/views.py:479 +#: windows/views.py:518 msgid "Unsuccessful connections" msgstr "Erfolglose Verbindungen" -#: windows/views.py:496 +#: windows/views.py:535 msgid "Last failure reason" -msgstr "" +msgstr "Letzter Fehlergrund" -#: windows/views.py:513 -#, fuzzy +#: windows/views.py:552 msgid "Total connection attempts" -msgstr "Letzte Verbindung" +msgstr "Gesamte Verbindungsversuche" -#: windows/views.py:530 -#, fuzzy +#: windows/views.py:569 msgid "Average connection time (ms)" -msgstr "Wiederverbindung fehlgeschlagen:" +msgstr "Durchschnittliche Verbindungszeit (ms)" -#: windows/views.py:547 -#, fuzzy +#: windows/views.py:586 msgid "Most recent connection duration" -msgstr "Verbindungsmanager öffnen" +msgstr "Dauer der letzten Verbindung" -#: windows/views.py:566 +#: windows/views.py:605 msgid "Statistics" msgstr "Statistiken" -#: windows/views.py:584 windows/views.py:1730 +#: windows/views.py:623 windows/views.py:1850 msgid "Create" msgstr "Erstellen" -#: windows/views.py:588 -#, fuzzy +#: windows/views.py:627 msgid "Create connection" -msgstr "Letzte Verbindung" +msgstr "Verbindung erstellen" -#: windows/views.py:591 -#, fuzzy +#: windows/views.py:630 msgid "Create directory" msgstr "Neues Verzeichnis" -#: 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 +#: windows/views.py:659 windows/views.py:883 windows/views.py:1520 +#: windows/views.py:1896 windows/views.py:2188 windows/views.py:2592 +#: windows/views.py:2648 msgid "Cancel" msgstr "Abbrechen" -#: 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 +#: windows/main/controller.py:324 windows/views.py:664 windows/views.py:2193 +#: windows/views.py:2597 windows/views.py:2812 msgid "Save" msgstr "Speichern" -#: windows/views.py:632 +#: windows/views.py:671 msgid "Test" msgstr "Testen" -#: windows/views.py:639 +#: windows/views.py:678 msgid "Connect" msgstr "Verbinden" -#: windows/views.py:742 +#: windows/views.py:781 windows/views.py:2388 msgid "Language" msgstr "Sprache" -#: windows/views.py:747 +#: windows/views.py:786 msgid "English" msgstr "Englisch" -#: windows/views.py:747 +#: windows/views.py:786 msgid "Italian" msgstr "Italienisch" -#: windows/views.py:747 +#: windows/views.py:786 msgid "French" msgstr "Französisch" -#: windows/views.py:759 +#: windows/views.py:798 msgid "Locale" msgstr "Lokale" -#: windows/views.py:780 -#, fuzzy +#: windows/views.py:819 msgid "Column content" -msgstr "Neue Verbindung" +msgstr "Spalteninhalt" -#: windows/views.py:790 +#: windows/views.py:829 msgid "Syntax" msgstr "Syntax" -#: windows/views.py:847 +#: windows/views.py:886 msgid "Ok" msgstr "Ok" -#: windows/views.py:878 +#: windows/views.py:917 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:887 +#: windows/views.py:926 msgid "File" msgstr "Datei" -#: windows/views.py:890 +#: windows/views.py:929 msgid "About" msgstr "Über" -#: windows/views.py:893 +#: windows/views.py:932 msgid "Help" msgstr "Hilfe" -#: windows/views.py:898 +#: windows/views.py:937 msgid "Open connection manager" msgstr "Verbindungsmanager öffnen" -#: windows/views.py:902 +#: windows/views.py:941 msgid "Disconnect from server" msgstr "Vom Server trennen" -#: windows/views.py:904 +#: windows/views.py:943 msgid "tool" msgstr "Werkzeug" -#: windows/views.py:904 +#: windows/views.py:943 windows/views.py:2628 msgid "Refresh" msgstr "Aktualisieren" -#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 -#: windows/views.py:2077 windows/views.py:2221 +#: windows/views.py:947 windows/views.py:949 windows/views.py:1874 +#: windows/views.py:2632 windows/views.py:2798 msgid "Add" msgstr "Hinzufügen" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 +#: windows/views.py:954 +#, python-brace-format +msgid "{mode}" +msgstr "{mode}" + +#: windows/views.py:991 windows/views.py:995 windows/views.py:2765 +#: windows/views.py:2770 msgid "MyMenuItem" msgstr "MeinMenüElement" -#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 +#: windows/views.py:998 windows/views.py:1924 windows/views.py:2777 msgid "MyMenu" msgstr "MeinMenü" -#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 -#: windows/views.py:1402 +#: windows/views.py:1013 windows/views.py:1548 windows/views.py:1555 +#: windows/views.py:1562 msgid "MyLabel" msgstr "MeinLabel" -#: windows/views.py:972 +#: windows/views.py:1019 msgid "Databases" msgstr "Datenbanken" -#: windows/views.py:973 windows/views.py:1340 +#: windows/views.py:1020 windows/views.py:1367 msgid "Size" msgstr "Größe" -#: windows/views.py:974 +#: windows/views.py:1021 msgid "Elements" msgstr "Elemente" -#: windows/views.py:975 +#: windows/views.py:1022 msgid "Modified at" msgstr "Geändert am" -#: windows/views.py:976 +#: windows/views.py:1023 windows/views.py:1382 msgid "Tables" msgstr "Tabellen" -#: windows/views.py:983 +#: windows/views.py:1030 msgid "System" msgstr "System" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1029 -#: windows/views.py:1344 +#: windows/components/dataview.py:89 windows/views.py:1076 +#: windows/views.py:1371 msgid "Collation" msgstr "Sortierung" -#: windows/views.py:1058 +#: windows/views.py:1105 msgid "Encryption" -msgstr "" +msgstr "Verschlüsselung" -#: windows/views.py:1070 +#: windows/main/controller.py:1272 windows/main/controller.py:1294 +#: windows/main/controller.py:1298 windows/views.py:1117 msgid "Read Only" -msgstr "" +msgstr "Schreibgeschützt" -#: windows/views.py:1087 -#, fuzzy +#: windows/views.py:1134 msgid "Tablespace" -msgstr "Tabellen" +msgstr "Tablespace" -#: windows/views.py:1108 -#, fuzzy +#: windows/views.py:1155 msgid "Connection limit" -msgstr "Verbindung verloren" +msgstr "Verbindungslimit" -#: windows/views.py:1151 -#, fuzzy +#: windows/views.py:1198 msgid "Profile" -msgstr "Datei" +msgstr "Profil" -#: windows/views.py:1177 -#, fuzzy +#: windows/views.py:1224 msgid "Default tablespace" -msgstr "Tabelle löschen" +msgstr "Standard-Tablespace" -#: windows/views.py:1198 -#, fuzzy +#: windows/views.py:1245 msgid "Temporary tablespace" -msgstr "Temporär" +msgstr "Temporärer Tablespace" -#: windows/views.py:1224 +#: windows/views.py:1271 msgid "Quota" -msgstr "" +msgstr "Quota" -#: windows/views.py:1243 +#: windows/views.py:1290 msgid "Unlimited quota" -msgstr "" +msgstr "Unbegrenzte Quota" -#: windows/views.py:1260 +#: windows/views.py:1307 msgid "Account status" -msgstr "" +msgstr "Kontostatus" -#: windows/views.py:1281 -#, fuzzy +#: windows/views.py:1328 msgid "Password expire" -msgstr "Passwort" +msgstr "Passwortablauf" -#: windows/views.py:1302 -msgid "Table:" -msgstr "Tabelle:" +#: windows/views.py:1352 +msgid "Add new table" +msgstr "Neue Tabelle hinzufügen" -#: 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:1354 +msgid "Clone table" +msgstr "Tabelle klonen" -#: windows/views.py:1315 -msgid "Clone" -msgstr "Klonen" +#: windows/main/controller.py:1821 windows/views.py:1356 +msgid "Delete table" +msgstr "Tabelle löschen" -#: windows/views.py:1339 +#: windows/views.py:1366 windows/views.py:2457 msgid "Rows" msgstr "Zeilen" -#: windows/views.py:1342 +#: windows/views.py:1369 msgid "Updated at" msgstr "Aktualisiert am" -#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 -#: windows/views.py:2173 windows/views.py:2709 +#: windows/views.py:1387 +msgid "Add new view" +msgstr "Neue Ansicht hinzufügen" + +#: windows/views.py:1389 +msgid "Clone view" +msgstr "Klonen" + +#: windows/views.py:1391 +msgid "Delete view" +msgstr "Löschen" + +#: windows/views.py:1399 windows/views.py:1423 windows/views.py:1447 +#: windows/views.py:1471 windows/views.py:1495 +msgid "Definition" +msgstr "Definition" + +#: windows/views.py:1406 +msgid "Views" +msgstr "Ansichten" + +#: windows/views.py:1411 +msgid "Add new procedure" +msgstr "Neue Prozedur hinzufügen" + +#: windows/views.py:1413 +msgid "Clone procedure" +msgstr "Prozedur klonen" + +#: windows/views.py:1415 +msgid "Delete procedure" +msgstr "Prozedur löschen" + +#: windows/views.py:1430 +msgid "Procedures" +msgstr "Prozeduren" + +#: windows/views.py:1435 +msgid "Add new function" +msgstr "Neue Funktion hinzufügen" + +#: windows/views.py:1437 +msgid "Clone function" +msgstr "Funktion klonen" + +#: windows/views.py:1439 +msgid "Delete function" +msgstr "Funktion löschen" + +#: windows/views.py:1454 +msgid "Functions" +msgstr "Funktionen" + +#: windows/views.py:1459 +msgid "Add new trigger" +msgstr "Neuen Trigger hinzufügen" + +#: windows/views.py:1461 +msgid "Clone trigger" +msgstr "Trigger klonen" + +#: windows/views.py:1463 +msgid "Delete trigger" +msgstr "Trigger löschen" + +#: windows/views.py:1478 +msgid "Triggers" +msgstr "Trigger" + +#: windows/views.py:1483 +msgid "Add new event" +msgstr "Neues Ereignis hinzufügen" + +#: windows/views.py:1485 +msgid "Clone event" +msgstr "Klonen" + +#: windows/views.py:1487 +msgid "Delete event" +msgstr "Ereignis löschen" + +#: windows/views.py:1502 +msgid "Events" +msgstr "Ereignisse" + +#: windows/views.py:1530 windows/views.py:1901 windows/views.py:2646 +#: windows/views.py:2730 msgid "Apply" msgstr "Anwenden" -#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 -#: windows/views.py:2909 +#: windows/views.py:1542 windows/views.py:1721 msgid "Options" msgstr "Optionen" -#: windows/views.py:1413 +#: windows/views.py:1573 msgid "Diagram" msgstr "Diagramm" -#: windows/views.py:1424 +#: windows/views.py:1584 msgid "Database" msgstr "Datenbank" -#: windows/views.py:1479 windows/views.py:2849 +#: windows/views.py:1639 msgid "Base" msgstr "Basis" -#: windows/views.py:1493 windows/views.py:2863 +#: windows/views.py:1653 msgid "Auto Increment" msgstr "Auto Inkrement" -#: windows/views.py:1521 windows/views.py:2891 +#: windows/views.py:1681 msgid "Default Collation" msgstr "Standard-Sortierung" -#: windows/views.py:1531 +#: windows/views.py:1691 msgid "Convert data" -msgstr "" +msgstr "Daten konvertieren" -#: windows/views.py:1539 +#: windows/views.py:1699 msgid "Row format" -msgstr "" +msgstr "Zeilenformat" -#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 -#: windows/views.py:1756 windows/views.py:2081 +#: windows/views.py:1732 windows/views.py:1760 windows/views.py:1788 +#: windows/views.py:1876 windows/views.py:2326 windows/views.py:2636 msgid "Remove" msgstr "Entfernen" -#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 +#: windows/views.py:1734 windows/views.py:1762 windows/views.py:1790 +#: windows/views.py:2328 windows/views.py:2738 msgid "Clear" msgstr "Löschen" -#: windows/views.py:1595 windows/views.py:2923 +#: windows/views.py:1747 msgid "Indexes" msgstr "Indizes" -#: windows/views.py:1639 +#: windows/views.py:1758 windows/views.py:1786 windows/views.py:2324 +msgid "Insert" +msgstr "Einfügen" + +#: windows/views.py:1775 msgid "Foreign Keys" msgstr "Fremdschlüssel" -#: windows/views.py:1683 +#: windows/views.py:1803 msgid "Checks" msgstr "Prüfungen" -#: windows/views.py:1750 windows/views.py:2944 +#: windows/views.py:1870 msgid "Columns:" msgstr "Spalten:" -#: windows/views.py:1760 -#, fuzzy +#: windows/views.py:1880 msgid "Move Up" msgstr "Nach oben bewegen\tCTRL+UP" -#: windows/views.py:1762 -#, fuzzy +#: windows/views.py:1882 msgid "Move Down" msgstr "Nach unten bewegen\tCTRL+D" -#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 -#: windows/views.py:3017 +#: windows/views.py:1914 windows/views.py:1921 msgid "Add Index" msgstr "Index hinzufügen" -#: windows/views.py:1798 windows/views.py:3014 +#: windows/views.py:1918 msgid "Add PrimaryKey" msgstr "Primärschlüssel hinzufügen" -#: windows/views.py:1815 +#: windows/views.py:1935 msgid "Table" msgstr "Tabelle" -#: windows/views.py:1851 -#, fuzzy -msgid "Definer" -msgstr "Einfügen" - -#: windows/views.py:1871 +#: windows/views.py:1977 windows/views.py:2243 msgid "Schema" -msgstr "" +msgstr "Schema" -#: windows/views.py:1897 -msgid "SQL security" -msgstr "" - -#: windows/views.py:1904 -#, fuzzy -msgid "DEFINER" -msgstr "Einfügen" +#: windows/views.py:2005 windows/views.py:2314 +msgid "General" +msgstr "Allgemein" -#: windows/views.py:1904 -#, fuzzy -msgid "INVOKER" -msgstr "Einfügen" - -#: windows/views.py:1916 +#: windows/views.py:2010 msgid "Algorithm" -msgstr "" +msgstr "Algorithmus" -#: windows/views.py:1918 -#, fuzzy +#: windows/views.py:2012 msgid "UNDEFINED" msgstr "Ohne Vorzeichen" -#: windows/views.py:1921 +#: windows/views.py:2015 msgid "MERGE" -msgstr "" +msgstr "MERGE" -#: windows/views.py:1924 -#, fuzzy +#: windows/views.py:2018 msgid "TEMPTABLE" -msgstr "Tabelle" +msgstr "TEMPTABLE" -#: windows/views.py:1934 +#: windows/views.py:2028 msgid "View constraint" -msgstr "" +msgstr "Ansichtseinschränkung" -#: windows/views.py:1936 -#, fuzzy +#: windows/views.py:2030 msgid "None" -msgstr "Klonen" +msgstr "Keiner" -#: windows/views.py:1939 -#, fuzzy +#: windows/views.py:2033 msgid "LOCAL" -msgstr "Lokale" +msgstr "LOKAL" -#: windows/views.py:1942 -#, fuzzy +#: windows/views.py:2036 msgid "CASCADE" -msgstr "Abbrechen" +msgstr "KASKADE" -#: windows/views.py:1945 -#, fuzzy +#: windows/views.py:2039 msgid "CHECK ONLY" -msgstr "Prüfen" +msgstr "NUR PRÜFEN" -#: windows/views.py:1948 +#: windows/views.py:2042 msgid "READ ONLY" -msgstr "" +msgstr "SCHREIBGESCHÜTZT" + +#: windows/views.py:2055 windows/views.py:2483 +msgid "Behavior" +msgstr "Verhalten" + +#: windows/views.py:2062 windows/views.py:2490 +msgid "Definer" +msgstr "Definierer" + +#: windows/views.py:2070 windows/views.py:2498 +msgid "*" +msgstr "*" + +#: windows/views.py:2082 windows/views.py:2510 +msgid "SQL security" +msgstr "SQL-Sicherheit" + +#: windows/views.py:2089 windows/views.py:2517 +msgid "DEFINER" +msgstr "DEFINIERER" -#: windows/views.py:1960 +#: windows/views.py:2089 windows/views.py:2517 +msgid "INVOKER" +msgstr "AUFRUFER" + +#: windows/views.py:2103 msgid "Force" -msgstr "" +msgstr "Erzwingen" -#: windows/views.py:1972 +#: windows/views.py:2115 msgid "Security barrier" -msgstr "" +msgstr "Sicherheitsbarriere" -#: windows/views.py:2054 -msgid "Views" -msgstr "Ansichten" +#: windows/views.py:2128 windows/views.py:2532 +msgid "Security" +msgstr "Sicherheit" -#: windows/views.py:2062 -msgid "Triggers" -msgstr "Trigger" +#: windows/views.py:2205 +msgid "View" +msgstr "Ansichten" -#: windows/views.py:2073 +#: windows/views.py:2262 #, fuzzy -msgid "Refrsh" -msgstr "Aktualisieren" +msgid "Type" +msgstr "Datentyp" -#: windows/views.py:2079 -#, fuzzy +#: windows/views.py:2269 +msgid "Procedure (doesn't return a result)" +msgstr "Prozedur (gibt kein Ergebnis zurück)" + +#: windows/views.py:2269 +msgid "Function (return a result)" +msgstr "Funktion (gibt ein Ergebnis zurück)" + +#: windows/views.py:2279 +msgid "Return type" +msgstr "Rückgabetyp" + +#: windows/views.py:2299 +msgid "Comment" +msgstr "Kommentar" + +#: windows/components/dataview.py:111 windows/views.py:2335 +msgid "#" +msgstr "#" + +#: windows/views.py:2337 +msgid "Datatype" +msgstr "Datentyp" + +#: windows/views.py:2338 +msgid "Context" +msgstr "Kontext" + +#: windows/views.py:2345 +msgid "Parameters" +msgstr "Parameter" + +#: windows/views.py:2354 +msgid "Data access" +msgstr "Datenzugriff" + +#: windows/views.py:2361 +msgid "CONTAINS SQL" +msgstr "CONTAINS SQL" + +#: windows/views.py:2361 +msgid "NO SQL" +msgstr "NO SQL" + +#: windows/views.py:2361 +msgid "READS SQL DATA" +msgstr "READS SQL DATA" + +#: windows/views.py:2361 +msgid "MODIFIES SQL DATA" +msgstr "MODIFIES SQL DATA" + +#: windows/views.py:2371 +msgid "Deterministic" +msgstr "Deterministisch" + +#: windows/views.py:2395 +msgid "SQL" +msgstr "SQL" + +#: windows/views.py:2395 +msgid "PLPGSQL" +msgstr "PLPGSQL" + +#: windows/views.py:2405 +msgid "Volatility" +msgstr "Volatilität" + +#: windows/views.py:2412 +msgid "VOLATILE" +msgstr "VOLATIL" + +#: windows/views.py:2412 +msgid "STABLE" +msgstr "STABIL" + +#: windows/views.py:2412 +msgid "IMMUTABLE" +msgstr "UNVERÄNDERLICH" + +#: windows/views.py:2422 +msgid "Parallel" +msgstr "Parallel" + +#: windows/views.py:2429 +msgid "UNSAFE" +msgstr "UNSICHER" + +#: windows/views.py:2429 +msgid "RESTRICTED" +msgstr "EINGESCHRÄNKT" + +#: windows/views.py:2429 +msgid "SAFE" +msgstr "SICHER" + +#: windows/views.py:2441 +msgid "Cost" +msgstr "Kosten" + +#: windows/views.py:2609 +msgid "Routine" +msgstr "Routine" + +#: windows/views.py:2617 +msgid "Trigger" +msgstr "Trigger" + +#: windows/views.py:2634 msgid "Duplicate" -msgstr "Datensatz duplizieren" +msgstr "Duplizieren" -#: windows/views.py:2085 +#: windows/views.py:2640 msgid "Apply changes automatically" msgstr "Änderungen automatisch anwenden" -#: windows/views.py:2087 windows/views.py:2088 +#: windows/views.py:2642 windows/views.py:2643 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -697,125 +893,87 @@ msgstr "" "Wenn aktiviert, werden Tabellenbearbeitungen sofort angewendet, ohne auf " "Anwenden oder Abbrechen zu drücken" -#: windows/views.py:2101 +#: windows/views.py:2656 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" +msgstr "{database_name}.{table_name} - Zeilen {from_row} - {to_row} von {total_rows}" -#: windows/views.py:2109 -#, fuzzy +#: windows/views.py:2664 msgid "First" -msgstr "Filter" +msgstr "Erste" -#: windows/views.py:2127 +#: windows/views.py:2682 msgid "Last" -msgstr "" +msgstr "Letzte" -#: windows/views.py:2136 +#: windows/views.py:2691 msgid "Filters" msgstr "Filter" -#: windows/views.py:2176 +#: windows/views.py:2733 +msgid "" +"Apply filters in data\n" +"CTRL+ENTER" +msgstr "" +"Filter in Daten anwenden\n" +"STRG+EINGABE" + +#: windows/views.py:2734 msgid "CTRL+ENTER" -msgstr "CTRL+ENTER" +msgstr "STRG+EINGABE" -#: windows/views.py:2196 +#: windows/views.py:2762 msgid "Insert row" msgstr "Zeile einfügen" -#: windows/views.py:2204 +#: windows/components/popup.py:31 windows/views.py:2774 +msgid "NULL" +msgstr "NULL" + +#: windows/views.py:2781 msgid "Data" msgstr "Daten" -#: windows/main/controller.py:278 windows/main/controller.py:287 -#: windows/main/controller.py:288 windows/views.py:2221 -#, fuzzy +#: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" -msgstr "Abfrage" +msgstr "Neue Abfrage" -#: windows/views.py:2223 windows/views.py:2660 +#: windows/views.py:2800 msgid "Close" msgstr "Schließen" -#: windows/main/controller.py:279 windows/main/controller.py:289 -#: windows/main/controller.py:290 windows/views.py:2223 -#, fuzzy +#: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" -msgstr "Abfrage" +msgstr "Abfrage schließen" -#: windows/views.py:2227 +#: windows/views.py:2804 msgid "Run" -msgstr "" +msgstr "Ausführen" -#: windows/main/controller.py:280 windows/main/controller.py:292 -#: windows/main/controller.py:293 windows/views.py:2227 -#, fuzzy +#: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" -msgstr "SSH-Executable" +msgstr "Ausführen" -#: windows/views.py:2229 +#: windows/views.py:2806 msgid "Run all" -msgstr "" +msgstr "Alle ausführen" -#: windows/main/controller.py:295 windows/views.py:2229 +#: windows/views.py:2806 msgid "Execute all statements" -msgstr "" +msgstr "Alle Anweisungen ausführen" -#: windows/main/controller.py:282 windows/main/controller.py:297 -#: windows/main/controller.py:298 windows/views.py:2231 +#: windows/main/controller.py:323 windows/views.py:2808 msgid "Stop" -msgstr "" +msgstr "Stopp" -#: windows/views.py:2287 +#: windows/views.py:2873 msgid "a page" -msgstr "" +msgstr "eine Seite" -#: windows/views.py:2315 +#: windows/views.py:2923 msgid "Query" msgstr "Abfrage" -#: windows/views.py:2626 -#, fuzzy -msgid "Character set" -msgstr "Erstellt am" - -#: windows/views.py:2644 windows/views.py:2663 -msgid "New" -msgstr "Neu" - -#: windows/views.py:2683 -msgid "Insert record" -msgstr "Datensatz einfügen" - -#: windows/views.py:2688 -msgid "Duplicate record" -msgstr "Datensatz duplizieren" - -#: windows/views.py:2695 -msgid "Delete record" -msgstr "Datensatz löschen" - -#: windows/views.py:2733 windows/views.py:2964 -msgid "Up" -msgstr "Hoch" - -#: windows/views.py:2740 windows/views.py:2971 -msgid "Down" -msgstr "Runter" - -#: windows/views.py:3100 -msgid "Save Starments" -msgstr "" - -#: windows/views.py:3108 -#, fuzzy -msgid "Location" -msgstr "Sortierung" - -#: windows/views.py:3115 -msgid "*.sql" -msgstr "" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 msgid "Allow NULL" @@ -836,7 +994,7 @@ 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/components/dataview.py:85 windows/components/dataview.py:256 msgid "Expression" msgstr "Ausdruck" @@ -848,75 +1006,71 @@ msgstr "Ohne Vorzeichen" msgid "Zerofill" msgstr "Nullfüllung" -#: windows/components/dataview.py:109 -msgid "#" -msgstr "#" - -#: windows/components/dataview.py:117 +#: windows/components/dataview.py:119 msgid "Data type" msgstr "Datentyp" -#: windows/components/dataview.py:121 +#: windows/components/dataview.py:123 msgid "Length/Set" msgstr "Länge/Menge" -#: windows/components/dataview.py:155 +#: windows/components/dataview.py:169 msgid "Add column\tCTRL+INS" -msgstr "Spalte hinzufügen\tCTRL+INS" +msgstr "Spalte hinzufügen\tSTRG+EINFG" -#: windows/components/dataview.py:161 +#: windows/components/dataview.py:175 msgid "Remove column\tCTRL+DEL" -msgstr "Spalte entfernen\tCTRL+DEL" +msgstr "Spalte entfernen\tSTRG+ENTF" -#: windows/components/dataview.py:169 +#: windows/components/dataview.py:183 msgid "Move up\tCTRL+UP" -msgstr "Nach oben bewegen\tCTRL+UP" +msgstr "Nach oben bewegen\tSTRG+PFEIL OBEN" -#: windows/components/dataview.py:176 +#: windows/components/dataview.py:190 msgid "Move down\tCTRL+D" -msgstr "Nach unten bewegen\tCTRL+D" +msgstr "Nach unten bewegen\tSTRG+PFEIL UNTEN" -#: windows/components/dataview.py:199 +#: windows/components/dataview.py:214 msgid "Create new index" msgstr "Neuen Index erstellen" -#: windows/components/dataview.py:214 +#: windows/components/dataview.py:229 msgid "Append to index" msgstr "An Index anhängen" -#: windows/components/dataview.py:228 +#: windows/components/dataview.py:243 msgid "Column(s)/Expression" msgstr "Spalte(n)/Ausdruck" -#: windows/components/dataview.py:229 +#: windows/components/dataview.py:244 msgid "Condition" msgstr "Bedingung" -#: windows/components/dataview.py:259 +#: windows/components/dataview.py:274 msgid "Column(s)" msgstr "Spalte(n)" -#: windows/components/dataview.py:265 +#: windows/components/dataview.py:280 msgid "Reference table" msgstr "Referenztabelle" -#: windows/components/dataview.py:271 +#: windows/components/dataview.py:286 msgid "Reference column(s)" msgstr "Referenzspalte(n)" -#: windows/components/dataview.py:277 +#: windows/components/dataview.py:292 msgid "On UPDATE" msgstr "Bei UPDATE" -#: windows/components/dataview.py:283 +#: windows/components/dataview.py:298 msgid "On DELETE" msgstr "Bei DELETE" -#: windows/components/dataview.py:298 +#: windows/components/dataview.py:313 msgid "Add foreign key" msgstr "Fremdschlüssel hinzufügen" -#: windows/components/dataview.py:304 +#: windows/components/dataview.py:319 msgid "Remove foreign key" msgstr "Fremdschlüssel entfernen" @@ -924,176 +1078,185 @@ msgstr "Fremdschlüssel entfernen" msgid "No default value" msgstr "Kein Standardwert" -#: windows/components/popup.py:31 -msgid "NULL" -msgstr "NULL" - #: windows/components/popup.py:35 msgid "AUTO INCREMENT" -msgstr "AUTO INCREMENT" +msgstr "AUTO INKREMENT" #: windows/components/popup.py:39 msgid "Text/Expression" msgstr "Text/Ausdruck" -#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 +#: windows/dialogs/connections/view.py:126 windows/main/query/renderer.py:192 msgid "Unknown error" -msgstr "" +msgstr "Unbekannter Fehler" -#: windows/dialogs/connections/view.py:414 +#: windows/dialogs/connections/view.py:416 msgid "Connection established successfully" -msgstr "" +msgstr "Verbindung erfolgreich hergestellt" -#: windows/dialogs/connections/view.py:426 +#: windows/dialogs/connections/view.py:428 #, python-brace-format msgid "Do you want save the connection {connection_name}?" -msgstr "" +msgstr "Möchten Sie die Verbindung {connection_name} speichern?" -#: windows/dialogs/connections/view.py:429 +#: windows/dialogs/connections/view.py:431 msgid "Confirm save" msgstr "Speichern bestätigen" -#: windows/dialogs/connections/view.py:481 +#: windows/dialogs/connections/view.py:483 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" +"Sie haben ungespeicherte Änderungen. Möchten Sie sie vor dem Fortfahren " +"speichern?" -#: windows/dialogs/connections/view.py:483 +#: windows/dialogs/connections/view.py:485 msgid "Unsaved changes" -msgstr "" +msgstr "Ungespeicherte Änderungen" -#: windows/dialogs/connections/view.py:750 +#: windows/dialogs/connections/view.py:773 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" +"Diese Verbindung kann ohne TLS nicht funktionieren. TLS wurde automatisch" +" aktiviert." -#: windows/dialogs/connections/view.py:775 -#, fuzzy, python-brace-format +#: windows/dialogs/connections/view.py:798 +#, python-brace-format msgid "" "Connection error:\n" "{error}" -msgstr "Verbindungsfehler" +msgstr "" +"Verbindungsfehler:\n" +"{error}" -#: windows/dialogs/connections/view.py:776 +#: windows/dialogs/connections/view.py:799 msgid "Connection error" msgstr "Verbindungsfehler" -#: windows/dialogs/connections/view.py:802 -#, fuzzy, python-brace-format +#: windows/dialogs/connections/view.py:825 +#, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" -msgstr "Möchten Sie die Datensätze löschen?" +msgstr "Möchten Sie die Verbindung '{connection_name}' löschen?" -#: windows/dialogs/connections/view.py:805 -#: windows/dialogs/connections/view.py:822 +#: windows/dialogs/connections/view.py:828 +#: windows/dialogs/connections/view.py:845 msgid "Confirm delete" msgstr "Löschen bestätigen" -#: windows/dialogs/connections/view.py:819 -#, fuzzy, python-brace-format +#: windows/dialogs/connections/view.py:842 +#, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" -msgstr "Möchten Sie die Datensätze löschen?" +msgstr "Möchten Sie das Verzeichnis '{directory_name}' löschen?" -#: windows/main/controller.py:275 +#: windows/main/controller.py:316 #, python-brace-format msgid "{text} ({shortcut})" -msgstr "" +msgstr "{text} ({shortcut})" -#: windows/main/controller.py:281 windows/main/controller.py:294 -#, fuzzy +#: windows/main/controller.py:322 msgid "Execute all" -msgstr "SSH-Executable" +msgstr "Alle ausführen" -#: windows/main/controller.py:471 windows/main/controller.py:479 -#, fuzzy +#: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" -msgstr "Abfrage" +msgstr "Abfrage (1)" -#: windows/main/controller.py:497 +#: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" -msgstr "" +msgstr "Abfrage ({query_number})" -#: windows/main/controller.py:530 +#: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" -msgstr "" +msgstr "Sie haben ungespeicherte Änderungen. Vor dem Schließen speichern?" -#: windows/main/controller.py:531 +#: windows/main/controller.py:519 msgid "Unsaved query" -msgstr "" +msgstr "Ungespeicherte Abfrage" -#: windows/main/controller.py:576 -#, fuzzy +#: windows/main/controller.py:564 msgid "Save query" -msgstr "Abfrage" +msgstr "Abfrage speichern" -#: windows/main/controller.py:579 +#: windows/main/controller.py:567 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" -msgstr "" +msgstr "SQL-Dateien (*.sql)|*.sql|Alle Dateien (*.*)|*.*" -#: 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 +#: windows/main/controller.py:595 windows/main/controller.py:626 +#: windows/main/controller.py:653 windows/main/database/list.py:119 +#: windows/main/database/routine.py:591 windows/main/database/routine.py:635 +#: windows/main/database/view.py:268 windows/main/database/view.py:297 +#: windows/main/query/controller.py:179 msgid "Error" msgstr "Fehler" -#: windows/main/controller.py:622 +#: windows/main/controller.py:633 #, python-brace-format msgid "-- Saved query to {file_path}" -msgstr "" +msgstr "-- Abfrage gespeichert in {file_path}" -#: windows/main/controller.py:647 +#: windows/main/controller.py:659 #, python-brace-format msgid "-- Autosaved query to {file_path}" -msgstr "" +msgstr "-- Abfrage automatisch gespeichert in {file_path}" -#: windows/main/controller.py:704 +#: windows/main/controller.py:725 msgid "days" msgstr "Tage" -#: windows/main/controller.py:705 +#: windows/main/controller.py:726 msgid "hours" msgstr "Stunden" -#: windows/main/controller.py:706 +#: windows/main/controller.py:727 msgid "minutes" msgstr "Minuten" -#: windows/main/controller.py:707 +#: windows/main/controller.py:728 msgid "seconds" msgstr "Sekunden" -#: windows/main/controller.py:715 +#: windows/main/controller.py:736 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Verwendeter Speicher: {used} ({percentage:.2%})" -#: windows/main/controller.py:751 +#: windows/main/controller.py:772 msgid "Settings saved successfully" -msgstr "" +msgstr "Einstellungen erfolgreich gespeichert" -#: windows/main/controller.py:952 +#: windows/main/controller.py:1006 #, python-brace-format msgid "~{estimated} (Loading...)" -msgstr "" +msgstr "~{estimated} (Wird geladen...)" -#: windows/main/controller.py:954 +#: windows/main/controller.py:1008 msgid "~ (Loading...)" -msgstr "" +msgstr "~ (Wird geladen...)" + +#: windows/main/controller.py:1243 +msgid "Write Mode (2:00)" +msgstr "Schreibmodus (2:00)" + +#: windows/main/controller.py:1294 +msgid "Write Mode" +msgstr "Schreibmodus" -#: windows/main/controller.py:1119 +#: windows/main/controller.py:1305 msgid "Version" msgstr "Version" -#: windows/main/controller.py:1121 +#: windows/main/controller.py:1307 msgid "Uptime" msgstr "Betriebszeit" -#: windows/main/controller.py:1199 +#: windows/main/controller.py:1444 #, python-brace-format msgid "Do you want discard the change to {database_name}?" -msgstr "" +msgstr "Möchten Sie die Änderung an {database_name} verwerfen?" -#: windows/main/controller.py:1232 +#: windows/main/controller.py:1477 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1102,120 +1265,185 @@ msgid "" "- Yes: open dump flow (coming soon, no drop).\n" "- No: drop the database now." msgstr "" +"Möchten Sie vor dem Löschen der Datenbank '{database_name}' einen Dump " +"erstellen?\n" +"\n" +"Dump ist noch nicht implementiert.\n" +"- Ja: Dump-Flow öffnen (demnächst, kein Löschen).\n" +"- Nein: Datenbank jetzt löschen." -#: windows/main/controller.py:1237 windows/main/controller.py:1258 -#, fuzzy +#: windows/main/controller.py:1482 windows/main/controller.py:1503 msgid "Delete database" -msgstr "Tabelle löschen" +msgstr "Datenbank löschen" -#: windows/main/controller.py:1243 +#: windows/main/controller.py:1488 msgid "Dump is not implemented yet. No action has been performed." -msgstr "" +msgstr "Dump ist noch nicht implementiert. Es wurde keine Aktion ausgeführt." -#: windows/main/controller.py:1244 +#: windows/main/controller.py:1489 msgid "Dump not available" -msgstr "" +msgstr "Dump nicht verfügbar" -#: windows/main/controller.py:1257 +#: windows/main/controller.py:1502 msgid "Database deletion is not supported by this engine." -msgstr "" +msgstr "Das Löschen von Datenbanken wird von dieser Engine nicht unterstützt." -#: windows/main/controller.py:1272 +#: windows/main/controller.py:1517 msgid "Database deleted successfully" -msgstr "" +msgstr "Datenbank erfolgreich gelöscht" -#: windows/main/controller.py:1273 windows/main/database/view.py:253 -#: windows/main/database/view.py:279 +#: windows/main/controller.py:1518 windows/main/database/routine.py:556 +#: windows/main/database/routine.py:620 windows/main/database/view.py:256 +#: windows/main/database/view.py:291 msgid "Success" -msgstr "" +msgstr "Erfolg" -#: windows/main/controller.py:1392 +#: windows/main/controller.py:1792 #, python-brace-format msgid "Do you want discard the change to {table_name}?" -msgstr "" +msgstr "Möchten Sie die Änderung an {table_name} verwerfen?" -#: windows/main/controller.py:1418 -#, fuzzy, python-brace-format +#: windows/main/controller.py:1818 +#, 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" +msgstr "Möchten Sie die Tabelle {table_name} löschen?" -#: windows/main/controller.py:1440 +#: windows/main/controller.py:1840 #, python-brace-format msgid "{table_name} (COPY)" -msgstr "" +msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1563 +#: windows/main/controller.py:1999 msgid "Do you want delete the records?" msgstr "Möchten Sie die Datensätze löschen?" -#: windows/main/database/list.py:69 +#: windows/main/controller.py:2104 +msgid "Database connection lost. Do you want to reconnect?" +msgstr "Datenbankverbindung verloren. Möchten Sie erneut verbinden?" + +#: windows/main/controller.py:2105 windows/main/database/list.py:108 +msgid "Connection lost" +msgstr "Verbindung verloren" + +#: windows/main/controller.py:2113 +msgid "Connection restored successfully." +msgstr "Verbindung erfolgreich wiederhergestellt." + +#: windows/main/controller.py:2114 +msgid "Connection restored" +msgstr "Verbindung wiederhergestellt" + +#: windows/main/controller.py:2120 +#, python-brace-format +msgid "Could not reconnect: {error}" +msgstr "Wiederverbindung fehlgeschlagen: {error}" + +#: windows/main/controller.py:2121 +msgid "Reconnection failed" +msgstr "Wiederverbindung fehlgeschlagen" + +#: windows/main/database/list.py:104 msgid "The connection to the database was lost." msgstr "Die Verbindung zur Datenbank wurde verloren." -#: windows/main/database/list.py:71 +#: windows/main/database/list.py:106 msgid "Do you want to reconnect?" msgstr "Möchten Sie erneut verbinden?" -#: windows/main/database/list.py:73 -msgid "Connection lost" -msgstr "Verbindung verloren" - -#: windows/main/database/list.py:83 +#: windows/main/database/list.py:118 msgid "Reconnection failed:" msgstr "Wiederverbindung fehlgeschlagen:" -#: windows/main/database/view.py:252 +#: windows/main/database/routine.py:545 +msgid "Function created successfully" +msgstr "Funktion erfolgreich erstellt" + +#: windows/main/database/routine.py:547 +msgid "Function updated successfully" +msgstr "Funktion erfolgreich aktualisiert" + +#: windows/main/database/routine.py:551 +msgid "Procedure created successfully" +msgstr "Prozedur erfolgreich erstellt" + +#: windows/main/database/routine.py:553 +msgid "Procedure updated successfully" +msgstr "Prozedur erfolgreich aktualisiert" + +#: windows/main/database/routine.py:590 +#, python-brace-format +msgid "Error saving routine: {}" +msgstr "Fehler beim Speichern der Routine: {}" + +#: windows/main/database/routine.py:604 +msgid "Function" +msgstr "Funktion" + +#: windows/main/database/routine.py:604 +msgid "Procedure" +msgstr "Prozedur" + +#: windows/main/database/routine.py:607 +#, python-brace-format +msgid "Are you sure you want to delete {} '{}'?" +msgstr "Sind Sie sicher, dass Sie {} '{}' löschen möchten?" + +#: windows/main/database/routine.py:610 windows/main/database/view.py:282 +msgid "Confirm Delete" +msgstr "Löschen bestätigen" + +#: windows/main/database/routine.py:619 +#, python-brace-format +msgid "{} deleted successfully" +msgstr "{} erfolgreich gelöscht" + +#: windows/main/database/routine.py:634 +#, python-brace-format +msgid "Error deleting routine: {}" +msgstr "Fehler beim Löschen der Routine: {}" + +#: windows/main/database/view.py:255 msgid "View created successfully" -msgstr "" +msgstr "Ansicht erfolgreich erstellt" -#: windows/main/database/view.py:252 +#: windows/main/database/view.py:255 msgid "View updated successfully" -msgstr "" +msgstr "Ansicht erfolgreich aktualisiert" -#: windows/main/database/view.py:256 +#: windows/main/database/view.py:268 #, python-brace-format msgid "Error saving view: {}" -msgstr "" +msgstr "Fehler beim Speichern der Ansicht: {}" -#: windows/main/database/view.py:269 +#: windows/main/database/view.py:281 #, python-brace-format msgid "Are you sure you want to delete view '{}'?" -msgstr "" +msgstr "Sind Sie sicher, dass Sie die Ansicht '{}' löschen möchten?" -#: windows/main/database/view.py:270 -#, fuzzy -msgid "Confirm Delete" -msgstr "Löschen bestätigen" - -#: windows/main/database/view.py:279 +#: windows/main/database/view.py:291 msgid "View deleted successfully" -msgstr "" +msgstr "Ansicht erfolgreich gelöscht" -#: windows/main/database/view.py:282 +#: windows/main/database/view.py:297 #, python-brace-format msgid "Error deleting view: {}" -msgstr "" +msgstr "Fehler beim Löschen der Ansicht: {}" -#: windows/main/query/controller.py:110 +#: windows/main/query/controller.py:112 #, python-brace-format msgid "{elapsed_ms:.0f} ms" -msgstr "" +msgstr "{elapsed_ms:.0f} ms" -#: windows/main/query/controller.py:112 +#: windows/main/query/controller.py:114 #, python-brace-format msgid "{elapsed_s:.2f} s" -msgstr "" +msgstr "{elapsed_s:.2f} s" -#: windows/main/query/controller.py:115 -#, fuzzy +#: windows/main/query/controller.py:117 msgid "none" -msgstr "Klonen" +msgstr "keine" -#: windows/main/query/controller.py:121 +#: windows/main/query/controller.py:123 #, python-brace-format msgid "" "Query execution stopped after {elapsed}.\n" @@ -1224,188 +1452,68 @@ msgid "" "Failed: {failed}.\n" "Last statement: #{last}." msgstr "" +"Abfrageausführung gestoppt nach {elapsed}.\n" +"Abgeschlossene Anweisungen: {completed}/{total}.\n" +"Erfolgreiche: {success}.\n" +"Fehlgeschlagene: {failed}.\n" +"Letzte Anweisung: #{last}." -#: windows/main/query/controller.py:134 +#: windows/main/query/controller.py:136 msgid "Query execution cancelled" -msgstr "" +msgstr "Abfrageausführung abgebrochen" -#: windows/main/query/controller.py:176 -#, fuzzy +#: windows/main/query/controller.py:178 msgid "No active database connection" -msgstr "Neue Verbindung" +msgstr "Keine aktive Datenbankverbindung" + +#: windows/main/query/controller.py:227 +msgid "Database connection lost" +msgstr "Datenbankverbindung verloren" + +#: windows/main/query/history.py:55 +msgid "(empty query)" +msgstr "(leere Abfrage)" #: windows/main/query/renderer.py:53 #, python-brace-format msgid "{affected_rows} rows affected" -msgstr "" +msgstr "{affected_rows} Zeilen betroffen" #: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 #, python-brace-format msgid "Query {query_number}" -msgstr "" +msgstr "Abfrage {query_number}" #: windows/main/query/renderer.py:65 #, python-brace-format msgid "Query {query_number} (Error)" -msgstr "" +msgstr "Abfrage {query_number} (Fehler)" #: windows/main/query/renderer.py:79 #, python-brace-format msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" -msgstr "" +msgstr "Abfrage {query_number} ({rows_count} Zeilen × {columns_count} Spalten)" #: windows/main/query/renderer.py:165 #, python-brace-format msgid "{rows_count} rows" -msgstr "" +msgstr "{rows_count} Zeilen" #: windows/main/query/renderer.py:167 #, python-brace-format msgid "{elapsed_ms:.1f} ms" -msgstr "" +msgstr "{elapsed_ms:.1f} ms" #: windows/main/query/renderer.py:171 #, python-brace-format msgid "{warnings_count} warnings" -msgstr "" +msgstr "{warnings_count} Warnungen" #: windows/main/query/renderer.py:186 -#, fuzzy msgid "Error:" msgstr "Fehler" -#~ msgid "Created at:" -#~ msgstr "" - -#~ msgid "Last connection:" -#~ msgstr "" - -#~ msgid "Successful connections:" -#~ msgstr "" - -#~ msgid "Unsuccessful connections:" -#~ msgstr "" - -#~ msgid "Session Manager" -#~ msgstr "" - -#~ msgid "Session name" -#~ msgstr "" - -#~ msgid "Connection type" -#~ msgstr "" - -#~ msgid "Open" -#~ msgstr "" - -#~ msgid "Open session manager" -#~ msgstr "" - -#~ msgid "Foreign Key" -#~ msgstr "" - -#~ msgid "New Session" -#~ msgstr "Neue Sitzung" - -#~ 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" +#: windows/main/table/records.py:334 +msgid "Error saving records" +msgstr "Fehler beim Speichern der Datensätze" diff --git a/locale/en_US/LC_MESSAGES/petersql.mo b/locale/en_US/LC_MESSAGES/petersql.mo index a950b1a..55287fb 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 5e23444..612415d 100644 --- a/locale/en_US/LC_MESSAGES/petersql.po +++ b/locale/en_US/LC_MESSAGES/petersql.po @@ -2,12 +2,11 @@ # Copyright (C) 2026 ORGANIZATION # This file is distributed under the same license as the PeterSQL project. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-23 10:07+0100\n" +"POT-Creation-Date: 2026-06-06 11:23+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: en_US\n" @@ -47,1017 +46,1215 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "OpenSSH client not found." -#: structures/engines/mariadb/context.py:611 -#: structures/engines/mysql/context.py:622 -#: structures/engines/postgresql/context.py:645 -#: structures/engines/sqlite/context.py:524 +#: structures/engines/context.py:567 +msgid "This connection is read-only." +msgstr "This connection is read-only." + +#: structures/engines/context.py:578 +#, fuzzy, python-brace-format +msgid "Database connection lost: {error}" +msgstr "" +"Connection error:\n" +"{error}" + +#: structures/engines/mariadb/context.py:658 +#: structures/engines/mysql/context.py:670 +#: structures/engines/postgresql/context.py:701 +#: structures/engines/sqlite/context.py:564 #, python-brace-format msgid "Table{table_index:03}" -msgstr "" +msgstr "Table{table_index:03}" -#: structures/engines/mariadb/context.py:639 -#: structures/engines/mysql/context.py:650 -#: structures/engines/postgresql/context.py:670 -#: structures/engines/sqlite/context.py:548 +#: structures/engines/mariadb/context.py:686 +#: structures/engines/mysql/context.py:698 +#: structures/engines/postgresql/context.py:726 +#: structures/engines/sqlite/context.py:588 #, python-brace-format msgid "Column{column_index:03}" -msgstr "" +msgstr "Column{column_index:03}" -#: structures/engines/mariadb/context.py:657 -#: structures/engines/mysql/context.py:668 -#: structures/engines/postgresql/context.py:688 -#: structures/engines/sqlite/context.py:566 +#: structures/engines/mariadb/context.py:704 +#: structures/engines/mysql/context.py:716 +#: structures/engines/postgresql/context.py:744 +#: structures/engines/sqlite/context.py:606 #, python-brace-format msgid "Index{index_number:03}" -msgstr "" +msgstr "Index{index_number:03}" -#: structures/engines/mariadb/context.py:697 -#: structures/engines/mysql/context.py:706 -#: structures/engines/postgresql/context.py:728 -#: structures/engines/sqlite/context.py:608 +#: structures/engines/mariadb/context.py:744 +#: structures/engines/mysql/context.py:754 +#: structures/engines/postgresql/context.py:784 +#: structures/engines/sqlite/context.py:646 #, python-brace-format msgid "ForeignKey{foreign_key_number:03}" -msgstr "" +msgstr "ForeignKey{foreign_key_number:03}" -#: structures/engines/mariadb/context.py:730 -#: structures/engines/mysql/context.py:737 -#: structures/engines/postgresql/context.py:758 -#: structures/engines/sqlite/context.py:636 +#: structures/engines/mariadb/context.py:777 +#: structures/engines/mysql/context.py:785 +#: structures/engines/postgresql/context.py:814 +#: structures/engines/sqlite/context.py:674 #, python-brace-format msgid "View{view_index:03}" -msgstr "" +msgstr "View{view_index:03}" -#: structures/engines/mariadb/context.py:781 +#: structures/engines/mariadb/context.py:830 #, python-brace-format msgid "Trigger{trigger_index:03}" -msgstr "" +msgstr "Trigger{trigger_index:03}" -#: windows/dialogs/connections/view.py:415 -#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 -#: windows/views.py:33 +#: windows/dialogs/connections/view.py:417 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1303 +#: windows/views.py:62 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:1006 -#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 -#: windows/views.py:2821 +#: windows/components/dataview.py:115 windows/components/dataview.py:240 +#: windows/components/dataview.py:253 windows/components/dataview.py:268 +#: windows/views.py:76 windows/views.py:126 windows/views.py:1053 +#: windows/views.py:1365 windows/views.py:1398 windows/views.py:1422 +#: windows/views.py:1446 windows/views.py:1470 windows/views.py:1494 +#: windows/views.py:1611 windows/views.py:1957 windows/views.py:2228 +#: windows/views.py:2336 msgid "Name" msgstr "Name" -#: windows/views.py:48 windows/views.py:428 +#: windows/views.py:77 windows/views.py:467 msgid "Last connection" msgstr "Last connection" -#: windows/dialogs/connections/view.py:653 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:90 msgid "New directory" msgstr "New directory" -#: windows/dialogs/connections/model.py:206 -#: windows/dialogs/connections/view.py:613 windows/views.py:65 +#: windows/dialogs/connections/model.py:212 +#: windows/dialogs/connections/view.py:615 windows/views.py:94 msgid "New connection" msgstr "New connection" -#: windows/views.py:71 +#: windows/views.py:100 msgid "Rename" msgstr "Rename" -#: windows/views.py:76 +#: windows/views.py:105 msgid "Clone connection" msgstr "Clone connection" -#: 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 +#: windows/views.py:110 windows/views.py:642 windows/views.py:1525 +#: windows/views.py:1893 windows/views.py:2183 windows/views.py:2587 msgid "Delete" msgstr "Delete" -#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 -#: windows/views.py:2876 +#: windows/views.py:140 windows/views.py:1370 windows/views.py:1666 msgid "Engine" msgstr "Engine" -#: windows/views.py:132 +#: windows/views.py:161 msgid "Host + port" msgstr "Host + port" -#: windows/views.py:148 +#: windows/views.py:177 msgid "Username" msgstr "Username" -#: windows/views.py:161 windows/views.py:1132 +#: windows/views.py:190 windows/views.py:1179 msgid "Password" msgstr "Password" -#: windows/views.py:174 -#, fuzzy +#: windows/views.py:203 msgid "Connection timeout" -msgstr "Connection" +msgstr "Connection timeout" -#: windows/views.py:192 +#: windows/views.py:221 msgid "Use TLS" msgstr "Use TLS" -#: windows/views.py:203 +#: windows/views.py:232 +msgid "Mark read only" +msgstr "Mark read only" + +#: windows/views.py:243 msgid "Use SSH tunnel" msgstr "Use SSH tunnel" -#: windows/views.py:214 +#: windows/views.py:254 msgid "Compressed client/server protocol" -msgstr "" +msgstr "Compressed client/server protocol" -#: windows/views.py:233 +#: windows/views.py:273 msgid "Filename" msgstr "Filename" -#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 +#: windows/views.py:278 windows/views.py:397 msgid "Select a file" msgstr "Select a file" -#: windows/views.py:238 windows/views.py:358 +#: windows/views.py:278 windows/views.py:397 msgid "*.*" msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 -#: windows/views.py:2834 +#: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 msgid "Comments" msgstr "Comments" -#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 -#: windows/views.py:884 +#: windows/main/controller.py:772 windows/views.py:309 windows/views.py:769 +#: windows/views.py:923 msgid "Settings" msgstr "Settings" -#: windows/views.py:278 +#: windows/views.py:317 msgid "SSH executable" msgstr "SSH executable" -#: windows/views.py:283 +#: windows/views.py:322 msgid "ssh" msgstr "ssh" -#: windows/views.py:291 +#: windows/views.py:330 msgid "SSH host + port" msgstr "SSH host + port" -#: windows/views.py:303 +#: windows/views.py:342 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:312 +#: windows/views.py:351 msgid "SSH username" msgstr "SSH username" -#: windows/views.py:325 +#: windows/views.py:364 msgid "SSH password" msgstr "SSH password" -#: windows/views.py:338 +#: windows/views.py:377 msgid "Local port" msgstr "Local port" -#: windows/views.py:344 +#: windows/views.py:383 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:353 +#: windows/views.py:392 msgid "Identity file" msgstr "Identity file" -#: windows/views.py:369 +#: windows/views.py:408 msgid "Remote host + port" msgstr "Remote host + port" -#: windows/views.py:381 +#: windows/views.py:420 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:390 +#: windows/views.py:429 msgid "SSH extra args" -msgstr "" +msgstr "SSH extra args" -#: windows/views.py:405 +#: windows/views.py:444 msgid "SSH Tunnel" msgstr "SSH Tunnel" -#: windows/views.py:411 windows/views.py:1341 +#: windows/views.py:450 windows/views.py:1368 msgid "Created at" msgstr "Created at" -#: windows/views.py:445 +#: windows/views.py:484 msgid "Successful connections" -msgstr "" +msgstr "Successful connections" -#: windows/views.py:462 +#: windows/views.py:501 msgid "Last successful connection" -msgstr "" +msgstr "Last successful connection" -#: windows/views.py:479 +#: windows/views.py:518 msgid "Unsuccessful connections" -msgstr "" +msgstr "Unsuccessful connections" -#: windows/views.py:496 +#: windows/views.py:535 msgid "Last failure reason" -msgstr "" +msgstr "Last failure reason" -#: windows/views.py:513 +#: windows/views.py:552 msgid "Total connection attempts" -msgstr "" +msgstr "Total connection attempts" -#: windows/views.py:530 -#, fuzzy +#: windows/views.py:569 msgid "Average connection time (ms)" -msgstr "Connection" +msgstr "Average connection time (ms)" -#: windows/views.py:547 +#: windows/views.py:586 msgid "Most recent connection duration" -msgstr "" +msgstr "Most recent connection duration" -#: windows/views.py:566 +#: windows/views.py:605 msgid "Statistics" -msgstr "" +msgstr "Statistics" -#: windows/views.py:584 windows/views.py:1730 +#: windows/views.py:623 windows/views.py:1850 msgid "Create" -msgstr "" +msgstr "Create" -#: windows/views.py:588 +#: windows/views.py:627 msgid "Create connection" -msgstr "" +msgstr "Create connection" -#: windows/views.py:591 +#: windows/views.py:630 msgid "Create directory" -msgstr "" +msgstr "Create directory" -#: 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 +#: windows/views.py:659 windows/views.py:883 windows/views.py:1520 +#: windows/views.py:1896 windows/views.py:2188 windows/views.py:2592 +#: windows/views.py:2648 msgid "Cancel" -msgstr "" +msgstr "Cancel" -#: 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 +#: windows/main/controller.py:324 windows/views.py:664 windows/views.py:2193 +#: windows/views.py:2597 windows/views.py:2812 msgid "Save" -msgstr "" +msgstr "Save" -#: windows/views.py:632 +#: windows/views.py:671 msgid "Test" -msgstr "" +msgstr "Test" -#: windows/views.py:639 +#: windows/views.py:678 msgid "Connect" -msgstr "" +msgstr "Connect" -#: windows/views.py:742 +#: windows/views.py:781 windows/views.py:2388 msgid "Language" -msgstr "" +msgstr "Language" -#: windows/views.py:747 +#: windows/views.py:786 msgid "English" -msgstr "" +msgstr "English" -#: windows/views.py:747 +#: windows/views.py:786 msgid "Italian" -msgstr "" +msgstr "Italian" -#: windows/views.py:747 +#: windows/views.py:786 msgid "French" -msgstr "" +msgstr "French" -#: windows/views.py:759 +#: windows/views.py:798 msgid "Locale" -msgstr "" +msgstr "Locale" -#: windows/views.py:780 -#, fuzzy +#: windows/views.py:819 msgid "Column content" msgstr "Clone connection" -#: windows/views.py:790 +#: windows/views.py:829 msgid "Syntax" -msgstr "" +msgstr "Syntax" -#: windows/views.py:847 +#: windows/views.py:886 msgid "Ok" -msgstr "" +msgstr "Ok" -#: windows/views.py:878 +#: windows/views.py:917 msgid "PeterSQL" -msgstr "" +msgstr "PeterSQL" -#: windows/views.py:887 +#: windows/views.py:926 msgid "File" -msgstr "" +msgstr "File" -#: windows/views.py:890 +#: windows/views.py:929 msgid "About" -msgstr "" +msgstr "About" -#: windows/views.py:893 +#: windows/views.py:932 msgid "Help" -msgstr "" +msgstr "Help" -#: windows/views.py:898 +#: windows/views.py:937 msgid "Open connection manager" -msgstr "" +msgstr "Open connection manager" -#: windows/views.py:902 +#: windows/views.py:941 msgid "Disconnect from server" -msgstr "" +msgstr "Disconnect from server" -#: windows/views.py:904 +#: windows/views.py:943 msgid "tool" -msgstr "" +msgstr "tool" -#: windows/views.py:904 +#: windows/views.py:943 windows/views.py:2628 msgid "Refresh" -msgstr "" +msgstr "Refresh" -#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 -#: windows/views.py:2077 windows/views.py:2221 +#: windows/views.py:947 windows/views.py:949 windows/views.py:1874 +#: windows/views.py:2632 windows/views.py:2798 msgid "Add" -msgstr "" +msgstr "Add" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 +#: windows/views.py:954 +#, python-brace-format +msgid "{mode}" +msgstr "{mode}" + +#: windows/views.py:991 windows/views.py:995 windows/views.py:2765 +#: windows/views.py:2770 msgid "MyMenuItem" -msgstr "" +msgstr "MyMenuItem" -#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 +#: windows/views.py:998 windows/views.py:1924 windows/views.py:2777 msgid "MyMenu" -msgstr "" +msgstr "MyMenu" -#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 -#: windows/views.py:1402 +#: windows/views.py:1013 windows/views.py:1548 windows/views.py:1555 +#: windows/views.py:1562 msgid "MyLabel" -msgstr "" +msgstr "MyLabel" -#: windows/views.py:972 +#: windows/views.py:1019 msgid "Databases" -msgstr "" +msgstr "Databases" -#: windows/views.py:973 windows/views.py:1340 +#: windows/views.py:1020 windows/views.py:1367 msgid "Size" -msgstr "" +msgstr "Size" -#: windows/views.py:974 +#: windows/views.py:1021 msgid "Elements" -msgstr "" +msgstr "Elements" -#: windows/views.py:975 +#: windows/views.py:1022 msgid "Modified at" -msgstr "" +msgstr "Modified at" -#: windows/views.py:976 +#: windows/views.py:1023 windows/views.py:1382 msgid "Tables" -msgstr "" +msgstr "Tables" -#: windows/views.py:983 +#: windows/views.py:1030 msgid "System" -msgstr "" +msgstr "System" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1029 -#: windows/views.py:1344 +#: windows/components/dataview.py:89 windows/views.py:1076 +#: windows/views.py:1371 msgid "Collation" -msgstr "" +msgstr "Collation" -#: windows/views.py:1058 +#: windows/views.py:1105 msgid "Encryption" -msgstr "" +msgstr "Encryption" -#: windows/views.py:1070 +#: windows/main/controller.py:1272 windows/main/controller.py:1294 +#: windows/main/controller.py:1298 windows/views.py:1117 msgid "Read Only" -msgstr "" +msgstr "Read Only" -#: windows/views.py:1087 +#: windows/views.py:1134 msgid "Tablespace" -msgstr "" +msgstr "Tablespace" -#: windows/views.py:1108 +#: windows/views.py:1155 msgid "Connection limit" -msgstr "" +msgstr "Connection limit" -#: windows/views.py:1151 +#: windows/views.py:1198 msgid "Profile" -msgstr "" +msgstr "Profile" -#: windows/views.py:1177 +#: windows/views.py:1224 msgid "Default tablespace" -msgstr "" +msgstr "Default tablespace" -#: windows/views.py:1198 +#: windows/views.py:1245 msgid "Temporary tablespace" -msgstr "" +msgstr "Temporary tablespace" -#: windows/views.py:1224 +#: windows/views.py:1271 msgid "Quota" -msgstr "" +msgstr "Quota" -#: windows/views.py:1243 +#: windows/views.py:1290 msgid "Unlimited quota" -msgstr "" +msgstr "Unlimited quota" -#: windows/views.py:1260 +#: windows/views.py:1307 msgid "Account status" -msgstr "" +msgstr "Account status" -#: windows/views.py:1281 +#: windows/views.py:1328 msgid "Password expire" -msgstr "" +msgstr "Password expire" -#: windows/views.py:1302 -msgid "Table:" -msgstr "" +#: windows/views.py:1352 +msgid "Add new table" +msgstr "Add new table" -#: 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:1354 +msgid "Clone table" +msgstr "Clone table" -#: windows/views.py:1315 -msgid "Clone" -msgstr "" +#: windows/main/controller.py:1821 windows/views.py:1356 +msgid "Delete table" +msgstr "Delete table" -#: windows/views.py:1339 +#: windows/views.py:1366 windows/views.py:2457 msgid "Rows" -msgstr "" +msgstr "Rows" -#: windows/views.py:1342 +#: windows/views.py:1369 msgid "Updated at" -msgstr "" +msgstr "Updated at" -#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 -#: windows/views.py:2173 windows/views.py:2709 +#: windows/views.py:1387 +msgid "Add new view" +msgstr "Add new view" + +#: windows/views.py:1389 +msgid "Clone view" +msgstr "Clone view" + +#: windows/views.py:1391 +msgid "Delete view" +msgstr "Delete" + +#: windows/views.py:1399 windows/views.py:1423 windows/views.py:1447 +#: windows/views.py:1471 windows/views.py:1495 +msgid "Definition" +msgstr "Definition" + +#: windows/views.py:1406 +msgid "Views" +msgstr "Views" + +#: windows/views.py:1411 +msgid "Add new procedure" +msgstr "Add new procedure" + +#: windows/views.py:1413 +msgid "Clone procedure" +msgstr "Clone procedure" + +#: windows/views.py:1415 +msgid "Delete procedure" +msgstr "Delete procedure" + +#: windows/views.py:1430 +msgid "Procedures" +msgstr "Procedures" + +#: windows/views.py:1435 +msgid "Add new function" +msgstr "New connection" + +#: windows/views.py:1437 +msgid "Clone function" +msgstr "Clone connection" + +#: windows/views.py:1439 +msgid "Delete function" +msgstr "Last connection" + +#: windows/views.py:1454 +msgid "Functions" +msgstr "Connection" + +#: windows/views.py:1459 +msgid "Add new trigger" +msgstr "Add new trigger" + +#: windows/views.py:1461 +msgid "Clone trigger" +msgstr "Clone trigger" + +#: windows/views.py:1463 +msgid "Delete trigger" +msgstr "Delete" + +#: windows/views.py:1478 +msgid "Triggers" +msgstr "Triggers" + +#: windows/views.py:1483 +msgid "Add new event" +msgstr "Add new event" + +#: windows/views.py:1485 +msgid "Clone event" +msgstr "Clone event" + +#: windows/views.py:1487 +msgid "Delete event" +msgstr "Delete" + +#: windows/views.py:1502 +msgid "Events" +msgstr "Events" + +#: windows/views.py:1530 windows/views.py:1901 windows/views.py:2646 +#: windows/views.py:2730 msgid "Apply" -msgstr "" +msgstr "Apply" -#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 -#: windows/views.py:2909 +#: windows/views.py:1542 windows/views.py:1721 msgid "Options" -msgstr "" +msgstr "Options" -#: windows/views.py:1413 +#: windows/views.py:1573 msgid "Diagram" -msgstr "" +msgstr "Diagram" -#: windows/views.py:1424 +#: windows/views.py:1584 msgid "Database" -msgstr "" +msgstr "Database" -#: windows/views.py:1479 windows/views.py:2849 +#: windows/views.py:1639 msgid "Base" -msgstr "" +msgstr "Base" -#: windows/views.py:1493 windows/views.py:2863 +#: windows/views.py:1653 msgid "Auto Increment" -msgstr "" +msgstr "Auto Increment" -#: windows/views.py:1521 windows/views.py:2891 +#: windows/views.py:1681 msgid "Default Collation" -msgstr "" +msgstr "Default Collation" -#: windows/views.py:1531 +#: windows/views.py:1691 msgid "Convert data" -msgstr "" +msgstr "Convert data" -#: windows/views.py:1539 +#: windows/views.py:1699 msgid "Row format" -msgstr "" +msgstr "Row format" -#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 -#: windows/views.py:1756 windows/views.py:2081 +#: windows/views.py:1732 windows/views.py:1760 windows/views.py:1788 +#: windows/views.py:1876 windows/views.py:2326 windows/views.py:2636 msgid "Remove" -msgstr "" +msgstr "Remove" -#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 +#: windows/views.py:1734 windows/views.py:1762 windows/views.py:1790 +#: windows/views.py:2328 windows/views.py:2738 msgid "Clear" -msgstr "" +msgstr "Clear" -#: windows/views.py:1595 windows/views.py:2923 +#: windows/views.py:1747 msgid "Indexes" -msgstr "" +msgstr "Indexes" -#: windows/views.py:1639 +#: windows/views.py:1758 windows/views.py:1786 windows/views.py:2324 +msgid "Insert" +msgstr "Insert" + +#: windows/views.py:1775 msgid "Foreign Keys" -msgstr "" +msgstr "Foreign Keys" -#: windows/views.py:1683 +#: windows/views.py:1803 msgid "Checks" -msgstr "" +msgstr "Checks" -#: windows/views.py:1750 windows/views.py:2944 +#: windows/views.py:1870 msgid "Columns:" -msgstr "" +msgstr "Columns:" -#: windows/views.py:1760 +#: windows/views.py:1880 msgid "Move Up" -msgstr "" +msgstr "Move Up" -#: windows/views.py:1762 +#: windows/views.py:1882 msgid "Move Down" -msgstr "" +msgstr "Move Down" -#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 -#: windows/views.py:3017 +#: windows/views.py:1914 windows/views.py:1921 msgid "Add Index" -msgstr "" +msgstr "Add Index" -#: windows/views.py:1798 windows/views.py:3014 +#: windows/views.py:1918 msgid "Add PrimaryKey" -msgstr "" +msgstr "Add PrimaryKey" -#: windows/views.py:1815 +#: windows/views.py:1935 msgid "Table" -msgstr "" +msgstr "Table" -#: windows/views.py:1851 -msgid "Definer" -msgstr "" - -#: windows/views.py:1871 +#: windows/views.py:1977 windows/views.py:2243 msgid "Schema" -msgstr "" +msgstr "Schema" -#: windows/views.py:1897 -msgid "SQL security" -msgstr "" - -#: windows/views.py:1904 -msgid "DEFINER" -msgstr "" +#: windows/views.py:2005 windows/views.py:2314 +msgid "General" +msgstr "General" -#: windows/views.py:1904 -msgid "INVOKER" -msgstr "" - -#: windows/views.py:1916 +#: windows/views.py:2010 msgid "Algorithm" -msgstr "" +msgstr "Algorithm" -#: windows/views.py:1918 +#: windows/views.py:2012 msgid "UNDEFINED" -msgstr "" +msgstr "UNDEFINED" -#: windows/views.py:1921 +#: windows/views.py:2015 msgid "MERGE" -msgstr "" +msgstr "MERGE" -#: windows/views.py:1924 +#: windows/views.py:2018 msgid "TEMPTABLE" -msgstr "" +msgstr "TEMPTABLE" -#: windows/views.py:1934 +#: windows/views.py:2028 msgid "View constraint" -msgstr "" +msgstr "View constraint" -#: windows/views.py:1936 +#: windows/views.py:2030 msgid "None" -msgstr "" +msgstr "None" -#: windows/views.py:1939 +#: windows/views.py:2033 msgid "LOCAL" -msgstr "" +msgstr "LOCAL" -#: windows/views.py:1942 +#: windows/views.py:2036 msgid "CASCADE" -msgstr "" +msgstr "CASCADE" -#: windows/views.py:1945 +#: windows/views.py:2039 msgid "CHECK ONLY" -msgstr "" +msgstr "CHECK ONLY" -#: windows/views.py:1948 +#: windows/views.py:2042 msgid "READ ONLY" -msgstr "" +msgstr "READ ONLY" + +#: windows/views.py:2055 windows/views.py:2483 +msgid "Behavior" +msgstr "Behavior" + +#: windows/views.py:2062 windows/views.py:2490 +msgid "Definer" +msgstr "Definer" + +#: windows/views.py:2070 windows/views.py:2498 +msgid "*" +msgstr "*" -#: windows/views.py:1960 +#: windows/views.py:2082 windows/views.py:2510 +msgid "SQL security" +msgstr "SQL security" + +#: windows/views.py:2089 windows/views.py:2517 +msgid "DEFINER" +msgstr "DEFINER" + +#: windows/views.py:2089 windows/views.py:2517 +msgid "INVOKER" +msgstr "INVOKER" + +#: windows/views.py:2103 msgid "Force" -msgstr "" +msgstr "Force" -#: windows/views.py:1972 +#: windows/views.py:2115 msgid "Security barrier" -msgstr "" +msgstr "Security barrier" -#: windows/views.py:2054 -msgid "Views" -msgstr "" +#: windows/views.py:2128 windows/views.py:2532 +msgid "Security" +msgstr "Security" -#: windows/views.py:2062 -msgid "Triggers" -msgstr "" +#: windows/views.py:2205 +msgid "View" +msgstr "View" -#: windows/views.py:2073 -msgid "Refrsh" -msgstr "" +#: windows/views.py:2262 +#, fuzzy +msgid "Type" +msgstr "Data type" + +#: windows/views.py:2269 +msgid "Procedure (doesn't return a result)" +msgstr "Procedure (doesn't return a result)" + +#: windows/views.py:2269 +msgid "Function (return a result)" +msgstr "Function (return a result)" + +#: windows/views.py:2279 +msgid "Return type" +msgstr "Return type" + +#: windows/views.py:2299 +msgid "Comment" +msgstr "Comment" + +#: windows/components/dataview.py:111 windows/views.py:2335 +msgid "#" +msgstr "#" + +#: windows/views.py:2337 +msgid "Datatype" +msgstr "Datatype" + +#: windows/views.py:2338 +msgid "Context" +msgstr "Context" + +#: windows/views.py:2345 +msgid "Parameters" +msgstr "Parameters" + +#: windows/views.py:2354 +msgid "Data access" +msgstr "Data access" + +#: windows/views.py:2361 +msgid "CONTAINS SQL" +msgstr "CONTAINS SQL" + +#: windows/views.py:2361 +msgid "NO SQL" +msgstr "NO SQL" + +#: windows/views.py:2361 +msgid "READS SQL DATA" +msgstr "READS SQL DATA" + +#: windows/views.py:2361 +msgid "MODIFIES SQL DATA" +msgstr "MODIFIES SQL DATA" + +#: windows/views.py:2371 +msgid "Deterministic" +msgstr "Deterministic" + +#: windows/views.py:2395 +msgid "SQL" +msgstr "SQL" + +#: windows/views.py:2395 +msgid "PLPGSQL" +msgstr "PLPGSQL" + +#: windows/views.py:2405 +msgid "Volatility" +msgstr "Volatility" + +#: windows/views.py:2412 +msgid "VOLATILE" +msgstr "VOLATILE" + +#: windows/views.py:2412 +msgid "STABLE" +msgstr "STABLE" + +#: windows/views.py:2412 +msgid "IMMUTABLE" +msgstr "IMMUTABLE" + +#: windows/views.py:2422 +msgid "Parallel" +msgstr "Parallel" + +#: windows/views.py:2429 +msgid "UNSAFE" +msgstr "UNSAFE" -#: windows/views.py:2079 +#: windows/views.py:2429 +msgid "RESTRICTED" +msgstr "RESTRICTED" + +#: windows/views.py:2429 +msgid "SAFE" +msgstr "SAFE" + +#: windows/views.py:2441 +msgid "Cost" +msgstr "Cost" + +#: windows/views.py:2609 +msgid "Routine" +msgstr "Routine" + +#: windows/views.py:2617 +msgid "Trigger" +msgstr "Trigger" + +#: windows/views.py:2634 msgid "Duplicate" -msgstr "" +msgstr "Duplicate" -#: windows/views.py:2085 +#: windows/views.py:2640 msgid "Apply changes automatically" -msgstr "" +msgstr "Apply changes automatically" -#: windows/views.py:2087 windows/views.py:2088 +#: windows/views.py:2642 windows/views.py:2643 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" msgstr "" +"If enabled, table edits are applied immediately without pressing Apply or" +" Cancel" -#: windows/views.py:2101 +#: windows/views.py:2656 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" +msgstr "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -#: windows/views.py:2109 +#: windows/views.py:2664 msgid "First" -msgstr "" +msgstr "First" -#: windows/views.py:2127 +#: windows/views.py:2682 msgid "Last" -msgstr "" +msgstr "Last" -#: windows/views.py:2136 +#: windows/views.py:2691 msgid "Filters" +msgstr "Filters" + +#: windows/views.py:2733 +msgid "" +"Apply filters in data\n" +"CTRL+ENTER" msgstr "" +"Apply filters in data\n" +"CTRL+ENTER" -#: windows/views.py:2176 +#: windows/views.py:2734 msgid "CTRL+ENTER" -msgstr "" +msgstr "CTRL+ENTER" -#: windows/views.py:2196 +#: windows/views.py:2762 msgid "Insert row" -msgstr "" +msgstr "Insert row" + +#: windows/components/popup.py:31 windows/views.py:2774 +msgid "NULL" +msgstr "NULL" -#: windows/views.py:2204 +#: windows/views.py:2781 msgid "Data" -msgstr "" +msgstr "Data" -#: windows/main/controller.py:278 windows/main/controller.py:287 -#: windows/main/controller.py:288 windows/views.py:2221 -#, fuzzy +#: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" -msgstr "New directory" +msgstr "New query" -#: windows/views.py:2223 windows/views.py:2660 +#: windows/views.py:2800 msgid "Close" -msgstr "" +msgstr "Close" -#: windows/main/controller.py:279 windows/main/controller.py:289 -#: windows/main/controller.py:290 windows/views.py:2223 +#: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" -msgstr "" +msgstr "Close query" -#: windows/views.py:2227 +#: windows/views.py:2804 msgid "Run" -msgstr "" +msgstr "Run" -#: windows/main/controller.py:280 windows/main/controller.py:292 -#: windows/main/controller.py:293 windows/views.py:2227 -#, fuzzy +#: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" -msgstr "SSH executable" +msgstr "Execute" -#: windows/views.py:2229 +#: windows/views.py:2806 msgid "Run all" -msgstr "" +msgstr "Run all" -#: windows/main/controller.py:295 windows/views.py:2229 +#: windows/views.py:2806 msgid "Execute all statements" -msgstr "" +msgstr "Execute all statements" -#: windows/main/controller.py:282 windows/main/controller.py:297 -#: windows/main/controller.py:298 windows/views.py:2231 +#: windows/main/controller.py:323 windows/views.py:2808 msgid "Stop" -msgstr "" +msgstr "Stop" -#: windows/views.py:2287 +#: windows/views.py:2873 msgid "a page" -msgstr "" +msgstr "a page" -#: windows/views.py:2315 +#: windows/views.py:2923 msgid "Query" -msgstr "" - -#: windows/views.py:2626 -msgid "Character set" -msgstr "" - -#: windows/views.py:2644 windows/views.py:2663 -msgid "New" -msgstr "" - -#: windows/views.py:2683 -msgid "Insert record" -msgstr "" - -#: windows/views.py:2688 -msgid "Duplicate record" -msgstr "" - -#: windows/views.py:2695 -msgid "Delete record" -msgstr "" - -#: windows/views.py:2733 windows/views.py:2964 -msgid "Up" -msgstr "" - -#: windows/views.py:2740 windows/views.py:2971 -msgid "Down" -msgstr "" - -#: windows/views.py:3100 -msgid "Save Starments" -msgstr "" - -#: windows/views.py:3108 -msgid "Location" -msgstr "" - -#: windows/views.py:3115 -msgid "*.sql" -msgstr "" +msgstr "Query" #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 msgid "Allow NULL" -msgstr "" +msgstr "Allow NULL" #: windows/components/dataview.py:28 msgid "Check" -msgstr "" +msgstr "Check" #: windows/components/dataview.py:32 windows/components/dataview.py:56 #: windows/components/dataview.py:78 msgid "Default" -msgstr "" +msgstr "Default" #: windows/components/dataview.py:36 windows/components/dataview.py:60 #: windows/components/dataview.py:82 msgid "Virtuality" -msgstr "" +msgstr "Virtuality" #: windows/components/dataview.py:39 windows/components/dataview.py:63 -#: windows/components/dataview.py:85 windows/components/dataview.py:241 +#: windows/components/dataview.py:85 windows/components/dataview.py:256 msgid "Expression" -msgstr "" +msgstr "Expression" #: windows/components/dataview.py:51 msgid "Unsigned" -msgstr "" +msgstr "Unsigned" #: windows/components/dataview.py:53 msgid "Zerofill" -msgstr "" +msgstr "Zerofill" -#: windows/components/dataview.py:109 -msgid "#" -msgstr "" - -#: windows/components/dataview.py:117 +#: windows/components/dataview.py:119 msgid "Data type" -msgstr "" +msgstr "Data type" -#: windows/components/dataview.py:121 +#: windows/components/dataview.py:123 msgid "Length/Set" -msgstr "" +msgstr "Length/Set" -#: windows/components/dataview.py:155 +#: windows/components/dataview.py:169 msgid "Add column\tCTRL+INS" -msgstr "" +msgstr "Add column\tCTRL+INS" -#: windows/components/dataview.py:161 +#: windows/components/dataview.py:175 msgid "Remove column\tCTRL+DEL" -msgstr "" +msgstr "Remove column\tCTRL+DEL" -#: windows/components/dataview.py:169 +#: windows/components/dataview.py:183 msgid "Move up\tCTRL+UP" -msgstr "" +msgstr "Move up\tCTRL+UP" -#: windows/components/dataview.py:176 +#: windows/components/dataview.py:190 msgid "Move down\tCTRL+D" -msgstr "" +msgstr "Move down\tCTRL+D" -#: windows/components/dataview.py:199 +#: windows/components/dataview.py:214 msgid "Create new index" -msgstr "" +msgstr "Create new index" -#: windows/components/dataview.py:214 +#: windows/components/dataview.py:229 msgid "Append to index" -msgstr "" +msgstr "Append to index" -#: windows/components/dataview.py:228 +#: windows/components/dataview.py:243 msgid "Column(s)/Expression" -msgstr "" +msgstr "Column(s)/Expression" -#: windows/components/dataview.py:229 +#: windows/components/dataview.py:244 msgid "Condition" -msgstr "" +msgstr "Condition" -#: windows/components/dataview.py:259 +#: windows/components/dataview.py:274 msgid "Column(s)" -msgstr "" +msgstr "Column(s)" -#: windows/components/dataview.py:265 +#: windows/components/dataview.py:280 msgid "Reference table" -msgstr "" +msgstr "Reference table" -#: windows/components/dataview.py:271 +#: windows/components/dataview.py:286 msgid "Reference column(s)" -msgstr "" +msgstr "Reference column(s)" -#: windows/components/dataview.py:277 +#: windows/components/dataview.py:292 msgid "On UPDATE" -msgstr "" +msgstr "On UPDATE" -#: windows/components/dataview.py:283 +#: windows/components/dataview.py:298 msgid "On DELETE" -msgstr "" +msgstr "On DELETE" -#: windows/components/dataview.py:298 +#: windows/components/dataview.py:313 msgid "Add foreign key" -msgstr "" +msgstr "Add foreign key" -#: windows/components/dataview.py:304 +#: windows/components/dataview.py:319 msgid "Remove foreign key" -msgstr "" +msgstr "Remove foreign key" #: windows/components/popup.py:26 msgid "No default value" -msgstr "" - -#: windows/components/popup.py:31 -msgid "NULL" -msgstr "" +msgstr "No default value" #: windows/components/popup.py:35 msgid "AUTO INCREMENT" -msgstr "" +msgstr "AUTO INCREMENT" #: windows/components/popup.py:39 msgid "Text/Expression" -msgstr "" +msgstr "Text/Expression" -#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 +#: windows/dialogs/connections/view.py:126 windows/main/query/renderer.py:192 msgid "Unknown error" -msgstr "" +msgstr "Unknown error" -#: windows/dialogs/connections/view.py:414 +#: windows/dialogs/connections/view.py:416 msgid "Connection established successfully" -msgstr "" +msgstr "Connection established successfully" -#: windows/dialogs/connections/view.py:426 +#: windows/dialogs/connections/view.py:428 #, python-brace-format msgid "Do you want save the connection {connection_name}?" -msgstr "" +msgstr "Do you want save the connection {connection_name}?" -#: windows/dialogs/connections/view.py:429 +#: windows/dialogs/connections/view.py:431 msgid "Confirm save" -msgstr "" +msgstr "Confirm save" -#: windows/dialogs/connections/view.py:481 +#: windows/dialogs/connections/view.py:483 msgid "You have unsaved changes. Do you want to save them before continuing?" -msgstr "" +msgstr "You have unsaved changes. Do you want to save them before continuing?" -#: windows/dialogs/connections/view.py:483 +#: windows/dialogs/connections/view.py:485 msgid "Unsaved changes" -msgstr "" +msgstr "Unsaved changes" -#: windows/dialogs/connections/view.py:750 +#: windows/dialogs/connections/view.py:773 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" +"This connection cannot work without TLS. TLS has been enabled " +"automatically." -#: windows/dialogs/connections/view.py:775 +#: windows/dialogs/connections/view.py:798 #, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "" +"Connection error:\n" +"{error}" -#: windows/dialogs/connections/view.py:776 +#: windows/dialogs/connections/view.py:799 msgid "Connection error" -msgstr "" +msgstr "Connection error" -#: windows/dialogs/connections/view.py:802 +#: windows/dialogs/connections/view.py:825 #, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" -msgstr "" +msgstr "Do you want to delete the connection '{connection_name}'?" -#: windows/dialogs/connections/view.py:805 -#: windows/dialogs/connections/view.py:822 +#: windows/dialogs/connections/view.py:828 +#: windows/dialogs/connections/view.py:845 msgid "Confirm delete" -msgstr "" +msgstr "Confirm delete" -#: windows/dialogs/connections/view.py:819 +#: windows/dialogs/connections/view.py:842 #, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" -msgstr "" +msgstr "Do you want to delete the directory '{directory_name}'?" -#: windows/main/controller.py:275 +#: windows/main/controller.py:316 #, python-brace-format msgid "{text} ({shortcut})" -msgstr "" +msgstr "{text} ({shortcut})" -#: windows/main/controller.py:281 windows/main/controller.py:294 -#, fuzzy +#: windows/main/controller.py:322 msgid "Execute all" -msgstr "SSH executable" +msgstr "Execute all" -#: windows/main/controller.py:471 windows/main/controller.py:479 +#: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" -msgstr "" +msgstr "Query (1)" -#: windows/main/controller.py:497 +#: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" -msgstr "" +msgstr "Query ({query_number})" -#: windows/main/controller.py:530 +#: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" -msgstr "" +msgstr "You have unsaved changes. Save before closing?" -#: windows/main/controller.py:531 +#: windows/main/controller.py:519 msgid "Unsaved query" -msgstr "" +msgstr "Unsaved query" -#: windows/main/controller.py:576 +#: windows/main/controller.py:564 msgid "Save query" -msgstr "" +msgstr "Save query" -#: windows/main/controller.py:579 +#: windows/main/controller.py:567 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" -msgstr "" +msgstr "SQL files (*.sql)|*.sql|All files (*.*)|*.*" -#: 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 +#: windows/main/controller.py:595 windows/main/controller.py:626 +#: windows/main/controller.py:653 windows/main/database/list.py:119 +#: windows/main/database/routine.py:591 windows/main/database/routine.py:635 +#: windows/main/database/view.py:268 windows/main/database/view.py:297 +#: windows/main/query/controller.py:179 msgid "Error" -msgstr "" +msgstr "Error" -#: windows/main/controller.py:622 +#: windows/main/controller.py:633 #, python-brace-format msgid "-- Saved query to {file_path}" -msgstr "" +msgstr "-- Saved query to {file_path}" -#: windows/main/controller.py:647 +#: windows/main/controller.py:659 #, python-brace-format msgid "-- Autosaved query to {file_path}" -msgstr "" +msgstr "-- Autosaved query to {file_path}" -#: windows/main/controller.py:704 +#: windows/main/controller.py:725 msgid "days" -msgstr "" +msgstr "days" -#: windows/main/controller.py:705 +#: windows/main/controller.py:726 msgid "hours" -msgstr "" +msgstr "hours" -#: windows/main/controller.py:706 +#: windows/main/controller.py:727 msgid "minutes" -msgstr "" +msgstr "minutes" -#: windows/main/controller.py:707 +#: windows/main/controller.py:728 msgid "seconds" -msgstr "" +msgstr "seconds" -#: windows/main/controller.py:715 +#: windows/main/controller.py:736 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" -msgstr "" +msgstr "Memory used: {used} ({percentage:.2%})" -#: windows/main/controller.py:751 +#: windows/main/controller.py:772 msgid "Settings saved successfully" -msgstr "" +msgstr "Settings saved successfully" -#: windows/main/controller.py:952 +#: windows/main/controller.py:1006 #, python-brace-format msgid "~{estimated} (Loading...)" -msgstr "" +msgstr "~{estimated} (Loading...)" -#: windows/main/controller.py:954 +#: windows/main/controller.py:1008 msgid "~ (Loading...)" -msgstr "" +msgstr "~ (Loading...)" + +#: windows/main/controller.py:1243 +msgid "Write Mode (2:00)" +msgstr "Write Mode (2:00)" -#: windows/main/controller.py:1119 +#: windows/main/controller.py:1294 +msgid "Write Mode" +msgstr "Write Mode" + +#: windows/main/controller.py:1305 msgid "Version" -msgstr "" +msgstr "Version" -#: windows/main/controller.py:1121 +#: windows/main/controller.py:1307 msgid "Uptime" -msgstr "" +msgstr "Uptime" -#: windows/main/controller.py:1199 +#: windows/main/controller.py:1444 #, python-brace-format msgid "Do you want discard the change to {database_name}?" -msgstr "" +msgstr "Do you want discard the change to {database_name}?" -#: windows/main/controller.py:1232 +#: windows/main/controller.py:1477 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1066,118 +1263,188 @@ msgid "" "- Yes: open dump flow (coming soon, no drop).\n" "- No: drop the database now." msgstr "" +"Do you want to create a dump before dropping database '{database_name}'?\n" +"\n" +"Dump is not implemented yet.\n" +"- Yes: open dump flow (coming soon, no drop).\n" +"- No: drop the database now." -#: windows/main/controller.py:1237 windows/main/controller.py:1258 +#: windows/main/controller.py:1482 windows/main/controller.py:1503 msgid "Delete database" -msgstr "" +msgstr "Delete database" -#: windows/main/controller.py:1243 +#: windows/main/controller.py:1488 msgid "Dump is not implemented yet. No action has been performed." -msgstr "" +msgstr "Dump is not implemented yet. No action has been performed." -#: windows/main/controller.py:1244 +#: windows/main/controller.py:1489 msgid "Dump not available" -msgstr "" +msgstr "Dump not available" -#: windows/main/controller.py:1257 +#: windows/main/controller.py:1502 msgid "Database deletion is not supported by this engine." -msgstr "" +msgstr "Database deletion is not supported by this engine." -#: windows/main/controller.py:1272 +#: windows/main/controller.py:1517 msgid "Database deleted successfully" -msgstr "" +msgstr "Database deleted successfully" -#: windows/main/controller.py:1273 windows/main/database/view.py:253 -#: windows/main/database/view.py:279 +#: windows/main/controller.py:1518 windows/main/database/routine.py:556 +#: windows/main/database/routine.py:620 windows/main/database/view.py:256 +#: windows/main/database/view.py:291 msgid "Success" -msgstr "" +msgstr "Success" -#: windows/main/controller.py:1392 +#: windows/main/controller.py:1792 #, python-brace-format msgid "Do you want discard the change to {table_name}?" -msgstr "" +msgstr "Do you want discard the change to {table_name}?" -#: windows/main/controller.py:1418 +#: windows/main/controller.py:1818 #, python-brace-format msgid "Do you want delete the table {table_name}?" -msgstr "" - -#: windows/main/controller.py:1421 -msgid "Delete table" -msgstr "" +msgstr "Do you want delete the table {table_name}?" -#: windows/main/controller.py:1440 +#: windows/main/controller.py:1840 #, python-brace-format msgid "{table_name} (COPY)" -msgstr "" +msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1563 +#: windows/main/controller.py:1999 msgid "Do you want delete the records?" +msgstr "Do you want delete the records?" + +#: windows/main/controller.py:2104 +#, fuzzy +msgid "Database connection lost. Do you want to reconnect?" +msgstr "Do you want to reconnect?" + +#: windows/main/controller.py:2105 windows/main/database/list.py:108 +msgid "Connection lost" +msgstr "Connection lost" + +#: windows/main/controller.py:2113 +#, fuzzy +msgid "Connection restored successfully." +msgstr "Connection established successfully" + +#: windows/main/controller.py:2114 +#, fuzzy +msgid "Connection restored" +msgstr "Connection error" + +#: windows/main/controller.py:2120 +#, python-brace-format +msgid "Could not reconnect: {error}" msgstr "" -#: windows/main/database/list.py:69 +#: windows/main/controller.py:2121 +#, fuzzy +msgid "Reconnection failed" +msgstr "Reconnection failed:" + +#: windows/main/database/list.py:104 msgid "The connection to the database was lost." -msgstr "" +msgstr "The connection to the database was lost." -#: windows/main/database/list.py:71 +#: windows/main/database/list.py:106 msgid "Do you want to reconnect?" -msgstr "" +msgstr "Do you want to reconnect?" -#: windows/main/database/list.py:73 -msgid "Connection lost" -msgstr "" - -#: windows/main/database/list.py:83 +#: windows/main/database/list.py:118 msgid "Reconnection failed:" -msgstr "" +msgstr "Reconnection failed:" + +#: windows/main/database/routine.py:545 +msgid "Function created successfully" +msgstr "Function created successfully" + +#: windows/main/database/routine.py:547 +msgid "Function updated successfully" +msgstr "Function updated successfully" + +#: windows/main/database/routine.py:551 +msgid "Procedure created successfully" +msgstr "Procedure created successfully" + +#: windows/main/database/routine.py:553 +msgid "Procedure updated successfully" +msgstr "Procedure updated successfully" + +#: windows/main/database/routine.py:590 +#, python-brace-format +msgid "Error saving routine: {}" +msgstr "Error saving routine: {}" + +#: windows/main/database/routine.py:604 +msgid "Function" +msgstr "Function" -#: windows/main/database/view.py:252 +#: windows/main/database/routine.py:604 +msgid "Procedure" +msgstr "Procedure" + +#: windows/main/database/routine.py:607 +#, python-brace-format +msgid "Are you sure you want to delete {} '{}'?" +msgstr "Are you sure you want to delete {} '{}'? + +#: windows/main/database/routine.py:610 windows/main/database/view.py:282 +msgid "Confirm Delete" +msgstr "Confirm Delete" + +#: windows/main/database/routine.py:619 +#, python-brace-format +msgid "{} deleted successfully" +msgstr "{} deleted successfully" + +#: windows/main/database/routine.py:634 +#, python-brace-format +msgid "Error deleting routine: {}" +msgstr "Error deleting routine: {}" + +#: windows/main/database/view.py:255 msgid "View created successfully" -msgstr "" +msgstr "View created successfully" -#: windows/main/database/view.py:252 +#: windows/main/database/view.py:255 msgid "View updated successfully" -msgstr "" +msgstr "View updated successfully" -#: windows/main/database/view.py:256 +#: windows/main/database/view.py:268 #, python-brace-format msgid "Error saving view: {}" -msgstr "" +msgstr "Error saving view: {}" -#: windows/main/database/view.py:269 +#: windows/main/database/view.py:281 #, python-brace-format msgid "Are you sure you want to delete view '{}'?" -msgstr "" +msgstr "Are you sure you want to delete view '{}'?" -#: windows/main/database/view.py:270 -msgid "Confirm Delete" -msgstr "" - -#: windows/main/database/view.py:279 +#: windows/main/database/view.py:291 msgid "View deleted successfully" -msgstr "" +msgstr "View deleted successfully" -#: windows/main/database/view.py:282 +#: windows/main/database/view.py:297 #, python-brace-format msgid "Error deleting view: {}" -msgstr "" +msgstr "Error deleting view: {}" -#: windows/main/query/controller.py:110 +#: windows/main/query/controller.py:112 #, python-brace-format msgid "{elapsed_ms:.0f} ms" -msgstr "" +msgstr "{elapsed_ms:.0f} ms" -#: windows/main/query/controller.py:112 +#: windows/main/query/controller.py:114 #, python-brace-format msgid "{elapsed_s:.2f} s" -msgstr "" +msgstr "{elapsed_s:.2f} s" -#: windows/main/query/controller.py:115 -#, fuzzy +#: windows/main/query/controller.py:117 msgid "none" -msgstr "Engine" +msgstr "none" -#: windows/main/query/controller.py:121 +#: windows/main/query/controller.py:123 #, python-brace-format msgid "" "Query execution stopped after {elapsed}.\n" @@ -1186,191 +1453,69 @@ msgid "" "Failed: {failed}.\n" "Last statement: #{last}." msgstr "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." -#: windows/main/query/controller.py:134 +#: windows/main/query/controller.py:136 msgid "Query execution cancelled" -msgstr "" +msgstr "Query execution cancelled" -#: windows/main/query/controller.py:176 +#: windows/main/query/controller.py:178 msgid "No active database connection" -msgstr "" +msgstr "No active database connection" + +#: windows/main/query/controller.py:227 +#, fuzzy +msgid "Database connection lost" +msgstr "Connection lost" + +#: windows/main/query/history.py:55 +msgid "(empty query)" +msgstr "New directory" #: windows/main/query/renderer.py:53 #, python-brace-format msgid "{affected_rows} rows affected" -msgstr "" +msgstr "{affected_rows} rows affected" #: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 #, python-brace-format msgid "Query {query_number}" -msgstr "" +msgstr "Query {query_number}" #: windows/main/query/renderer.py:65 #, python-brace-format msgid "Query {query_number} (Error)" -msgstr "" +msgstr "Query {query_number} (Error)" #: windows/main/query/renderer.py:79 #, python-brace-format msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" -msgstr "" +msgstr "Query {query_number} ({rows_count} rows × {columns_count} cols)" #: windows/main/query/renderer.py:165 #, python-brace-format msgid "{rows_count} rows" -msgstr "" +msgstr "{rows_count} rows" #: windows/main/query/renderer.py:167 #, python-brace-format msgid "{elapsed_ms:.1f} ms" -msgstr "" +msgstr "{elapsed_ms:.1f} ms" #: windows/main/query/renderer.py:171 #, python-brace-format msgid "{warnings_count} warnings" -msgstr "" +msgstr "{warnings_count} warnings" #: windows/main/query/renderer.py:186 msgid "Error:" -msgstr "" - -#~ msgid "Created at:" -#~ msgstr "" - -#~ msgid "Last connection:" -#~ msgstr "" - -#~ msgid "Successful connections:" -#~ msgstr "" - -#~ msgid "Unsuccessful connections:" -#~ msgstr "" - -#~ msgid "Session Manager" -#~ msgstr "" - -#~ msgid "Session name" -#~ msgstr "" - -#~ msgid "Connection type" -#~ msgstr "" - -#~ msgid "Open" -#~ msgstr "" - -#~ msgid "Open session manager" -#~ msgstr "" - -#~ msgid "Foreign Key" -#~ msgstr "" - -#~ msgid "New Session" -#~ msgstr "" - -#~ msgid "connection" -#~ 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 "" +msgstr "Error:" -#~ msgid "Zero Fill" -#~ msgstr "" +#: windows/main/table/records.py:334 +msgid "Error saving records" +msgstr "Error saving records" diff --git a/locale/es_ES/LC_MESSAGES/petersql.mo b/locale/es_ES/LC_MESSAGES/petersql.mo index 3da8b8a..eb576fc 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 e3de2d7..ff42e00 100644 --- a/locale/es_ES/LC_MESSAGES/petersql.po +++ b/locale/es_ES/LC_MESSAGES/petersql.po @@ -2,12 +2,11 @@ # Copyright (C) 2026 ORGANIZATION # This file is distributed under the same license as the PeterSQL project. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-23 10:07+0100\n" +"POT-Creation-Date: 2026-06-06 11:23+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: es_ES\n" @@ -47,651 +46,848 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "Cliente OpenSSH no encontrado." -#: structures/engines/mariadb/context.py:611 -#: structures/engines/mysql/context.py:622 -#: structures/engines/postgresql/context.py:645 -#: structures/engines/sqlite/context.py:524 +#: structures/engines/context.py:567 +msgid "This connection is read-only." +msgstr "Esta conexión es de solo lectura." + +#: structures/engines/context.py:578 +#, fuzzy, python-brace-format +msgid "Database connection lost: {error}" +msgstr "" +"Error de conexión:\n" +"{error}" + +#: structures/engines/mariadb/context.py:658 +#: structures/engines/mysql/context.py:670 +#: structures/engines/postgresql/context.py:701 +#: structures/engines/sqlite/context.py:564 #, python-brace-format msgid "Table{table_index:03}" -msgstr "" +msgstr "Table{table_index:03}" -#: structures/engines/mariadb/context.py:639 -#: structures/engines/mysql/context.py:650 -#: structures/engines/postgresql/context.py:670 -#: structures/engines/sqlite/context.py:548 +#: structures/engines/mariadb/context.py:686 +#: structures/engines/mysql/context.py:698 +#: structures/engines/postgresql/context.py:726 +#: structures/engines/sqlite/context.py:588 #, python-brace-format msgid "Column{column_index:03}" -msgstr "" +msgstr "Column{column_index:03}" -#: structures/engines/mariadb/context.py:657 -#: structures/engines/mysql/context.py:668 -#: structures/engines/postgresql/context.py:688 -#: structures/engines/sqlite/context.py:566 +#: structures/engines/mariadb/context.py:704 +#: structures/engines/mysql/context.py:716 +#: structures/engines/postgresql/context.py:744 +#: structures/engines/sqlite/context.py:606 #, python-brace-format msgid "Index{index_number:03}" -msgstr "" +msgstr "Index{index_number:03}" -#: structures/engines/mariadb/context.py:697 -#: structures/engines/mysql/context.py:706 -#: structures/engines/postgresql/context.py:728 -#: structures/engines/sqlite/context.py:608 +#: structures/engines/mariadb/context.py:744 +#: structures/engines/mysql/context.py:754 +#: structures/engines/postgresql/context.py:784 +#: structures/engines/sqlite/context.py:646 #, python-brace-format msgid "ForeignKey{foreign_key_number:03}" -msgstr "" +msgstr "ForeignKey{foreign_key_number:03}" -#: structures/engines/mariadb/context.py:730 -#: structures/engines/mysql/context.py:737 -#: structures/engines/postgresql/context.py:758 -#: structures/engines/sqlite/context.py:636 +#: structures/engines/mariadb/context.py:777 +#: structures/engines/mysql/context.py:785 +#: structures/engines/postgresql/context.py:814 +#: structures/engines/sqlite/context.py:674 #, python-brace-format msgid "View{view_index:03}" -msgstr "" +msgstr "View{view_index:03}" -#: structures/engines/mariadb/context.py:781 +#: structures/engines/mariadb/context.py:830 #, python-brace-format msgid "Trigger{trigger_index:03}" -msgstr "" +msgstr "Trigger{trigger_index:03}" -#: windows/dialogs/connections/view.py:415 -#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 -#: windows/views.py:33 +#: windows/dialogs/connections/view.py:417 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1303 +#: windows/views.py:62 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:1006 -#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 -#: windows/views.py:2821 +#: windows/components/dataview.py:115 windows/components/dataview.py:240 +#: windows/components/dataview.py:253 windows/components/dataview.py:268 +#: windows/views.py:76 windows/views.py:126 windows/views.py:1053 +#: windows/views.py:1365 windows/views.py:1398 windows/views.py:1422 +#: windows/views.py:1446 windows/views.py:1470 windows/views.py:1494 +#: windows/views.py:1611 windows/views.py:1957 windows/views.py:2228 +#: windows/views.py:2336 msgid "Name" msgstr "Nombre" -#: windows/views.py:48 windows/views.py:428 +#: windows/views.py:77 windows/views.py:467 msgid "Last connection" msgstr "Última conexión" -#: windows/dialogs/connections/view.py:653 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:90 msgid "New directory" msgstr "Nuevo directorio" -#: windows/dialogs/connections/model.py:206 -#: windows/dialogs/connections/view.py:613 windows/views.py:65 +#: windows/dialogs/connections/model.py:212 +#: windows/dialogs/connections/view.py:615 windows/views.py:94 msgid "New connection" msgstr "Nueva conexión" -#: windows/views.py:71 -#, fuzzy +#: windows/views.py:100 msgid "Rename" -msgstr "Nombre" +msgstr "Renombrar" -#: windows/views.py:76 -#, fuzzy +#: windows/views.py:105 msgid "Clone connection" -msgstr "Nueva conexión" +msgstr "Clonar conexión" -#: 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 +#: windows/views.py:110 windows/views.py:642 windows/views.py:1525 +#: windows/views.py:1893 windows/views.py:2183 windows/views.py:2587 msgid "Delete" msgstr "Eliminar" -#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 -#: windows/views.py:2876 +#: windows/views.py:140 windows/views.py:1370 windows/views.py:1666 msgid "Engine" msgstr "Motor" -#: windows/views.py:132 +#: windows/views.py:161 msgid "Host + port" msgstr "Host + puerto" -#: windows/views.py:148 +#: windows/views.py:177 msgid "Username" msgstr "Nombre de usuario" -#: windows/views.py:161 windows/views.py:1132 +#: windows/views.py:190 windows/views.py:1179 msgid "Password" msgstr "Contraseña" -#: windows/views.py:174 -#, fuzzy +#: windows/views.py:203 msgid "Connection timeout" -msgstr "Conexión perdida" +msgstr "Tiempo de espera de conexión" -#: windows/views.py:192 +#: windows/views.py:221 msgid "Use TLS" msgstr "Usar TLS" -#: windows/views.py:203 +#: windows/views.py:232 +msgid "Mark read only" +msgstr "Marcar como solo lectura" + +#: windows/views.py:243 msgid "Use SSH tunnel" msgstr "Usar túnel SSH" -#: windows/views.py:214 +#: windows/views.py:254 msgid "Compressed client/server protocol" -msgstr "" +msgstr "Protocolo cliente/servidor comprimido" -#: windows/views.py:233 +#: windows/views.py:273 msgid "Filename" msgstr "Nombre de archivo" -#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 +#: windows/views.py:278 windows/views.py:397 msgid "Select a file" msgstr "Seleccionar un archivo" -#: windows/views.py:238 windows/views.py:358 -#, fuzzy +#: windows/views.py:278 windows/views.py:397 msgid "*.*" -msgstr "*. *" +msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 -#: windows/views.py:2834 +#: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 msgid "Comments" msgstr "Comentarios" -#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 -#: windows/views.py:884 +#: windows/main/controller.py:772 windows/views.py:309 windows/views.py:769 +#: windows/views.py:923 msgid "Settings" msgstr "Configuraciones" -#: windows/views.py:278 +#: windows/views.py:317 msgid "SSH executable" msgstr "Ejecutable SSH" -#: windows/views.py:283 +#: windows/views.py:322 msgid "ssh" msgstr "ssh" -#: windows/views.py:291 +#: windows/views.py:330 msgid "SSH host + port" msgstr "Host SSH + puerto" -#: windows/views.py:303 +#: windows/views.py:342 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:312 +#: windows/views.py:351 msgid "SSH username" msgstr "Nombre de usuario SSH" -#: windows/views.py:325 +#: windows/views.py:364 msgid "SSH password" msgstr "Contraseña SSH" -#: windows/views.py:338 +#: windows/views.py:377 msgid "Local port" msgstr "Puerto local" -#: windows/views.py:344 +#: windows/views.py:383 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:353 +#: windows/views.py:392 msgid "Identity file" msgstr "Archivo de identidad" -#: windows/views.py:369 -#, fuzzy +#: windows/views.py:408 msgid "Remote host + port" -msgstr "Host + puerto" +msgstr "Host remoto + puerto" -#: windows/views.py:381 +#: windows/views.py:420 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)." -#: windows/views.py:390 +#: windows/views.py:429 msgid "SSH extra args" -msgstr "" +msgstr "Argumentos extra SSH" -#: windows/views.py:405 +#: windows/views.py:444 msgid "SSH Tunnel" msgstr "Túnel SSH" -#: windows/views.py:411 windows/views.py:1341 +#: windows/views.py:450 windows/views.py:1368 msgid "Created at" msgstr "Creado en" -#: windows/views.py:445 +#: windows/views.py:484 msgid "Successful connections" msgstr "Conexiones exitosas" -#: windows/views.py:462 -#, fuzzy +#: windows/views.py:501 msgid "Last successful connection" -msgstr "Conexiones exitosas" +msgstr "Última conexión exitosa" -#: windows/views.py:479 +#: windows/views.py:518 msgid "Unsuccessful connections" msgstr "Conexiones fallidas" -#: windows/views.py:496 +#: windows/views.py:535 msgid "Last failure reason" -msgstr "" +msgstr "Último motivo de fallo" -#: windows/views.py:513 -#, fuzzy +#: windows/views.py:552 msgid "Total connection attempts" -msgstr "Última conexión" +msgstr "Total de intentos de conexión" -#: windows/views.py:530 -#, fuzzy +#: windows/views.py:569 msgid "Average connection time (ms)" -msgstr "Reconexión fallida:" +msgstr "Tiempo promedio de conexión (ms)" -#: windows/views.py:547 -#, fuzzy +#: windows/views.py:586 msgid "Most recent connection duration" -msgstr "Abrir administrador de conexiones" +msgstr "Duración de la conexión más reciente" -#: windows/views.py:566 +#: windows/views.py:605 msgid "Statistics" msgstr "Estadísticas" -#: windows/views.py:584 windows/views.py:1730 +#: windows/views.py:623 windows/views.py:1850 msgid "Create" msgstr "Crear" -#: windows/views.py:588 -#, fuzzy +#: windows/views.py:627 msgid "Create connection" -msgstr "Última conexión" +msgstr "Crear conexión" -#: windows/views.py:591 -#, fuzzy +#: windows/views.py:630 msgid "Create directory" msgstr "Nuevo directorio" -#: 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 +#: windows/views.py:659 windows/views.py:883 windows/views.py:1520 +#: windows/views.py:1896 windows/views.py:2188 windows/views.py:2592 +#: windows/views.py:2648 msgid "Cancel" msgstr "Cancelar" -#: 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 +#: windows/main/controller.py:324 windows/views.py:664 windows/views.py:2193 +#: windows/views.py:2597 windows/views.py:2812 msgid "Save" msgstr "Guardar" -#: windows/views.py:632 +#: windows/views.py:671 msgid "Test" msgstr "Probar" -#: windows/views.py:639 +#: windows/views.py:678 msgid "Connect" msgstr "Conectar" -#: windows/views.py:742 +#: windows/views.py:781 windows/views.py:2388 msgid "Language" msgstr "Idioma" -#: windows/views.py:747 +#: windows/views.py:786 msgid "English" msgstr "Inglés" -#: windows/views.py:747 +#: windows/views.py:786 msgid "Italian" msgstr "Italiano" -#: windows/views.py:747 +#: windows/views.py:786 msgid "French" msgstr "Francés" -#: windows/views.py:759 +#: windows/views.py:798 msgid "Locale" msgstr "Localización" -#: windows/views.py:780 -#, fuzzy +#: windows/views.py:819 msgid "Column content" msgstr "Nueva conexión" -#: windows/views.py:790 +#: windows/views.py:829 msgid "Syntax" msgstr "Sintaxis" -#: windows/views.py:847 +#: windows/views.py:886 msgid "Ok" msgstr "Ok" -#: windows/views.py:878 +#: windows/views.py:917 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:887 +#: windows/views.py:926 msgid "File" msgstr "Archivo" -#: windows/views.py:890 +#: windows/views.py:929 msgid "About" msgstr "Acerca de" -#: windows/views.py:893 +#: windows/views.py:932 msgid "Help" msgstr "Ayuda" -#: windows/views.py:898 +#: windows/views.py:937 msgid "Open connection manager" msgstr "Abrir administrador de conexiones" -#: windows/views.py:902 +#: windows/views.py:941 msgid "Disconnect from server" msgstr "Desconectar del servidor" -#: windows/views.py:904 +#: windows/views.py:943 msgid "tool" msgstr "herramienta" -#: windows/views.py:904 +#: windows/views.py:943 windows/views.py:2628 msgid "Refresh" msgstr "Actualizar" -#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 -#: windows/views.py:2077 windows/views.py:2221 +#: windows/views.py:947 windows/views.py:949 windows/views.py:1874 +#: windows/views.py:2632 windows/views.py:2798 msgid "Add" msgstr "Agregar" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 +#: windows/views.py:954 +#, python-brace-format +msgid "{mode}" +msgstr "{mode}" + +#: windows/views.py:991 windows/views.py:995 windows/views.py:2765 +#: windows/views.py:2770 msgid "MyMenuItem" msgstr "MiElementoMenu" -#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 +#: windows/views.py:998 windows/views.py:1924 windows/views.py:2777 msgid "MyMenu" msgstr "MiMenu" -#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 -#: windows/views.py:1402 +#: windows/views.py:1013 windows/views.py:1548 windows/views.py:1555 +#: windows/views.py:1562 msgid "MyLabel" msgstr "MiEtiqueta" -#: windows/views.py:972 +#: windows/views.py:1019 msgid "Databases" msgstr "Bases de datos" -#: windows/views.py:973 windows/views.py:1340 +#: windows/views.py:1020 windows/views.py:1367 msgid "Size" msgstr "Tamaño" -#: windows/views.py:974 +#: windows/views.py:1021 msgid "Elements" msgstr "Elementos" -#: windows/views.py:975 +#: windows/views.py:1022 msgid "Modified at" msgstr "Modificado en" -#: windows/views.py:976 +#: windows/views.py:1023 windows/views.py:1382 msgid "Tables" msgstr "Tablas" -#: windows/views.py:983 +#: windows/views.py:1030 msgid "System" msgstr "Sistema" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1029 -#: windows/views.py:1344 +#: windows/components/dataview.py:89 windows/views.py:1076 +#: windows/views.py:1371 msgid "Collation" msgstr "Intercalación" -#: windows/views.py:1058 +#: windows/views.py:1105 msgid "Encryption" -msgstr "" +msgstr "Cifrado" -#: windows/views.py:1070 +#: windows/main/controller.py:1272 windows/main/controller.py:1294 +#: windows/main/controller.py:1298 windows/views.py:1117 msgid "Read Only" -msgstr "" +msgstr "Solo lectura" -#: windows/views.py:1087 -#, fuzzy +#: windows/views.py:1134 msgid "Tablespace" -msgstr "Tablas" +msgstr "Tablespace" -#: windows/views.py:1108 -#, fuzzy +#: windows/views.py:1155 msgid "Connection limit" -msgstr "Conexión perdida" +msgstr "Límite de conexión" -#: windows/views.py:1151 -#, fuzzy +#: windows/views.py:1198 msgid "Profile" -msgstr "Archivo" +msgstr "Perfil" -#: windows/views.py:1177 -#, fuzzy +#: windows/views.py:1224 msgid "Default tablespace" -msgstr "Eliminar tabla" +msgstr "Tablespace predeterminado" -#: windows/views.py:1198 -#, fuzzy +#: windows/views.py:1245 msgid "Temporary tablespace" -msgstr "Temporal" +msgstr "Tablespace temporal" -#: windows/views.py:1224 +#: windows/views.py:1271 msgid "Quota" -msgstr "" +msgstr "Quota" -#: windows/views.py:1243 +#: windows/views.py:1290 msgid "Unlimited quota" -msgstr "" +msgstr "Cuota ilimitada" -#: windows/views.py:1260 +#: windows/views.py:1307 msgid "Account status" -msgstr "" +msgstr "Estado de cuenta" -#: windows/views.py:1281 -#, fuzzy +#: windows/views.py:1328 msgid "Password expire" -msgstr "Contraseña" +msgstr "Expiración de contraseña" -#: windows/views.py:1302 -msgid "Table:" -msgstr "Tabla:" +#: windows/views.py:1352 +msgid "Add new table" +msgstr "Agregar nueva tabla" -#: 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:1354 +msgid "Clone table" +msgstr "Clonar tabla" -#: windows/views.py:1315 -msgid "Clone" -msgstr "Clonar" +#: windows/main/controller.py:1821 windows/views.py:1356 +msgid "Delete table" +msgstr "Eliminar tabla" -#: windows/views.py:1339 +#: windows/views.py:1366 windows/views.py:2457 msgid "Rows" msgstr "Filas" -#: windows/views.py:1342 +#: windows/views.py:1369 msgid "Updated at" msgstr "Actualizado en" -#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 -#: windows/views.py:2173 windows/views.py:2709 +#: windows/views.py:1387 +msgid "Add new view" +msgstr "Agregar nueva vista" + +#: windows/views.py:1389 +msgid "Clone view" +msgstr "Clonar" + +#: windows/views.py:1391 +msgid "Delete view" +msgstr "Eliminar" + +#: windows/views.py:1399 windows/views.py:1423 windows/views.py:1447 +#: windows/views.py:1471 windows/views.py:1495 +msgid "Definition" +msgstr "Definición" + +#: windows/views.py:1406 +msgid "Views" +msgstr "Vistas" + +#: windows/views.py:1411 +msgid "Add new procedure" +msgstr "Agregar nuevo procedimiento" + +#: windows/views.py:1413 +msgid "Clone procedure" +msgstr "Clonar procedimiento" + +#: windows/views.py:1415 +msgid "Delete procedure" +msgstr "Eliminar procedimiento" + +#: windows/views.py:1430 +msgid "Procedures" +msgstr "Procedimientos" + +#: windows/views.py:1435 +msgid "Add new function" +msgstr "Agregar nueva función" + +#: windows/views.py:1437 +msgid "Clone function" +msgstr "Clonar función" + +#: windows/views.py:1439 +msgid "Delete function" +msgstr "Eliminar función" + +#: windows/views.py:1454 +msgid "Functions" +msgstr "Funciones" + +#: windows/views.py:1459 +msgid "Add new trigger" +msgstr "Agregar nuevo disparador" + +#: windows/views.py:1461 +msgid "Clone trigger" +msgstr "Clonar disparador" + +#: windows/views.py:1463 +msgid "Delete trigger" +msgstr "Eliminar disparador" + +#: windows/views.py:1478 +msgid "Triggers" +msgstr "Disparadores" + +#: windows/views.py:1483 +msgid "Add new event" +msgstr "Agregar nuevo evento" + +#: windows/views.py:1485 +msgid "Clone event" +msgstr "Clonar" + +#: windows/views.py:1487 +msgid "Delete event" +msgstr "Eliminar evento" + +#: windows/views.py:1502 +msgid "Events" +msgstr "Eventos" + +#: windows/views.py:1530 windows/views.py:1901 windows/views.py:2646 +#: windows/views.py:2730 msgid "Apply" msgstr "Aplicar" -#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 -#: windows/views.py:2909 +#: windows/views.py:1542 windows/views.py:1721 msgid "Options" msgstr "Opciones" -#: windows/views.py:1413 +#: windows/views.py:1573 msgid "Diagram" msgstr "Diagrama" -#: windows/views.py:1424 +#: windows/views.py:1584 msgid "Database" msgstr "Base de datos" -#: windows/views.py:1479 windows/views.py:2849 +#: windows/views.py:1639 msgid "Base" msgstr "Base" -#: windows/views.py:1493 windows/views.py:2863 +#: windows/views.py:1653 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1521 windows/views.py:2891 +#: windows/views.py:1681 msgid "Default Collation" msgstr "Intercalación predeterminada" -#: windows/views.py:1531 +#: windows/views.py:1691 msgid "Convert data" -msgstr "" +msgstr "Convertir datos" -#: windows/views.py:1539 +#: windows/views.py:1699 msgid "Row format" -msgstr "" +msgstr "Formato de fila" -#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 -#: windows/views.py:1756 windows/views.py:2081 +#: windows/views.py:1732 windows/views.py:1760 windows/views.py:1788 +#: windows/views.py:1876 windows/views.py:2326 windows/views.py:2636 msgid "Remove" msgstr "Eliminar" -#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 +#: windows/views.py:1734 windows/views.py:1762 windows/views.py:1790 +#: windows/views.py:2328 windows/views.py:2738 msgid "Clear" msgstr "Limpiar" -#: windows/views.py:1595 windows/views.py:2923 +#: windows/views.py:1747 msgid "Indexes" msgstr "Índices" -#: windows/views.py:1639 +#: windows/views.py:1758 windows/views.py:1786 windows/views.py:2324 +msgid "Insert" +msgstr "Insertar" + +#: windows/views.py:1775 msgid "Foreign Keys" msgstr "Claves foráneas" -#: windows/views.py:1683 +#: windows/views.py:1803 msgid "Checks" msgstr "Comprobaciones" -#: windows/views.py:1750 windows/views.py:2944 +#: windows/views.py:1870 msgid "Columns:" msgstr "Columnas:" -#: windows/views.py:1760 -#, fuzzy +#: windows/views.py:1880 msgid "Move Up" msgstr "Mover arriba\tCTRL+UP" -#: windows/views.py:1762 -#, fuzzy +#: windows/views.py:1882 msgid "Move Down" msgstr "Mover abajo\tCTRL+D" -#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 -#: windows/views.py:3017 +#: windows/views.py:1914 windows/views.py:1921 msgid "Add Index" msgstr "Agregar índice" -#: windows/views.py:1798 windows/views.py:3014 +#: windows/views.py:1918 msgid "Add PrimaryKey" msgstr "Agregar clave primaria" -#: windows/views.py:1815 +#: windows/views.py:1935 msgid "Table" msgstr "Tabla" -#: windows/views.py:1851 -#, fuzzy -msgid "Definer" -msgstr "Insertar" - -#: windows/views.py:1871 +#: windows/views.py:1977 windows/views.py:2243 msgid "Schema" -msgstr "" +msgstr "Schema" -#: windows/views.py:1897 -msgid "SQL security" -msgstr "" +#: windows/views.py:2005 windows/views.py:2314 +msgid "General" +msgstr "General" -#: windows/views.py:1904 -#, fuzzy -msgid "DEFINER" -msgstr "Insertar" - -#: windows/views.py:1904 -#, fuzzy -msgid "INVOKER" -msgstr "Insertar" - -#: windows/views.py:1916 +#: windows/views.py:2010 msgid "Algorithm" -msgstr "" +msgstr "Algoritmo" -#: windows/views.py:1918 -#, fuzzy +#: windows/views.py:2012 msgid "UNDEFINED" msgstr "Sin signo" -#: windows/views.py:1921 +#: windows/views.py:2015 msgid "MERGE" -msgstr "" +msgstr "MERGE" -#: windows/views.py:1924 -#, fuzzy +#: windows/views.py:2018 msgid "TEMPTABLE" -msgstr "Tabla" +msgstr "TEMPTABLE" -#: windows/views.py:1934 +#: windows/views.py:2028 msgid "View constraint" -msgstr "" +msgstr "Restricción de vista" -#: windows/views.py:1936 -#, fuzzy +#: windows/views.py:2030 msgid "None" -msgstr "Clonar" +msgstr "Ninguno" -#: windows/views.py:1939 -#, fuzzy +#: windows/views.py:2033 msgid "LOCAL" -msgstr "Localización" +msgstr "LOCAL" -#: windows/views.py:1942 -#, fuzzy +#: windows/views.py:2036 msgid "CASCADE" -msgstr "Cancelar" +msgstr "CASCADA" -#: windows/views.py:1945 -#, fuzzy +#: windows/views.py:2039 msgid "CHECK ONLY" -msgstr "Verificar" +msgstr "SOLO VERIFICAR" -#: windows/views.py:1948 +#: windows/views.py:2042 msgid "READ ONLY" -msgstr "" +msgstr "SOLO LECTURA" + +#: windows/views.py:2055 windows/views.py:2483 +msgid "Behavior" +msgstr "Comportamiento" -#: windows/views.py:1960 +#: windows/views.py:2062 windows/views.py:2490 +msgid "Definer" +msgstr "Definidor" + +#: windows/views.py:2070 windows/views.py:2498 +msgid "*" +msgstr "*" + +#: windows/views.py:2082 windows/views.py:2510 +msgid "SQL security" +msgstr "Seguridad SQL" + +#: windows/views.py:2089 windows/views.py:2517 +msgid "DEFINER" +msgstr "DEFINIDOR" + +#: windows/views.py:2089 windows/views.py:2517 +msgid "INVOKER" +msgstr "INVOCADOR" + +#: windows/views.py:2103 msgid "Force" -msgstr "" +msgstr "Forzar" -#: windows/views.py:1972 +#: windows/views.py:2115 msgid "Security barrier" -msgstr "" +msgstr "Barrera de seguridad" -#: windows/views.py:2054 -msgid "Views" -msgstr "Vistas" +#: windows/views.py:2128 windows/views.py:2532 +msgid "Security" +msgstr "Seguridad" -#: windows/views.py:2062 -msgid "Triggers" -msgstr "Disparadores" +#: windows/views.py:2205 +msgid "View" +msgstr "Vistas" -#: windows/views.py:2073 +#: windows/views.py:2262 #, fuzzy -msgid "Refrsh" -msgstr "Actualizar" +msgid "Type" +msgstr "Tipo de datos" -#: windows/views.py:2079 -#, fuzzy +#: windows/views.py:2269 +msgid "Procedure (doesn't return a result)" +msgstr "Procedimiento (no devuelve un resultado)" + +#: windows/views.py:2269 +msgid "Function (return a result)" +msgstr "Función (devuelve un resultado)" + +#: windows/views.py:2279 +msgid "Return type" +msgstr "Tipo de retorno" + +#: windows/views.py:2299 +msgid "Comment" +msgstr "Comentario" + +#: windows/components/dataview.py:111 windows/views.py:2335 +msgid "#" +msgstr "#" + +#: windows/views.py:2337 +msgid "Datatype" +msgstr "Tipo de dato" + +#: windows/views.py:2338 +msgid "Context" +msgstr "Contexto" + +#: windows/views.py:2345 +msgid "Parameters" +msgstr "Parámetros" + +#: windows/views.py:2354 +msgid "Data access" +msgstr "Acceso a datos" + +#: windows/views.py:2361 +msgid "CONTAINS SQL" +msgstr "CONTAINS SQL" + +#: windows/views.py:2361 +msgid "NO SQL" +msgstr "NO SQL" + +#: windows/views.py:2361 +msgid "READS SQL DATA" +msgstr "READS SQL DATA" + +#: windows/views.py:2361 +msgid "MODIFIES SQL DATA" +msgstr "MODIFIES SQL DATA" + +#: windows/views.py:2371 +msgid "Deterministic" +msgstr "Determinista" + +#: windows/views.py:2395 +msgid "SQL" +msgstr "SQL" + +#: windows/views.py:2395 +msgid "PLPGSQL" +msgstr "PLPGSQL" + +#: windows/views.py:2405 +msgid "Volatility" +msgstr "Volatilidad" + +#: windows/views.py:2412 +msgid "VOLATILE" +msgstr "VOLÁTIL" + +#: windows/views.py:2412 +msgid "STABLE" +msgstr "ESTABLE" + +#: windows/views.py:2412 +msgid "IMMUTABLE" +msgstr "INMUTABLE" + +#: windows/views.py:2422 +msgid "Parallel" +msgstr "Paralelo" + +#: windows/views.py:2429 +msgid "UNSAFE" +msgstr "INSEGURO" + +#: windows/views.py:2429 +msgid "RESTRICTED" +msgstr "RESTRINGIDO" + +#: windows/views.py:2429 +msgid "SAFE" +msgstr "SEGURO" + +#: windows/views.py:2441 +msgid "Cost" +msgstr "Costo" + +#: windows/views.py:2609 +msgid "Routine" +msgstr "Rutina" + +#: windows/views.py:2617 +msgid "Trigger" +msgstr "Disparador" + +#: windows/views.py:2634 msgid "Duplicate" -msgstr "Duplicar registro" +msgstr "Duplicar" -#: windows/views.py:2085 +#: windows/views.py:2640 msgid "Apply changes automatically" msgstr "Aplicar cambios automáticamente" -#: windows/views.py:2087 windows/views.py:2088 +#: windows/views.py:2642 windows/views.py:2643 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -699,125 +895,87 @@ msgstr "" "Si está habilitado, las ediciones de la tabla se aplican inmediatamente " "sin presionar Aplicar o Cancelar" -#: windows/views.py:2101 +#: windows/views.py:2656 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" +msgstr "{database_name}.{table_name} - filas {from_row} - {to_row} de {total_rows}" -#: windows/views.py:2109 -#, fuzzy +#: windows/views.py:2664 msgid "First" -msgstr "Filtros" +msgstr "Primero" -#: windows/views.py:2127 +#: windows/views.py:2682 msgid "Last" -msgstr "" +msgstr "Último" -#: windows/views.py:2136 +#: windows/views.py:2691 msgid "Filters" msgstr "Filtros" -#: windows/views.py:2176 +#: windows/views.py:2733 +msgid "" +"Apply filters in data\n" +"CTRL+ENTER" +msgstr "" +"Aplicar filtros en datos\n" +"CTRL+ENTER" + +#: windows/views.py:2734 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2196 +#: windows/views.py:2762 msgid "Insert row" msgstr "Insertar fila" -#: windows/views.py:2204 +#: windows/components/popup.py:31 windows/views.py:2774 +msgid "NULL" +msgstr "NULL" + +#: windows/views.py:2781 msgid "Data" msgstr "Datos" -#: windows/main/controller.py:278 windows/main/controller.py:287 -#: windows/main/controller.py:288 windows/views.py:2221 -#, fuzzy +#: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" -msgstr "Consulta" +msgstr "Nueva consulta" -#: windows/views.py:2223 windows/views.py:2660 +#: windows/views.py:2800 msgid "Close" msgstr "Cerrar" -#: windows/main/controller.py:279 windows/main/controller.py:289 -#: windows/main/controller.py:290 windows/views.py:2223 -#, fuzzy +#: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" -msgstr "Consulta" +msgstr "Cerrar consulta" -#: windows/views.py:2227 +#: windows/views.py:2804 msgid "Run" -msgstr "" +msgstr "Ejecutar" -#: windows/main/controller.py:280 windows/main/controller.py:292 -#: windows/main/controller.py:293 windows/views.py:2227 -#, fuzzy +#: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" -msgstr "Ejecutable SSH" +msgstr "Ejecutar" -#: windows/views.py:2229 +#: windows/views.py:2806 msgid "Run all" -msgstr "" +msgstr "Ejecutar todo" -#: windows/main/controller.py:295 windows/views.py:2229 +#: windows/views.py:2806 msgid "Execute all statements" -msgstr "" +msgstr "Ejecutar todas las declaraciones" -#: windows/main/controller.py:282 windows/main/controller.py:297 -#: windows/main/controller.py:298 windows/views.py:2231 +#: windows/main/controller.py:323 windows/views.py:2808 msgid "Stop" -msgstr "" +msgstr "Detener" -#: windows/views.py:2287 +#: windows/views.py:2873 msgid "a page" -msgstr "" +msgstr "una página" -#: windows/views.py:2315 +#: windows/views.py:2923 msgid "Query" msgstr "Consulta" -#: windows/views.py:2626 -#, fuzzy -msgid "Character set" -msgstr "Creado en" - -#: windows/views.py:2644 windows/views.py:2663 -msgid "New" -msgstr "Nuevo" - -#: windows/views.py:2683 -msgid "Insert record" -msgstr "Insertar registro" - -#: windows/views.py:2688 -msgid "Duplicate record" -msgstr "Duplicar registro" - -#: windows/views.py:2695 -msgid "Delete record" -msgstr "Eliminar registro" - -#: windows/views.py:2733 windows/views.py:2964 -msgid "Up" -msgstr "Arriba" - -#: windows/views.py:2740 windows/views.py:2971 -msgid "Down" -msgstr "Abajo" - -#: windows/views.py:3100 -msgid "Save Starments" -msgstr "" - -#: windows/views.py:3108 -#, fuzzy -msgid "Location" -msgstr "Intercalación" - -#: windows/views.py:3115 -msgid "*.sql" -msgstr "" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 msgid "Allow NULL" @@ -838,7 +996,7 @@ 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/components/dataview.py:85 windows/components/dataview.py:256 msgid "Expression" msgstr "Expresión" @@ -850,75 +1008,71 @@ msgstr "Sin signo" msgid "Zerofill" msgstr "Relleno cero" -#: windows/components/dataview.py:109 -msgid "#" -msgstr "#" - -#: windows/components/dataview.py:117 +#: windows/components/dataview.py:119 msgid "Data type" msgstr "Tipo de datos" -#: windows/components/dataview.py:121 +#: windows/components/dataview.py:123 msgid "Length/Set" msgstr "Longitud/Conjunto" -#: windows/components/dataview.py:155 +#: windows/components/dataview.py:169 msgid "Add column\tCTRL+INS" msgstr "Agregar columna\tCTRL+INS" -#: windows/components/dataview.py:161 +#: windows/components/dataview.py:175 msgid "Remove column\tCTRL+DEL" msgstr "Eliminar columna\tCTRL+DEL" -#: windows/components/dataview.py:169 +#: windows/components/dataview.py:183 msgid "Move up\tCTRL+UP" msgstr "Mover arriba\tCTRL+UP" -#: windows/components/dataview.py:176 +#: windows/components/dataview.py:190 msgid "Move down\tCTRL+D" msgstr "Mover abajo\tCTRL+D" -#: windows/components/dataview.py:199 +#: windows/components/dataview.py:214 msgid "Create new index" msgstr "Crear nuevo índice" -#: windows/components/dataview.py:214 +#: windows/components/dataview.py:229 msgid "Append to index" msgstr "Agregar al índice" -#: windows/components/dataview.py:228 +#: windows/components/dataview.py:243 msgid "Column(s)/Expression" msgstr "Columna(s)/Expresión" -#: windows/components/dataview.py:229 +#: windows/components/dataview.py:244 msgid "Condition" msgstr "Condición" -#: windows/components/dataview.py:259 +#: windows/components/dataview.py:274 msgid "Column(s)" msgstr "Columna(s)" -#: windows/components/dataview.py:265 +#: windows/components/dataview.py:280 msgid "Reference table" msgstr "Tabla de referencia" -#: windows/components/dataview.py:271 +#: windows/components/dataview.py:286 msgid "Reference column(s)" msgstr "Columna(s) de referencia" -#: windows/components/dataview.py:277 +#: windows/components/dataview.py:292 msgid "On UPDATE" msgstr "En UPDATE" -#: windows/components/dataview.py:283 +#: windows/components/dataview.py:298 msgid "On DELETE" msgstr "En DELETE" -#: windows/components/dataview.py:298 +#: windows/components/dataview.py:313 msgid "Add foreign key" msgstr "Agregar clave foránea" -#: windows/components/dataview.py:304 +#: windows/components/dataview.py:319 msgid "Remove foreign key" msgstr "Eliminar clave foránea" @@ -926,10 +1080,6 @@ msgstr "Eliminar clave foránea" msgid "No default value" msgstr "Sin valor predeterminado" -#: windows/components/popup.py:31 -msgid "NULL" -msgstr "NULL" - #: windows/components/popup.py:35 msgid "AUTO INCREMENT" msgstr "AUTO INCREMENTO" @@ -938,164 +1088,175 @@ msgstr "AUTO INCREMENTO" msgid "Text/Expression" msgstr "Texto/Expresión" -#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 +#: windows/dialogs/connections/view.py:126 windows/main/query/renderer.py:192 msgid "Unknown error" -msgstr "" +msgstr "Error desconocido" -#: windows/dialogs/connections/view.py:414 +#: windows/dialogs/connections/view.py:416 msgid "Connection established successfully" -msgstr "" +msgstr "Conexión establecida exitosamente" -#: windows/dialogs/connections/view.py:426 +#: windows/dialogs/connections/view.py:428 #, python-brace-format msgid "Do you want save the connection {connection_name}?" -msgstr "" +msgstr "¿Desea guardar la conexión {connection_name}?" -#: windows/dialogs/connections/view.py:429 +#: windows/dialogs/connections/view.py:431 msgid "Confirm save" msgstr "Confirmar guardar" -#: windows/dialogs/connections/view.py:481 +#: windows/dialogs/connections/view.py:483 msgid "You have unsaved changes. Do you want to save them before continuing?" -msgstr "" +msgstr "Tiene cambios sin guardar. ¿Desea guardarlos antes de continuar?" -#: windows/dialogs/connections/view.py:483 +#: windows/dialogs/connections/view.py:485 msgid "Unsaved changes" -msgstr "" +msgstr "Cambios sin guardar" -#: windows/dialogs/connections/view.py:750 +#: windows/dialogs/connections/view.py:773 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" +"Esta conexión no puede funcionar sin TLS. TLS ha sido habilitado " +"automáticamente." -#: windows/dialogs/connections/view.py:775 -#, fuzzy, python-brace-format +#: windows/dialogs/connections/view.py:798 +#, python-brace-format msgid "" "Connection error:\n" "{error}" -msgstr "Error de conexión" +msgstr "" +"Error de conexión:\n" +"{error}" -#: windows/dialogs/connections/view.py:776 +#: windows/dialogs/connections/view.py:799 msgid "Connection error" msgstr "Error de conexión" -#: windows/dialogs/connections/view.py:802 -#, fuzzy, python-brace-format +#: windows/dialogs/connections/view.py:825 +#, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" -msgstr "¿Quieres eliminar los registros?" +msgstr "¿Desea eliminar la conexión '{connection_name}'?" -#: windows/dialogs/connections/view.py:805 -#: windows/dialogs/connections/view.py:822 +#: windows/dialogs/connections/view.py:828 +#: windows/dialogs/connections/view.py:845 msgid "Confirm delete" msgstr "Confirmar eliminar" -#: windows/dialogs/connections/view.py:819 -#, fuzzy, python-brace-format +#: windows/dialogs/connections/view.py:842 +#, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" -msgstr "¿Quieres eliminar los registros?" +msgstr "¿Desea eliminar el directorio '{directory_name}'?" -#: windows/main/controller.py:275 +#: windows/main/controller.py:316 #, python-brace-format msgid "{text} ({shortcut})" -msgstr "" +msgstr "{text} ({shortcut})" -#: windows/main/controller.py:281 windows/main/controller.py:294 -#, fuzzy +#: windows/main/controller.py:322 msgid "Execute all" -msgstr "Ejecutable SSH" +msgstr "Ejecutar todo" -#: windows/main/controller.py:471 windows/main/controller.py:479 -#, fuzzy +#: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" -msgstr "Consulta" +msgstr "Consulta (1)" -#: windows/main/controller.py:497 +#: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" -msgstr "" +msgstr "Consulta ({query_number})" -#: windows/main/controller.py:530 +#: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" -msgstr "" +msgstr "Tiene cambios sin guardar. ¿Guardar antes de cerrar?" -#: windows/main/controller.py:531 +#: windows/main/controller.py:519 msgid "Unsaved query" -msgstr "" +msgstr "Consulta sin guardar" -#: windows/main/controller.py:576 -#, fuzzy +#: windows/main/controller.py:564 msgid "Save query" -msgstr "Consulta" +msgstr "Guardar consulta" -#: windows/main/controller.py:579 +#: windows/main/controller.py:567 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" -msgstr "" +msgstr "Archivos SQL (*.sql)|*.sql|Todos los archivos (*.*)|*.*" -#: 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 +#: windows/main/controller.py:595 windows/main/controller.py:626 +#: windows/main/controller.py:653 windows/main/database/list.py:119 +#: windows/main/database/routine.py:591 windows/main/database/routine.py:635 +#: windows/main/database/view.py:268 windows/main/database/view.py:297 +#: windows/main/query/controller.py:179 msgid "Error" msgstr "Error" -#: windows/main/controller.py:622 +#: windows/main/controller.py:633 #, python-brace-format msgid "-- Saved query to {file_path}" -msgstr "" +msgstr "-- Consulta guardada en {file_path}" -#: windows/main/controller.py:647 +#: windows/main/controller.py:659 #, python-brace-format msgid "-- Autosaved query to {file_path}" -msgstr "" +msgstr "-- Consulta autoguardada en {file_path}" -#: windows/main/controller.py:704 +#: windows/main/controller.py:725 msgid "days" msgstr "días" -#: windows/main/controller.py:705 +#: windows/main/controller.py:726 msgid "hours" msgstr "horas" -#: windows/main/controller.py:706 +#: windows/main/controller.py:727 msgid "minutes" msgstr "minutos" -#: windows/main/controller.py:707 +#: windows/main/controller.py:728 msgid "seconds" msgstr "segundos" -#: windows/main/controller.py:715 +#: windows/main/controller.py:736 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizada: {used} ({percentage:.2%})" -#: windows/main/controller.py:751 +#: windows/main/controller.py:772 msgid "Settings saved successfully" -msgstr "" +msgstr "Configuraciones guardadas exitosamente" -#: windows/main/controller.py:952 +#: windows/main/controller.py:1006 #, python-brace-format msgid "~{estimated} (Loading...)" -msgstr "" +msgstr "~{estimated} (Cargando...)" -#: windows/main/controller.py:954 +#: windows/main/controller.py:1008 msgid "~ (Loading...)" -msgstr "" +msgstr "~ (Cargando...)" + +#: windows/main/controller.py:1243 +msgid "Write Mode (2:00)" +msgstr "Modo escritura (2:00)" + +#: windows/main/controller.py:1294 +msgid "Write Mode" +msgstr "Modo escritura" -#: windows/main/controller.py:1119 +#: windows/main/controller.py:1305 msgid "Version" msgstr "Versión" -#: windows/main/controller.py:1121 +#: windows/main/controller.py:1307 msgid "Uptime" msgstr "Tiempo de actividad" -#: windows/main/controller.py:1199 +#: windows/main/controller.py:1444 #, python-brace-format msgid "Do you want discard the change to {database_name}?" -msgstr "" +msgstr "¿Desea descartar el cambio a {database_name}?" -#: windows/main/controller.py:1232 +#: windows/main/controller.py:1477 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1104,120 +1265,189 @@ msgid "" "- Yes: open dump flow (coming soon, no drop).\n" "- No: drop the database now." msgstr "" +"¿Desea crear un volcado antes de eliminar la base de datos " +"'{database_name}'?\n" +"\n" +"El volcado no está implementado aún.\n" +"- Sí: abrir flujo de volcado (próximamente, sin eliminación).\n" +"- No: eliminar la base de datos ahora." -#: windows/main/controller.py:1237 windows/main/controller.py:1258 -#, fuzzy +#: windows/main/controller.py:1482 windows/main/controller.py:1503 msgid "Delete database" -msgstr "Eliminar tabla" +msgstr "Eliminar base de datos" -#: windows/main/controller.py:1243 +#: windows/main/controller.py:1488 msgid "Dump is not implemented yet. No action has been performed." -msgstr "" +msgstr "El volcado no está implementado aún. No se ha realizado ninguna acción." -#: windows/main/controller.py:1244 +#: windows/main/controller.py:1489 msgid "Dump not available" -msgstr "" +msgstr "Volcado no disponible" -#: windows/main/controller.py:1257 +#: windows/main/controller.py:1502 msgid "Database deletion is not supported by this engine." -msgstr "" +msgstr "La eliminación de bases de datos no es compatible con este motor." -#: windows/main/controller.py:1272 +#: windows/main/controller.py:1517 msgid "Database deleted successfully" -msgstr "" +msgstr "Base de datos eliminada exitosamente" -#: windows/main/controller.py:1273 windows/main/database/view.py:253 -#: windows/main/database/view.py:279 +#: windows/main/controller.py:1518 windows/main/database/routine.py:556 +#: windows/main/database/routine.py:620 windows/main/database/view.py:256 +#: windows/main/database/view.py:291 msgid "Success" -msgstr "" +msgstr "Éxito" -#: windows/main/controller.py:1392 +#: windows/main/controller.py:1792 #, python-brace-format msgid "Do you want discard the change to {table_name}?" -msgstr "" +msgstr "¿Desea descartar el cambio a {table_name}?" -#: windows/main/controller.py:1418 -#, fuzzy, python-brace-format +#: windows/main/controller.py:1818 +#, python-brace-format msgid "Do you want delete the table {table_name}?" -msgstr "¿Quieres eliminar los registros?" +msgstr "¿Desea eliminar la tabla {table_name}?" -#: windows/main/controller.py:1421 -msgid "Delete table" -msgstr "Eliminar tabla" - -#: windows/main/controller.py:1440 +#: windows/main/controller.py:1840 #, python-brace-format msgid "{table_name} (COPY)" -msgstr "" +msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1563 +#: windows/main/controller.py:1999 msgid "Do you want delete the records?" msgstr "¿Quieres eliminar los registros?" -#: windows/main/database/list.py:69 +#: windows/main/controller.py:2104 +#, fuzzy +msgid "Database connection lost. Do you want to reconnect?" +msgstr "¿Quieres reconectar?" + +#: windows/main/controller.py:2105 windows/main/database/list.py:108 +msgid "Connection lost" +msgstr "Conexión perdida" + +#: windows/main/controller.py:2113 +#, fuzzy +msgid "Connection restored successfully." +msgstr "Conexión establecida exitosamente" + +#: windows/main/controller.py:2114 +#, fuzzy +msgid "Connection restored" +msgstr "Error de conexión" + +#: windows/main/controller.py:2120 +#, python-brace-format +msgid "Could not reconnect: {error}" +msgstr "" + +#: windows/main/controller.py:2121 +#, fuzzy +msgid "Reconnection failed" +msgstr "Reconexión fallida:" + +#: windows/main/database/list.py:104 msgid "The connection to the database was lost." msgstr "Se perdió la conexión a la base de datos." -#: windows/main/database/list.py:71 +#: windows/main/database/list.py:106 msgid "Do you want to reconnect?" msgstr "¿Quieres reconectar?" -#: windows/main/database/list.py:73 -msgid "Connection lost" -msgstr "Conexión perdida" - -#: windows/main/database/list.py:83 +#: windows/main/database/list.py:118 msgid "Reconnection failed:" msgstr "Reconexión fallida:" -#: windows/main/database/view.py:252 +#: windows/main/database/routine.py:545 +msgid "Function created successfully" +msgstr "Función creada exitosamente" + +#: windows/main/database/routine.py:547 +msgid "Function updated successfully" +msgstr "Función actualizada exitosamente" + +#: windows/main/database/routine.py:551 +msgid "Procedure created successfully" +msgstr "Procedimiento creado exitosamente" + +#: windows/main/database/routine.py:553 +msgid "Procedure updated successfully" +msgstr "Procedimiento actualizado exitosamente" + +#: windows/main/database/routine.py:590 +#, python-brace-format +msgid "Error saving routine: {}" +msgstr "Error al guardar rutina: {}" + +#: windows/main/database/routine.py:604 +msgid "Function" +msgstr "Función" + +#: windows/main/database/routine.py:604 +msgid "Procedure" +msgstr "Procedimiento" + +#: windows/main/database/routine.py:607 +#, python-brace-format +msgid "Are you sure you want to delete {} '{}'?" +msgstr "¿Está seguro de que desea eliminar {} '{}'?" + +#: windows/main/database/routine.py:610 windows/main/database/view.py:282 +msgid "Confirm Delete" +msgstr "Confirmar eliminar" + +#: windows/main/database/routine.py:619 +#, python-brace-format +msgid "{} deleted successfully" +msgstr "{} eliminado exitosamente" + +#: windows/main/database/routine.py:634 +#, python-brace-format +msgid "Error deleting routine: {}" +msgstr "Error al eliminar rutina: {}" + +#: windows/main/database/view.py:255 msgid "View created successfully" -msgstr "" +msgstr "Vista creada exitosamente" -#: windows/main/database/view.py:252 +#: windows/main/database/view.py:255 msgid "View updated successfully" -msgstr "" +msgstr "Vista actualizada exitosamente" -#: windows/main/database/view.py:256 +#: windows/main/database/view.py:268 #, python-brace-format msgid "Error saving view: {}" -msgstr "" +msgstr "Error al guardar vista: {}" -#: windows/main/database/view.py:269 +#: windows/main/database/view.py:281 #, python-brace-format msgid "Are you sure you want to delete view '{}'?" -msgstr "" - -#: windows/main/database/view.py:270 -#, fuzzy -msgid "Confirm Delete" -msgstr "Confirmar eliminar" +msgstr "¿Está seguro de que desea eliminar la vista '{}'?" -#: windows/main/database/view.py:279 +#: windows/main/database/view.py:291 msgid "View deleted successfully" -msgstr "" +msgstr "Vista eliminada exitosamente" -#: windows/main/database/view.py:282 +#: windows/main/database/view.py:297 #, python-brace-format msgid "Error deleting view: {}" -msgstr "" +msgstr "Error al eliminar vista: {}" -#: windows/main/query/controller.py:110 +#: windows/main/query/controller.py:112 #, python-brace-format msgid "{elapsed_ms:.0f} ms" -msgstr "" +msgstr "{elapsed_ms:.0f} ms" -#: windows/main/query/controller.py:112 +#: windows/main/query/controller.py:114 #, python-brace-format msgid "{elapsed_s:.2f} s" -msgstr "" +msgstr "{elapsed_s:.2f} s" -#: windows/main/query/controller.py:115 -#, fuzzy +#: windows/main/query/controller.py:117 msgid "none" -msgstr "Clonar" +msgstr "ninguno" -#: windows/main/query/controller.py:121 +#: windows/main/query/controller.py:123 #, python-brace-format msgid "" "Query execution stopped after {elapsed}.\n" @@ -1226,188 +1456,68 @@ msgid "" "Failed: {failed}.\n" "Last statement: #{last}." msgstr "" +"Ejecución de consulta detenida después de {elapsed}.\n" +"Declaraciones completadas: {completed}/{total}.\n" +"Exitosas: {success}.\n" +"Fallidas: {failed}.\n" +"Última declaración: #{last}." -#: windows/main/query/controller.py:134 +#: windows/main/query/controller.py:136 msgid "Query execution cancelled" -msgstr "" +msgstr "Ejecución de consulta cancelada" -#: windows/main/query/controller.py:176 -#, fuzzy +#: windows/main/query/controller.py:178 msgid "No active database connection" -msgstr "Nueva conexión" +msgstr "Sin conexión de base de datos activa" + +#: windows/main/query/controller.py:227 +msgid "Database connection lost" +msgstr "Conexión de base de datos perdida" + +#: windows/main/query/history.py:55 +msgid "(empty query)" +msgstr "(consulta vacía)" #: windows/main/query/renderer.py:53 #, python-brace-format msgid "{affected_rows} rows affected" -msgstr "" +msgstr "{affected_rows} filas afectadas" #: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 #, python-brace-format msgid "Query {query_number}" -msgstr "" +msgstr "Consulta {query_number}" #: windows/main/query/renderer.py:65 #, python-brace-format msgid "Query {query_number} (Error)" -msgstr "" +msgstr "Consulta {query_number} (Error)" #: windows/main/query/renderer.py:79 #, python-brace-format msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" -msgstr "" +msgstr "Consulta {query_number} ({rows_count} filas × {columns_count} columnas)" #: windows/main/query/renderer.py:165 #, python-brace-format msgid "{rows_count} rows" -msgstr "" +msgstr "{rows_count} filas" #: windows/main/query/renderer.py:167 #, python-brace-format msgid "{elapsed_ms:.1f} ms" -msgstr "" +msgstr "{elapsed_ms:.1f} ms" #: windows/main/query/renderer.py:171 #, python-brace-format msgid "{warnings_count} warnings" -msgstr "" +msgstr "{warnings_count} advertencias" #: windows/main/query/renderer.py:186 -#, fuzzy msgid "Error:" msgstr "Error" -#~ msgid "Created at:" -#~ msgstr "" - -#~ msgid "Last connection:" -#~ msgstr "" - -#~ msgid "Successful connections:" -#~ msgstr "" - -#~ msgid "Unsuccessful connections:" -#~ msgstr "" - -#~ msgid "Session Manager" -#~ msgstr "" - -#~ msgid "Session name" -#~ msgstr "" - -#~ msgid "Connection type" -#~ msgstr "" - -#~ msgid "Open" -#~ msgstr "" - -#~ msgid "Open session manager" -#~ msgstr "" - -#~ msgid "Foreign Key" -#~ msgstr "" - -#~ msgid "New Session" -#~ msgstr "Nueva sesión" - -#~ 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" +#: windows/main/table/records.py:334 +msgid "Error saving records" +msgstr "Error al guardar registros" diff --git a/locale/fr_FR/LC_MESSAGES/petersql.mo b/locale/fr_FR/LC_MESSAGES/petersql.mo index b40cd26..e2c0964 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 5ba12c3..90b296b 100644 --- a/locale/fr_FR/LC_MESSAGES/petersql.po +++ b/locale/fr_FR/LC_MESSAGES/petersql.po @@ -2,12 +2,11 @@ # Copyright (C) 2026 ORGANIZATION # This file is distributed under the same license as the PeterSQL project. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-23 10:07+0100\n" +"POT-Creation-Date: 2026-06-06 11:23+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: fr_FR\n" @@ -47,649 +46,846 @@ msgstr "To" msgid "OpenSSH client not found." msgstr "Client OpenSSH introuvable." -#: structures/engines/mariadb/context.py:611 -#: structures/engines/mysql/context.py:622 -#: structures/engines/postgresql/context.py:645 -#: structures/engines/sqlite/context.py:524 +#: structures/engines/context.py:567 +msgid "This connection is read-only." +msgstr "Cette connexion est en lecture seule." + +#: structures/engines/context.py:578 +#, fuzzy, python-brace-format +msgid "Database connection lost: {error}" +msgstr "" +"Erreur de connexion:\n" +"{error}" + +#: structures/engines/mariadb/context.py:658 +#: structures/engines/mysql/context.py:670 +#: structures/engines/postgresql/context.py:701 +#: structures/engines/sqlite/context.py:564 #, python-brace-format msgid "Table{table_index:03}" -msgstr "" +msgstr "Table{table_index:03}" -#: structures/engines/mariadb/context.py:639 -#: structures/engines/mysql/context.py:650 -#: structures/engines/postgresql/context.py:670 -#: structures/engines/sqlite/context.py:548 +#: structures/engines/mariadb/context.py:686 +#: structures/engines/mysql/context.py:698 +#: structures/engines/postgresql/context.py:726 +#: structures/engines/sqlite/context.py:588 #, python-brace-format msgid "Column{column_index:03}" -msgstr "" +msgstr "Column{column_index:03}" -#: structures/engines/mariadb/context.py:657 -#: structures/engines/mysql/context.py:668 -#: structures/engines/postgresql/context.py:688 -#: structures/engines/sqlite/context.py:566 +#: structures/engines/mariadb/context.py:704 +#: structures/engines/mysql/context.py:716 +#: structures/engines/postgresql/context.py:744 +#: structures/engines/sqlite/context.py:606 #, python-brace-format msgid "Index{index_number:03}" -msgstr "" +msgstr "Index{index_number:03}" -#: structures/engines/mariadb/context.py:697 -#: structures/engines/mysql/context.py:706 -#: structures/engines/postgresql/context.py:728 -#: structures/engines/sqlite/context.py:608 +#: structures/engines/mariadb/context.py:744 +#: structures/engines/mysql/context.py:754 +#: structures/engines/postgresql/context.py:784 +#: structures/engines/sqlite/context.py:646 #, python-brace-format msgid "ForeignKey{foreign_key_number:03}" -msgstr "" +msgstr "ForeignKey{foreign_key_number:03}" -#: structures/engines/mariadb/context.py:730 -#: structures/engines/mysql/context.py:737 -#: structures/engines/postgresql/context.py:758 -#: structures/engines/sqlite/context.py:636 +#: structures/engines/mariadb/context.py:777 +#: structures/engines/mysql/context.py:785 +#: structures/engines/postgresql/context.py:814 +#: structures/engines/sqlite/context.py:674 #, python-brace-format msgid "View{view_index:03}" -msgstr "" +msgstr "View{view_index:03}" -#: structures/engines/mariadb/context.py:781 +#: structures/engines/mariadb/context.py:830 #, python-brace-format msgid "Trigger{trigger_index:03}" -msgstr "" +msgstr "Trigger{trigger_index:03}" -#: windows/dialogs/connections/view.py:415 -#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 -#: windows/views.py:33 +#: windows/dialogs/connections/view.py:417 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1303 +#: windows/views.py:62 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:1006 -#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 -#: windows/views.py:2821 +#: windows/components/dataview.py:115 windows/components/dataview.py:240 +#: windows/components/dataview.py:253 windows/components/dataview.py:268 +#: windows/views.py:76 windows/views.py:126 windows/views.py:1053 +#: windows/views.py:1365 windows/views.py:1398 windows/views.py:1422 +#: windows/views.py:1446 windows/views.py:1470 windows/views.py:1494 +#: windows/views.py:1611 windows/views.py:1957 windows/views.py:2228 +#: windows/views.py:2336 msgid "Name" msgstr "Nom" -#: windows/views.py:48 windows/views.py:428 +#: windows/views.py:77 windows/views.py:467 msgid "Last connection" msgstr "Dernière connexion" -#: windows/dialogs/connections/view.py:653 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:90 msgid "New directory" msgstr "Nouveau répertoire" -#: windows/dialogs/connections/model.py:206 -#: windows/dialogs/connections/view.py:613 windows/views.py:65 +#: windows/dialogs/connections/model.py:212 +#: windows/dialogs/connections/view.py:615 windows/views.py:94 msgid "New connection" msgstr "Nouvelle connexion" -#: windows/views.py:71 -#, fuzzy +#: windows/views.py:100 msgid "Rename" -msgstr "Nom" +msgstr "Renommer" -#: windows/views.py:76 -#, fuzzy +#: windows/views.py:105 msgid "Clone connection" -msgstr "Nouvelle connexion" +msgstr "Cloner la connexion" -#: 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 +#: windows/views.py:110 windows/views.py:642 windows/views.py:1525 +#: windows/views.py:1893 windows/views.py:2183 windows/views.py:2587 msgid "Delete" msgstr "Supprimer" -#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 -#: windows/views.py:2876 +#: windows/views.py:140 windows/views.py:1370 windows/views.py:1666 msgid "Engine" msgstr "Moteur" -#: windows/views.py:132 +#: windows/views.py:161 msgid "Host + port" msgstr "Hôte + port" -#: windows/views.py:148 +#: windows/views.py:177 msgid "Username" msgstr "Nom d'utilisateur" -#: windows/views.py:161 windows/views.py:1132 +#: windows/views.py:190 windows/views.py:1179 msgid "Password" msgstr "Mot de passe" -#: windows/views.py:174 -#, fuzzy +#: windows/views.py:203 msgid "Connection timeout" -msgstr "Connexion perdue" +msgstr "Délai de connexion" -#: windows/views.py:192 +#: windows/views.py:221 msgid "Use TLS" -msgstr "" +msgstr "Utiliser TLS" -#: windows/views.py:203 +#: windows/views.py:232 +msgid "Mark read only" +msgstr "Marquer en lecture seule" + +#: windows/views.py:243 msgid "Use SSH tunnel" msgstr "Utiliser un tunnel SSH" -#: windows/views.py:214 +#: windows/views.py:254 msgid "Compressed client/server protocol" -msgstr "" +msgstr "Protocole client/serveur compressé" -#: windows/views.py:233 +#: windows/views.py:273 msgid "Filename" msgstr "Nom de fichier" -#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 +#: windows/views.py:278 windows/views.py:397 msgid "Select a file" msgstr "Sélectionner un fichier" -#: windows/views.py:238 windows/views.py:358 -#, fuzzy +#: windows/views.py:278 windows/views.py:397 msgid "*.*" -msgstr "*. *" +msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 -#: windows/views.py:2834 +#: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 msgid "Comments" msgstr "Commentaires" -#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 -#: windows/views.py:884 +#: windows/main/controller.py:772 windows/views.py:309 windows/views.py:769 +#: windows/views.py:923 msgid "Settings" msgstr "Paramètres" -#: windows/views.py:278 +#: windows/views.py:317 msgid "SSH executable" msgstr "Exécutable SSH" -#: windows/views.py:283 +#: windows/views.py:322 msgid "ssh" msgstr "ssh" -#: windows/views.py:291 +#: windows/views.py:330 msgid "SSH host + port" msgstr "Hôte SSH + port" -#: windows/views.py:303 +#: windows/views.py:342 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" -msgstr "" +msgstr "SSH host + port (the SSH server that forwards traffic to the DB)" -#: windows/views.py:312 +#: windows/views.py:351 msgid "SSH username" msgstr "Nom d'utilisateur SSH" -#: windows/views.py:325 +#: windows/views.py:364 msgid "SSH password" msgstr "Mot de passe SSH" -#: windows/views.py:338 +#: windows/views.py:377 msgid "Local port" msgstr "Port local" -#: windows/views.py:344 +#: windows/views.py:383 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:353 +#: windows/views.py:392 msgid "Identity file" -msgstr "" +msgstr "Identity file" -#: windows/views.py:369 -#, fuzzy +#: windows/views.py:408 msgid "Remote host + port" -msgstr "Hôte + port" +msgstr "Hôte distant + port" -#: windows/views.py:381 +#: windows/views.py:420 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." -msgstr "" +msgstr "Remote host/port is the real DB target (defaults to DB Host/Port)." -#: windows/views.py:390 +#: windows/views.py:429 msgid "SSH extra args" -msgstr "" +msgstr "Arguments SSH supplémentaires" -#: windows/views.py:405 +#: windows/views.py:444 msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:411 windows/views.py:1341 +#: windows/views.py:450 windows/views.py:1368 msgid "Created at" msgstr "Créé le" -#: windows/views.py:445 +#: windows/views.py:484 msgid "Successful connections" msgstr "Connexions réussies" -#: windows/views.py:462 -#, fuzzy +#: windows/views.py:501 msgid "Last successful connection" -msgstr "Connexions réussies" +msgstr "Dernière connexion réussie" -#: windows/views.py:479 +#: windows/views.py:518 msgid "Unsuccessful connections" msgstr "Connexions échouées" -#: windows/views.py:496 +#: windows/views.py:535 msgid "Last failure reason" -msgstr "" +msgstr "Dernière raison de l'échec" -#: windows/views.py:513 -#, fuzzy +#: windows/views.py:552 msgid "Total connection attempts" -msgstr "Dernière connexion" +msgstr "Total des tentatives de connexion" -#: windows/views.py:530 -#, fuzzy +#: windows/views.py:569 msgid "Average connection time (ms)" -msgstr "Échec de la reconnexion :" +msgstr "Temps moyen de connexion (ms)" -#: windows/views.py:547 -#, fuzzy +#: windows/views.py:586 msgid "Most recent connection duration" -msgstr "Ouvrir le gestionnaire de connexions" +msgstr "Durée de la connexion la plus récente" -#: windows/views.py:566 +#: windows/views.py:605 msgid "Statistics" msgstr "Statistiques" -#: windows/views.py:584 windows/views.py:1730 +#: windows/views.py:623 windows/views.py:1850 msgid "Create" msgstr "Créer" -#: windows/views.py:588 -#, fuzzy +#: windows/views.py:627 msgid "Create connection" -msgstr "Dernière connexion" +msgstr "Créer une connexion" -#: windows/views.py:591 -#, fuzzy +#: windows/views.py:630 msgid "Create directory" msgstr "Nouveau répertoire" -#: 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 +#: windows/views.py:659 windows/views.py:883 windows/views.py:1520 +#: windows/views.py:1896 windows/views.py:2188 windows/views.py:2592 +#: windows/views.py:2648 msgid "Cancel" msgstr "Annuler" -#: 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 +#: windows/main/controller.py:324 windows/views.py:664 windows/views.py:2193 +#: windows/views.py:2597 windows/views.py:2812 msgid "Save" msgstr "Enregistrer" -#: windows/views.py:632 +#: windows/views.py:671 msgid "Test" msgstr "Tester" -#: windows/views.py:639 +#: windows/views.py:678 msgid "Connect" msgstr "Connecter" -#: windows/views.py:742 +#: windows/views.py:781 windows/views.py:2388 msgid "Language" msgstr "Langue" -#: windows/views.py:747 +#: windows/views.py:786 msgid "English" msgstr "Anglais" -#: windows/views.py:747 +#: windows/views.py:786 msgid "Italian" msgstr "Italien" -#: windows/views.py:747 +#: windows/views.py:786 msgid "French" msgstr "Français" -#: windows/views.py:759 +#: windows/views.py:798 msgid "Locale" msgstr "Localisation" -#: windows/views.py:780 -#, fuzzy +#: windows/views.py:819 msgid "Column content" -msgstr "Nouvelle connexion" +msgstr "Contenu de colonne" -#: windows/views.py:790 +#: windows/views.py:829 msgid "Syntax" msgstr "Syntaxe" -#: windows/views.py:847 +#: windows/views.py:886 msgid "Ok" msgstr "Ok" -#: windows/views.py:878 +#: windows/views.py:917 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:887 +#: windows/views.py:926 msgid "File" msgstr "Fichier" -#: windows/views.py:890 +#: windows/views.py:929 msgid "About" msgstr "À propos" -#: windows/views.py:893 +#: windows/views.py:932 msgid "Help" msgstr "Aide" -#: windows/views.py:898 +#: windows/views.py:937 msgid "Open connection manager" msgstr "Ouvrir le gestionnaire de connexions" -#: windows/views.py:902 +#: windows/views.py:941 msgid "Disconnect from server" msgstr "Se déconnecter du serveur" -#: windows/views.py:904 +#: windows/views.py:943 msgid "tool" msgstr "outil" -#: windows/views.py:904 +#: windows/views.py:943 windows/views.py:2628 msgid "Refresh" msgstr "Actualiser" -#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 -#: windows/views.py:2077 windows/views.py:2221 +#: windows/views.py:947 windows/views.py:949 windows/views.py:1874 +#: windows/views.py:2632 windows/views.py:2798 msgid "Add" msgstr "Ajouter" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 +#: windows/views.py:954 +#, python-brace-format +msgid "{mode}" +msgstr "{mode}" + +#: windows/views.py:991 windows/views.py:995 windows/views.py:2765 +#: windows/views.py:2770 msgid "MyMenuItem" msgstr "MonÉlémentMenu" -#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 +#: windows/views.py:998 windows/views.py:1924 windows/views.py:2777 msgid "MyMenu" msgstr "MonMenu" -#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 -#: windows/views.py:1402 +#: windows/views.py:1013 windows/views.py:1548 windows/views.py:1555 +#: windows/views.py:1562 msgid "MyLabel" msgstr "MonÉtiquette" -#: windows/views.py:972 +#: windows/views.py:1019 msgid "Databases" msgstr "Bases de données" -#: windows/views.py:973 windows/views.py:1340 +#: windows/views.py:1020 windows/views.py:1367 msgid "Size" msgstr "Taille" -#: windows/views.py:974 +#: windows/views.py:1021 msgid "Elements" msgstr "Éléments" -#: windows/views.py:975 +#: windows/views.py:1022 msgid "Modified at" msgstr "Modifié le" -#: windows/views.py:976 +#: windows/views.py:1023 windows/views.py:1382 msgid "Tables" msgstr "Tables" -#: windows/views.py:983 +#: windows/views.py:1030 msgid "System" msgstr "Système" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1029 -#: windows/views.py:1344 +#: windows/components/dataview.py:89 windows/views.py:1076 +#: windows/views.py:1371 msgid "Collation" msgstr "Classement" -#: windows/views.py:1058 +#: windows/views.py:1105 msgid "Encryption" -msgstr "" +msgstr "Chiffrement" -#: windows/views.py:1070 +#: windows/main/controller.py:1272 windows/main/controller.py:1294 +#: windows/main/controller.py:1298 windows/views.py:1117 msgid "Read Only" -msgstr "" +msgstr "Lecture seule" -#: windows/views.py:1087 -#, fuzzy +#: windows/views.py:1134 msgid "Tablespace" -msgstr "Tables" +msgstr "Tablespace" -#: windows/views.py:1108 -#, fuzzy +#: windows/views.py:1155 msgid "Connection limit" -msgstr "Connexion perdue" +msgstr "Limite de connexion" -#: windows/views.py:1151 -#, fuzzy +#: windows/views.py:1198 msgid "Profile" -msgstr "Fichier" +msgstr "Profil" -#: windows/views.py:1177 -#, fuzzy +#: windows/views.py:1224 msgid "Default tablespace" -msgstr "Supprimer la table" +msgstr "Tablespace par défaut" -#: windows/views.py:1198 -#, fuzzy +#: windows/views.py:1245 msgid "Temporary tablespace" -msgstr "Temporaire" +msgstr "Tablespace temporaire" -#: windows/views.py:1224 +#: windows/views.py:1271 msgid "Quota" -msgstr "" +msgstr "Quota" -#: windows/views.py:1243 +#: windows/views.py:1290 msgid "Unlimited quota" -msgstr "" +msgstr "Quota illimitée" -#: windows/views.py:1260 +#: windows/views.py:1307 msgid "Account status" -msgstr "" +msgstr "Statut du compte" -#: windows/views.py:1281 -#, fuzzy +#: windows/views.py:1328 msgid "Password expire" msgstr "Mot de passe" -#: windows/views.py:1302 -msgid "Table:" -msgstr "Table :" +#: windows/views.py:1352 +msgid "Add new table" +msgstr "Ajouter une nouvelle table" -#: 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:1354 +msgid "Clone table" +msgstr "Cloner la table" -#: windows/views.py:1315 -msgid "Clone" -msgstr "Cloner" +#: windows/main/controller.py:1821 windows/views.py:1356 +msgid "Delete table" +msgstr "Supprimer la table" -#: windows/views.py:1339 +#: windows/views.py:1366 windows/views.py:2457 msgid "Rows" msgstr "Lignes" -#: windows/views.py:1342 +#: windows/views.py:1369 msgid "Updated at" msgstr "Mis à jour le" -#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 -#: windows/views.py:2173 windows/views.py:2709 +#: windows/views.py:1387 +msgid "Add new view" +msgstr "Ajouter une nouvelle vue" + +#: windows/views.py:1389 +msgid "Clone view" +msgstr "Cloner" + +#: windows/views.py:1391 +msgid "Delete view" +msgstr "Supprimer" + +#: windows/views.py:1399 windows/views.py:1423 windows/views.py:1447 +#: windows/views.py:1471 windows/views.py:1495 +msgid "Definition" +msgstr "Définition" + +#: windows/views.py:1406 +msgid "Views" +msgstr "Vues" + +#: windows/views.py:1411 +msgid "Add new procedure" +msgstr "Ajouter une nouvelle procédure" + +#: windows/views.py:1413 +msgid "Clone procedure" +msgstr "Cloner la procédure" + +#: windows/views.py:1415 +msgid "Delete procedure" +msgstr "Supprimer la procédure" + +#: windows/views.py:1430 +msgid "Procedures" +msgstr "Procédures" + +#: windows/views.py:1435 +msgid "Add new function" +msgstr "Ajouter une nouvelle fonction" + +#: windows/views.py:1437 +msgid "Clone function" +msgstr "Cloner la fonction" + +#: windows/views.py:1439 +msgid "Delete function" +msgstr "Supprimer la fonction" + +#: windows/views.py:1454 +msgid "Functions" +msgstr "Fonctions" + +#: windows/views.py:1459 +msgid "Add new trigger" +msgstr "Ajouter un nouveau déclencheur" + +#: windows/views.py:1461 +msgid "Clone trigger" +msgstr "Cloner le déclencheur" + +#: windows/views.py:1463 +msgid "Delete trigger" +msgstr "Supprimer le déclencheur" + +#: windows/views.py:1478 +msgid "Triggers" +msgstr "Déclencheurs" + +#: windows/views.py:1483 +msgid "Add new event" +msgstr "Ajouter un nouvel événement" + +#: windows/views.py:1485 +msgid "Clone event" +msgstr "Cloner" + +#: windows/views.py:1487 +msgid "Delete event" +msgstr "Supprimer l'événement" + +#: windows/views.py:1502 +msgid "Events" +msgstr "Événements" + +#: windows/views.py:1530 windows/views.py:1901 windows/views.py:2646 +#: windows/views.py:2730 msgid "Apply" msgstr "Appliquer" -#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 -#: windows/views.py:2909 +#: windows/views.py:1542 windows/views.py:1721 msgid "Options" msgstr "Options" -#: windows/views.py:1413 +#: windows/views.py:1573 msgid "Diagram" msgstr "Diagramme" -#: windows/views.py:1424 +#: windows/views.py:1584 msgid "Database" msgstr "Base de données" -#: windows/views.py:1479 windows/views.py:2849 +#: windows/views.py:1639 msgid "Base" msgstr "Base" -#: windows/views.py:1493 windows/views.py:2863 +#: windows/views.py:1653 msgid "Auto Increment" msgstr "Auto incrément" -#: windows/views.py:1521 windows/views.py:2891 +#: windows/views.py:1681 msgid "Default Collation" msgstr "Classement par défaut" -#: windows/views.py:1531 +#: windows/views.py:1691 msgid "Convert data" -msgstr "" +msgstr "Convertir les données" -#: windows/views.py:1539 +#: windows/views.py:1699 msgid "Row format" -msgstr "" +msgstr "Format de ligne" -#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 -#: windows/views.py:1756 windows/views.py:2081 +#: windows/views.py:1732 windows/views.py:1760 windows/views.py:1788 +#: windows/views.py:1876 windows/views.py:2326 windows/views.py:2636 msgid "Remove" msgstr "Supprimer" -#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 +#: windows/views.py:1734 windows/views.py:1762 windows/views.py:1790 +#: windows/views.py:2328 windows/views.py:2738 msgid "Clear" msgstr "Effacer" -#: windows/views.py:1595 windows/views.py:2923 +#: windows/views.py:1747 msgid "Indexes" msgstr "Index" -#: windows/views.py:1639 +#: windows/views.py:1758 windows/views.py:1786 windows/views.py:2324 +msgid "Insert" +msgstr "Insérer" + +#: windows/views.py:1775 msgid "Foreign Keys" msgstr "Clés étrangères" -#: windows/views.py:1683 +#: windows/views.py:1803 msgid "Checks" msgstr "Contrôles" -#: windows/views.py:1750 windows/views.py:2944 +#: windows/views.py:1870 msgid "Columns:" msgstr "Colonnes :" -#: windows/views.py:1760 -#, fuzzy +#: windows/views.py:1880 msgid "Move Up" msgstr "Déplacer vers le haut\tCTRL+UP" -#: windows/views.py:1762 -#, fuzzy +#: windows/views.py:1882 msgid "Move Down" msgstr "Déplacer vers le bas\tCTRL+D" -#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 -#: windows/views.py:3017 +#: windows/views.py:1914 windows/views.py:1921 msgid "Add Index" msgstr "Ajouter un index" -#: windows/views.py:1798 windows/views.py:3014 +#: windows/views.py:1918 msgid "Add PrimaryKey" msgstr "Ajouter une clé primaire" -#: windows/views.py:1815 +#: windows/views.py:1935 msgid "Table" msgstr "Table" -#: windows/views.py:1851 -#, fuzzy -msgid "Definer" -msgstr "Insérer" - -#: windows/views.py:1871 +#: windows/views.py:1977 windows/views.py:2243 msgid "Schema" -msgstr "" - -#: windows/views.py:1897 -msgid "SQL security" -msgstr "" +msgstr "Schema" -#: windows/views.py:1904 -#, fuzzy -msgid "DEFINER" -msgstr "Insérer" +#: windows/views.py:2005 windows/views.py:2314 +msgid "General" +msgstr "Général" -#: windows/views.py:1904 -#, fuzzy -msgid "INVOKER" -msgstr "Insérer" - -#: windows/views.py:1916 +#: windows/views.py:2010 msgid "Algorithm" -msgstr "" +msgstr "Algorithme" -#: windows/views.py:1918 -#, fuzzy +#: windows/views.py:2012 msgid "UNDEFINED" msgstr "Non signé" -#: windows/views.py:1921 +#: windows/views.py:2015 msgid "MERGE" -msgstr "" +msgstr "MERGE" -#: windows/views.py:1924 -#, fuzzy +#: windows/views.py:2018 msgid "TEMPTABLE" -msgstr "Table" +msgstr "TEMPTABLE" -#: windows/views.py:1934 +#: windows/views.py:2028 msgid "View constraint" -msgstr "" +msgstr "Contrainte de vue" -#: windows/views.py:1936 -#, fuzzy +#: windows/views.py:2030 msgid "None" -msgstr "Cloner" +msgstr "Aucun" -#: windows/views.py:1939 -#, fuzzy +#: windows/views.py:2033 msgid "LOCAL" -msgstr "Localisation" +msgstr "LOCAL" -#: windows/views.py:1942 -#, fuzzy +#: windows/views.py:2036 msgid "CASCADE" -msgstr "Annuler" +msgstr "CASCADE" -#: windows/views.py:1945 -#, fuzzy +#: windows/views.py:2039 msgid "CHECK ONLY" -msgstr "Vérifier" +msgstr "VÉRIFIER SEULEMENT" -#: windows/views.py:1948 +#: windows/views.py:2042 msgid "READ ONLY" -msgstr "" +msgstr "LECTURE SEULE" + +#: windows/views.py:2055 windows/views.py:2483 +msgid "Behavior" +msgstr "Comportement" + +#: windows/views.py:2062 windows/views.py:2490 +msgid "Definer" +msgstr "Définisseur" + +#: windows/views.py:2070 windows/views.py:2498 +msgid "*" +msgstr "*" + +#: windows/views.py:2082 windows/views.py:2510 +msgid "SQL security" +msgstr "Sécurité SQL" + +#: windows/views.py:2089 windows/views.py:2517 +msgid "DEFINER" +msgstr "DÉFINISSEUR" -#: windows/views.py:1960 +#: windows/views.py:2089 windows/views.py:2517 +msgid "INVOKER" +msgstr "APPELANT" + +#: windows/views.py:2103 msgid "Force" -msgstr "" +msgstr "Forcer" -#: windows/views.py:1972 +#: windows/views.py:2115 msgid "Security barrier" -msgstr "" +msgstr "Barrière de sécurité" -#: windows/views.py:2054 -msgid "Views" -msgstr "Vues" +#: windows/views.py:2128 windows/views.py:2532 +msgid "Security" +msgstr "Sécurité" -#: windows/views.py:2062 -msgid "Triggers" -msgstr "Déclencheurs" +#: windows/views.py:2205 +msgid "View" +msgstr "Vues" -#: windows/views.py:2073 +#: windows/views.py:2262 #, fuzzy -msgid "Refrsh" -msgstr "Actualiser" +msgid "Type" +msgstr "Type de données" -#: windows/views.py:2079 -#, fuzzy +#: windows/views.py:2269 +msgid "Procedure (doesn't return a result)" +msgstr "Procédure (ne renvoie pas de résultat)" + +#: windows/views.py:2269 +msgid "Function (return a result)" +msgstr "Fonction (renvoie un résultat)" + +#: windows/views.py:2279 +msgid "Return type" +msgstr "Type de retour" + +#: windows/views.py:2299 +msgid "Comment" +msgstr "Commentaire" + +#: windows/components/dataview.py:111 windows/views.py:2335 +msgid "#" +msgstr "#" + +#: windows/views.py:2337 +msgid "Datatype" +msgstr "Type de données" + +#: windows/views.py:2338 +msgid "Context" +msgstr "Contexte" + +#: windows/views.py:2345 +msgid "Parameters" +msgstr "Paramètres" + +#: windows/views.py:2354 +msgid "Data access" +msgstr "Accès aux données" + +#: windows/views.py:2361 +msgid "CONTAINS SQL" +msgstr "CONTAINS SQL" + +#: windows/views.py:2361 +msgid "NO SQL" +msgstr "NO SQL" + +#: windows/views.py:2361 +msgid "READS SQL DATA" +msgstr "READS SQL DATA" + +#: windows/views.py:2361 +msgid "MODIFIES SQL DATA" +msgstr "MODIFIES SQL DATA" + +#: windows/views.py:2371 +msgid "Deterministic" +msgstr "Déterministe" + +#: windows/views.py:2395 +msgid "SQL" +msgstr "SQL" + +#: windows/views.py:2395 +msgid "PLPGSQL" +msgstr "PLPGSQL" + +#: windows/views.py:2405 +msgid "Volatility" +msgstr "Volatilité" + +#: windows/views.py:2412 +msgid "VOLATILE" +msgstr "VOLATILE" + +#: windows/views.py:2412 +msgid "STABLE" +msgstr "STABLE" + +#: windows/views.py:2412 +msgid "IMMUTABLE" +msgstr "IMMUTABLE" + +#: windows/views.py:2422 +msgid "Parallel" +msgstr "Parallèle" + +#: windows/views.py:2429 +msgid "UNSAFE" +msgstr "UNSAFE" + +#: windows/views.py:2429 +msgid "RESTRICTED" +msgstr "RESTRICTED" + +#: windows/views.py:2429 +msgid "SAFE" +msgstr "SAFE" + +#: windows/views.py:2441 +msgid "Cost" +msgstr "Coût" + +#: windows/views.py:2609 +msgid "Routine" +msgstr "Routine" + +#: windows/views.py:2617 +msgid "Trigger" +msgstr "Déclencheur" + +#: windows/views.py:2634 msgid "Duplicate" -msgstr "Dupliquer un enregistrement" +msgstr "Dupliquer" -#: windows/views.py:2085 +#: windows/views.py:2640 msgid "Apply changes automatically" msgstr "Appliquer les modifications automatiquement" -#: windows/views.py:2087 windows/views.py:2088 +#: windows/views.py:2642 windows/views.py:2643 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -697,125 +893,87 @@ msgstr "" "Si activé, les modifications de la table sont appliquées immédiatement " "sans appuyer sur Appliquer ou Annuler" -#: windows/views.py:2101 +#: windows/views.py:2656 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" +msgstr "{database_name}.{table_name} - lignes {from_row} - {to_row} sur {total_rows}" -#: windows/views.py:2109 -#, fuzzy +#: windows/views.py:2664 msgid "First" -msgstr "Filtres" +msgstr "Premier" -#: windows/views.py:2127 +#: windows/views.py:2682 msgid "Last" -msgstr "" +msgstr "Dernier" -#: windows/views.py:2136 +#: windows/views.py:2691 msgid "Filters" msgstr "Filtres" -#: windows/views.py:2176 +#: windows/views.py:2733 +msgid "" +"Apply filters in data\n" +"CTRL+ENTER" +msgstr "" +"Appliquer les filtres dans les données\n" +"CTRL+ENTRÉE" + +#: windows/views.py:2734 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2196 +#: windows/views.py:2762 msgid "Insert row" msgstr "Insérer une ligne" -#: windows/views.py:2204 +#: windows/components/popup.py:31 windows/views.py:2774 +msgid "NULL" +msgstr "NULL" + +#: windows/views.py:2781 msgid "Data" msgstr "Données" -#: windows/main/controller.py:278 windows/main/controller.py:287 -#: windows/main/controller.py:288 windows/views.py:2221 -#, fuzzy +#: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" -msgstr "Requête" +msgstr "Nouvelle requête" -#: windows/views.py:2223 windows/views.py:2660 +#: windows/views.py:2800 msgid "Close" msgstr "Fermer" -#: windows/main/controller.py:279 windows/main/controller.py:289 -#: windows/main/controller.py:290 windows/views.py:2223 -#, fuzzy +#: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" -msgstr "Requête" +msgstr "Fermer la requête" -#: windows/views.py:2227 +#: windows/views.py:2804 msgid "Run" -msgstr "" +msgstr "Exécuter" -#: windows/main/controller.py:280 windows/main/controller.py:292 -#: windows/main/controller.py:293 windows/views.py:2227 -#, fuzzy +#: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" -msgstr "Exécutable SSH" +msgstr "Exécuter" -#: windows/views.py:2229 +#: windows/views.py:2806 msgid "Run all" -msgstr "" +msgstr "Tout exécuter" -#: windows/main/controller.py:295 windows/views.py:2229 +#: windows/views.py:2806 msgid "Execute all statements" -msgstr "" +msgstr "Exécuter toutes les instructions" -#: windows/main/controller.py:282 windows/main/controller.py:297 -#: windows/main/controller.py:298 windows/views.py:2231 +#: windows/main/controller.py:323 windows/views.py:2808 msgid "Stop" -msgstr "" +msgstr "Arrêter" -#: windows/views.py:2287 +#: windows/views.py:2873 msgid "a page" -msgstr "" +msgstr "une page" -#: windows/views.py:2315 +#: windows/views.py:2923 msgid "Query" msgstr "Requête" -#: windows/views.py:2626 -#, fuzzy -msgid "Character set" -msgstr "Créé le" - -#: windows/views.py:2644 windows/views.py:2663 -msgid "New" -msgstr "Nouveau" - -#: windows/views.py:2683 -msgid "Insert record" -msgstr "Insérer un enregistrement" - -#: windows/views.py:2688 -msgid "Duplicate record" -msgstr "Dupliquer un enregistrement" - -#: windows/views.py:2695 -msgid "Delete record" -msgstr "Supprimer un enregistrement" - -#: windows/views.py:2733 windows/views.py:2964 -msgid "Up" -msgstr "Haut" - -#: windows/views.py:2740 windows/views.py:2971 -msgid "Down" -msgstr "Bas" - -#: windows/views.py:3100 -msgid "Save Starments" -msgstr "" - -#: windows/views.py:3108 -#, fuzzy -msgid "Location" -msgstr "Classement" - -#: windows/views.py:3115 -msgid "*.sql" -msgstr "" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 msgid "Allow NULL" @@ -836,7 +994,7 @@ 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/components/dataview.py:85 windows/components/dataview.py:256 msgid "Expression" msgstr "Expression" @@ -848,75 +1006,71 @@ msgstr "Non signé" msgid "Zerofill" msgstr "Remplissage zéro" -#: windows/components/dataview.py:109 -msgid "#" -msgstr "#" - -#: windows/components/dataview.py:117 +#: windows/components/dataview.py:119 msgid "Data type" msgstr "Type de données" -#: windows/components/dataview.py:121 +#: windows/components/dataview.py:123 msgid "Length/Set" msgstr "Longueur/Ensemble" -#: windows/components/dataview.py:155 +#: windows/components/dataview.py:169 msgid "Add column\tCTRL+INS" msgstr "Ajouter une colonne\tCTRL+INS" -#: windows/components/dataview.py:161 +#: windows/components/dataview.py:175 msgid "Remove column\tCTRL+DEL" msgstr "Supprimer une colonne\tCTRL+DEL" -#: windows/components/dataview.py:169 +#: windows/components/dataview.py:183 msgid "Move up\tCTRL+UP" msgstr "Déplacer vers le haut\tCTRL+UP" -#: windows/components/dataview.py:176 +#: windows/components/dataview.py:190 msgid "Move down\tCTRL+D" msgstr "Déplacer vers le bas\tCTRL+D" -#: windows/components/dataview.py:199 +#: windows/components/dataview.py:214 msgid "Create new index" msgstr "Créer un nouvel index" -#: windows/components/dataview.py:214 +#: windows/components/dataview.py:229 msgid "Append to index" msgstr "Ajouter à l'index" -#: windows/components/dataview.py:228 +#: windows/components/dataview.py:243 msgid "Column(s)/Expression" msgstr "Colonne(s)/Expression" -#: windows/components/dataview.py:229 +#: windows/components/dataview.py:244 msgid "Condition" msgstr "Condition" -#: windows/components/dataview.py:259 +#: windows/components/dataview.py:274 msgid "Column(s)" msgstr "Colonne(s)" -#: windows/components/dataview.py:265 +#: windows/components/dataview.py:280 msgid "Reference table" msgstr "Table de référence" -#: windows/components/dataview.py:271 +#: windows/components/dataview.py:286 msgid "Reference column(s)" msgstr "Colonne(s) de référence" -#: windows/components/dataview.py:277 +#: windows/components/dataview.py:292 msgid "On UPDATE" msgstr "Sur UPDATE" -#: windows/components/dataview.py:283 +#: windows/components/dataview.py:298 msgid "On DELETE" msgstr "Sur DELETE" -#: windows/components/dataview.py:298 +#: windows/components/dataview.py:313 msgid "Add foreign key" msgstr "Ajouter une clé étrangère" -#: windows/components/dataview.py:304 +#: windows/components/dataview.py:319 msgid "Remove foreign key" msgstr "Supprimer une clé étrangère" @@ -924,10 +1078,6 @@ msgstr "Supprimer une clé étrangère" msgid "No default value" msgstr "Aucune valeur par défaut" -#: windows/components/popup.py:31 -msgid "NULL" -msgstr "NULL" - #: windows/components/popup.py:35 msgid "AUTO INCREMENT" msgstr "AUTO INCREMENT" @@ -936,164 +1086,177 @@ msgstr "AUTO INCREMENT" msgid "Text/Expression" msgstr "Texte/Expression" -#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 +#: windows/dialogs/connections/view.py:126 windows/main/query/renderer.py:192 msgid "Unknown error" -msgstr "" +msgstr "Erreur inconnue" -#: windows/dialogs/connections/view.py:414 +#: windows/dialogs/connections/view.py:416 msgid "Connection established successfully" -msgstr "" +msgstr "Connexion établie avec succès" -#: windows/dialogs/connections/view.py:426 +#: windows/dialogs/connections/view.py:428 #, python-brace-format msgid "Do you want save the connection {connection_name}?" -msgstr "" +msgstr "Voulez-vous enregistrer la connexion {connection_name}?" -#: windows/dialogs/connections/view.py:429 +#: windows/dialogs/connections/view.py:431 msgid "Confirm save" msgstr "Confirmer la sauvegarde" -#: windows/dialogs/connections/view.py:481 +#: windows/dialogs/connections/view.py:483 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" +"Vous avez des modifications non enregistrées. Voulez-vous les enregistrer" +" avant de continuer?" -#: windows/dialogs/connections/view.py:483 +#: windows/dialogs/connections/view.py:485 msgid "Unsaved changes" -msgstr "" +msgstr "Modifications non enregistrées" -#: windows/dialogs/connections/view.py:750 +#: windows/dialogs/connections/view.py:773 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" +"Cette connexion ne peut pas fonctionner sans TLS. TLS a été activé " +"automatiquement." -#: windows/dialogs/connections/view.py:775 -#, fuzzy, python-brace-format +#: windows/dialogs/connections/view.py:798 +#, python-brace-format msgid "" "Connection error:\n" "{error}" -msgstr "Erreur de connexion" +msgstr "" +"Erreur de connexion:\n" +"{error}" -#: windows/dialogs/connections/view.py:776 +#: windows/dialogs/connections/view.py:799 msgid "Connection error" msgstr "Erreur de connexion" -#: windows/dialogs/connections/view.py:802 -#, fuzzy, python-brace-format +#: windows/dialogs/connections/view.py:825 +#, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" -msgstr "Voulez-vous supprimer les enregistrements ?" +msgstr "Voulez-vous supprimer la connexion '{connection_name}'?" -#: windows/dialogs/connections/view.py:805 -#: windows/dialogs/connections/view.py:822 +#: windows/dialogs/connections/view.py:828 +#: windows/dialogs/connections/view.py:845 msgid "Confirm delete" msgstr "Confirmer la suppression" -#: windows/dialogs/connections/view.py:819 -#, fuzzy, python-brace-format +#: windows/dialogs/connections/view.py:842 +#, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" -msgstr "Voulez-vous supprimer les enregistrements ?" +msgstr "Voulez-vous supprimer le répertoire '{directory_name}'?" -#: windows/main/controller.py:275 +#: windows/main/controller.py:316 #, python-brace-format msgid "{text} ({shortcut})" -msgstr "" +msgstr "{text} ({shortcut})" -#: windows/main/controller.py:281 windows/main/controller.py:294 -#, fuzzy +#: windows/main/controller.py:322 msgid "Execute all" -msgstr "Exécutable SSH" +msgstr "Tout exécuter" -#: windows/main/controller.py:471 windows/main/controller.py:479 -#, fuzzy +#: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" -msgstr "Requête" +msgstr "Requête (1)" -#: windows/main/controller.py:497 +#: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" -msgstr "" +msgstr "Requête ({query_number})" -#: windows/main/controller.py:530 +#: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" -msgstr "" +msgstr "Vous avez des modifications non enregistrées. Enregistrer avant de fermer?" -#: windows/main/controller.py:531 +#: windows/main/controller.py:519 msgid "Unsaved query" -msgstr "" +msgstr "Requête non enregistrée" -#: windows/main/controller.py:576 -#, fuzzy +#: windows/main/controller.py:564 msgid "Save query" -msgstr "Requête" +msgstr "Enregistrer la requête" -#: windows/main/controller.py:579 +#: windows/main/controller.py:567 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" -msgstr "" +msgstr "Fichiers SQL (*.sql)|*.sql|Tous les fichiers (*.*)|*.*" -#: 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 +#: windows/main/controller.py:595 windows/main/controller.py:626 +#: windows/main/controller.py:653 windows/main/database/list.py:119 +#: windows/main/database/routine.py:591 windows/main/database/routine.py:635 +#: windows/main/database/view.py:268 windows/main/database/view.py:297 +#: windows/main/query/controller.py:179 msgid "Error" msgstr "Erreur" -#: windows/main/controller.py:622 +#: windows/main/controller.py:633 #, python-brace-format msgid "-- Saved query to {file_path}" -msgstr "" +msgstr "-- Requête enregistrée dans {file_path}" -#: windows/main/controller.py:647 +#: windows/main/controller.py:659 #, python-brace-format msgid "-- Autosaved query to {file_path}" -msgstr "" +msgstr "-- Requête auto-enregistrée dans {file_path}" -#: windows/main/controller.py:704 +#: windows/main/controller.py:725 msgid "days" msgstr "jours" -#: windows/main/controller.py:705 +#: windows/main/controller.py:726 msgid "hours" msgstr "heures" -#: windows/main/controller.py:706 +#: windows/main/controller.py:727 msgid "minutes" msgstr "minutes" -#: windows/main/controller.py:707 +#: windows/main/controller.py:728 msgid "seconds" msgstr "secondes" -#: windows/main/controller.py:715 +#: windows/main/controller.py:736 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Mémoire utilisée : {used} ({percentage:.2%})" -#: windows/main/controller.py:751 +#: windows/main/controller.py:772 msgid "Settings saved successfully" -msgstr "" +msgstr "Paramètres enregistrés avec succès" -#: windows/main/controller.py:952 +#: windows/main/controller.py:1006 #, python-brace-format msgid "~{estimated} (Loading...)" -msgstr "" +msgstr "~{estimated} (Chargement...)" -#: windows/main/controller.py:954 +#: windows/main/controller.py:1008 msgid "~ (Loading...)" -msgstr "" +msgstr "~ (Chargement...)" + +#: windows/main/controller.py:1243 +msgid "Write Mode (2:00)" +msgstr "Mode écriture (2:00)" -#: windows/main/controller.py:1119 +#: windows/main/controller.py:1294 +msgid "Write Mode" +msgstr "Mode écriture" + +#: windows/main/controller.py:1305 msgid "Version" msgstr "Version" -#: windows/main/controller.py:1121 +#: windows/main/controller.py:1307 msgid "Uptime" msgstr "Temps de fonctionnement" -#: windows/main/controller.py:1199 +#: windows/main/controller.py:1444 #, python-brace-format msgid "Do you want discard the change to {database_name}?" -msgstr "" +msgstr "Voulez-vous annuler le changement à {database_name}?" -#: windows/main/controller.py:1232 +#: windows/main/controller.py:1477 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1102,120 +1265,191 @@ msgid "" "- Yes: open dump flow (coming soon, no drop).\n" "- No: drop the database now." msgstr "" +"Voulez-vous créer une sauvegarde avant de supprimer la base de données " +"'{database_name}'?\n" +"\n" +"La sauvegarde n'est pas encore implémentée.\n" +"- Oui: ouvrir le flux de sauvegarde (bientôt, pas de suppression).\n" +"- Non: supprimer la base de données maintenant." -#: windows/main/controller.py:1237 windows/main/controller.py:1258 -#, fuzzy +#: windows/main/controller.py:1482 windows/main/controller.py:1503 msgid "Delete database" -msgstr "Supprimer la table" +msgstr "Supprimer la base de données" -#: windows/main/controller.py:1243 +#: windows/main/controller.py:1488 msgid "Dump is not implemented yet. No action has been performed." msgstr "" +"La sauvegarde n'est pas encore implémentée. Aucune action n'a été " +"effectuée." -#: windows/main/controller.py:1244 +#: windows/main/controller.py:1489 msgid "Dump not available" -msgstr "" +msgstr "Sauvegarde non disponible" -#: windows/main/controller.py:1257 +#: windows/main/controller.py:1502 msgid "Database deletion is not supported by this engine." -msgstr "" +msgstr "La suppression de base de données n'est pas supportée par ce moteur." -#: windows/main/controller.py:1272 +#: windows/main/controller.py:1517 msgid "Database deleted successfully" -msgstr "" +msgstr "Base de données supprimée avec succès" -#: windows/main/controller.py:1273 windows/main/database/view.py:253 -#: windows/main/database/view.py:279 +#: windows/main/controller.py:1518 windows/main/database/routine.py:556 +#: windows/main/database/routine.py:620 windows/main/database/view.py:256 +#: windows/main/database/view.py:291 msgid "Success" -msgstr "" +msgstr "Succès" -#: windows/main/controller.py:1392 +#: windows/main/controller.py:1792 #, python-brace-format msgid "Do you want discard the change to {table_name}?" -msgstr "" +msgstr "Voulez-vous annuler le changement à {table_name}?" -#: windows/main/controller.py:1418 -#, fuzzy, python-brace-format +#: windows/main/controller.py:1818 +#, 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" +msgstr "Voulez-vous supprimer la table {table_name}?" -#: windows/main/controller.py:1440 +#: windows/main/controller.py:1840 #, python-brace-format msgid "{table_name} (COPY)" -msgstr "" +msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1563 +#: windows/main/controller.py:1999 msgid "Do you want delete the records?" msgstr "Voulez-vous supprimer les enregistrements ?" -#: windows/main/database/list.py:69 +#: windows/main/controller.py:2104 +#, fuzzy +msgid "Database connection lost. Do you want to reconnect?" +msgstr "Voulez-vous vous reconnecter ?" + +#: windows/main/controller.py:2105 windows/main/database/list.py:108 +msgid "Connection lost" +msgstr "Connexion perdue" + +#: windows/main/controller.py:2113 +#, fuzzy +msgid "Connection restored successfully." +msgstr "Connexion établie avec succès" + +#: windows/main/controller.py:2114 +#, fuzzy +msgid "Connection restored" +msgstr "Erreur de connexion" + +#: windows/main/controller.py:2120 +#, python-brace-format +msgid "Could not reconnect: {error}" +msgstr "" + +#: windows/main/controller.py:2121 +#, fuzzy +msgid "Reconnection failed" +msgstr "Échec de la reconnexion :" + +#: windows/main/database/list.py:104 msgid "The connection to the database was lost." msgstr "La connexion à la base de données a été perdue." -#: windows/main/database/list.py:71 +#: windows/main/database/list.py:106 msgid "Do you want to reconnect?" msgstr "Voulez-vous vous reconnecter ?" -#: windows/main/database/list.py:73 -msgid "Connection lost" -msgstr "Connexion perdue" - -#: windows/main/database/list.py:83 +#: windows/main/database/list.py:118 msgid "Reconnection failed:" msgstr "Échec de la reconnexion :" -#: windows/main/database/view.py:252 +#: windows/main/database/routine.py:545 +msgid "Function created successfully" +msgstr "Fonction créée avec succès" + +#: windows/main/database/routine.py:547 +msgid "Function updated successfully" +msgstr "Fonction mise à jour avec succès" + +#: windows/main/database/routine.py:551 +msgid "Procedure created successfully" +msgstr "Procédure créée avec succès" + +#: windows/main/database/routine.py:553 +msgid "Procedure updated successfully" +msgstr "Procédure mise à jour avec succès" + +#: windows/main/database/routine.py:590 +#, python-brace-format +msgid "Error saving routine: {}" +msgstr "Erreur lors de l'enregistrement de la routine: {}" + +#: windows/main/database/routine.py:604 +msgid "Function" +msgstr "Fonction" + +#: windows/main/database/routine.py:604 +msgid "Procedure" +msgstr "Procédure" + +#: windows/main/database/routine.py:607 +#, python-brace-format +msgid "Are you sure you want to delete {} '{}'?" +msgstr "Êtes-vous sûr de vouloir supprimer {} '{}'?" + +#: windows/main/database/routine.py:610 windows/main/database/view.py:282 +msgid "Confirm Delete" +msgstr "Confirmer la suppression" + +#: windows/main/database/routine.py:619 +#, python-brace-format +msgid "{} deleted successfully" +msgstr "{} supprimé avec succès" + +#: windows/main/database/routine.py:634 +#, python-brace-format +msgid "Error deleting routine: {}" +msgstr "Erreur lors de la suppression de la routine: {}" + +#: windows/main/database/view.py:255 msgid "View created successfully" -msgstr "" +msgstr "Vue créée avec succès" -#: windows/main/database/view.py:252 +#: windows/main/database/view.py:255 msgid "View updated successfully" -msgstr "" +msgstr "Vue mise à jour avec succès" -#: windows/main/database/view.py:256 +#: windows/main/database/view.py:268 #, python-brace-format msgid "Error saving view: {}" -msgstr "" +msgstr "Erreur lors de l'enregistrement de la vue: {}" -#: windows/main/database/view.py:269 +#: windows/main/database/view.py:281 #, python-brace-format msgid "Are you sure you want to delete view '{}'?" -msgstr "" - -#: windows/main/database/view.py:270 -#, fuzzy -msgid "Confirm Delete" -msgstr "Confirmer la suppression" +msgstr "Êtes-vous sûr de vouloir supprimer la vue '{}'?" -#: windows/main/database/view.py:279 +#: windows/main/database/view.py:291 msgid "View deleted successfully" -msgstr "" +msgstr "Vue supprimée avec succès" -#: windows/main/database/view.py:282 +#: windows/main/database/view.py:297 #, python-brace-format msgid "Error deleting view: {}" -msgstr "" +msgstr "Erreur lors de la suppression de la vue: {}" -#: windows/main/query/controller.py:110 +#: windows/main/query/controller.py:112 #, python-brace-format msgid "{elapsed_ms:.0f} ms" -msgstr "" +msgstr "{elapsed_ms:.0f} ms" -#: windows/main/query/controller.py:112 +#: windows/main/query/controller.py:114 #, python-brace-format msgid "{elapsed_s:.2f} s" -msgstr "" +msgstr "{elapsed_s:.2f} s" -#: windows/main/query/controller.py:115 -#, fuzzy +#: windows/main/query/controller.py:117 msgid "none" -msgstr "Cloner" +msgstr "aucun" -#: windows/main/query/controller.py:121 +#: windows/main/query/controller.py:123 #, python-brace-format msgid "" "Query execution stopped after {elapsed}.\n" @@ -1224,188 +1458,68 @@ msgid "" "Failed: {failed}.\n" "Last statement: #{last}." msgstr "" +"Exécution de la requête arrêtée après {elapsed}.\n" +"Instructions complétées: {completed}/{total}.\n" +"Réussies: {success}.\n" +"Échouées: {failed}.\n" +"Dernière instruction: #{last}." -#: windows/main/query/controller.py:134 +#: windows/main/query/controller.py:136 msgid "Query execution cancelled" -msgstr "" +msgstr "Exécution de la requête annulée" -#: windows/main/query/controller.py:176 -#, fuzzy +#: windows/main/query/controller.py:178 msgid "No active database connection" -msgstr "Nouvelle connexion" +msgstr "Aucune connexion de base de données active" + +#: windows/main/query/controller.py:227 +msgid "Database connection lost" +msgstr "Connexion de base de données perdue" + +#: windows/main/query/history.py:55 +msgid "(empty query)" +msgstr "(requête vide)" #: windows/main/query/renderer.py:53 #, python-brace-format msgid "{affected_rows} rows affected" -msgstr "" +msgstr "{affected_rows} lignes affectées" #: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 #, python-brace-format msgid "Query {query_number}" -msgstr "" +msgstr "Requête {query_number}" #: windows/main/query/renderer.py:65 #, python-brace-format msgid "Query {query_number} (Error)" -msgstr "" +msgstr "Requête {query_number} (Erreur)" #: windows/main/query/renderer.py:79 #, python-brace-format msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" -msgstr "" +msgstr "Requête {query_number} ({rows_count} lignes × {columns_count} colonnes)" #: windows/main/query/renderer.py:165 #, python-brace-format msgid "{rows_count} rows" -msgstr "" +msgstr "{rows_count} lignes" #: windows/main/query/renderer.py:167 #, python-brace-format msgid "{elapsed_ms:.1f} ms" -msgstr "" +msgstr "{elapsed_ms:.1f} ms" #: windows/main/query/renderer.py:171 #, python-brace-format msgid "{warnings_count} warnings" -msgstr "" +msgstr "{warnings_count} avertissements" #: windows/main/query/renderer.py:186 -#, fuzzy msgid "Error:" msgstr "Erreur" -#~ msgid "Created at:" -#~ msgstr "" - -#~ msgid "Last connection:" -#~ msgstr "" - -#~ msgid "Successful connections:" -#~ msgstr "" - -#~ msgid "Unsuccessful connections:" -#~ msgstr "" - -#~ msgid "Session Manager" -#~ msgstr "" - -#~ msgid "Session name" -#~ msgstr "" - -#~ msgid "Connection type" -#~ msgstr "" - -#~ msgid "Open" -#~ msgstr "" - -#~ msgid "Open session manager" -#~ msgstr "" - -#~ msgid "Foreign Key" -#~ msgstr "" - -#~ msgid "New Session" -#~ msgstr "Nouvelle session" - -#~ 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" +#: windows/main/table/records.py:334 +msgid "Error saving records" +msgstr "Erreur lors de l'enregistrement des enregistrements" diff --git a/locale/it_IT/LC_MESSAGES/petersql.mo b/locale/it_IT/LC_MESSAGES/petersql.mo index bbf4054..b6bd5f5 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 9855aa5..13f2716 100644 --- a/locale/it_IT/LC_MESSAGES/petersql.po +++ b/locale/it_IT/LC_MESSAGES/petersql.po @@ -2,12 +2,11 @@ # Copyright (C) 2026 ORGANIZATION # This file is distributed under the same license as the PeterSQL project. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-23 10:07+0100\n" +"POT-Creation-Date: 2026-06-06 11:23+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: it_IT\n" @@ -47,638 +46,846 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "Client OpenSSH non trovato." -#: structures/engines/mariadb/context.py:611 -#: structures/engines/mysql/context.py:622 -#: structures/engines/postgresql/context.py:645 -#: structures/engines/sqlite/context.py:524 +#: structures/engines/context.py:567 +msgid "This connection is read-only." +msgstr "Questa connessione è in sola lettura." + +#: structures/engines/context.py:578 +#, fuzzy, python-brace-format +msgid "Database connection lost: {error}" +msgstr "" +"Errore di connessione:\n" +"{error}" + +#: structures/engines/mariadb/context.py:658 +#: structures/engines/mysql/context.py:670 +#: structures/engines/postgresql/context.py:701 +#: structures/engines/sqlite/context.py:564 #, python-brace-format msgid "Table{table_index:03}" -msgstr "" +msgstr "Table{table_index:03}" -#: structures/engines/mariadb/context.py:639 -#: structures/engines/mysql/context.py:650 -#: structures/engines/postgresql/context.py:670 -#: structures/engines/sqlite/context.py:548 +#: structures/engines/mariadb/context.py:686 +#: structures/engines/mysql/context.py:698 +#: structures/engines/postgresql/context.py:726 +#: structures/engines/sqlite/context.py:588 #, python-brace-format msgid "Column{column_index:03}" -msgstr "" +msgstr "Column{column_index:03}" -#: structures/engines/mariadb/context.py:657 -#: structures/engines/mysql/context.py:668 -#: structures/engines/postgresql/context.py:688 -#: structures/engines/sqlite/context.py:566 +#: structures/engines/mariadb/context.py:704 +#: structures/engines/mysql/context.py:716 +#: structures/engines/postgresql/context.py:744 +#: structures/engines/sqlite/context.py:606 #, python-brace-format msgid "Index{index_number:03}" -msgstr "" +msgstr "Index{index_number:03}" -#: structures/engines/mariadb/context.py:697 -#: structures/engines/mysql/context.py:706 -#: structures/engines/postgresql/context.py:728 -#: structures/engines/sqlite/context.py:608 +#: structures/engines/mariadb/context.py:744 +#: structures/engines/mysql/context.py:754 +#: structures/engines/postgresql/context.py:784 +#: structures/engines/sqlite/context.py:646 #, python-brace-format msgid "ForeignKey{foreign_key_number:03}" -msgstr "" +msgstr "ForeignKey{foreign_key_number:03}" -#: structures/engines/mariadb/context.py:730 -#: structures/engines/mysql/context.py:737 -#: structures/engines/postgresql/context.py:758 -#: structures/engines/sqlite/context.py:636 +#: structures/engines/mariadb/context.py:777 +#: structures/engines/mysql/context.py:785 +#: structures/engines/postgresql/context.py:814 +#: structures/engines/sqlite/context.py:674 #, python-brace-format msgid "View{view_index:03}" -msgstr "" +msgstr "View{view_index:03}" -#: structures/engines/mariadb/context.py:781 +#: structures/engines/mariadb/context.py:830 #, python-brace-format msgid "Trigger{trigger_index:03}" -msgstr "" +msgstr "Trigger{trigger_index:03}" -#: windows/dialogs/connections/view.py:415 -#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 -#: windows/views.py:33 +#: windows/dialogs/connections/view.py:417 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1303 +#: windows/views.py:62 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:1006 -#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 -#: windows/views.py:2821 +#: windows/components/dataview.py:115 windows/components/dataview.py:240 +#: windows/components/dataview.py:253 windows/components/dataview.py:268 +#: windows/views.py:76 windows/views.py:126 windows/views.py:1053 +#: windows/views.py:1365 windows/views.py:1398 windows/views.py:1422 +#: windows/views.py:1446 windows/views.py:1470 windows/views.py:1494 +#: windows/views.py:1611 windows/views.py:1957 windows/views.py:2228 +#: windows/views.py:2336 msgid "Name" msgstr "Nome" -#: windows/views.py:48 windows/views.py:428 +#: windows/views.py:77 windows/views.py:467 msgid "Last connection" msgstr "Ultima connessione" -#: windows/dialogs/connections/view.py:653 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:90 msgid "New directory" msgstr "Nuova directory" -#: windows/dialogs/connections/model.py:206 -#: windows/dialogs/connections/view.py:613 windows/views.py:65 +#: windows/dialogs/connections/model.py:212 +#: windows/dialogs/connections/view.py:615 windows/views.py:94 msgid "New connection" msgstr "Nuova connessione" -#: windows/views.py:71 +#: windows/views.py:100 msgid "Rename" msgstr "Rinomina" -#: windows/views.py:76 +#: windows/views.py:105 msgid "Clone connection" -msgstr "Chiudi connessione" +msgstr "Clona connessione" -#: 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 +#: windows/views.py:110 windows/views.py:642 windows/views.py:1525 +#: windows/views.py:1893 windows/views.py:2183 windows/views.py:2587 msgid "Delete" msgstr "Elimina" -#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 -#: windows/views.py:2876 +#: windows/views.py:140 windows/views.py:1370 windows/views.py:1666 msgid "Engine" msgstr "Motore" -#: windows/views.py:132 +#: windows/views.py:161 msgid "Host + port" msgstr "Host + porta" -#: windows/views.py:148 +#: windows/views.py:177 msgid "Username" msgstr "Nome utente" -#: windows/views.py:161 windows/views.py:1132 +#: windows/views.py:190 windows/views.py:1179 msgid "Password" msgstr "Password" -#: windows/views.py:174 +#: windows/views.py:203 msgid "Connection timeout" msgstr "Timeout connessione" -#: windows/views.py:192 +#: windows/views.py:221 msgid "Use TLS" -msgstr "" +msgstr "Usa TLS" -#: windows/views.py:203 +#: windows/views.py:232 +msgid "Mark read only" +msgstr "Segna come sola lettura" + +#: windows/views.py:243 msgid "Use SSH tunnel" msgstr "Usa tunnel SSH" -#: windows/views.py:214 +#: windows/views.py:254 msgid "Compressed client/server protocol" -msgstr "" +msgstr "Protocollo client/server compresso" -#: windows/views.py:233 +#: windows/views.py:273 msgid "Filename" msgstr "Nome file" -#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 +#: windows/views.py:278 windows/views.py:397 msgid "Select a file" msgstr "Seleziona un file" -#: windows/views.py:238 windows/views.py:358 +#: windows/views.py:278 windows/views.py:397 msgid "*.*" msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 -#: windows/views.py:2834 +#: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 msgid "Comments" msgstr "Commenti" -#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 -#: windows/views.py:884 +#: windows/main/controller.py:772 windows/views.py:309 windows/views.py:769 +#: windows/views.py:923 msgid "Settings" msgstr "Impostazioni" -#: windows/views.py:278 +#: windows/views.py:317 msgid "SSH executable" msgstr "Eseguibile SSH" -#: windows/views.py:283 +#: windows/views.py:322 msgid "ssh" msgstr "ssh" -#: windows/views.py:291 +#: windows/views.py:330 msgid "SSH host + port" msgstr "Host SSH + porta" -#: windows/views.py:303 +#: windows/views.py:342 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" -msgstr "" +msgstr "SSH host + port (the SSH server that forwards traffic to the DB)" -#: windows/views.py:312 +#: windows/views.py:351 msgid "SSH username" msgstr "Nome utente SSH" -#: windows/views.py:325 +#: windows/views.py:364 msgid "SSH password" msgstr "Password SSH" -#: windows/views.py:338 +#: windows/views.py:377 msgid "Local port" msgstr "Porta locale" -#: windows/views.py:344 +#: windows/views.py:383 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:353 +#: windows/views.py:392 msgid "Identity file" -msgstr "" +msgstr "File identità" -#: windows/views.py:369 +#: windows/views.py:408 msgid "Remote host + port" -msgstr "Remoto host + porta" +msgstr "Host remoto + porta" -#: windows/views.py:381 +#: windows/views.py:420 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." -msgstr "" +msgstr "Host/porta remoto è il target reale del DB (predefinito a Host/Porta DB)." -#: windows/views.py:390 +#: windows/views.py:429 msgid "SSH extra args" -msgstr "" +msgstr "Argomenti extra SSH" -#: windows/views.py:405 +#: windows/views.py:444 msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:411 windows/views.py:1341 +#: windows/views.py:450 windows/views.py:1368 msgid "Created at" msgstr "Creato il" -#: windows/views.py:445 +#: windows/views.py:484 msgid "Successful connections" msgstr "Connessioni riuscite" -#: windows/views.py:462 +#: windows/views.py:501 msgid "Last successful connection" msgstr "Ultima connessione riuscita" -#: windows/views.py:479 +#: windows/views.py:518 msgid "Unsuccessful connections" msgstr "Connessioni non riuscite" -#: windows/views.py:496 +#: windows/views.py:535 msgid "Last failure reason" -msgstr "" +msgstr "Ultimo motivo di fallimento" -#: windows/views.py:513 +#: windows/views.py:552 msgid "Total connection attempts" -msgstr "Totale connessioni" +msgstr "Totale tentativi di connessione" -#: windows/views.py:530 +#: windows/views.py:569 msgid "Average connection time (ms)" -msgstr "Media in ms del tempo di connessione" +msgstr "Tempo medio di connessione (ms)" -#: windows/views.py:547 +#: windows/views.py:586 msgid "Most recent connection duration" -msgstr "" +msgstr "Durata connessione più recente" -#: windows/views.py:566 +#: windows/views.py:605 msgid "Statistics" msgstr "Statistiche" -#: windows/views.py:584 windows/views.py:1730 +#: windows/views.py:623 windows/views.py:1850 msgid "Create" msgstr "Crea" -#: windows/views.py:588 +#: windows/views.py:627 msgid "Create connection" -msgstr "Nuova connessione" +msgstr "Crea connessione" -#: windows/views.py:591 +#: windows/views.py:630 msgid "Create directory" -msgstr "Nuova directory" +msgstr "Crea directory" -#: 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 +#: windows/views.py:659 windows/views.py:883 windows/views.py:1520 +#: windows/views.py:1896 windows/views.py:2188 windows/views.py:2592 +#: windows/views.py:2648 msgid "Cancel" msgstr "Annulla" -#: 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 +#: windows/main/controller.py:324 windows/views.py:664 windows/views.py:2193 +#: windows/views.py:2597 windows/views.py:2812 msgid "Save" msgstr "Salva" -#: windows/views.py:632 +#: windows/views.py:671 msgid "Test" msgstr "Testa" -#: windows/views.py:639 +#: windows/views.py:678 msgid "Connect" msgstr "Connetti" -#: windows/views.py:742 +#: windows/views.py:781 windows/views.py:2388 msgid "Language" msgstr "Lingua" -#: windows/views.py:747 +#: windows/views.py:786 msgid "English" msgstr "Inglese" -#: windows/views.py:747 +#: windows/views.py:786 msgid "Italian" msgstr "Italiano" -#: windows/views.py:747 +#: windows/views.py:786 msgid "French" msgstr "Francese" -#: windows/views.py:759 +#: windows/views.py:798 msgid "Locale" msgstr "Localizzazione" -#: windows/views.py:780 -#, fuzzy +#: windows/views.py:819 msgid "Column content" -msgstr "Chiudi connessione" +msgstr "Contenuto colonna" -#: windows/views.py:790 +#: windows/views.py:829 msgid "Syntax" msgstr "Sintassi" -#: windows/views.py:847 +#: windows/views.py:886 msgid "Ok" msgstr "Ok" -#: windows/views.py:878 +#: windows/views.py:917 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:887 +#: windows/views.py:926 msgid "File" msgstr "File" -#: windows/views.py:890 +#: windows/views.py:929 msgid "About" msgstr "Informazioni" -#: windows/views.py:893 +#: windows/views.py:932 msgid "Help" msgstr "Aiuto" -#: windows/views.py:898 +#: windows/views.py:937 msgid "Open connection manager" msgstr "Apri gestore connessioni" -#: windows/views.py:902 +#: windows/views.py:941 msgid "Disconnect from server" msgstr "Disconnetti dal server" -#: windows/views.py:904 +#: windows/views.py:943 msgid "tool" msgstr "strumento" -#: windows/views.py:904 +#: windows/views.py:943 windows/views.py:2628 msgid "Refresh" msgstr "Aggiorna" -#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 -#: windows/views.py:2077 windows/views.py:2221 +#: windows/views.py:947 windows/views.py:949 windows/views.py:1874 +#: windows/views.py:2632 windows/views.py:2798 msgid "Add" msgstr "Aggiungi" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 +#: windows/views.py:954 +#, python-brace-format +msgid "{mode}" +msgstr "{mode}" + +#: windows/views.py:991 windows/views.py:995 windows/views.py:2765 +#: windows/views.py:2770 msgid "MyMenuItem" msgstr "IlMioElementoMenu" -#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 +#: windows/views.py:998 windows/views.py:1924 windows/views.py:2777 msgid "MyMenu" msgstr "IlMioMenu" -#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 -#: windows/views.py:1402 +#: windows/views.py:1013 windows/views.py:1548 windows/views.py:1555 +#: windows/views.py:1562 msgid "MyLabel" msgstr "LaMiaEtichetta" -#: windows/views.py:972 +#: windows/views.py:1019 msgid "Databases" msgstr "Database" -#: windows/views.py:973 windows/views.py:1340 +#: windows/views.py:1020 windows/views.py:1367 msgid "Size" msgstr "Dimensione" -#: windows/views.py:974 +#: windows/views.py:1021 msgid "Elements" msgstr "Elementi" -#: windows/views.py:975 +#: windows/views.py:1022 msgid "Modified at" msgstr "Modificato il" -#: windows/views.py:976 +#: windows/views.py:1023 windows/views.py:1382 msgid "Tables" msgstr "Tabelle" -#: windows/views.py:983 +#: windows/views.py:1030 msgid "System" msgstr "Sistema" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1029 -#: windows/views.py:1344 +#: windows/components/dataview.py:89 windows/views.py:1076 +#: windows/views.py:1371 msgid "Collation" msgstr "Ordinamento" -#: windows/views.py:1058 +#: windows/views.py:1105 msgid "Encryption" -msgstr "" +msgstr "Crittografia" -#: windows/views.py:1070 +#: windows/main/controller.py:1272 windows/main/controller.py:1294 +#: windows/main/controller.py:1298 windows/views.py:1117 msgid "Read Only" -msgstr "" +msgstr "Sola lettura" -#: windows/views.py:1087 -#, fuzzy +#: windows/views.py:1134 msgid "Tablespace" -msgstr "Tabelle" +msgstr "Tablespace" -#: windows/views.py:1108 -#, fuzzy +#: windows/views.py:1155 msgid "Connection limit" -msgstr "Connessione persa" +msgstr "Limite connessioni" -#: windows/views.py:1151 -#, fuzzy +#: windows/views.py:1198 msgid "Profile" -msgstr "File" +msgstr "Profilo" -#: windows/views.py:1177 -#, fuzzy +#: windows/views.py:1224 msgid "Default tablespace" -msgstr "Elimina tabella" +msgstr "Tablespace predefinito" -#: windows/views.py:1198 -#, fuzzy +#: windows/views.py:1245 msgid "Temporary tablespace" -msgstr "Temporaneo" +msgstr "Tablespace temporaneo" -#: windows/views.py:1224 +#: windows/views.py:1271 msgid "Quota" -msgstr "" +msgstr "Quota" -#: windows/views.py:1243 +#: windows/views.py:1290 msgid "Unlimited quota" -msgstr "" +msgstr "Quota illimitata" -#: windows/views.py:1260 +#: windows/views.py:1307 msgid "Account status" -msgstr "" +msgstr "Stato account" -#: windows/views.py:1281 -#, fuzzy +#: windows/views.py:1328 msgid "Password expire" -msgstr "Password" +msgstr "Scadenza password" -#: windows/views.py:1302 -msgid "Table:" -msgstr "Tabella:" +#: windows/views.py:1352 +msgid "Add new table" +msgstr "Aggiungi nuova tabella" -#: 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:1354 +msgid "Clone table" +msgstr "Clona tabella" -#: windows/views.py:1315 -msgid "Clone" -msgstr "Clona" +#: windows/main/controller.py:1821 windows/views.py:1356 +msgid "Delete table" +msgstr "Elimina tabella" -#: windows/views.py:1339 +#: windows/views.py:1366 windows/views.py:2457 msgid "Rows" msgstr "Righe" -#: windows/views.py:1342 +#: windows/views.py:1369 msgid "Updated at" msgstr "Aggiornato il" -#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 -#: windows/views.py:2173 windows/views.py:2709 +#: windows/views.py:1387 +msgid "Add new view" +msgstr "Aggiungi nuova vista" + +#: windows/views.py:1389 +msgid "Clone view" +msgstr "Clona" + +#: windows/views.py:1391 +msgid "Delete view" +msgstr "Elimina" + +#: windows/views.py:1399 windows/views.py:1423 windows/views.py:1447 +#: windows/views.py:1471 windows/views.py:1495 +msgid "Definition" +msgstr "Definizione" + +#: windows/views.py:1406 +msgid "Views" +msgstr "Viste" + +#: windows/views.py:1411 +msgid "Add new procedure" +msgstr "Aggiungi nuova procedura" + +#: windows/views.py:1413 +msgid "Clone procedure" +msgstr "Clona procedura" + +#: windows/views.py:1415 +msgid "Delete procedure" +msgstr "Elimina procedura" + +#: windows/views.py:1430 +msgid "Procedures" +msgstr "Procedure" + +#: windows/views.py:1435 +msgid "Add new function" +msgstr "Aggiungi nuova funzione" + +#: windows/views.py:1437 +msgid "Clone function" +msgstr "Clona funzione" + +#: windows/views.py:1439 +msgid "Delete function" +msgstr "Elimina funzione" + +#: windows/views.py:1454 +msgid "Functions" +msgstr "Funzioni" + +#: windows/views.py:1459 +msgid "Add new trigger" +msgstr "Aggiungi nuovo trigger" + +#: windows/views.py:1461 +msgid "Clone trigger" +msgstr "Clona trigger" + +#: windows/views.py:1463 +msgid "Delete trigger" +msgstr "Elimina trigger" + +#: windows/views.py:1478 +msgid "Triggers" +msgstr "Trigger" + +#: windows/views.py:1483 +msgid "Add new event" +msgstr "Aggiungi nuovo evento" + +#: windows/views.py:1485 +msgid "Clone event" +msgstr "Clona" + +#: windows/views.py:1487 +msgid "Delete event" +msgstr "Elimina evento" + +#: windows/views.py:1502 +msgid "Events" +msgstr "Eventi" + +#: windows/views.py:1530 windows/views.py:1901 windows/views.py:2646 +#: windows/views.py:2730 msgid "Apply" msgstr "Applica" -#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 -#: windows/views.py:2909 +#: windows/views.py:1542 windows/views.py:1721 msgid "Options" msgstr "Opzioni" -#: windows/views.py:1413 +#: windows/views.py:1573 msgid "Diagram" msgstr "Diagramma" -#: windows/views.py:1424 +#: windows/views.py:1584 msgid "Database" msgstr "Database" -#: windows/views.py:1479 windows/views.py:2849 +#: windows/views.py:1639 msgid "Base" msgstr "Base" -#: windows/views.py:1493 windows/views.py:2863 +#: windows/views.py:1653 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1521 windows/views.py:2891 +#: windows/views.py:1681 msgid "Default Collation" msgstr "Ordinamento predefinito" -#: windows/views.py:1531 +#: windows/views.py:1691 msgid "Convert data" -msgstr "" +msgstr "Converti dati" -#: windows/views.py:1539 +#: windows/views.py:1699 msgid "Row format" -msgstr "" +msgstr "Formato riga" -#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 -#: windows/views.py:1756 windows/views.py:2081 +#: windows/views.py:1732 windows/views.py:1760 windows/views.py:1788 +#: windows/views.py:1876 windows/views.py:2326 windows/views.py:2636 msgid "Remove" msgstr "Rimuovi" -#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 +#: windows/views.py:1734 windows/views.py:1762 windows/views.py:1790 +#: windows/views.py:2328 windows/views.py:2738 msgid "Clear" msgstr "Pulisci" -#: windows/views.py:1595 windows/views.py:2923 +#: windows/views.py:1747 msgid "Indexes" msgstr "Indici" -#: windows/views.py:1639 +#: windows/views.py:1758 windows/views.py:1786 windows/views.py:2324 +msgid "Insert" +msgstr "Inserisci" + +#: windows/views.py:1775 msgid "Foreign Keys" msgstr "Chiavi esterne" -#: windows/views.py:1683 +#: windows/views.py:1803 msgid "Checks" msgstr "Vincoli" -#: windows/views.py:1750 windows/views.py:2944 +#: windows/views.py:1870 msgid "Columns:" msgstr "Colonne:" -#: windows/views.py:1760 -#, fuzzy +#: windows/views.py:1880 msgid "Move Up" msgstr "Sposta su\tCTRL+UP" -#: windows/views.py:1762 -#, fuzzy +#: windows/views.py:1882 msgid "Move Down" msgstr "Sposta giù\tCTRL+D" -#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 -#: windows/views.py:3017 +#: windows/views.py:1914 windows/views.py:1921 msgid "Add Index" msgstr "Aggiungi indice" -#: windows/views.py:1798 windows/views.py:3014 +#: windows/views.py:1918 msgid "Add PrimaryKey" msgstr "Aggiungi chiave primaria" -#: windows/views.py:1815 +#: windows/views.py:1935 msgid "Table" msgstr "Tabella" -#: windows/views.py:1851 -#, fuzzy -msgid "Definer" -msgstr "Inserisci" - -#: windows/views.py:1871 +#: windows/views.py:1977 windows/views.py:2243 msgid "Schema" -msgstr "" +msgstr "Schema" -#: windows/views.py:1897 -msgid "SQL security" -msgstr "" +#: windows/views.py:2005 windows/views.py:2314 +msgid "General" +msgstr "Generale" -#: windows/views.py:1904 -#, fuzzy -msgid "DEFINER" -msgstr "Inserisci" - -#: windows/views.py:1904 -#, fuzzy -msgid "INVOKER" -msgstr "Inserisci" - -#: windows/views.py:1916 +#: windows/views.py:2010 msgid "Algorithm" -msgstr "" +msgstr "Algoritmo" -#: windows/views.py:1918 -#, fuzzy +#: windows/views.py:2012 msgid "UNDEFINED" msgstr "Senza segno" -#: windows/views.py:1921 +#: windows/views.py:2015 msgid "MERGE" -msgstr "" +msgstr "MERGE" -#: windows/views.py:1924 -#, fuzzy +#: windows/views.py:2018 msgid "TEMPTABLE" -msgstr "Tabella" +msgstr "TEMPTABLE" -#: windows/views.py:1934 +#: windows/views.py:2028 msgid "View constraint" -msgstr "" +msgstr "Vincolo vista" -#: windows/views.py:1936 -#, fuzzy +#: windows/views.py:2030 msgid "None" -msgstr "Clona" +msgstr "Nessuno" -#: windows/views.py:1939 -#, fuzzy +#: windows/views.py:2033 msgid "LOCAL" -msgstr "Localizzazione" +msgstr "LOCALE" -#: windows/views.py:1942 -#, fuzzy +#: windows/views.py:2036 msgid "CASCADE" -msgstr "Annulla" +msgstr "A CASCATA" -#: windows/views.py:1945 -#, fuzzy +#: windows/views.py:2039 msgid "CHECK ONLY" -msgstr "Verifica" +msgstr "SOLO VERIFICA" -#: windows/views.py:1948 +#: windows/views.py:2042 msgid "READ ONLY" -msgstr "" +msgstr "SOLA LETTURA" + +#: windows/views.py:2055 windows/views.py:2483 +msgid "Behavior" +msgstr "Comportamento" + +#: windows/views.py:2062 windows/views.py:2490 +msgid "Definer" +msgstr "Definitore" + +#: windows/views.py:2070 windows/views.py:2498 +msgid "*" +msgstr "*" + +#: windows/views.py:2082 windows/views.py:2510 +msgid "SQL security" +msgstr "Sicurezza SQL" + +#: windows/views.py:2089 windows/views.py:2517 +msgid "DEFINER" +msgstr "DEFINITORE" -#: windows/views.py:1960 +#: windows/views.py:2089 windows/views.py:2517 +msgid "INVOKER" +msgstr "INVOCATORE" + +#: windows/views.py:2103 msgid "Force" -msgstr "" +msgstr "Forza" -#: windows/views.py:1972 +#: windows/views.py:2115 msgid "Security barrier" -msgstr "" +msgstr "Barriera di sicurezza" -#: windows/views.py:2054 -msgid "Views" -msgstr "Viste" +#: windows/views.py:2128 windows/views.py:2532 +msgid "Security" +msgstr "Sicurezza" -#: windows/views.py:2062 -msgid "Triggers" -msgstr "Trigger" +#: windows/views.py:2205 +msgid "View" +msgstr "Viste" -#: windows/views.py:2073 +#: windows/views.py:2262 #, fuzzy -msgid "Refrsh" -msgstr "Aggiorna" +msgid "Type" +msgstr "Tipo di dati" -#: windows/views.py:2079 -#, fuzzy +#: windows/views.py:2269 +msgid "Procedure (doesn't return a result)" +msgstr "Procedura (non restituisce un risultato)" + +#: windows/views.py:2269 +msgid "Function (return a result)" +msgstr "Funzione (restituisce un risultato)" + +#: windows/views.py:2279 +msgid "Return type" +msgstr "Tipo di ritorno" + +#: windows/views.py:2299 +msgid "Comment" +msgstr "Commento" + +#: windows/components/dataview.py:111 windows/views.py:2335 +msgid "#" +msgstr "#" + +#: windows/views.py:2337 +msgid "Datatype" +msgstr "Tipo di dati" + +#: windows/views.py:2338 +msgid "Context" +msgstr "Contesto" + +#: windows/views.py:2345 +msgid "Parameters" +msgstr "Parametri" + +#: windows/views.py:2354 +msgid "Data access" +msgstr "Accesso dati" + +#: windows/views.py:2361 +msgid "CONTAINS SQL" +msgstr "CONTAINS SQL" + +#: windows/views.py:2361 +msgid "NO SQL" +msgstr "NO SQL" + +#: windows/views.py:2361 +msgid "READS SQL DATA" +msgstr "READS SQL DATA" + +#: windows/views.py:2361 +msgid "MODIFIES SQL DATA" +msgstr "MODIFIES SQL DATA" + +#: windows/views.py:2371 +msgid "Deterministic" +msgstr "Deterministico" + +#: windows/views.py:2395 +msgid "SQL" +msgstr "SQL" + +#: windows/views.py:2395 +msgid "PLPGSQL" +msgstr "PLPGSQL" + +#: windows/views.py:2405 +msgid "Volatility" +msgstr "Volatilità" + +#: windows/views.py:2412 +msgid "VOLATILE" +msgstr "VOLATILE" + +#: windows/views.py:2412 +msgid "STABLE" +msgstr "STABILE" + +#: windows/views.py:2412 +msgid "IMMUTABLE" +msgstr "IMMUTABILE" + +#: windows/views.py:2422 +msgid "Parallel" +msgstr "Parallelo" + +#: windows/views.py:2429 +msgid "UNSAFE" +msgstr "UNSAFE" + +#: windows/views.py:2429 +msgid "RESTRICTED" +msgstr "RESTRICTED" + +#: windows/views.py:2429 +msgid "SAFE" +msgstr "SAFE" + +#: windows/views.py:2441 +msgid "Cost" +msgstr "Costo" + +#: windows/views.py:2609 +msgid "Routine" +msgstr "Routine" + +#: windows/views.py:2617 +msgid "Trigger" +msgstr "Trigger" + +#: windows/views.py:2634 msgid "Duplicate" -msgstr "Duplica record" +msgstr "Duplica" -#: windows/views.py:2085 +#: windows/views.py:2640 msgid "Apply changes automatically" msgstr "Applica modifiche automaticamente" -#: windows/views.py:2087 windows/views.py:2088 +#: windows/views.py:2642 windows/views.py:2643 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -686,125 +893,87 @@ msgstr "" "Se abilitato, le modifiche alla tabella vengono applicate immediatamente " "senza premere Applica o Annulla" -#: windows/views.py:2101 +#: windows/views.py:2656 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" +msgstr "{database_name}.{table_name} - righe {from_row} - {to_row} di {total_rows}" -#: windows/views.py:2109 -#, fuzzy +#: windows/views.py:2664 msgid "First" -msgstr "Filtri" +msgstr "Primo" -#: windows/views.py:2127 +#: windows/views.py:2682 msgid "Last" -msgstr "" +msgstr "Ultimo" -#: windows/views.py:2136 +#: windows/views.py:2691 msgid "Filters" msgstr "Filtri" -#: windows/views.py:2176 +#: windows/views.py:2733 +msgid "" +"Apply filters in data\n" +"CTRL+ENTER" +msgstr "" +"Applica filtri ai dati\n" +"CTRL+ENTER" + +#: windows/views.py:2734 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2196 +#: windows/views.py:2762 msgid "Insert row" msgstr "Inserisci riga" -#: windows/views.py:2204 +#: windows/components/popup.py:31 windows/views.py:2774 +msgid "NULL" +msgstr "NULL" + +#: windows/views.py:2781 msgid "Data" msgstr "Dati" -#: windows/main/controller.py:278 windows/main/controller.py:287 -#: windows/main/controller.py:288 windows/views.py:2221 -#, fuzzy +#: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" -msgstr "Query" +msgstr "Nuova query" -#: windows/views.py:2223 windows/views.py:2660 +#: windows/views.py:2800 msgid "Close" msgstr "Chiudi" -#: windows/main/controller.py:279 windows/main/controller.py:289 -#: windows/main/controller.py:290 windows/views.py:2223 -#, fuzzy +#: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" -msgstr "Query" +msgstr "Chiudi query" -#: windows/views.py:2227 +#: windows/views.py:2804 msgid "Run" -msgstr "" +msgstr "Esegui" -#: windows/main/controller.py:280 windows/main/controller.py:292 -#: windows/main/controller.py:293 windows/views.py:2227 -#, fuzzy +#: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" -msgstr "Eseguibile SSH" +msgstr "Esegui" -#: windows/views.py:2229 +#: windows/views.py:2806 msgid "Run all" -msgstr "" +msgstr "Esegui tutto" -#: windows/main/controller.py:295 windows/views.py:2229 +#: windows/views.py:2806 msgid "Execute all statements" -msgstr "" +msgstr "Esegui tutte le istruzioni" -#: windows/main/controller.py:282 windows/main/controller.py:297 -#: windows/main/controller.py:298 windows/views.py:2231 +#: windows/main/controller.py:323 windows/views.py:2808 msgid "Stop" -msgstr "" +msgstr "Ferma" -#: windows/views.py:2287 +#: windows/views.py:2873 msgid "a page" -msgstr "" +msgstr "una pagina" -#: windows/views.py:2315 +#: windows/views.py:2923 msgid "Query" msgstr "Query" -#: windows/views.py:2626 -#, fuzzy -msgid "Character set" -msgstr "Creato il" - -#: windows/views.py:2644 windows/views.py:2663 -msgid "New" -msgstr "Nuovo" - -#: windows/views.py:2683 -msgid "Insert record" -msgstr "Inserisci record" - -#: windows/views.py:2688 -msgid "Duplicate record" -msgstr "Duplica record" - -#: windows/views.py:2695 -msgid "Delete record" -msgstr "Elimina record" - -#: windows/views.py:2733 windows/views.py:2964 -msgid "Up" -msgstr "Su" - -#: windows/views.py:2740 windows/views.py:2971 -msgid "Down" -msgstr "Giù" - -#: windows/views.py:3100 -msgid "Save Starments" -msgstr "" - -#: windows/views.py:3108 -#, fuzzy -msgid "Location" -msgstr "Ordinamento" - -#: windows/views.py:3115 -msgid "*.sql" -msgstr "" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 msgid "Allow NULL" @@ -825,7 +994,7 @@ 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/components/dataview.py:85 windows/components/dataview.py:256 msgid "Expression" msgstr "Espressione" @@ -837,75 +1006,71 @@ msgstr "Senza segno" msgid "Zerofill" msgstr "Riempimento zero" -#: windows/components/dataview.py:109 -msgid "#" -msgstr "#" - -#: windows/components/dataview.py:117 +#: windows/components/dataview.py:119 msgid "Data type" msgstr "Tipo di dati" -#: windows/components/dataview.py:121 +#: windows/components/dataview.py:123 msgid "Length/Set" msgstr "Lunghezza/Insieme" -#: windows/components/dataview.py:155 +#: windows/components/dataview.py:169 msgid "Add column\tCTRL+INS" msgstr "Aggiungi colonna\tCTRL+INS" -#: windows/components/dataview.py:161 +#: windows/components/dataview.py:175 msgid "Remove column\tCTRL+DEL" msgstr "Rimuovi colonna\tCTRL+DEL" -#: windows/components/dataview.py:169 +#: windows/components/dataview.py:183 msgid "Move up\tCTRL+UP" msgstr "Sposta su\tCTRL+UP" -#: windows/components/dataview.py:176 +#: windows/components/dataview.py:190 msgid "Move down\tCTRL+D" msgstr "Sposta giù\tCTRL+D" -#: windows/components/dataview.py:199 +#: windows/components/dataview.py:214 msgid "Create new index" msgstr "Crea nuovo indice" -#: windows/components/dataview.py:214 +#: windows/components/dataview.py:229 msgid "Append to index" msgstr "Aggiungi all'indice" -#: windows/components/dataview.py:228 +#: windows/components/dataview.py:243 msgid "Column(s)/Expression" msgstr "Colonna(e)/Espressione" -#: windows/components/dataview.py:229 +#: windows/components/dataview.py:244 msgid "Condition" msgstr "Condizione" -#: windows/components/dataview.py:259 +#: windows/components/dataview.py:274 msgid "Column(s)" msgstr "Colonna(e)" -#: windows/components/dataview.py:265 +#: windows/components/dataview.py:280 msgid "Reference table" msgstr "Tabella di riferimento" -#: windows/components/dataview.py:271 +#: windows/components/dataview.py:286 msgid "Reference column(s)" msgstr "Colonna(e) di riferimento" -#: windows/components/dataview.py:277 +#: windows/components/dataview.py:292 msgid "On UPDATE" msgstr "Su AGGIORNAMENTO" -#: windows/components/dataview.py:283 +#: windows/components/dataview.py:298 msgid "On DELETE" msgstr "Su ELIMINA" -#: windows/components/dataview.py:298 +#: windows/components/dataview.py:313 msgid "Add foreign key" msgstr "Aggiungi chiave esterna" -#: windows/components/dataview.py:304 +#: windows/components/dataview.py:319 msgid "Remove foreign key" msgstr "Rimuovi chiave esterna" @@ -913,10 +1078,6 @@ msgstr "Rimuovi chiave esterna" msgid "No default value" msgstr "Nessun valore predefinito" -#: windows/components/popup.py:31 -msgid "NULL" -msgstr "NULL" - #: windows/components/popup.py:35 msgid "AUTO INCREMENT" msgstr "AUTO INCREMENTO" @@ -925,164 +1086,175 @@ msgstr "AUTO INCREMENTO" msgid "Text/Expression" msgstr "Testo/Espressione" -#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 +#: windows/dialogs/connections/view.py:126 windows/main/query/renderer.py:192 msgid "Unknown error" -msgstr "" +msgstr "Errore sconosciuto" -#: windows/dialogs/connections/view.py:414 +#: windows/dialogs/connections/view.py:416 msgid "Connection established successfully" -msgstr "" +msgstr "Connessione stabilita con successo" -#: windows/dialogs/connections/view.py:426 +#: windows/dialogs/connections/view.py:428 #, python-brace-format msgid "Do you want save the connection {connection_name}?" -msgstr "" +msgstr "Vuoi salvare la connessione {connection_name}?" -#: windows/dialogs/connections/view.py:429 +#: windows/dialogs/connections/view.py:431 msgid "Confirm save" msgstr "Conferma salvataggio" -#: windows/dialogs/connections/view.py:481 +#: windows/dialogs/connections/view.py:483 msgid "You have unsaved changes. Do you want to save them before continuing?" -msgstr "" +msgstr "Hai modifiche non salvate. Vuoi salvarle prima di continuare?" -#: windows/dialogs/connections/view.py:483 +#: windows/dialogs/connections/view.py:485 msgid "Unsaved changes" -msgstr "" +msgstr "Modifiche non salvate" -#: windows/dialogs/connections/view.py:750 +#: windows/dialogs/connections/view.py:773 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" +"Questa connessione non può funzionare senza TLS. TLS è stato abilitato " +"automaticamente." -#: windows/dialogs/connections/view.py:775 -#, fuzzy, python-brace-format +#: windows/dialogs/connections/view.py:798 +#, python-brace-format msgid "" "Connection error:\n" "{error}" -msgstr "Errore di connessione" +msgstr "" +"Errore di connessione:\n" +"{error}" -#: windows/dialogs/connections/view.py:776 +#: windows/dialogs/connections/view.py:799 msgid "Connection error" msgstr "Errore di connessione" -#: windows/dialogs/connections/view.py:802 -#, fuzzy, python-brace-format +#: windows/dialogs/connections/view.py:825 +#, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" -msgstr "Vuoi eliminare i record?" +msgstr "Vuoi eliminare la connessione '{connection_name}'?" -#: windows/dialogs/connections/view.py:805 -#: windows/dialogs/connections/view.py:822 +#: windows/dialogs/connections/view.py:828 +#: windows/dialogs/connections/view.py:845 msgid "Confirm delete" msgstr "Conferma eliminazione" -#: windows/dialogs/connections/view.py:819 -#, fuzzy, python-brace-format +#: windows/dialogs/connections/view.py:842 +#, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" -msgstr "Vuoi eliminare i record?" +msgstr "Vuoi eliminare la directory '{directory_name}'?" -#: windows/main/controller.py:275 +#: windows/main/controller.py:316 #, python-brace-format msgid "{text} ({shortcut})" -msgstr "" +msgstr "{text} ({shortcut})" -#: windows/main/controller.py:281 windows/main/controller.py:294 -#, fuzzy +#: windows/main/controller.py:322 msgid "Execute all" -msgstr "Eseguibile SSH" +msgstr "Esegui tutto" -#: windows/main/controller.py:471 windows/main/controller.py:479 -#, fuzzy +#: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" -msgstr "Query" +msgstr "Query (1)" -#: windows/main/controller.py:497 +#: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" -msgstr "" +msgstr "Query ({query_number})" -#: windows/main/controller.py:530 +#: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" -msgstr "" +msgstr "Hai modifiche non salvate. Salvare prima di chiudere?" -#: windows/main/controller.py:531 +#: windows/main/controller.py:519 msgid "Unsaved query" -msgstr "" +msgstr "Query non salvata" -#: windows/main/controller.py:576 -#, fuzzy +#: windows/main/controller.py:564 msgid "Save query" -msgstr "Query" +msgstr "Salva query" -#: windows/main/controller.py:579 +#: windows/main/controller.py:567 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" -msgstr "" +msgstr "File SQL (*.sql)|*.sql|Tutti i file (*.*)|*.*" -#: 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 +#: windows/main/controller.py:595 windows/main/controller.py:626 +#: windows/main/controller.py:653 windows/main/database/list.py:119 +#: windows/main/database/routine.py:591 windows/main/database/routine.py:635 +#: windows/main/database/view.py:268 windows/main/database/view.py:297 +#: windows/main/query/controller.py:179 msgid "Error" msgstr "Errore" -#: windows/main/controller.py:622 +#: windows/main/controller.py:633 #, python-brace-format msgid "-- Saved query to {file_path}" -msgstr "" +msgstr "-- Query salvata in {file_path}" -#: windows/main/controller.py:647 +#: windows/main/controller.py:659 #, python-brace-format msgid "-- Autosaved query to {file_path}" -msgstr "" +msgstr "-- Query salvata automaticamente in {file_path}" -#: windows/main/controller.py:704 +#: windows/main/controller.py:725 msgid "days" msgstr "giorni" -#: windows/main/controller.py:705 +#: windows/main/controller.py:726 msgid "hours" msgstr "ore" -#: windows/main/controller.py:706 +#: windows/main/controller.py:727 msgid "minutes" msgstr "minuti" -#: windows/main/controller.py:707 +#: windows/main/controller.py:728 msgid "seconds" msgstr "secondi" -#: windows/main/controller.py:715 +#: windows/main/controller.py:736 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizzata: {used} ({percentage:.2%})" -#: windows/main/controller.py:751 +#: windows/main/controller.py:772 msgid "Settings saved successfully" -msgstr "" +msgstr "Impostazioni salvate con successo" -#: windows/main/controller.py:952 +#: windows/main/controller.py:1006 #, python-brace-format msgid "~{estimated} (Loading...)" -msgstr "" +msgstr "~{estimated} (Caricamento...)" -#: windows/main/controller.py:954 +#: windows/main/controller.py:1008 msgid "~ (Loading...)" -msgstr "" +msgstr "~ (Caricamento...)" + +#: windows/main/controller.py:1243 +msgid "Write Mode (2:00)" +msgstr "Modalità scrittura (2:00)" -#: windows/main/controller.py:1119 +#: windows/main/controller.py:1294 +msgid "Write Mode" +msgstr "Modalità scrittura" + +#: windows/main/controller.py:1305 msgid "Version" msgstr "Versione" -#: windows/main/controller.py:1121 +#: windows/main/controller.py:1307 msgid "Uptime" msgstr "Tempo di attività" -#: windows/main/controller.py:1199 +#: windows/main/controller.py:1444 #, python-brace-format msgid "Do you want discard the change to {database_name}?" -msgstr "" +msgstr "Vuoi annullare le modifiche a {database_name}?" -#: windows/main/controller.py:1232 +#: windows/main/controller.py:1477 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1091,120 +1263,188 @@ msgid "" "- Yes: open dump flow (coming soon, no drop).\n" "- No: drop the database now." msgstr "" +"Vuoi creare un dump prima di eliminare il database '{database_name}'?\n" +"\n" +"Il dump non è ancora implementato.\n" +"- Sì: apri flusso dump (prossimamente, nessuna eliminazione).\n" +"- No: elimina il database ora." -#: windows/main/controller.py:1237 windows/main/controller.py:1258 -#, fuzzy +#: windows/main/controller.py:1482 windows/main/controller.py:1503 msgid "Delete database" -msgstr "Elimina tabella" +msgstr "Elimina database" -#: windows/main/controller.py:1243 +#: windows/main/controller.py:1488 msgid "Dump is not implemented yet. No action has been performed." -msgstr "" +msgstr "Il dump non è ancora implementato. Nessuna azione è stata eseguita." -#: windows/main/controller.py:1244 +#: windows/main/controller.py:1489 msgid "Dump not available" -msgstr "" +msgstr "Dump non disponibile" -#: windows/main/controller.py:1257 +#: windows/main/controller.py:1502 msgid "Database deletion is not supported by this engine." -msgstr "" +msgstr "L'eliminazione del database non è supportata da questo motore." -#: windows/main/controller.py:1272 +#: windows/main/controller.py:1517 msgid "Database deleted successfully" -msgstr "" +msgstr "Database eliminato con successo" -#: windows/main/controller.py:1273 windows/main/database/view.py:253 -#: windows/main/database/view.py:279 +#: windows/main/controller.py:1518 windows/main/database/routine.py:556 +#: windows/main/database/routine.py:620 windows/main/database/view.py:256 +#: windows/main/database/view.py:291 msgid "Success" -msgstr "" +msgstr "Successo" -#: windows/main/controller.py:1392 +#: windows/main/controller.py:1792 #, python-brace-format msgid "Do you want discard the change to {table_name}?" -msgstr "" +msgstr "Vuoi annullare le modifiche a {table_name}?" -#: windows/main/controller.py:1418 -#, fuzzy, python-brace-format +#: windows/main/controller.py:1818 +#, 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" +msgstr "Vuoi eliminare la tabella {table_name}?" -#: windows/main/controller.py:1440 +#: windows/main/controller.py:1840 #, python-brace-format msgid "{table_name} (COPY)" -msgstr "" +msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1563 +#: windows/main/controller.py:1999 msgid "Do you want delete the records?" msgstr "Vuoi eliminare i record?" -#: windows/main/database/list.py:69 +#: windows/main/controller.py:2104 +#, fuzzy +msgid "Database connection lost. Do you want to reconnect?" +msgstr "Vuoi riconnetterti?" + +#: windows/main/controller.py:2105 windows/main/database/list.py:108 +msgid "Connection lost" +msgstr "Connessione persa" + +#: windows/main/controller.py:2113 +#, fuzzy +msgid "Connection restored successfully." +msgstr "Connessione stabilita con successo" + +#: windows/main/controller.py:2114 +#, fuzzy +msgid "Connection restored" +msgstr "Errore di connessione" + +#: windows/main/controller.py:2120 +#, python-brace-format +msgid "Could not reconnect: {error}" +msgstr "" + +#: windows/main/controller.py:2121 +#, fuzzy +msgid "Reconnection failed" +msgstr "Riconnessione fallita:" + +#: windows/main/database/list.py:104 msgid "The connection to the database was lost." msgstr "La connessione al database è stata persa." -#: windows/main/database/list.py:71 +#: windows/main/database/list.py:106 msgid "Do you want to reconnect?" msgstr "Vuoi riconnetterti?" -#: windows/main/database/list.py:73 -msgid "Connection lost" -msgstr "Connessione persa" - -#: windows/main/database/list.py:83 +#: windows/main/database/list.py:118 msgid "Reconnection failed:" msgstr "Riconnessione fallita:" -#: windows/main/database/view.py:252 +#: windows/main/database/routine.py:545 +msgid "Function created successfully" +msgstr "Funzione creata con successo" + +#: windows/main/database/routine.py:547 +msgid "Function updated successfully" +msgstr "Funzione aggiornata con successo" + +#: windows/main/database/routine.py:551 +msgid "Procedure created successfully" +msgstr "Procedura creata con successo" + +#: windows/main/database/routine.py:553 +msgid "Procedure updated successfully" +msgstr "Procedura aggiornata con successo" + +#: windows/main/database/routine.py:590 +#, python-brace-format +msgid "Error saving routine: {}" +msgstr "Errore nel salvataggio della routine: {}" + +#: windows/main/database/routine.py:604 +msgid "Function" +msgstr "Funzione" + +#: windows/main/database/routine.py:604 +msgid "Procedure" +msgstr "Procedura" + +#: windows/main/database/routine.py:607 +#, python-brace-format +msgid "Are you sure you want to delete {} '{}'?" +msgstr "Sei sicuro di voler eliminare {} '{}'?" + +#: windows/main/database/routine.py:610 windows/main/database/view.py:282 +msgid "Confirm Delete" +msgstr "Conferma eliminazione" + +#: windows/main/database/routine.py:619 +#, python-brace-format +msgid "{} deleted successfully" +msgstr "{} eliminato con successo" + +#: windows/main/database/routine.py:634 +#, python-brace-format +msgid "Error deleting routine: {}" +msgstr "Errore nell'eliminazione della routine: {}" + +#: windows/main/database/view.py:255 msgid "View created successfully" -msgstr "" +msgstr "Vista creata con successo" -#: windows/main/database/view.py:252 +#: windows/main/database/view.py:255 msgid "View updated successfully" -msgstr "" +msgstr "Vista aggiornata con successo" -#: windows/main/database/view.py:256 +#: windows/main/database/view.py:268 #, python-brace-format msgid "Error saving view: {}" -msgstr "" +msgstr "Errore nel salvataggio della vista: {}" -#: windows/main/database/view.py:269 +#: windows/main/database/view.py:281 #, python-brace-format msgid "Are you sure you want to delete view '{}'?" -msgstr "" - -#: windows/main/database/view.py:270 -#, fuzzy -msgid "Confirm Delete" -msgstr "Conferma eliminazione" +msgstr "Sei sicuro di voler eliminare la vista '{}'?" -#: windows/main/database/view.py:279 +#: windows/main/database/view.py:291 msgid "View deleted successfully" -msgstr "" +msgstr "Vista eliminata con successo" -#: windows/main/database/view.py:282 +#: windows/main/database/view.py:297 #, python-brace-format msgid "Error deleting view: {}" -msgstr "" +msgstr "Errore nell'eliminazione della vista: {}" -#: windows/main/query/controller.py:110 +#: windows/main/query/controller.py:112 #, python-brace-format msgid "{elapsed_ms:.0f} ms" -msgstr "" +msgstr "{elapsed_ms:.0f} ms" -#: windows/main/query/controller.py:112 +#: windows/main/query/controller.py:114 #, python-brace-format msgid "{elapsed_s:.2f} s" -msgstr "" +msgstr "{elapsed_s:.2f} s" -#: windows/main/query/controller.py:115 -#, fuzzy +#: windows/main/query/controller.py:117 msgid "none" -msgstr "Clona" +msgstr "nessuno" -#: windows/main/query/controller.py:121 +#: windows/main/query/controller.py:123 #, python-brace-format msgid "" "Query execution stopped after {elapsed}.\n" @@ -1213,182 +1453,69 @@ msgid "" "Failed: {failed}.\n" "Last statement: #{last}." msgstr "" +"Esecuzione query fermata dopo {elapsed}.\n" +"Istruzioni completate: {completed}/{total}.\n" +"Riuscite: {success}.\n" +"Fallite: {failed}.\n" +"Ultima istruzione: #{last}." -#: windows/main/query/controller.py:134 +#: windows/main/query/controller.py:136 msgid "Query execution cancelled" -msgstr "" +msgstr "Esecuzione query annullata" -#: windows/main/query/controller.py:176 -#, fuzzy +#: windows/main/query/controller.py:178 msgid "No active database connection" -msgstr "Nuova connessione" +msgstr "Nessuna connessione database attiva" + +#: windows/main/query/controller.py:227 +#, fuzzy +msgid "Database connection lost" +msgstr "Connessione persa" + +#: windows/main/query/history.py:55 +msgid "(empty query)" +msgstr "(query vuota)" #: windows/main/query/renderer.py:53 #, python-brace-format msgid "{affected_rows} rows affected" -msgstr "" +msgstr "{affected_rows} righe interessate" #: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 #, python-brace-format msgid "Query {query_number}" -msgstr "" +msgstr "Query {query_number}" #: windows/main/query/renderer.py:65 #, python-brace-format msgid "Query {query_number} (Error)" -msgstr "" +msgstr "Query {query_number} (Errore)" #: windows/main/query/renderer.py:79 #, python-brace-format msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" -msgstr "" +msgstr "Query {query_number} ({rows_count} righe × {columns_count} colonne)" #: windows/main/query/renderer.py:165 #, python-brace-format msgid "{rows_count} rows" -msgstr "" +msgstr "{rows_count} righe" #: windows/main/query/renderer.py:167 #, python-brace-format msgid "{elapsed_ms:.1f} ms" -msgstr "" +msgstr "{elapsed_ms:.1f} ms" #: windows/main/query/renderer.py:171 #, python-brace-format msgid "{warnings_count} warnings" -msgstr "" +msgstr "{warnings_count} avvisi" #: windows/main/query/renderer.py:186 -#, fuzzy msgid "Error:" msgstr "Errore" -#~ msgid "Created at:" -#~ msgstr "" - -#~ msgid "Last connection:" -#~ msgstr "" - -#~ msgid "Successful connections:" -#~ msgstr "" - -#~ msgid "Unsuccessful connections:" -#~ msgstr "" - -#~ msgid "Session name" -#~ msgstr "Nome sessione" - -#~ msgid "Open" -#~ msgstr "" - -#~ msgid "Open session manager" -#~ msgstr "" - -#~ msgid "Foreign Key" -#~ msgstr "" - -#~ msgid "New Session" -#~ msgstr "Nuova sessione" - -#~ 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" +#: windows/main/table/records.py:334 +msgid "Error saving records" +msgstr "Errore nel salvataggio dei record" diff --git a/locale/petersql.pot b/locale/petersql.pot index fcc7476..16a6e5b 100644 --- a/locale/petersql.pot +++ b/locale/petersql.pot @@ -27,736 +27,927 @@ msgstr "" msgid "OpenSSH client not found." msgstr "" -#: structures/engines/mariadb/context.py:611 -#: structures/engines/mysql/context.py:622 -#: structures/engines/postgresql/context.py:645 -#: structures/engines/sqlite/context.py:524 +#: structures/engines/context.py:567 +msgid "This connection is read-only." +msgstr "" + +#: structures/engines/context.py:578 +#, python-brace-format +msgid "Database connection lost: {error}" +msgstr "" + +#: structures/engines/mariadb/context.py:658 +#: structures/engines/mysql/context.py:670 +#: structures/engines/postgresql/context.py:701 +#: structures/engines/sqlite/context.py:564 #, 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 +#: structures/engines/mariadb/context.py:686 +#: structures/engines/mysql/context.py:698 +#: structures/engines/postgresql/context.py:726 +#: structures/engines/sqlite/context.py:588 #, 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 +#: structures/engines/mariadb/context.py:704 +#: structures/engines/mysql/context.py:716 +#: structures/engines/postgresql/context.py:744 +#: structures/engines/sqlite/context.py:606 #, 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 +#: structures/engines/mariadb/context.py:744 +#: structures/engines/mysql/context.py:754 +#: structures/engines/postgresql/context.py:784 +#: structures/engines/sqlite/context.py:646 #, 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 +#: structures/engines/mariadb/context.py:777 +#: structures/engines/mysql/context.py:785 +#: structures/engines/postgresql/context.py:814 +#: structures/engines/sqlite/context.py:674 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:781 +#: structures/engines/mariadb/context.py:830 #, 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 +#: windows/dialogs/connections/view.py:417 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1303 +#: windows/views.py:62 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:1006 -#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 -#: windows/views.py:2821 +#: windows/components/dataview.py:115 windows/components/dataview.py:240 +#: windows/components/dataview.py:253 windows/components/dataview.py:268 +#: windows/views.py:76 windows/views.py:126 windows/views.py:1053 +#: windows/views.py:1365 windows/views.py:1398 windows/views.py:1422 +#: windows/views.py:1446 windows/views.py:1470 windows/views.py:1494 +#: windows/views.py:1611 windows/views.py:1957 windows/views.py:2228 +#: windows/views.py:2336 msgid "Name" msgstr "" -#: windows/views.py:48 windows/views.py:428 +#: windows/views.py:77 windows/views.py:467 msgid "Last connection" msgstr "" -#: windows/dialogs/connections/view.py:653 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:90 msgid "New directory" msgstr "" -#: windows/dialogs/connections/model.py:206 -#: windows/dialogs/connections/view.py:613 windows/views.py:65 +#: windows/dialogs/connections/model.py:212 +#: windows/dialogs/connections/view.py:615 windows/views.py:94 msgid "New connection" msgstr "" -#: windows/views.py:71 +#: windows/views.py:100 msgid "Rename" msgstr "" -#: windows/views.py:76 +#: windows/views.py:105 msgid "Clone connection" msgstr "" -#: 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 +#: windows/views.py:110 windows/views.py:642 windows/views.py:1525 +#: windows/views.py:1893 windows/views.py:2183 windows/views.py:2587 msgid "Delete" msgstr "" -#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 -#: windows/views.py:2876 +#: windows/views.py:140 windows/views.py:1370 windows/views.py:1666 msgid "Engine" msgstr "" -#: windows/views.py:132 +#: windows/views.py:161 msgid "Host + port" msgstr "" -#: windows/views.py:148 +#: windows/views.py:177 msgid "Username" msgstr "" -#: windows/views.py:161 windows/views.py:1132 +#: windows/views.py:190 windows/views.py:1179 msgid "Password" msgstr "" -#: windows/views.py:174 +#: windows/views.py:203 msgid "Connection timeout" msgstr "" -#: windows/views.py:192 +#: windows/views.py:221 msgid "Use TLS" msgstr "" -#: windows/views.py:203 +#: windows/views.py:232 +msgid "Mark read only" +msgstr "" + +#: windows/views.py:243 msgid "Use SSH tunnel" msgstr "" -#: windows/views.py:214 +#: windows/views.py:254 msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 +#: windows/views.py:273 msgid "Filename" msgstr "" -#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 +#: windows/views.py:278 windows/views.py:397 msgid "Select a file" msgstr "" -#: windows/views.py:238 windows/views.py:358 +#: windows/views.py:278 windows/views.py:397 msgid "*.*" msgstr "" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 -#: windows/views.py:2834 +#: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 msgid "Comments" msgstr "" -#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 -#: windows/views.py:884 +#: windows/main/controller.py:772 windows/views.py:309 windows/views.py:769 +#: windows/views.py:923 msgid "Settings" msgstr "" -#: windows/views.py:278 +#: windows/views.py:317 msgid "SSH executable" msgstr "" -#: windows/views.py:283 +#: windows/views.py:322 msgid "ssh" msgstr "" -#: windows/views.py:291 +#: windows/views.py:330 msgid "SSH host + port" msgstr "" -#: windows/views.py:303 +#: windows/views.py:342 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "" -#: windows/views.py:312 +#: windows/views.py:351 msgid "SSH username" msgstr "" -#: windows/views.py:325 +#: windows/views.py:364 msgid "SSH password" msgstr "" -#: windows/views.py:338 +#: windows/views.py:377 msgid "Local port" msgstr "" -#: windows/views.py:344 +#: windows/views.py:383 msgid "if the value is set to 0, the first available port will be used" msgstr "" -#: windows/views.py:353 +#: windows/views.py:392 msgid "Identity file" msgstr "" -#: windows/views.py:369 +#: windows/views.py:408 msgid "Remote host + port" msgstr "" -#: windows/views.py:381 +#: windows/views.py:420 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "" -#: windows/views.py:390 +#: windows/views.py:429 msgid "SSH extra args" msgstr "" -#: windows/views.py:405 +#: windows/views.py:444 msgid "SSH Tunnel" msgstr "" -#: windows/views.py:411 windows/views.py:1341 +#: windows/views.py:450 windows/views.py:1368 msgid "Created at" msgstr "" -#: windows/views.py:445 +#: windows/views.py:484 msgid "Successful connections" msgstr "" -#: windows/views.py:462 +#: windows/views.py:501 msgid "Last successful connection" msgstr "" -#: windows/views.py:479 +#: windows/views.py:518 msgid "Unsuccessful connections" msgstr "" -#: windows/views.py:496 +#: windows/views.py:535 msgid "Last failure reason" msgstr "" -#: windows/views.py:513 +#: windows/views.py:552 msgid "Total connection attempts" msgstr "" -#: windows/views.py:530 +#: windows/views.py:569 msgid "Average connection time (ms)" msgstr "" -#: windows/views.py:547 +#: windows/views.py:586 msgid "Most recent connection duration" msgstr "" -#: windows/views.py:566 +#: windows/views.py:605 msgid "Statistics" msgstr "" -#: windows/views.py:584 windows/views.py:1730 +#: windows/views.py:623 windows/views.py:1850 msgid "Create" msgstr "" -#: windows/views.py:588 +#: windows/views.py:627 msgid "Create connection" msgstr "" -#: windows/views.py:591 +#: windows/views.py:630 msgid "Create directory" msgstr "" -#: 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 +#: windows/views.py:659 windows/views.py:883 windows/views.py:1520 +#: windows/views.py:1896 windows/views.py:2188 windows/views.py:2592 +#: windows/views.py:2648 msgid "Cancel" msgstr "" -#: 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 +#: windows/main/controller.py:324 windows/views.py:664 windows/views.py:2193 +#: windows/views.py:2597 windows/views.py:2812 msgid "Save" msgstr "" -#: windows/views.py:632 +#: windows/views.py:671 msgid "Test" msgstr "" -#: windows/views.py:639 +#: windows/views.py:678 msgid "Connect" msgstr "" -#: windows/views.py:742 +#: windows/views.py:781 windows/views.py:2388 msgid "Language" msgstr "" -#: windows/views.py:747 +#: windows/views.py:786 msgid "English" msgstr "" -#: windows/views.py:747 +#: windows/views.py:786 msgid "Italian" msgstr "" -#: windows/views.py:747 +#: windows/views.py:786 msgid "French" msgstr "" -#: windows/views.py:759 +#: windows/views.py:798 msgid "Locale" msgstr "" -#: windows/views.py:780 +#: windows/views.py:819 msgid "Column content" msgstr "" -#: windows/views.py:790 +#: windows/views.py:829 msgid "Syntax" msgstr "" -#: windows/views.py:847 +#: windows/views.py:886 msgid "Ok" msgstr "" -#: windows/views.py:878 +#: windows/views.py:917 msgid "PeterSQL" msgstr "" -#: windows/views.py:887 +#: windows/views.py:926 msgid "File" msgstr "" -#: windows/views.py:890 +#: windows/views.py:929 msgid "About" msgstr "" -#: windows/views.py:893 +#: windows/views.py:932 msgid "Help" msgstr "" -#: windows/views.py:898 +#: windows/views.py:937 msgid "Open connection manager" msgstr "" -#: windows/views.py:902 +#: windows/views.py:941 msgid "Disconnect from server" msgstr "" -#: windows/views.py:904 +#: windows/views.py:943 msgid "tool" msgstr "" -#: windows/views.py:904 +#: windows/views.py:943 windows/views.py:2628 msgid "Refresh" msgstr "" -#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 -#: windows/views.py:2077 windows/views.py:2221 +#: windows/views.py:947 windows/views.py:949 windows/views.py:1874 +#: windows/views.py:2632 windows/views.py:2798 msgid "Add" msgstr "" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 +#: windows/views.py:954 +#, python-brace-format +msgid "{mode}" +msgstr "" + +#: windows/views.py:991 windows/views.py:995 windows/views.py:2765 +#: windows/views.py:2770 msgid "MyMenuItem" msgstr "" -#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 +#: windows/views.py:998 windows/views.py:1924 windows/views.py:2777 msgid "MyMenu" msgstr "" -#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 -#: windows/views.py:1402 +#: windows/views.py:1013 windows/views.py:1548 windows/views.py:1555 +#: windows/views.py:1562 msgid "MyLabel" msgstr "" -#: windows/views.py:972 +#: windows/views.py:1019 msgid "Databases" msgstr "" -#: windows/views.py:973 windows/views.py:1340 +#: windows/views.py:1020 windows/views.py:1367 msgid "Size" msgstr "" -#: windows/views.py:974 +#: windows/views.py:1021 msgid "Elements" msgstr "" -#: windows/views.py:975 +#: windows/views.py:1022 msgid "Modified at" msgstr "" -#: windows/views.py:976 +#: windows/views.py:1023 windows/views.py:1382 msgid "Tables" msgstr "" -#: windows/views.py:983 +#: windows/views.py:1030 msgid "System" msgstr "" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1029 -#: windows/views.py:1344 +#: windows/components/dataview.py:89 windows/views.py:1076 +#: windows/views.py:1371 msgid "Collation" msgstr "" -#: windows/views.py:1058 +#: windows/views.py:1105 msgid "Encryption" msgstr "" -#: windows/views.py:1070 +#: windows/main/controller.py:1272 windows/main/controller.py:1294 +#: windows/main/controller.py:1298 windows/views.py:1117 msgid "Read Only" msgstr "" -#: windows/views.py:1087 +#: windows/views.py:1134 msgid "Tablespace" msgstr "" -#: windows/views.py:1108 +#: windows/views.py:1155 msgid "Connection limit" msgstr "" -#: windows/views.py:1151 +#: windows/views.py:1198 msgid "Profile" msgstr "" -#: windows/views.py:1177 +#: windows/views.py:1224 msgid "Default tablespace" msgstr "" -#: windows/views.py:1198 +#: windows/views.py:1245 msgid "Temporary tablespace" msgstr "" -#: windows/views.py:1224 +#: windows/views.py:1271 msgid "Quota" msgstr "" -#: windows/views.py:1243 +#: windows/views.py:1290 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1260 +#: windows/views.py:1307 msgid "Account status" msgstr "" -#: windows/views.py:1281 +#: windows/views.py:1328 msgid "Password expire" msgstr "" -#: windows/views.py:1302 -msgid "Table:" +#: windows/views.py:1352 +msgid "Add new table" msgstr "" -#: windows/views.py:1310 windows/views.py:1609 windows/views.py:1653 -#: windows/views.py:2721 windows/views.py:2952 -msgid "Insert" +#: windows/views.py:1354 +msgid "Clone table" msgstr "" -#: windows/views.py:1315 -msgid "Clone" +#: windows/main/controller.py:1821 windows/views.py:1356 +msgid "Delete table" msgstr "" -#: windows/views.py:1339 +#: windows/views.py:1366 windows/views.py:2457 msgid "Rows" msgstr "" -#: windows/views.py:1342 +#: windows/views.py:1369 msgid "Updated at" msgstr "" -#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 -#: windows/views.py:2173 windows/views.py:2709 +#: windows/views.py:1387 +msgid "Add new view" +msgstr "" + +#: windows/views.py:1389 +msgid "Clone view" +msgstr "" + +#: windows/views.py:1391 +msgid "Delete view" +msgstr "" + +#: windows/views.py:1399 windows/views.py:1423 windows/views.py:1447 +#: windows/views.py:1471 windows/views.py:1495 +msgid "Definition" +msgstr "" + +#: windows/views.py:1406 +msgid "Views" +msgstr "" + +#: windows/views.py:1411 +msgid "Add new procedure" +msgstr "" + +#: windows/views.py:1413 +msgid "Clone procedure" +msgstr "" + +#: windows/views.py:1415 +msgid "Delete procedure" +msgstr "" + +#: windows/views.py:1430 +msgid "Procedures" +msgstr "" + +#: windows/views.py:1435 +msgid "Add new function" +msgstr "" + +#: windows/views.py:1437 +msgid "Clone function" +msgstr "" + +#: windows/views.py:1439 +msgid "Delete function" +msgstr "" + +#: windows/views.py:1454 +msgid "Functions" +msgstr "" + +#: windows/views.py:1459 +msgid "Add new trigger" +msgstr "" + +#: windows/views.py:1461 +msgid "Clone trigger" +msgstr "" + +#: windows/views.py:1463 +msgid "Delete trigger" +msgstr "" + +#: windows/views.py:1478 +msgid "Triggers" +msgstr "" + +#: windows/views.py:1483 +msgid "Add new event" +msgstr "" + +#: windows/views.py:1485 +msgid "Clone event" +msgstr "" + +#: windows/views.py:1487 +msgid "Delete event" +msgstr "" + +#: windows/views.py:1502 +msgid "Events" +msgstr "" + +#: windows/views.py:1530 windows/views.py:1901 windows/views.py:2646 +#: windows/views.py:2730 msgid "Apply" msgstr "" -#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 -#: windows/views.py:2909 +#: windows/views.py:1542 windows/views.py:1721 msgid "Options" msgstr "" -#: windows/views.py:1413 +#: windows/views.py:1573 msgid "Diagram" msgstr "" -#: windows/views.py:1424 +#: windows/views.py:1584 msgid "Database" msgstr "" -#: windows/views.py:1479 windows/views.py:2849 +#: windows/views.py:1639 msgid "Base" msgstr "" -#: windows/views.py:1493 windows/views.py:2863 +#: windows/views.py:1653 msgid "Auto Increment" msgstr "" -#: windows/views.py:1521 windows/views.py:2891 +#: windows/views.py:1681 msgid "Default Collation" msgstr "" -#: windows/views.py:1531 +#: windows/views.py:1691 msgid "Convert data" msgstr "" -#: windows/views.py:1539 +#: windows/views.py:1699 msgid "Row format" msgstr "" -#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 -#: windows/views.py:1756 windows/views.py:2081 +#: windows/views.py:1732 windows/views.py:1760 windows/views.py:1788 +#: windows/views.py:1876 windows/views.py:2326 windows/views.py:2636 msgid "Remove" msgstr "" -#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 +#: windows/views.py:1734 windows/views.py:1762 windows/views.py:1790 +#: windows/views.py:2328 windows/views.py:2738 msgid "Clear" msgstr "" -#: windows/views.py:1595 windows/views.py:2923 +#: windows/views.py:1747 msgid "Indexes" msgstr "" -#: windows/views.py:1639 +#: windows/views.py:1758 windows/views.py:1786 windows/views.py:2324 +msgid "Insert" +msgstr "" + +#: windows/views.py:1775 msgid "Foreign Keys" msgstr "" -#: windows/views.py:1683 +#: windows/views.py:1803 msgid "Checks" msgstr "" -#: windows/views.py:1750 windows/views.py:2944 +#: windows/views.py:1870 msgid "Columns:" msgstr "" -#: windows/views.py:1760 +#: windows/views.py:1880 msgid "Move Up" msgstr "" -#: windows/views.py:1762 +#: windows/views.py:1882 msgid "Move Down" msgstr "" -#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 -#: windows/views.py:3017 +#: windows/views.py:1914 windows/views.py:1921 msgid "Add Index" msgstr "" -#: windows/views.py:1798 windows/views.py:3014 +#: windows/views.py:1918 msgid "Add PrimaryKey" msgstr "" -#: windows/views.py:1815 +#: windows/views.py:1935 msgid "Table" msgstr "" -#: windows/views.py:1851 -msgid "Definer" -msgstr "" - -#: windows/views.py:1871 +#: windows/views.py:1977 windows/views.py:2243 msgid "Schema" msgstr "" -#: windows/views.py:1897 -msgid "SQL security" -msgstr "" - -#: windows/views.py:1904 -msgid "DEFINER" +#: windows/views.py:2005 windows/views.py:2314 +msgid "General" msgstr "" -#: windows/views.py:1904 -msgid "INVOKER" -msgstr "" - -#: windows/views.py:1916 +#: windows/views.py:2010 msgid "Algorithm" msgstr "" -#: windows/views.py:1918 +#: windows/views.py:2012 msgid "UNDEFINED" msgstr "" -#: windows/views.py:1921 +#: windows/views.py:2015 msgid "MERGE" msgstr "" -#: windows/views.py:1924 +#: windows/views.py:2018 msgid "TEMPTABLE" msgstr "" -#: windows/views.py:1934 +#: windows/views.py:2028 msgid "View constraint" msgstr "" -#: windows/views.py:1936 +#: windows/views.py:2030 msgid "None" msgstr "" -#: windows/views.py:1939 +#: windows/views.py:2033 msgid "LOCAL" msgstr "" -#: windows/views.py:1942 +#: windows/views.py:2036 msgid "CASCADE" msgstr "" -#: windows/views.py:1945 +#: windows/views.py:2039 msgid "CHECK ONLY" msgstr "" -#: windows/views.py:1948 +#: windows/views.py:2042 msgid "READ ONLY" msgstr "" -#: windows/views.py:1960 +#: windows/views.py:2055 windows/views.py:2483 +msgid "Behavior" +msgstr "" + +#: windows/views.py:2062 windows/views.py:2490 +msgid "Definer" +msgstr "" + +#: windows/views.py:2070 windows/views.py:2498 +msgid "*" +msgstr "" + +#: windows/views.py:2082 windows/views.py:2510 +msgid "SQL security" +msgstr "" + +#: windows/views.py:2089 windows/views.py:2517 +msgid "DEFINER" +msgstr "" + +#: windows/views.py:2089 windows/views.py:2517 +msgid "INVOKER" +msgstr "" + +#: windows/views.py:2103 msgid "Force" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:2115 msgid "Security barrier" msgstr "" -#: windows/views.py:2054 -msgid "Views" +#: windows/views.py:2128 windows/views.py:2532 +msgid "Security" msgstr "" -#: windows/views.py:2062 -msgid "Triggers" +#: windows/views.py:2205 +msgid "View" +msgstr "" + +#: windows/views.py:2262 +msgid "Type" +msgstr "" + +#: windows/views.py:2269 +msgid "Procedure (doesn't return a result)" +msgstr "" + +#: windows/views.py:2269 +msgid "Function (return a result)" +msgstr "" + +#: windows/views.py:2279 +msgid "Return type" +msgstr "" + +#: windows/views.py:2299 +msgid "Comment" +msgstr "" + +#: windows/components/dataview.py:111 windows/views.py:2335 +msgid "#" +msgstr "" + +#: windows/views.py:2337 +msgid "Datatype" +msgstr "" + +#: windows/views.py:2338 +msgid "Context" +msgstr "" + +#: windows/views.py:2345 +msgid "Parameters" +msgstr "" + +#: windows/views.py:2354 +msgid "Data access" +msgstr "" + +#: windows/views.py:2361 +msgid "CONTAINS SQL" +msgstr "" + +#: windows/views.py:2361 +msgid "NO SQL" +msgstr "" + +#: windows/views.py:2361 +msgid "READS SQL DATA" +msgstr "" + +#: windows/views.py:2361 +msgid "MODIFIES SQL DATA" +msgstr "" + +#: windows/views.py:2371 +msgid "Deterministic" +msgstr "" + +#: windows/views.py:2395 +msgid "SQL" +msgstr "" + +#: windows/views.py:2395 +msgid "PLPGSQL" +msgstr "" + +#: windows/views.py:2405 +msgid "Volatility" +msgstr "" + +#: windows/views.py:2412 +msgid "VOLATILE" +msgstr "" + +#: windows/views.py:2412 +msgid "STABLE" +msgstr "" + +#: windows/views.py:2412 +msgid "IMMUTABLE" +msgstr "" + +#: windows/views.py:2422 +msgid "Parallel" +msgstr "" + +#: windows/views.py:2429 +msgid "UNSAFE" msgstr "" -#: windows/views.py:2073 -msgid "Refrsh" +#: windows/views.py:2429 +msgid "RESTRICTED" msgstr "" -#: windows/views.py:2079 +#: windows/views.py:2429 +msgid "SAFE" +msgstr "" + +#: windows/views.py:2441 +msgid "Cost" +msgstr "" + +#: windows/views.py:2609 +msgid "Routine" +msgstr "" + +#: windows/views.py:2617 +msgid "Trigger" +msgstr "" + +#: windows/views.py:2634 msgid "Duplicate" msgstr "" -#: windows/views.py:2085 +#: windows/views.py:2640 msgid "Apply changes automatically" msgstr "" -#: windows/views.py:2087 windows/views.py:2088 +#: windows/views.py:2642 windows/views.py:2643 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" msgstr "" -#: windows/views.py:2101 +#: windows/views.py:2656 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" -#: windows/views.py:2109 +#: windows/views.py:2664 msgid "First" msgstr "" -#: windows/views.py:2127 +#: windows/views.py:2682 msgid "Last" msgstr "" -#: windows/views.py:2136 +#: windows/views.py:2691 msgid "Filters" msgstr "" -#: windows/views.py:2176 +#: windows/views.py:2733 +msgid "" +"Apply filters in data\n" +"CTRL+ENTER" +msgstr "" + +#: windows/views.py:2734 msgid "CTRL+ENTER" msgstr "" -#: windows/views.py:2196 +#: windows/views.py:2762 msgid "Insert row" msgstr "" -#: windows/views.py:2204 +#: windows/components/popup.py:31 windows/views.py:2774 +msgid "NULL" +msgstr "" + +#: windows/views.py:2781 msgid "Data" msgstr "" -#: windows/main/controller.py:278 windows/main/controller.py:287 -#: windows/main/controller.py:288 windows/views.py:2221 +#: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" msgstr "" -#: windows/views.py:2223 windows/views.py:2660 +#: windows/views.py:2800 msgid "Close" msgstr "" -#: windows/main/controller.py:279 windows/main/controller.py:289 -#: windows/main/controller.py:290 windows/views.py:2223 +#: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" msgstr "" -#: windows/views.py:2227 +#: windows/views.py:2804 msgid "Run" msgstr "" -#: windows/main/controller.py:280 windows/main/controller.py:292 -#: windows/main/controller.py:293 windows/views.py:2227 +#: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" msgstr "" -#: windows/views.py:2229 +#: windows/views.py:2806 msgid "Run all" msgstr "" -#: windows/main/controller.py:295 windows/views.py:2229 +#: windows/views.py:2806 msgid "Execute all statements" msgstr "" -#: windows/main/controller.py:282 windows/main/controller.py:297 -#: windows/main/controller.py:298 windows/views.py:2231 +#: windows/main/controller.py:323 windows/views.py:2808 msgid "Stop" msgstr "" -#: windows/views.py:2287 +#: windows/views.py:2873 msgid "a page" msgstr "" -#: windows/views.py:2315 +#: windows/views.py:2923 msgid "Query" msgstr "" -#: windows/views.py:2626 -msgid "Character set" -msgstr "" - -#: windows/views.py:2644 windows/views.py:2663 -msgid "New" -msgstr "" - -#: windows/views.py:2683 -msgid "Insert record" -msgstr "" - -#: windows/views.py:2688 -msgid "Duplicate record" -msgstr "" - -#: windows/views.py:2695 -msgid "Delete record" -msgstr "" - -#: windows/views.py:2733 windows/views.py:2964 -msgid "Up" -msgstr "" - -#: windows/views.py:2740 windows/views.py:2971 -msgid "Down" -msgstr "" - -#: windows/views.py:3100 -msgid "Save Starments" -msgstr "" - -#: windows/views.py:3108 -msgid "Location" -msgstr "" - -#: windows/views.py:3115 -msgid "*.sql" -msgstr "" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 msgid "Allow NULL" @@ -777,7 +968,7 @@ msgid "Virtuality" msgstr "" #: windows/components/dataview.py:39 windows/components/dataview.py:63 -#: windows/components/dataview.py:85 windows/components/dataview.py:241 +#: windows/components/dataview.py:85 windows/components/dataview.py:256 msgid "Expression" msgstr "" @@ -789,75 +980,71 @@ msgstr "" msgid "Zerofill" msgstr "" -#: windows/components/dataview.py:109 -msgid "#" -msgstr "" - -#: windows/components/dataview.py:117 +#: windows/components/dataview.py:119 msgid "Data type" msgstr "" -#: windows/components/dataview.py:121 +#: windows/components/dataview.py:123 msgid "Length/Set" msgstr "" -#: windows/components/dataview.py:155 +#: windows/components/dataview.py:169 msgid "Add column\tCTRL+INS" msgstr "" -#: windows/components/dataview.py:161 +#: windows/components/dataview.py:175 msgid "Remove column\tCTRL+DEL" msgstr "" -#: windows/components/dataview.py:169 +#: windows/components/dataview.py:183 msgid "Move up\tCTRL+UP" msgstr "" -#: windows/components/dataview.py:176 +#: windows/components/dataview.py:190 msgid "Move down\tCTRL+D" msgstr "" -#: windows/components/dataview.py:199 +#: windows/components/dataview.py:214 msgid "Create new index" msgstr "" -#: windows/components/dataview.py:214 +#: windows/components/dataview.py:229 msgid "Append to index" msgstr "" -#: windows/components/dataview.py:228 +#: windows/components/dataview.py:243 msgid "Column(s)/Expression" msgstr "" -#: windows/components/dataview.py:229 +#: windows/components/dataview.py:244 msgid "Condition" msgstr "" -#: windows/components/dataview.py:259 +#: windows/components/dataview.py:274 msgid "Column(s)" msgstr "" -#: windows/components/dataview.py:265 +#: windows/components/dataview.py:280 msgid "Reference table" msgstr "" -#: windows/components/dataview.py:271 +#: windows/components/dataview.py:286 msgid "Reference column(s)" msgstr "" -#: windows/components/dataview.py:277 +#: windows/components/dataview.py:292 msgid "On UPDATE" msgstr "" -#: windows/components/dataview.py:283 +#: windows/components/dataview.py:298 msgid "On DELETE" msgstr "" -#: windows/components/dataview.py:298 +#: windows/components/dataview.py:313 msgid "Add foreign key" msgstr "" -#: windows/components/dataview.py:304 +#: windows/components/dataview.py:319 msgid "Remove foreign key" msgstr "" @@ -865,10 +1052,6 @@ msgstr "" msgid "No default value" msgstr "" -#: windows/components/popup.py:31 -msgid "NULL" -msgstr "" - #: windows/components/popup.py:35 msgid "AUTO INCREMENT" msgstr "" @@ -877,161 +1060,171 @@ msgstr "" msgid "Text/Expression" msgstr "" -#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 +#: windows/dialogs/connections/view.py:126 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:414 +#: windows/dialogs/connections/view.py:416 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:426 +#: windows/dialogs/connections/view.py:428 #, python-brace-format msgid "Do you want save the connection {connection_name}?" msgstr "" -#: windows/dialogs/connections/view.py:429 +#: windows/dialogs/connections/view.py:431 msgid "Confirm save" msgstr "" -#: windows/dialogs/connections/view.py:481 +#: windows/dialogs/connections/view.py:483 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:483 +#: windows/dialogs/connections/view.py:485 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:750 +#: windows/dialogs/connections/view.py:773 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:775 +#: windows/dialogs/connections/view.py:798 #, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "" -#: windows/dialogs/connections/view.py:776 +#: windows/dialogs/connections/view.py:799 msgid "Connection error" msgstr "" -#: windows/dialogs/connections/view.py:802 +#: windows/dialogs/connections/view.py:825 #, 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 +#: windows/dialogs/connections/view.py:828 +#: windows/dialogs/connections/view.py:845 msgid "Confirm delete" msgstr "" -#: windows/dialogs/connections/view.py:819 +#: windows/dialogs/connections/view.py:842 #, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "" -#: windows/main/controller.py:275 +#: windows/main/controller.py:316 #, python-brace-format msgid "{text} ({shortcut})" msgstr "" -#: windows/main/controller.py:281 windows/main/controller.py:294 +#: windows/main/controller.py:322 msgid "Execute all" msgstr "" -#: windows/main/controller.py:471 windows/main/controller.py:479 +#: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" msgstr "" -#: windows/main/controller.py:497 +#: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" msgstr "" -#: windows/main/controller.py:530 +#: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" msgstr "" -#: windows/main/controller.py:531 +#: windows/main/controller.py:519 msgid "Unsaved query" msgstr "" -#: windows/main/controller.py:576 +#: windows/main/controller.py:564 msgid "Save query" msgstr "" -#: windows/main/controller.py:579 +#: windows/main/controller.py:567 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 +#: windows/main/controller.py:595 windows/main/controller.py:626 +#: windows/main/controller.py:653 windows/main/database/list.py:119 +#: windows/main/database/routine.py:591 windows/main/database/routine.py:635 +#: windows/main/database/view.py:268 windows/main/database/view.py:297 +#: windows/main/query/controller.py:179 msgid "Error" msgstr "" -#: windows/main/controller.py:622 +#: windows/main/controller.py:633 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "" -#: windows/main/controller.py:647 +#: windows/main/controller.py:659 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "" -#: windows/main/controller.py:704 +#: windows/main/controller.py:725 msgid "days" msgstr "" -#: windows/main/controller.py:705 +#: windows/main/controller.py:726 msgid "hours" msgstr "" -#: windows/main/controller.py:706 +#: windows/main/controller.py:727 msgid "minutes" msgstr "" -#: windows/main/controller.py:707 +#: windows/main/controller.py:728 msgid "seconds" msgstr "" -#: windows/main/controller.py:715 +#: windows/main/controller.py:736 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "" -#: windows/main/controller.py:751 +#: windows/main/controller.py:772 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:952 +#: windows/main/controller.py:1006 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:954 +#: windows/main/controller.py:1008 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:1119 +#: windows/main/controller.py:1243 +msgid "Write Mode (2:00)" +msgstr "" + +#: windows/main/controller.py:1294 +msgid "Write Mode" +msgstr "" + +#: windows/main/controller.py:1305 msgid "Version" msgstr "" -#: windows/main/controller.py:1121 +#: windows/main/controller.py:1307 msgid "Uptime" msgstr "" -#: windows/main/controller.py:1199 +#: windows/main/controller.py:1444 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:1232 +#: windows/main/controller.py:1477 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1041,116 +1234,178 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:1237 windows/main/controller.py:1258 +#: windows/main/controller.py:1482 windows/main/controller.py:1503 msgid "Delete database" msgstr "" -#: windows/main/controller.py:1243 +#: windows/main/controller.py:1488 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:1244 +#: windows/main/controller.py:1489 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:1257 +#: windows/main/controller.py:1502 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:1272 +#: windows/main/controller.py:1517 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:1273 windows/main/database/view.py:253 -#: windows/main/database/view.py:279 +#: windows/main/controller.py:1518 windows/main/database/routine.py:556 +#: windows/main/database/routine.py:620 windows/main/database/view.py:256 +#: windows/main/database/view.py:291 msgid "Success" msgstr "" -#: windows/main/controller.py:1392 +#: windows/main/controller.py:1792 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:1418 +#: windows/main/controller.py:1818 #, 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:1440 +#: windows/main/controller.py:1840 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1563 +#: windows/main/controller.py:1999 msgid "Do you want delete the records?" msgstr "" -#: windows/main/database/list.py:69 +#: windows/main/controller.py:2104 +msgid "Database connection lost. Do you want to reconnect?" +msgstr "" + +#: windows/main/controller.py:2105 windows/main/database/list.py:108 +msgid "Connection lost" +msgstr "" + +#: windows/main/controller.py:2113 +msgid "Connection restored successfully." +msgstr "" + +#: windows/main/controller.py:2114 +msgid "Connection restored" +msgstr "" + +#: windows/main/controller.py:2120 +#, python-brace-format +msgid "Could not reconnect: {error}" +msgstr "" + +#: windows/main/controller.py:2121 +msgid "Reconnection failed" +msgstr "" + +#: windows/main/database/list.py:104 msgid "The connection to the database was lost." msgstr "" -#: windows/main/database/list.py:71 +#: windows/main/database/list.py:106 msgid "Do you want to reconnect?" msgstr "" -#: windows/main/database/list.py:73 -msgid "Connection lost" +#: windows/main/database/list.py:118 +msgid "Reconnection failed:" msgstr "" -#: windows/main/database/list.py:83 -msgid "Reconnection failed:" +#: windows/main/database/routine.py:545 +msgid "Function created successfully" +msgstr "" + +#: windows/main/database/routine.py:547 +msgid "Function updated successfully" +msgstr "" + +#: windows/main/database/routine.py:551 +msgid "Procedure created successfully" msgstr "" -#: windows/main/database/view.py:252 +#: windows/main/database/routine.py:553 +msgid "Procedure updated successfully" +msgstr "" + +#: windows/main/database/routine.py:590 +#, python-brace-format +msgid "Error saving routine: {}" +msgstr "" + +#: windows/main/database/routine.py:604 +msgid "Function" +msgstr "" + +#: windows/main/database/routine.py:604 +msgid "Procedure" +msgstr "" + +#: windows/main/database/routine.py:607 +#, python-brace-format +msgid "Are you sure you want to delete {} '{}'?" +msgstr "" + +#: windows/main/database/routine.py:610 windows/main/database/view.py:282 +msgid "Confirm Delete" +msgstr "" + +#: windows/main/database/routine.py:619 +#, python-brace-format +msgid "{} deleted successfully" +msgstr "" + +#: windows/main/database/routine.py:634 +#, python-brace-format +msgid "Error deleting routine: {}" +msgstr "" + +#: windows/main/database/view.py:255 msgid "View created successfully" msgstr "" -#: windows/main/database/view.py:252 +#: windows/main/database/view.py:255 msgid "View updated successfully" msgstr "" -#: windows/main/database/view.py:256 +#: windows/main/database/view.py:268 #, python-brace-format msgid "Error saving view: {}" msgstr "" -#: windows/main/database/view.py:269 +#: windows/main/database/view.py:281 #, python-brace-format 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 +#: windows/main/database/view.py:291 msgid "View deleted successfully" msgstr "" -#: windows/main/database/view.py:282 +#: windows/main/database/view.py:297 #, python-brace-format msgid "Error deleting view: {}" msgstr "" -#: windows/main/query/controller.py:110 +#: windows/main/query/controller.py:112 #, python-brace-format msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/query/controller.py:112 +#: windows/main/query/controller.py:114 #, python-brace-format msgid "{elapsed_s:.2f} s" msgstr "" -#: windows/main/query/controller.py:115 +#: windows/main/query/controller.py:117 msgid "none" msgstr "" -#: windows/main/query/controller.py:121 +#: windows/main/query/controller.py:123 #, python-brace-format msgid "" "Query execution stopped after {elapsed}.\n" @@ -1160,14 +1415,22 @@ msgid "" "Last statement: #{last}." msgstr "" -#: windows/main/query/controller.py:134 +#: windows/main/query/controller.py:136 msgid "Query execution cancelled" msgstr "" -#: windows/main/query/controller.py:176 +#: windows/main/query/controller.py:178 msgid "No active database connection" msgstr "" +#: windows/main/query/controller.py:227 +msgid "Database connection lost" +msgstr "" + +#: windows/main/query/history.py:55 +msgid "(empty query)" +msgstr "" + #: windows/main/query/renderer.py:53 #, python-brace-format msgid "{affected_rows} rows affected" @@ -1207,3 +1470,7 @@ msgstr "" msgid "Error:" msgstr "" +#: windows/main/table/records.py:334 +msgid "Error saving records" +msgstr "" + diff --git a/main.py b/main.py index 325e4f3..d0cce66 100755 --- a/main.py +++ b/main.py @@ -3,6 +3,7 @@ import os from pathlib import Path +from typing import Optional import wx @@ -10,7 +11,7 @@ from icons import IconRegistry from helpers.loader import Loader -from helpers.logger import logger +from helpers.logger import configure_logging, enable_fault_handler, install_global_exception_hooks, logger from helpers.settings import Settings, SettingsRepository from windows.components.stc.styles import apply_stc_theme, set_theme_loader @@ -23,9 +24,6 @@ class PeterSQL(wx.App): locale: wx.Locale = wx.Locale() - settings_repository = SettingsRepository(WORKDIR / "settings.yml") - settings: Settings = settings_repository.load() - main_frame: wx.Frame = None icon_registry_16: IconRegistry @@ -34,13 +32,26 @@ class PeterSQL(wx.App): theme_loader: ThemeLoader + def __init__( + self, + *args, + settings_path: Optional[Path] = None, + **kwargs, + ): + if settings_path is None: + settings_path = WORKDIR / "settings.yml" + + self.settings_repository = SettingsRepository(settings_path) + self.settings = self.settings_repository.load() + super().__init__(*args, **kwargs) + def OnInit(self) -> bool: Loader.loading.subscribe(self._on_loading_change) self.icon_registry_16 = IconRegistry(WORKDIR / "icons", 16) self._init_theme_loader() - + self.theme_manager = ThemeManager(apply_function=apply_stc_theme) self.syntax_registry = SyntaxRegistry([JSON, SQL, XML, YAML, MARKDOWN, HTML, REGEX, CSV, BASE64, TEXT]) @@ -49,6 +60,11 @@ def OnInit(self) -> bool: self.open_session_manager() return True + + def OnExceptionInMainLoop(self) -> bool: + # wx calls this hook implicitly when an exception escapes an event callback in MainLoop. + logger.exception("Unhandled exception raised inside wx main loop") + return True def _init_theme_loader(self) -> None: theme_name = self.settings.get_value("ui", "appearance", "theme", default="petersql") @@ -94,6 +110,11 @@ def open_session_manager(self) -> None: def open_main_frame(self) -> None: try: from windows.main.controller import MainFrameController + from windows.splash import SplashController + + splash = SplashController() + splash.Show() + splash.Update() self.main_frame = MainFrameController() size_values = self.settings.get_value("ui", "window", "size", default=[1920, 1080]) @@ -107,12 +128,14 @@ def open_main_frame(self) -> None: self.main_frame.SetIcon( wx.Icon(str(WORKDIR / "icons" / "petersql.ico")) ) - self.main_frame.Show() - self.main_frame.Bind(wx.EVT_SIZE, self._on_size) self.main_frame.Bind(wx.EVT_MOVE, self._on_move) + + splash.start_close(on_done=self.main_frame.Show) except Exception as ex: logger.error(ex, exc_info=True) + if 'splash' in locals(): + splash.Destroy() def _on_size(self, event: wx.SizeEvent) -> None: size = event.GetSize() @@ -138,5 +161,10 @@ def do_exit(self, event: wx.Event) -> None: if __name__ == "__main__": + logs_directory = WORKDIR / "logs" + configure_logging(logs_directory / "petersql.log") + enable_fault_handler(logs_directory / "fault.log") + install_global_exception_hooks() + app = PeterSQL() app.MainLoop() diff --git a/pyproject.toml b/pyproject.toml index e252424..490bdac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.14" dependencies = [ "babel>=2.18.0", + "keyring>=25.0.0", "oracledb>=3.4.2", "psutil>=7.2.2", "psycopg2-binary>=2.9.11", @@ -17,8 +18,12 @@ dependencies = [ [project.optional-dependencies] dev = [ + "memray>=1.19.3", "mypy>=1.19.1", + "pillow>=12.2.0", "pre-commit>=4.5.1", + "pyautogui>=0.9.54", + "pyscreeze>=1.0.1", "pytest>=9.0.2", "pytest-cov>=7.0.0", "pytest-mock>=3.15.1", diff --git a/screenshot/connection_dialog_configured.png b/screenshot/connection_dialog_configured.png new file mode 100644 index 0000000..4e3c196 Binary files /dev/null and b/screenshot/connection_dialog_configured.png differ diff --git a/screenshot/connection_dialog_ssh_tunnel.png b/screenshot/connection_dialog_ssh_tunnel.png new file mode 100644 index 0000000..94880aa Binary files /dev/null and b/screenshot/connection_dialog_ssh_tunnel.png differ diff --git a/screenshot/mysql_main_window_add_database.png b/screenshot/mysql_main_window_add_database.png new file mode 100644 index 0000000..4236899 Binary files /dev/null and b/screenshot/mysql_main_window_add_database.png differ diff --git a/scripts/locales.py b/scripts/locales.py index 8dd3d49..654b39c 100755 --- a/scripts/locales.py +++ b/scripts/locales.py @@ -1,11 +1,8 @@ #!/usr/bin/env python3 import argparse import os -import shutil import subprocess import sys -import toml -from datetime import datetime from pathlib import Path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -17,52 +14,12 @@ LOCALE_DIR = WORKDIR.joinpath("locale") POT_FILE = LOCALE_DIR.joinpath(f"{APP_NAME}.pot") -PYPROJECT_FILE = WORKDIR.joinpath("pyproject.toml") - - -def get_project_info(): - """Read project information from pyproject.toml""" - try: - with open(PYPROJECT_FILE, 'r', encoding='utf-8') as f: - data = toml.load(f) - project = data.get('project', {}) - return { - 'name': project.get('name'), - 'version': project.get('version') - } - except Exception: - return {'name': 'PeterSQL', 'version': '0.1.0'} + def run(cmd): subprocess.run(cmd, shell=True, check=True) -def generate_header(lang): - """Generate proper header for .po files based on language""" - project_info = get_project_info() - current_date = datetime.now().strftime("%Y-%m-%d %H:%M%z") - - language = Language.from_code(lang) - lang_name = f"{language.label} ({lang})" - - return f'''# {lang_name} translations for {project_info['name']}. -# Copyright (C) 2026 {project_info['name']} -# This file is distributed under the same license as the {project_info['name']} project. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: {project_info['name']} {project_info['version']}\\n" -"POT-Creation-Date: {current_date}\\n" -"PO-Revision-Date: {current_date}\\n" -"Language: {lang}\\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=utf-8\\n" -"Content-Transfer-Encoding: 8bit\\n" - -''' - - def extract(): LOCALE_DIR.mkdir(parents=True, exist_ok=True) @@ -83,32 +40,19 @@ def update(): po_file = message_dir.joinpath(f"{APP_NAME}.po") if not po_file.exists(): - # Create new .po file with proper header - with open(po_file, 'w', encoding='utf-8') as f: - f.write(generate_header(lang)) - - # Then update with babel to add translations run( - f"pybabel update " + f"pybabel init " f"-i {POT_FILE} " f"-o {po_file} " f"-l {lang} " ) else: - # Always prepend header for existing files (one-time patch) - with open(po_file, 'r', encoding='utf-8') as f: - existing_content = f.read() - - with open(po_file, 'w', encoding='utf-8') as f: - f.write(generate_header(lang)) - f.write(existing_content) - - # Update with babel run( f"pybabel update " f"-i {POT_FILE} " f"-o {po_file} " f"-l {lang} " + f"--ignore-obsolete " ) @@ -122,7 +66,6 @@ def compile(): run(f"msgfmt {po_file} -o {mo_file}") - if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--clean", action="store_true") diff --git a/scripts/runtest.py b/scripts/runtest.py index 35f862a..889cf9f 100755 --- a/scripts/runtest.py +++ b/scripts/runtest.py @@ -1,9 +1,14 @@ #!/usr/bin/env python3 """ -Unified test runner -By default: runs unit tests only (excludes integration tests) -With --all: runs ALL tests (unit + integration) -With --update: runs ALL tests (unit + integration) and updates README badges +PeterSQL test runner + + (no args) Run all tests and update README badges + unit Unit tests only (tests/core/) + autocomplete Autocomplete golden-case tests + autocomplete --engine Restrict autocomplete tests to one engine + integration Integration tests only + integration --engine Restrict integration tests to one engine + ui UI tests (always refreshes screenshots) """ import argparse @@ -13,29 +18,22 @@ import sys import xml.etree.ElementTree as ET -# Add project root to Python path for imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - from structures.connection import ConnectionEngine README = "README.md" -TESTS_DIR = "tests/engines" RESULTS_FILE = "/tmp/pytest_results.txt" JUNIT_FILE = "/tmp/pytest_results.xml" SUITE_BADGES_START = "" SUITE_BADGES_END = "" -SUITE_ORDER = [ - "autocomplete", - "core", - "ui", - "mysql", - "mariadb", - "postgresql", - "sqlite", -] +SUITE_ORDER = ["autocomplete", "core", "ui", "mysql", "mariadb", "postgresql", "sqlite"] + +# --------------------------------------------------------------------------- +# Badge helpers +# --------------------------------------------------------------------------- def _build_suite_badges_table(suite_stats: dict[str, dict[str, int]]) -> str: lines = [ @@ -45,14 +43,12 @@ def _build_suite_badges_table(suite_stats: dict[str, dict[str, int]]) -> str: "| Suite | Passed | Skipped |", "|-------|--------|---------|", ] - for suite_name in SUITE_ORDER: passed = suite_stats[suite_name]["passed"] skipped = suite_stats[suite_name]["skipped"] passed_badge = f"![passed](https://img.shields.io/badge/passed-{passed}-brightgreen)" skipped_badge = f"![skipped](https://img.shields.io/badge/skipped-{skipped}-lightgrey)" lines.append(f"| {suite_name} | {passed_badge} | {skipped_badge} |") - lines += ["", SUITE_BADGES_END] return "\n".join(lines) @@ -61,7 +57,6 @@ def _extract_suite_name(case_node: ET.Element) -> str: file_path = case_node.attrib.get("file", "") class_name = case_node.attrib.get("classname", "") source = file_path or class_name.replace(".", "/") - mapping = [ ("tests/autocomplete/", "autocomplete"), ("tests/core/", "core"), @@ -71,271 +66,189 @@ def _extract_suite_name(case_node: ET.Element) -> str: ("tests/engines/postgresql/", "postgresql"), ("tests/engines/sqlite/", "sqlite"), ] - for prefix, suite_name in mapping: if source.startswith(prefix): return suite_name - return "" def _load_suite_stats(junit_file: str) -> dict[str, dict[str, int]]: - stats = { - suite_name: {"passed": 0, "skipped": 0} - for suite_name in SUITE_ORDER - } - + stats = {name: {"passed": 0, "skipped": 0} for name in SUITE_ORDER} if not os.path.exists(junit_file): return stats - try: tree = ET.parse(junit_file) except ET.ParseError: return stats - - root = tree.getroot() - for case in root.findall(".//testcase"): + for case in tree.getroot().findall(".//testcase"): suite_name = _extract_suite_name(case) if not suite_name: continue - if case.find("skipped") is not None: stats[suite_name]["skipped"] += 1 - continue - - if case.find("failure") is not None or case.find("error") is not None: - continue - - stats[suite_name]["passed"] += 1 - + elif case.find("failure") is None and case.find("error") is None: + stats[suite_name]["passed"] += 1 return stats -def _update_suite_badges_block(content: str, suite_stats: dict[str, dict[str, int]]) -> str: - replacement = _build_suite_badges_table(suite_stats) - block_pattern = re.compile( - rf"{re.escape(SUITE_BADGES_START)}.*?{re.escape(SUITE_BADGES_END)}", - re.DOTALL, - ) - - if block_pattern.search(content): - return block_pattern.sub(replacement, content) - - anchor = "For detailed test coverage matrix, statistics, and architecture, see **[tests/README.md](tests/README.md)**." - if anchor in content: - return content.replace(anchor, f"{anchor}\n\n{replacement}") - - return f"{content}\n\n{replacement}\n" - - -def get_engine_color(engine, results_content): - """Determine color from test results for a specific engine.""" - pattern = f"tests/engines/{engine}/" - - has_passed = f"{pattern}" in results_content and "PASSED" in results_content - has_failed = f"{pattern}" in results_content and "FAILED" in results_content - - if has_passed: - if has_failed: - return "orange" - else: - return "green" - elif has_failed: - return "red" - else: - return "lightgrey" - - -def extract_versions_badge(file_path, var_name): - """Extract versions from conftest file by importing the module directly.""" +def _extract_versions_badge(file_path: str, var_name: str) -> str: if not os.path.exists(file_path): return "" - try: import importlib.util - spec = importlib.util.spec_from_file_location("conftest", file_path) if spec is None or spec.loader is None: return "" - module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - versions = getattr(module, var_name, []) if not isinstance(versions, list): return "" - - # Extract version strings after : - versions = [v.split(':', 1)[1] for v in versions if ':' in v] - - # Sort versions using natural sort - def natural_sort_key(s): - return [int(t) if t.isdigit() else t.lower() for t in re.split(r'(\d+)', s)] - - versions.sort(key=natural_sort_key) - - # Join with | - result = '|'.join(versions) - - # URL encode | - result = result.replace('|', '%20%7C%20') - - return result - - except (ImportError, AttributeError, IOError, Exception): + versions = sorted( + [v.split(":", 1)[1] for v in versions if ":" in v], + key=lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", s)], + ) + return "%20%7C%20".join(versions) + except Exception: return "" -def update_badges(): - """Update README badges based on test results.""" - if not os.path.exists(RESULTS_FILE): +def _update_readme(results_content: str, suite_stats: dict[str, dict[str, int]]) -> None: + if not os.path.exists(README): return - try: - with open(RESULTS_FILE, 'r') as f: - results_content = f.read() - except IOError: - return - - # Extract coverage percentage - match = re.search(r'TOTAL\s+\d+\s+\d+\s+(\d+)%', results_content) - coverage = match.group(1) if match else "0" - - tests_total_match = re.search(r'\[(\d+)\s+items\]', results_content) - tests_total = tests_total_match.group(1) if tests_total_match else "unknown" - suite_stats = _load_suite_stats(JUNIT_FILE) - - print(f"\nCoverage: {coverage}%") - print("\nAnalyzing results for badge updates...") - - colors = {} - for engine in ConnectionEngine: - colors[engine] = get_engine_color(engine.value.dialect, results_content) - print(f" {engine.value.name}: {colors[engine]}") - - # Update README - if os.path.exists(README): - try: - with open(README, 'r') as f: - content = f.read() - - # Update coverage badge - if coverage: - content = re.sub(r'coverage-\d+%', f'coverage-{coverage}%', content) - print(f" Coverage badge updated: {coverage}%") - - # Update total tests badge + with open(README) as f: + content = f.read() + + match = re.search(r"TOTAL\s+\d+\s+\d+\s+(\d+)%", results_content) + if match: + coverage = match.group(1) + content = re.sub(r"coverage-\d+%", f"coverage-{coverage}%", content) + print(f" Coverage: {coverage}%") + + content = re.sub( + rf"{re.escape(SUITE_BADGES_START)}.*?{re.escape(SUITE_BADGES_END)}", + _build_suite_badges_table(suite_stats), + content, + flags=re.DOTALL, + ) + + for engine in ConnectionEngine: + versions = _extract_versions_badge( + f"tests/engines/{engine.name.lower()}/conftest.py", + f"{engine.name.upper()}_VERSIONS", + ) + color = "green" content = re.sub( - r'!\[Tests\]\(https://img\.shields\.io/badge/tests-[^\)]*\)', - f'![Tests](https://img.shields.io/badge/tests-{tests_total}-blue)', + rf"!\[{engine.value.name}\]\(https://img\.shields\.io/badge/{engine.value.name}-[^)]*\)", + f"![{engine.value.name}](https://img.shields.io/badge/{engine.value.name}-{versions}-{color})", content, ) - if "![Tests](https://img.shields.io/badge/tests-" not in content: - content = re.sub( - r'(!\[Coverage\]\(https://img\.shields\.io/badge/coverage-[^\)]*\))', - rf'\1\n![Tests](https://img.shields.io/badge/tests-{tests_total}-blue)', - content, - count=1, - ) - - print(f" Tests badge updated: {tests_total}") - - content = _update_suite_badges_block(content, suite_stats) - print(" Suite matrix badges updated") - - # Update engine badges + with open(README, "w") as f: + f.write(content) + print(" README.md updated") + except IOError as e: + print(f" Error updating README: {e}") - for engine, color in colors.items(): - versions = extract_versions_badge(f"tests/engines/{engine.name.lower()}/conftest.py", f'{engine.name.upper()}_VERSIONS') - content = re.sub( - rf'!\[{engine.value.name}\]\(https://img.shields.io/badge/{engine.value.name}-[^)]*\)', - f'![{engine.value.name}](https://img.shields.io/badge/{engine.value.name}-{versions}-{color})', - content - ) - with open(README, 'w') as f: - f.write(content) +# --------------------------------------------------------------------------- +# Runner +# --------------------------------------------------------------------------- - print("\nREADME.md updated") - - except IOError as e: - print(f"Error updating README: {e}") - - # Cleanup +def _run(cmd: list[str], capture: bool = False) -> int: + if not capture: + return subprocess.run(cmd).returncode try: - os.remove(RESULTS_FILE) - except OSError: - pass + with open(RESULTS_FILE, "w") as f: + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + for line in iter(process.stdout.readline, ""): + print(line, end="", flush=True) + f.write(line) + return process.wait() + except (IOError, OSError) as e: + print(f"Error: {e}") + return 1 + + +def main() -> int: + engine_choices = [e.value.name.lower() for e in ConnectionEngine] + + parser = argparse.ArgumentParser( + description="PeterSQL test runner", + epilog="Run without arguments to execute all tests and update README badges.", + ) + sub = parser.add_subparsers(dest="suite", metavar="suite") - try: - os.remove(JUNIT_FILE) - except OSError: - pass + sub.add_parser("unit", help="Unit tests only (tests/core/)") + ac = sub.add_parser("autocomplete", help="Autocomplete golden-case tests") + ac.add_argument("--engine", choices=engine_choices, help="Restrict to one engine") -def main(): - parser = argparse.ArgumentParser(description='Unified test runner') - parser.add_argument('--all', action='store_true', - help='Run all tests (unit + integration)') - parser.add_argument('--update', action='store_true', - help='Run all tests (unit + integration) and update README badges') + it = sub.add_parser("integration", help="Integration tests only") + it.add_argument("--engine", choices=engine_choices, help="Restrict to one engine") - args = parser.parse_args() + sub.add_parser("ui", help="UI tests (always refreshes screenshots)") - if args.all: - print("Running ALL tests (unit + integration)...") - result = subprocess.run(['uv', 'run', 'pytest', 'tests/', '--tb=no']) - exit_code = result.returncode - - elif args.update: - print("Running ALL tests (unit + integration) and updating badges...") - - # Run pytest with pipes to capture output in real-time - try: - with open(RESULTS_FILE, 'w') as f: - process = subprocess.Popen( - [ - 'uv', - 'run', - 'pytest', - 'tests/', - '--tb=no', - '--junitxml', - JUNIT_FILE, - ], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True - ) - - # Read and print output in real-time - for line in iter(process.stdout.readline, ''): - print(line, end='', flush=True) - f.write(line) - - exit_code = process.wait() - - # Now update badges - update_badges() - - except (IOError, OSError) as e: - print(f"Error running tests: {e}") - exit_code = 1 + args = parser.parse_args() + suite = args.suite + engine = getattr(args, "engine", None) - else: + if suite == "unit": print("Running unit tests...") - result = subprocess.run([ - 'uv', 'run', 'pytest', 'tests/', '--tb=short', '-m', 'not integration' + exit_code = _run([ + "uv", "run", "pytest", "tests/core/", + "--tb=short", ]) - exit_code = result.returncode - print(f"\nLocal tests completed") - print("\nNote: Integration tests excluded. Run with --update for all tests with badge updates, or --all for full test suite.") + elif suite == "autocomplete": + label = f" [{engine}]" if engine else "" + print(f"Running autocomplete tests{label}...") + cmd = ["uv", "run", "pytest", "tests/autocomplete/", "--tb=short"] + if engine: + cmd += ["-k", engine] + exit_code = _run(cmd) + + elif suite == "integration": + target = f"tests/engines/{engine}/" if engine else "tests/" + label = f" [{engine}]" if engine else "" + print(f"Running integration tests{label}...") + exit_code = _run([ + "uv", "run", "pytest", target, + "--tb=short", "-m", "integration", "--ignore=tests/ui", + ]) - print(f"\nDone. Pytest exit code: {exit_code}") + elif suite == "ui": + print("Running UI tests...") + exit_code = _run([ + "xvfb-run", "-a", + "uv", "run", "pytest", "tests/ui/test_scenarios.py", + "--tb=short", "-n", "1", "--refresh-screenshots", + ]) + + else: + print("Running all tests...") + exit_code = _run([ + "uv", "run", "pytest", "tests/", + "--tb=no", "--ignore=tests/ui", "--junitxml", JUNIT_FILE, + ], capture=True) + + print("\nUpdating README badges...") + results_content = "" + if os.path.exists(RESULTS_FILE): + with open(RESULTS_FILE) as f: + results_content = f.read() + _update_readme(results_content, _load_suite_stats(JUNIT_FILE)) + + for path in (RESULTS_FILE, JUNIT_FILE): + try: + os.remove(path) + except OSError: + pass + + print(f"\nDone. Exit code: {exit_code}") return exit_code -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main()) diff --git a/settings.yml b/settings.yml index bb68983..64cd242 100755 --- a/settings.yml +++ b/settings.yml @@ -2,11 +2,11 @@ language: en_US ui: window: size: - - 1769 - - 967 + - 2256 + - 1472 position: - - 26 - - 23 + - 0 + - 0 appearance: theme: petersql mode: auto diff --git a/structures/connection.py b/structures/connection.py index 2fcc64f..aebe510 100755 --- a/structures/connection.py +++ b/structures/connection.py @@ -1,5 +1,6 @@ import dataclasses import enum +import uuid from functools import lru_cache from typing import Any, NamedTuple, Optional, Union @@ -70,6 +71,7 @@ class Connection: configuration: Optional[Union[CredentialsConfiguration, SourceConfiguration]] comments: Optional[str] = "" ssh_tunnel: Optional[SSHTunnelConfiguration] = None + read_only: bool = False parent: Optional["ConnectionDirectory"] = dataclasses.field( default=None, compare=False, @@ -84,6 +86,7 @@ class Connection: total_connection_attempts: int = 0 average_connection_time_ms: Optional[int] = None most_recent_connection_duration_ms: Optional[int] = None + secret_id: Optional[str] = dataclasses.field(default=None) def __eq__(self, other: Any): if not isinstance(other, Connection): @@ -102,7 +105,7 @@ def copy(self): return dataclasses.replace(self) def to_dict(self): - return { + data = { "id": self.id, "type": "connection", "name": self.name, @@ -112,6 +115,7 @@ def to_dict(self): else None, "comments": self.comments, "ssh_tunnel": self.ssh_tunnel._asdict() if self.ssh_tunnel else None, + "read_only": self.read_only, "created_at": self.created_at, "last_connection_at": self.last_connection_at, "last_successful_connection_at": self.last_successful_connection_at, @@ -122,6 +126,9 @@ def to_dict(self): "average_connection_time_ms": self.average_connection_time_ms, "most_recent_connection_duration_ms": self.most_recent_connection_duration_ms, } + if self.secret_id is not None: + data["secret_id"] = self.secret_id + return data @property def is_valid(self): diff --git a/structures/engines/context.py b/structures/engines/context.py index 922345d..53797d9 100755 --- a/structures/engines/context.py +++ b/structures/engines/context.py @@ -1,9 +1,14 @@ import abc import contextlib import re +import threading -from typing import Any, Optional +from gettext import gettext as _ +from typing import Any, Callable, Optional +import pymysql +import psycopg2 +import sqlite3 import yaml from constants import WORKDIR @@ -32,6 +37,28 @@ SQL_SAFE_NAME_REGEX = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$") +class ConnectionLostError(Exception): + """Raised when the database connection has been lost and needs user intervention.""" + pass + +_WRITE_QUERY_RE = re.compile( + r"^\s*(INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TRUNCATE|REPLACE|GRANT|REVOKE|RENAME|LOCK)\b", + re.IGNORECASE, +) + +# PyMySQL/MySQL/MariaDB disconnect error codes and message fragments +_PYMYSQL_DISCONNECT_CODES: frozenset[int] = frozenset({ + 2006, # CR_SERVER_GONE_ERROR + 2013, # CR_SERVER_LOST + 2055, # CR_SERVER_LOST_EXTENDED +}) +_PYMYSQL_DISCONNECT_FRAGMENTS: tuple[str, ...] = ( + "server has gone away", + "lost connection", + "can't connect", +) + + class AbstractContext(abc.ABC): """Base context API for SQL engines.""" @@ -58,6 +85,8 @@ def __init__(self, connection: Connection): self.connection = connection self.databases = ObservableLazyList(self.get_databases) + self._connection_lost_handler: Optional[Callable[["AbstractContext", str], None]] = None + self._connection_lost_lock = threading.Lock() def __del__(self): """Ensure resources are released during object destruction.""" @@ -114,6 +143,10 @@ def after_connect(self, *args, **kwargs): """Run engine-specific setup right after a successful connection.""" pass + def set_write_mode(self, enabled: bool) -> None: + """Override the DB-level read-only session setting. No-op by default.""" + pass + def before_disconnect(self, *args, **kwargs): """Release pre-disconnect resources and restore base host settings.""" if self._ssh_tunnel is not None: @@ -360,6 +393,11 @@ def get_foreign_keys(self, table: SQLTable) -> list[SQLForeignKey]: """Return foreign keys for the given table.""" raise NotImplementedError + @abc.abstractmethod + def build_empty_database(self, /, name: str = "") -> SQLDatabase: + """Build a new in-memory database model with default values.""" + raise NotImplementedError + @abc.abstractmethod def build_empty_table( self, database: SQLDatabase, /, name: Optional[str] = None, **default_values @@ -524,6 +562,10 @@ def get_records( 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() + + if self.connection.read_only and _WRITE_QUERY_RE.match(query_clean): + raise PermissionError(_("This connection is read-only.")) + logger.debug("execute query: %s", query_clean) QUERY_LOGS.append(query_clean) @@ -532,10 +574,68 @@ def execute(self, query: str) -> bool: except Exception as ex: logger.error(query) QUERY_LOGS.append(f"/* {str(ex)} */") + if self._is_connection_lost(ex): + error_message = _("Database connection lost: {error}").format(error=str(ex)) + self._handle_connection_lost(error_message) + raise ConnectionLostError(error_message) from ex raise return True + def set_connection_lost_handler(self, handler: Optional[Callable[["AbstractContext", str], None]]) -> None: + """Register a callback invoked when a lost connection is detected during execute().""" + with self._connection_lost_lock: + self._connection_lost_handler = handler + + def _handle_connection_lost(self, error_message: str) -> None: + handler = None + with self._connection_lost_lock: + handler = self._connection_lost_handler + + if callable(handler): + try: + handler(self, error_message) + except Exception as ex: + logger.error("Connection lost handler failed: %s", ex, exc_info=True) + + @staticmethod + def _is_connection_lost(exc: Exception) -> bool: + """Return True when the exception indicates a lost DB connection. + + Only disconnect-specific errors are classified as connection loss. + Ordinary SQL errors (missing table, syntax, access denied, etc.) are + not treated as lost connections so they propagate normally. + """ + # PyMySQL / MySQL / MariaDB + # InterfaceError is always connection-level in PyMySQL. + if pymysql and isinstance(exc, pymysql.err.InterfaceError): + return True + # OperationalError covers both SQL errors and network errors; only + # flag the ones with known disconnect codes or message fragments. + if pymysql and isinstance(exc, pymysql.err.OperationalError): + code = exc.args[0] if exc.args else 0 + msg = str(exc).lower() + if code in _PYMYSQL_DISCONNECT_CODES or any(f in msg for f in _PYMYSQL_DISCONNECT_FRAGMENTS): + return True + + # PostgreSQL + # InterfaceError (e.g. "connection already closed") is always network-level. + if psycopg2 and isinstance(exc, psycopg2.InterfaceError): + return True + # OperationalError with pgcode=None is a connection-level error; + # OperationalError with a pgcode is a server-side SQL error. + if psycopg2 and isinstance(exc, psycopg2.OperationalError): + if getattr(exc, "pgcode", None) is None: + return True + + # SQLite + if sqlite3 and isinstance(exc, sqlite3.OperationalError): + message = str(exc).lower() + if any(token in message for token in ["database is locked", "disk i/o error", "unable to open"]): + return True + + return False + def fetchone(self) -> Any: """Fetch a single row from the active cursor.""" try: diff --git a/structures/engines/database.py b/structures/engines/database.py index 4238062..13a2756 100755 --- a/structures/engines/database.py +++ b/structures/engines/database.py @@ -54,14 +54,22 @@ def __eq__(self, other: Self) -> bool: return False if not all([ - getattr(self, field.name) != getattr(other, field.name) + getattr(self, field.name) == getattr(other, field.name) for field in dataclasses.fields(self) if field.compare and not isinstance(field, ObservableLazyList) ]): return False for observable_lazy_list in ["tables", "views", "procedures", "functions", "triggers", "events"]: - if not all([oll1 != oll2 for oll1, oll2 in zip(getattr(self, observable_lazy_list, None), getattr(other, observable_lazy_list, None))]): + self_list = getattr(self, observable_lazy_list, None) + other_list = getattr(other, observable_lazy_list, None) + if self_list is None: + self_list = [] + if other_list is None: + other_list = [] + if len(self_list) != len(other_list): + return False + if not all([oll1 == oll2 for oll1, oll2 in zip(self_list, other_list)]): return False return True @@ -141,6 +149,15 @@ def __post_init__(self): self.columns = ObservableLazyList(lambda: self.get_columns_handler(self)) self.checks = ObservableLazyList(lambda: self.get_checks_handler(self)) self.foreign_keys = ObservableLazyList(lambda: self.get_foreign_keys_handler(self)) + self.records = ObservableLazyList( + lambda: self.get_records_handler( + self, + filters=None, + limit=1000, + offset=0, + orders=None, + ) + ) def load_records(self, filters: Optional[str] = None, limit: int = 1000, offset: int = 0, orders: Optional[str] = None): self.records = ObservableLazyList(lambda: self.get_records_handler(self, filters=filters, limit=limit, offset=offset, orders=orders)) @@ -231,8 +248,15 @@ def is_new(self): def generate_uuid(length: int = 8) -> str: return str(uuid.uuid4())[::-1][:length] + @abc.abstractmethod def raw_create(self) -> str: - raise NotImplementedError(f"{self.__class__.__name__}.raw_create() is not implemented") + """Return the raw SQL string that would create the index. + + Concrete engine implementations must provide the SQL statement used to + create the index. The method is required because the dump process + relies on it for schema export. + """ + raise NotImplementedError def get_identifier_indexes(self) -> list['SQLIndex']: identifier_indexes = [] @@ -293,6 +317,41 @@ def quoted_name(self): def fully_qualified_name(self): return self.table.database.context.qualify(self.table.database.name, self.table.name, self.name) + @abc.abstractmethod + def add(self) -> bool: + """Add the column to the table. + + Concrete engine implementations must execute the appropriate SQL + statement and return ``True`` on success. + """ + raise NotImplementedError + + @abc.abstractmethod + def rename(self, new_name: str) -> bool: + """Rename the column to ``new_name``. + + Implementations should perform the ALTER operation and return ``True`` + on success. + """ + raise NotImplementedError + + @abc.abstractmethod + def drop(self) -> bool: + """Drop the column from the table. + + Implementations must execute the appropriate DROP COLUMN statement. + """ + raise NotImplementedError + + @abc.abstractmethod + def modify(self, current: Self): + """Modify the column to match the definition of ``current``. + + ``current`` is the existing column definition; the method should apply + any necessary ALTER statements to bring the database column in sync. + """ + raise NotImplementedError + def copy(self): cls = self.__class__ field_values = {f.name: getattr(self, f.name) for f in dataclasses.fields(cls)} @@ -361,11 +420,17 @@ def fully_qualified_name(self): @property def is_primary_key(self): - return any([i.type == SQLiteIndexType.PRIMARY for i in list(self.table.indexes) if self.name in i.columns]) + indexes = getattr(self.table, 'indexes', None) + if indexes is None: + return False + return any([i.type == SQLiteIndexType.PRIMARY for i in list(indexes) if self.name in i.columns]) @property def is_unique_key(self): - return any([i.type == SQLiteIndexType.UNIQUE for i in list(self.table.indexes) if self.name in i.columns]) + indexes = getattr(self.table, 'indexes', None) + if indexes is None: + return False + return any([i.type == SQLiteIndexType.UNIQUE for i in list(indexes) if self.name in i.columns]) @property def is_valid(self): @@ -507,8 +572,40 @@ def copy(self): field_values = {f.name: getattr(self, f.name) for f in dataclasses.fields(cls)} return cls(**field_values) + @abc.abstractmethod + def create(self) -> bool: + """Create the index in the database. + + Concrete engines must implement the appropriate CREATE INDEX statement + and return ``True`` on success. + """ + raise NotImplementedError + + @abc.abstractmethod + def drop(self) -> bool: + """Drop the index from the database. + + Implementations should handle primary‑key special cases where dropping + may be a no‑op. + """ + raise NotImplementedError + + @abc.abstractmethod + def alter(self, original_index: Self) -> bool: + """Alter the index to match ``original_index``. + + The default strategy is to drop and recreate; concrete classes may + provide a more efficient implementation. + """ + raise NotImplementedError + + @abc.abstractmethod def raw_create(self) -> str: - raise NotImplementedError(f"{self.__class__.__name__}.raw_create() is not implemented") + """Return the raw SQL string for creating the index. + + This is used by ``create`` implementations to execute the statement. + """ + raise NotImplementedError @dataclasses.dataclass(eq=False) @@ -533,7 +630,7 @@ def __eq__(self, other): return False if not all([ - getattr(self, field.name) != getattr(other, field.name) + getattr(self, field.name) == getattr(other, field.name) for field in dataclasses.fields(self) if field.compare ]): @@ -562,7 +659,7 @@ def reference_table_quoted_name(self) -> str: def copy(self): cls = self.__class__ - field_values = {f.name: getattr(self, f.name) for f in dataclasses.fields(cls)} + field_values = {f.name: getattr(self, f.name) for f in dataclasses.fields(cls) if f.init == True} return cls(**field_values) @@ -681,6 +778,17 @@ class SQLView(abc.ABC): name: str database: SQLDatabase = dataclasses.field(compare=False) statement: str + total_rows: Optional[int] = None + + get_columns_handler: Callable = dataclasses.field(compare=False, default_factory=lambda: lambda view: list()) + get_records_handler: Callable = dataclasses.field(compare=False, default_factory=lambda: lambda view, **kw: list()) + + def __post_init__(self): + self.columns = ObservableLazyList(lambda: self.get_columns_handler(self)) + self.records = ObservableLazyList(lambda: []) + + def load_records(self, filters=None, limit=1000, offset=0, orders=None): + self.records = ObservableLazyList(lambda: self.get_records_handler(self, filters=filters, limit=limit, offset=offset, orders=orders)) @property def is_new(self) -> bool: diff --git a/structures/engines/mariadb/context.py b/structures/engines/mariadb/context.py index b459c06..e30415a 100755 --- a/structures/engines/mariadb/context.py +++ b/structures/engines/mariadb/context.py @@ -84,6 +84,13 @@ def after_connect(self, *args, **kwargs): self.FUNCTIONS = tuple(dict.fromkeys(builtin_functions + user_functions)) + if self.connection.read_only: + self.execute("SET SESSION TRANSACTION READ ONLY;") + + def set_write_mode(self, enabled: bool) -> None: + mode = "READ WRITE" if enabled else "READ ONLY" + self.execute(f"SET SESSION TRANSACTION {mode};") + def _parse_type(self, column_type: str): """Parse a raw COLUMN_TYPE string from information_schema into structured field attributes. @@ -202,6 +209,7 @@ def connect(self, **connect_kwargs) -> None: port=self.port, connect_timeout=connect_timeout, compress=compressed_protocol, + autocommit=True, **connect_kwargs, ) if use_tls: @@ -293,6 +301,7 @@ def get_databases(self) -> list[SQLDatabase]: context=self, get_tables_handler=self.get_tables, get_procedures_handler=self.get_procedures, + get_functions_handler=self.get_functions, get_views_handler=self.get_views, get_triggers_handler=self.get_triggers, ) @@ -322,6 +331,31 @@ def get_procedures(self, database: SQLDatabase) -> list[MariaDBProcedure]: return results + def get_functions(self, database: SQLDatabase) -> list["MariaDBFunction"]: + from structures.engines.mariadb.database import MariaDBFunction + + results: list[MariaDBFunction] = [] + self.execute( + f""" + SELECT ROUTINE_NAME + FROM INFORMATION_SCHEMA.ROUTINES + WHERE ROUTINE_SCHEMA = '{database.name}' AND ROUTINE_TYPE = 'FUNCTION' + ORDER BY ROUTINE_NAME + """ + ) + for i, result in enumerate(self.fetchall()): + results.append( + MariaDBFunction( + id=i, + name=result["ROUTINE_NAME"], + database=database, + parameters="", + returns="", + statement="", + ) + ) + return results + def get_views(self, database: SQLDatabase): results: list[MariaDBView] = [] self.execute( @@ -334,6 +368,8 @@ def get_views(self, database: SQLDatabase): name=result["TABLE_NAME"], database=database, statement=result["VIEW_DEFINITION"], + get_columns_handler=self.get_columns, + get_records_handler=self.get_records, ) ) @@ -602,6 +638,17 @@ def get_records( return results + def build_empty_database(self, /, name: str = "") -> MariaDBDatabase: + return MariaDBDatabase( + id=MariaDBContext.get_temporary_id(self.databases), + name=name, + context=self, + get_tables_handler=self.get_tables, + get_procedures_handler=self.get_procedures, + get_views_handler=self.get_views, + get_triggers_handler=self.get_triggers, + ) + def build_empty_table( self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> MariaDBTable: @@ -734,6 +781,8 @@ def build_empty_view( name=name, database=database, statement=default_values.get("statement", ""), + get_columns_handler=self.get_columns, + get_records_handler=self.get_records, ) def build_empty_function( diff --git a/structures/engines/mariadb/database.py b/structures/engines/mariadb/database.py index 5a52634..606335a 100644 --- a/structures/engines/mariadb/database.py +++ b/structures/engines/mariadb/database.py @@ -17,18 +17,19 @@ class MariaDBDatabase(SQLDatabase): character_set: Optional[str] = None encryption: Optional[str] = None - def _build_database_clauses(self) -> list[str]: + def __post_init__(self): + super().__post_init__() + self._changed_fields: set[str] = set() + + def _build_database_clauses(self, fields: set[str] | None = None) -> list[str]: clauses: list[str] = [] - if self.character_set: + if (fields is None or "character_set" in fields) and self.character_set: clauses.append(f"CHARACTER SET {self.context.quote_identifier(self.character_set)}") - if self.default_collation: + if (fields is None or "default_collation" in fields) and self.default_collation: clauses.append(f"COLLATE {self.context.quote_identifier(self.default_collation)}") - if self.encryption: - clauses.append(f"ENCRYPTION = '{str(self.encryption).upper()}'") - return clauses def create(self) -> bool: @@ -40,13 +41,15 @@ def create(self) -> bool: return self.context.execute(query) def alter(self) -> bool: - clauses = self._build_database_clauses() + fields = self._changed_fields or None + clauses = self._build_database_clauses(fields) if not clauses: return False + name = getattr(self, "_original_name", self.name) self.context.execute( - f"ALTER DATABASE {self.context.quote_identifier(self.name)} {' '.join(clauses)}" + f"ALTER DATABASE {self.context.quote_identifier(name)} {' '.join(clauses)}" ) return True @@ -67,12 +70,19 @@ def raw_create(self) -> str: columns_and_indexes = columns + indexes + clauses: list[str] = [] + + if self.collation_name: + clauses.append(f"COLLATE='{self.collation_name}'") + + if self.engine: + clauses.append(f"ENGINE={self.engine}") + return f""" CREATE TABLE {self.fully_qualified_name} ( {', '.join(columns_and_indexes)} ) - COLLATE='{self.collation_name}' - ENGINE={self.engine}; + {' '.join(clauses)}; """ def alter_auto_increment(self, auto_increment: int): @@ -264,6 +274,10 @@ def drop(self) -> bool: return self.table.database.context.execute(f"DROP INDEX {self.quoted_name} ON {self.table.fully_qualified_name}") + def alter(self, original_index: Self) -> bool: + original_index.drop() + return self.create() + def modify(self, new: Self): self.drop() diff --git a/structures/engines/mariadb/datatype.py b/structures/engines/mariadb/datatype.py index 4060773..6cbf07c 100755 --- a/structures/engines/mariadb/datatype.py +++ b/structures/engines/mariadb/datatype.py @@ -37,9 +37,9 @@ class MariaDBDataType(StandardDataType): TIME = SQLDataType(name="TIME", category=DataTypeCategory.TEMPORAL) YEAR = SQLDataType(name="YEAR", category=DataTypeCategory.TEMPORAL) - ENUM = SQLDataType(name="ENUM", category=DataTypeCategory.TEXT, has_set=True) - SET = SQLDataType(name="SET", category=DataTypeCategory.TEXT, has_set=True) + ENUM = SQLDataType(name="ENUM", category=DataTypeCategory.OTHER, has_set=True) + SET = SQLDataType(name="SET", category=DataTypeCategory.OTHER, has_set=True) # Other BOOLEAN = StandardDataType.BOOLEAN - JSON = SQLDataType(name="JSON", category=DataTypeCategory.TEXT, format=DataTypeFormat.JSON) \ No newline at end of file + JSON = SQLDataType(name="JSON", category=DataTypeCategory.OTHER, format=DataTypeFormat.JSON) \ No newline at end of file diff --git a/structures/engines/mariadb/indextype.py b/structures/engines/mariadb/indextype.py index 8aba0c3..d6b0c36 100755 --- a/structures/engines/mariadb/indextype.py +++ b/structures/engines/mariadb/indextype.py @@ -3,7 +3,5 @@ class MariaDBIndexType(StandardIndexType): - BTREE = SQLIndexType(name="BTREE", prefix="btree_", bitmap=IconList.KEY_SPATIAL, enable_append=True) - HASH = SQLIndexType(name="HASH", prefix="hash_", bitmap=IconList.KEY_SPATIAL, enable_append=False) - FULLTEXT = SQLIndexType(name="FULLTEXT", prefix="ft_", bitmap=IconList.KEY_SPATIAL, enable_append=False) + FULLTEXT = SQLIndexType(name="FULLTEXT", prefix="ft_", bitmap=IconList.KEY_FULLTEXT, enable_append=False) SPATIAL = SQLIndexType(name="SPATIAL", prefix="spatial_", bitmap=IconList.KEY_SPATIAL, enable_append=False) diff --git a/structures/engines/mysql/context.py b/structures/engines/mysql/context.py index c8eaf8a..1c5c3af 100644 --- a/structures/engines/mysql/context.py +++ b/structures/engines/mysql/context.py @@ -85,6 +85,13 @@ def after_connect(self, *args, **kwargs): self.FUNCTIONS = tuple(dict.fromkeys(builtin_functions + user_functions)) + if self.connection.read_only: + self.execute("SET SESSION TRANSACTION READ ONLY;") + + def set_write_mode(self, enabled: bool) -> None: + mode = "READ WRITE" if enabled else "READ ONLY" + self.execute(f"SET SESSION TRANSACTION {mode};") + def _parse_type(self, column_type: str): """Parse a raw COLUMN_TYPE string from information_schema into structured field attributes. @@ -202,6 +209,7 @@ def connect(self, **connect_kwargs) -> None: cursorclass=pymysql.cursors.DictCursor, connect_timeout=connect_timeout, compress=compressed_protocol, + autocommit=True, **connect_kwargs, ) if use_tls: @@ -303,6 +311,7 @@ def get_databases(self) -> list[SQLDatabase]: context=self, get_tables_handler=self.get_tables, get_procedures_handler=self.get_procedures, + get_functions_handler=self.get_functions, get_views_handler=self.get_views, get_triggers_handler=self.get_triggers, ) @@ -332,6 +341,31 @@ def get_procedures(self, database: SQLDatabase) -> list[MySQLProcedure]: return results + def get_functions(self, database: SQLDatabase) -> list["MySQLFunction"]: + from structures.engines.mysql.database import MySQLFunction + + results: list[MySQLFunction] = [] + self.execute( + f""" + SELECT ROUTINE_NAME + FROM INFORMATION_SCHEMA.ROUTINES + WHERE ROUTINE_SCHEMA = '{database.name}' AND ROUTINE_TYPE = 'FUNCTION' + ORDER BY ROUTINE_NAME + """ + ) + for i, result in enumerate(self.fetchall()): + results.append( + MySQLFunction( + id=i, + name=result["ROUTINE_NAME"], + database=database, + parameters="", + returns="", + statement="", + ) + ) + return results + def get_views(self, database: SQLDatabase): results: list[MySQLView] = [] self.execute( @@ -344,6 +378,8 @@ def get_views(self, database: SQLDatabase): name=result["TABLE_NAME"], database=database, statement=result["VIEW_DEFINITION"] or "", + get_columns_handler=self.get_columns, + get_records_handler=self.get_records, ) ) @@ -613,6 +649,18 @@ def get_records( logger.debug(f"get records for table={table.name}") return results + def build_empty_database(self, /, name: str = "") -> MySQLDatabase: + return MySQLDatabase( + id=MySQLContext.get_temporary_id(self.databases), + name=name, + context=self, + get_tables_handler=self.get_tables, + get_procedures_handler=self.get_procedures, + get_functions_handler=self.get_functions, + get_views_handler=self.get_views, + get_triggers_handler=self.get_triggers, + ) + def build_empty_table( self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> MySQLTable: @@ -741,6 +789,8 @@ def build_empty_view( name=name, database=database, statement=default_values.get("statement", ""), + get_columns_handler=self.get_columns, + get_records_handler=self.get_records, ) def build_empty_function( @@ -760,7 +810,7 @@ def build_empty_function( parameters=default_values.get("parameters", ""), returns=default_values.get("returns", "INT"), deterministic=default_values.get("deterministic", False), - sql=default_values.get("sql", ""), + statement=default_values.get("statement", ""), ) def build_empty_procedure( diff --git a/structures/engines/mysql/database.py b/structures/engines/mysql/database.py index 399f4e3..e6a67b4 100644 --- a/structures/engines/mysql/database.py +++ b/structures/engines/mysql/database.py @@ -28,16 +28,20 @@ class MySQLDatabase(SQLDatabase): character_set: Optional[str] = None encryption: Optional[str] = None - def _build_database_clauses(self) -> list[str]: + def __post_init__(self): + super().__post_init__() + self._changed_fields: set[str] = set() + + def _build_database_clauses(self, fields: set[str] | None = None) -> list[str]: clauses: list[str] = [] - if self.character_set: + if (fields is None or "character_set" in fields) and self.character_set: clauses.append(f"CHARACTER SET {self.context.quote_identifier(self.character_set)}") - if self.default_collation: + if (fields is None or "default_collation" in fields) and self.default_collation: clauses.append(f"COLLATE {self.context.quote_identifier(self.default_collation)}") - if self.encryption: + if (fields is None or "encryption" in fields) and self.encryption: clauses.append(f"ENCRYPTION = '{str(self.encryption).upper()}'") return clauses @@ -51,13 +55,15 @@ def create(self) -> bool: return self.context.execute(query) def alter(self) -> bool: - clauses = self._build_database_clauses() + fields = self._changed_fields or None + clauses = self._build_database_clauses(fields) if not clauses: return False + name = getattr(self, "_original_name", self.name) self.context.execute( - f"ALTER DATABASE {self.context.quote_identifier(self.name)} {' '.join(clauses)}" + f"ALTER DATABASE {self.context.quote_identifier(name)} {' '.join(clauses)}" ) return True @@ -79,12 +85,19 @@ def raw_create(self) -> str: columns_and_indexes = columns + indexes + clauses: list[str] = [] + + if self.collation_name: + clauses.append(f"COLLATE='{self.collation_name}'") + + if self.engine: + clauses.append(f"ENGINE={self.engine}") + return f""" CREATE TABLE {self.fully_qualified_name} ( {', '.join(columns_and_indexes)} ) - COLLATE='{self.collation_name}' - ENGINE={self.engine}; + {' '.join(clauses)}; """ def alter_auto_increment(self, auto_increment: int): @@ -262,6 +275,10 @@ def drop(self) -> bool: return self.table.database.context.execute(f"DROP INDEX {self.quoted_name} ON {self.table.fully_qualified_name}") + def alter(self, original_index: Self) -> bool: + original_index.drop() + return self.create() + def modify(self, new: Self): self.drop() @@ -427,7 +444,7 @@ class MySQLFunction(SQLFunction): parameters: str = "" returns: str = "" deterministic: bool = False - sql: str = "" + statement: str = "" def _show_create_function(self) -> str: context = self.database.context @@ -439,7 +456,7 @@ def create(self) -> bool: return self.database.context.execute(self.raw_create()) def raw_create(self) -> str: - if not self.sql.strip() or not self.returns.strip(): + if not self.statement.strip() or not self.returns.strip(): return self._show_create_function() deterministic = "DETERMINISTIC" if self.deterministic else "NOT DETERMINISTIC" @@ -448,7 +465,7 @@ def raw_create(self) -> str: RETURNS {self.returns} {deterministic} BEGIN - {self.sql}; + {self.statement}; END """ diff --git a/structures/engines/mysql/datatype.py b/structures/engines/mysql/datatype.py index 0846df2..15afc2c 100644 --- a/structures/engines/mysql/datatype.py +++ b/structures/engines/mysql/datatype.py @@ -42,9 +42,9 @@ class MySQLDataType(StandardDataType): TIME = SQLDataType(name="TIME", category=DataTypeCategory.TEMPORAL) YEAR = SQLDataType(name="YEAR", category=DataTypeCategory.TEMPORAL) - ENUM = SQLDataType(name="ENUM", category=DataTypeCategory.TEXT, has_set=True) - SET = SQLDataType(name="SET", category=DataTypeCategory.TEXT, has_set=True) + ENUM = SQLDataType(name="ENUM", category=DataTypeCategory.OTHER, has_set=True) + SET = SQLDataType(name="SET", category=DataTypeCategory.OTHER, has_set=True) # Other BOOLEAN = StandardDataType.BOOLEAN - JSON = SQLDataType(name="JSON", category=DataTypeCategory.TEXT, format=DataTypeFormat.JSON) + JSON = SQLDataType(name="JSON", category=DataTypeCategory.OTHER, format=DataTypeFormat.JSON) diff --git a/structures/engines/postgresql/context.py b/structures/engines/postgresql/context.py index 964fd4f..0cfa5f6 100644 --- a/structures/engines/postgresql/context.py +++ b/structures/engines/postgresql/context.py @@ -74,6 +74,13 @@ def after_connect(self, *args, **kwargs): self._load_custom_types() + if self.connection.read_only: + self.execute("SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;") + + def set_write_mode(self, enabled: bool) -> None: + mode = "READ WRITE" if enabled else "READ ONLY" + self.execute(f"SET SESSION CHARACTERISTICS AS TRANSACTION {mode};") + def _load_custom_types(self) -> None: """Load user-defined enum types from the database.""" self.execute(""" @@ -254,6 +261,40 @@ def get_databases(self) -> list[SQLDatabase]: ) return results + def get_view_columns(self, view) -> list: + results = [] + if view is None or view.is_new: + return results + try: + QUERY_LOGS.append(f"/* get_view_columns for view={view.name} */") + schema = view.schema if view.schema else "public" + self.execute(f""" + SELECT column_name, data_type, character_maximum_length, numeric_precision, numeric_scale, + is_nullable, column_default + FROM information_schema.columns + WHERE table_schema = '{schema}' AND table_name = '{view.name}' + ORDER BY ordinal_position + """) + for i, row in enumerate(self.cursor.fetchall()): + is_nullable = row["is_nullable"] == "YES" + datatype = PostgreSQLDataType.get_by_name(row["data_type"]) + results.append( + PostgreSQLColumn( + id=i, + name=row["column_name"], + datatype=datatype, + is_nullable=is_nullable, + table=view, + server_default=row["column_default"], + length=row["character_maximum_length"], + numeric_precision=row["numeric_precision"], + numeric_scale=row["numeric_scale"], + ) + ) + except Exception: + pass + return results + def get_views(self, database: SQLDatabase) -> list[PostgreSQLView]: self.set_database(database) results = [] @@ -265,8 +306,11 @@ def get_views(self, database: SQLDatabase) -> list[PostgreSQLView]: PostgreSQLView( id=i, name=result["viewname"], + schema=result["schemaname"], database=database, statement=result["definition"], + get_columns_handler=self.get_view_columns, + get_records_handler=self.get_records, ) ) @@ -636,6 +680,18 @@ def get_records( return results + def build_empty_database(self, /, name: str = "") -> PostgreSQLDatabase: + return PostgreSQLDatabase( + id=PostgreSQLContext.get_temporary_id(self.databases), + name=name, + context=self, + get_tables_handler=self.get_tables, + get_views_handler=self.get_views, + get_functions_handler=self.get_functions, + get_procedures_handler=self.get_procedures, + get_triggers_handler=self.get_triggers, + ) + def build_empty_table( self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> PostgreSQLTable: @@ -760,8 +816,11 @@ def build_empty_view( return PostgreSQLView( id=id, name=name, + schema=default_values.get("schema", "public"), database=database, statement=default_values.get("statement", ""), + get_columns_handler=self.get_view_columns, + get_records_handler=self.get_records, ) def build_empty_function( diff --git a/structures/engines/postgresql/database.py b/structures/engines/postgresql/database.py index 57be67d..67ea3bb 100644 --- a/structures/engines/postgresql/database.py +++ b/structures/engines/postgresql/database.py @@ -1,4 +1,5 @@ import dataclasses +import re from typing import Self, Optional from helpers.logger import logger @@ -35,17 +36,25 @@ def create(self) -> bool: return self.context.execute(query) + def __post_init__(self): + super().__post_init__() + self._changed_fields: set[str] = set() + def alter(self) -> bool: statements: list[str] = [] - if self.tablespace: + name = getattr(self, "_original_name", self.name) + + check_all = not self._changed_fields + + if (check_all or "tablespace" in self._changed_fields) and self.tablespace: statements.append( - f"ALTER DATABASE {self.context.quote_identifier(self.name)} SET TABLESPACE {self.context.quote_identifier(self.tablespace)}" + f"ALTER DATABASE {self.context.quote_identifier(name)} SET TABLESPACE {self.context.quote_identifier(self.tablespace)}" ) - if self.connection_limit is not None: + if (check_all or "connection_limit" in self._changed_fields) and self.connection_limit is not None: statements.append( - f"ALTER DATABASE {self.context.quote_identifier(self.name)} CONNECTION LIMIT {int(self.connection_limit)}" + f"ALTER DATABASE {self.context.quote_identifier(name)} CONNECTION LIMIT {int(self.connection_limit)}" ) if not statements: @@ -63,7 +72,7 @@ def drop(self) -> bool: @dataclasses.dataclass(eq=False) class PostgreSQLTable(SQLTable): - schema: str = None + schema: Optional[str] = None @property def fully_qualified_name(self): @@ -142,38 +151,35 @@ def alter(self) -> bool: transaction.execute(f'ALTER TABLE {original_table.fully_qualified_name} RENAME TO {new_name_quoted};') # Handle column changes - for column in map_columns['added']: - transaction.execute(f'ALTER TABLE {self.fully_qualified_name} ADD COLUMN {str(PostgreSQLColumnBuilder(column))};') - - for column in map_columns['removed']: - col_name_quoted = self.database.context.quote_identifier(column.name) - transaction.execute(f'ALTER TABLE {self.fully_qualified_name} DROP COLUMN {col_name_quoted};') - - for column in map_columns['modified']: - original_column = column['original'] - current_column = column['current'] - if original_column.name != current_column.name: + for original_column, current_column in map_columns: + if original_column is None: + transaction.execute(f'ALTER TABLE {self.fully_qualified_name} ADD COLUMN {str(PostgreSQLColumnBuilder(current_column))};') + elif current_column is None: + col_name_quoted = self.database.context.quote_identifier(original_column.name) + transaction.execute(f'ALTER TABLE {self.fully_qualified_name} DROP COLUMN {col_name_quoted};') + elif original_column.name != current_column.name: old_name_quoted = self.database.context.quote_identifier(original_column.name) new_name_quoted = self.database.context.quote_identifier(current_column.name) transaction.execute(f'ALTER TABLE {self.fully_qualified_name} RENAME COLUMN {old_name_quoted} TO {new_name_quoted};') # For other changes, might need more complex ALTER statements # Handle index changes - for index in map_indexes['added']: - index.create() - - for index in map_indexes['removed']: - index.drop() - - for index in map_indexes['modified']: - index['current'].alter(index['original']) + for original_index, current_index in map_indexes: + if current_index is None: + original_index.drop() + elif original_index is None: + current_index.create() + elif original_index != current_index: + current_index.alter(original_index) # Handle foreign key changes - for fk in map_foreign_keys['added']: - fk.create() - - for fk in map_foreign_keys['removed']: - fk.drop() + for original_foreign_key, current_foreign_key in map_foreign_keys: + if current_foreign_key is None: + original_foreign_key.drop() + elif original_foreign_key is None: + current_foreign_key.create() + elif original_foreign_key != current_foreign_key: + original_foreign_key.modify(current_foreign_key) except Exception as ex: logger.error(ex, exc_info=True) @@ -398,9 +404,11 @@ def delete(self) -> bool: @dataclasses.dataclass class PostgreSQLView(SQLView): + schema: Optional[str] = "public" + @property def fully_qualified_name(self): - return self.database.context.qualify('public', self.name) + return self.database.context.qualify(self.schema, self.name) def raw_create(self) -> str: return f'CREATE VIEW {self.fully_qualified_name} AS {self.statement};' diff --git a/structures/engines/sqlite/context.py b/structures/engines/sqlite/context.py index 015d55b..9cc481c 100755 --- a/structures/engines/sqlite/context.py +++ b/structures/engines/sqlite/context.py @@ -103,7 +103,12 @@ def connect(self, **connect_kwargs) -> None: try: if not skip_before_connect: self.before_connect() - self._connection = sqlite3.connect(self.filename) + if self.connection.read_only and self.filename not in (":memory:", ""): + self._connection = sqlite3.connect( + f"file:{self.filename}?mode=ro", uri=True + ) + else: + self._connection = sqlite3.connect(self.filename) except Exception as e: logger.error(f"Failed to connect to SQLite: {e}") @@ -114,6 +119,15 @@ def connect(self, **connect_kwargs) -> None: if not skip_after_connect: self.after_connect() + def set_write_mode(self, enabled: bool) -> None: + # SQLite read_only is URI-level (?mode=ro) — must reconnect + # connection.read_only is already updated by the caller before this is invoked + if self._connection: + self._connection.close() + self._connection = None + self._cursor = None + self.connect() + def set_database(self, database: SQLDatabase) -> None: pass @@ -481,6 +495,27 @@ def get_records( results.append(SQLiteRecord(id=i, table=table, values=dict(record))) return results + def get_view_columns(self, view) -> list[SQLiteColumn]: + results = [] + if view is None or view.is_new: + return results + try: + self.execute(f"PRAGMA table_info(`{view.name}`)") + for i, row in enumerate(self.fetchall()): + results.append( + SQLiteColumn( + id=i, + name=row["name"], + datatype=SQLiteDataType.get_by_name(row["type"] or "TEXT"), + is_nullable=row["notnull"] == 0, + table=view, + server_default=row["dflt_value"], + ) + ) + except Exception: + pass + return results + def get_views(self, database: SQLDatabase): results: list[SQLiteView] = [] self.execute( @@ -493,6 +528,8 @@ def get_views(self, database: SQLDatabase): name=result["name"], database=database, statement=result["sql"], + get_columns_handler=self.get_view_columns, + get_records_handler=self.get_records, ) ) @@ -515,6 +552,9 @@ def get_triggers(self, database: SQLDatabase) -> list[SQLiteTrigger]: return results + def build_empty_database(self, /, name: str = "") -> SQLiteDatabase: + raise NotImplementedError("SQLite does not support creating databases") + def build_empty_table( self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> SQLiteTable: @@ -596,8 +636,6 @@ def build_empty_foreign_key( self, table: SQLiteTable, columns: list[str], - reference_table: str, - reference_columns: list[str], /, name: Optional[str] = None, **default_values, @@ -614,10 +652,10 @@ def build_empty_foreign_key( name=name, table=table, columns=columns, - reference_table="", - reference_columns=[], - on_update="", - on_delete="", + reference_table=default_values.get("reference_table", ""), + reference_columns=default_values.get("reference_columns", []), + on_update=default_values.get("on_update", "NO ACTION"), + on_delete=default_values.get("on_delete", "NO ACTION"), ) def build_empty_record( @@ -640,6 +678,8 @@ def build_empty_view( name=name, database=database, statement=default_values.get("statement", ""), + get_columns_handler=self.get_view_columns, + get_records_handler=self.get_records, ) def build_empty_function( diff --git a/structures/engines/sqlite/database.py b/structures/engines/sqlite/database.py index de82eb6..8aa1d93 100644 --- a/structures/engines/sqlite/database.py +++ b/structures/engines/sqlite/database.py @@ -16,13 +16,13 @@ @dataclasses.dataclass class SQLiteDatabase(SQLDatabase): def create(self) -> bool: - return False + raise NotImplementedError("SQLite databases are files and cannot be created through SQL") def alter(self) -> bool: - return False + raise NotImplementedError("SQLite databases are files and cannot be altered through SQL") def drop(self) -> bool: - return False + raise NotImplementedError("SQLite databases are files and cannot be dropped through SQL") @dataclasses.dataclass(eq=False) @@ -306,12 +306,12 @@ def add(self) -> bool: def rename(self, new_name: str) -> bool: return self.table.database.context.execute(f"ALTER TABLE {self.table.fully_qualified_name} RENAME COLUMN {self.quoted_name} TO `{new_name}`") - def modify(self): + def modify(self, current: Self): sql_safe_new_name = self.table.database.context.quote_identifier(f"_{self.table.name}_{self.generate_uuid()}") for i, c in enumerate(self.table.columns): if c.name == self.name: - self.table.columns[i] = self + self.table.columns[i] = current break with self.table.database.context.transaction() as transaction: @@ -323,8 +323,8 @@ def modify(self): transaction.execute(f"DROP TABLE {sql_safe_new_name};") - def drop(self, table: SQLTable, column: SQLColumn) -> bool: - return self.table.database.context.execute(f"ALTER TABLE {table.fully_qualified_name} DROP COLUMN {self.quoted_name}") + def drop(self) -> bool: + return self.table.database.context.execute(f"ALTER TABLE {self.table.fully_qualified_name} DROP COLUMN {self.quoted_name}") @dataclasses.dataclass(eq=False) @@ -350,34 +350,56 @@ def raw_create(self) -> str: ) def create(self) -> bool: + """Create the SQLite index. + + ``raw_create`` returns an empty string for implicit primary‑key + indexes or auto‑generated unique indexes. In those cases the operation + is a no‑op and should be considered successful. + """ statement = self.raw_create() if not statement: - return False - + return True return self.table.database.context.execute(statement) def drop(self) -> bool: + """Drop the SQLite index. + + Primary‑key indexes cannot be dropped; we treat that as a successful + no‑op. Auto‑generated unique indexes are also managed by the table + creation process, so dropping them is a no‑op as well. + """ if self.type == SQLiteIndexType.PRIMARY: - return False + return True if self.type == SQLiteIndexType.UNIQUE and self.name.startswith("sqlite_autoindex_"): - return False # sqlite_ UNIQUE is handled in table creation - + return True - print(f"DROP INDEX IF EXISTS {self.fully_qualified_name}") return self.table.database.context.execute( f"DROP INDEX IF EXISTS {self.fully_qualified_name}" ) - def modify(self, new_index: Self): - self.drop() + def alter(self, original_index: Self): + """Alter the index by dropping the original and creating this one. + """ + original_index.drop() + return self.create() - new_index.create() + # Backward compatibility: ``modify`` was previously used in the codebase. + def modify(self, new_index: Self): + self.alter(new_index) @dataclasses.dataclass(eq=False) class SQLiteForeignKey(SQLForeignKey): - pass + def create(self) -> bool: + raise NotImplementedError("SQLite does not support adding Foreign Keys constraints after table creation") + + def drop(self) -> bool: + raise NotImplementedError("SQLite does not support dropping Foreign Keys constraints") + + def alter(self) -> bool: + raise NotImplementedError("SQLite does not support altering Foreign Keys constraints") + class SQLiteRecord(SQLRecord): @@ -397,7 +419,7 @@ def raw_insert_record(self) -> str: # elif column. : if not columns_values: - assert False, "No columns values" + raise ValueError("No column values provided for insert operation") return f"""INSERT INTO `{self.table.name}` ({', '.join(columns_values.keys())}) VALUES ({', '.join(columns_values.values())})""" @@ -411,7 +433,7 @@ def raw_update_record(self) -> Optional[str]: if not (existing_record := self.table.database.context.fetchone()): logger.warning(f"Record not found for update: {identifier_columns}") - assert False, "Record not found for update with identifier columns" + raise ValueError("Record not found for update with identifier columns") changed_columns = [] @@ -445,7 +467,9 @@ def insert(self) -> bool: if raw_insert_record := self.raw_insert_record(): try: return transaction.execute(raw_insert_record) - except: + except PermissionError: + raise + except Exception: return False return False @@ -455,7 +479,9 @@ def update(self) -> bool: if raw_update_record := self.raw_update_record(): try: return transaction.execute(raw_update_record) - except: + except PermissionError: + raise + except Exception: return False return False @@ -465,19 +491,21 @@ def delete(self) -> bool: if raw_delete_record := self.raw_delete_record(): try: return transaction.execute(raw_delete_record) - except: + except PermissionError: + raise + except Exception: return False return False class SQLiteView(SQLView): - def __init__(self, /, id: int, name: str, database: SQLDatabase, statement: str): + def __init__(self, /, id: int, name: str, database: SQLDatabase, statement: str, **kwargs): match = re.search(r'CREATE\s+VIEW\s+.*?\s+AS\s+(.*)', statement, re.IGNORECASE | re.DOTALL) if match: statement = match.group(1).strip() - super().__init__(id=id, name=name, database=database, statement=statement) + super().__init__(id=id, name=name, database=database, statement=statement, **kwargs) def raw_create(self) -> str: return f"CREATE VIEW IF NOT EXISTS {self.fully_qualified_name} AS {self.statement}" diff --git a/structures/engines/sqlite/datatype.py b/structures/engines/sqlite/datatype.py index 01d397f..d4003e1 100755 --- a/structures/engines/sqlite/datatype.py +++ b/structures/engines/sqlite/datatype.py @@ -25,4 +25,6 @@ class SQLiteDataType(StandardDataType): NUMERIC = SQLDataType(name="NUMERIC", category=DataTypeCategory.REAL) DECIMAL = SQLDataType(name="DECIMAL", category=DataTypeCategory.REAL, has_precision=True, has_scale=True) - BOOLEAN = StandardDataType.BOOLEAN \ No newline at end of file + BOOLEAN = StandardDataType.BOOLEAN + + UNKNOWN = SQLDataType(name="UNKNOWN", category=DataTypeCategory.TEXT, alias=["ANY"]) diff --git a/structures/secrets.py b/structures/secrets.py new file mode 100644 index 0000000..c0899cc --- /dev/null +++ b/structures/secrets.py @@ -0,0 +1,68 @@ +import re +from typing import Optional + +import keyring + +SERVICE_NAME = "PeterSQL" + +_LEGACY_NUMERIC_ID_PATTERN = re.compile(r"^[0-9]+$") + + +def _database_password_key(secret_id: str) -> str: + return f"connection:{secret_id}:database_password" + + +def _ssh_password_key(secret_id: str) -> str: + return f"connection:{secret_id}:ssh_password" + + +def _is_legacy_numeric_id(secret_id: Optional[str]) -> bool: + if secret_id is None: + return False + return bool(_LEGACY_NUMERIC_ID_PATTERN.match(secret_id)) + + +def get_database_password(secret_id: str) -> Optional[str]: + key = _database_password_key(secret_id) + value = keyring.get_password(SERVICE_NAME, key) + return value if value else None + + +def set_database_password(secret_id: str, password: Optional[str]) -> None: + key = _database_password_key(secret_id) + if password is None or password == "": + delete_database_password(secret_id) + return + + keyring.set_password(SERVICE_NAME, key, password) + + +def delete_database_password(secret_id: str) -> None: + key = _database_password_key(secret_id) + try: + keyring.delete_password(SERVICE_NAME, key) + except keyring.errors.PasswordDeleteError: + pass + + +def get_ssh_password(secret_id: str) -> Optional[str]: + key = _ssh_password_key(secret_id) + value = keyring.get_password(SERVICE_NAME, key) + return value if value else None + + +def set_ssh_password(secret_id: str, password: Optional[str]) -> None: + key = _ssh_password_key(secret_id) + if password is None or password == "": + delete_ssh_password(secret_id) + return + + keyring.set_password(SERVICE_NAME, key, password) + + +def delete_ssh_password(secret_id: str) -> None: + key = _ssh_password_key(secret_id) + try: + keyring.delete_password(SERVICE_NAME, key) + except keyring.errors.PasswordDeleteError: + pass diff --git a/tests/autocomplete/__init__.py b/tests/autocomplete/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py index 6a8ab1b..9e3c1e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,6 +56,20 @@ def pytest_collection_modifyitems(config, items): ) +def pytest_addoption(parser): + parser.addoption( + "--refresh-screenshots", + action="store_true", + default=False, + help="Refresh UI scenario screenshots in screenshot/ directory", + ) + + +@pytest.fixture +def refresh_screenshots(pytestconfig) -> bool: + return bool(pytestconfig.getoption("refresh_screenshots")) + + @pytest.fixture(scope="session", autouse=True) def wx_app(): """Initialize wx.App for GUI tests""" diff --git a/tests/test_column_controller.py b/tests/core/test_column_controller.py similarity index 100% rename from tests/test_column_controller.py rename to tests/core/test_column_controller.py diff --git a/tests/test_configurations.py b/tests/core/test_configurations.py similarity index 100% rename from tests/test_configurations.py rename to tests/core/test_configurations.py diff --git a/tests/test_connections.py b/tests/core/test_connections_repository.py similarity index 52% rename from tests/test_connections.py rename to tests/core/test_connections_repository.py index 9289684..e2c2359 100644 --- a/tests/test_connections.py +++ b/tests/core/test_connections_repository.py @@ -1,5 +1,11 @@ + import os +import re import tempfile +import uuid +from collections import OrderedDict +from typing import Any, Dict, Optional + import pytest import yaml @@ -12,6 +18,33 @@ from windows.dialogs.connections.repository import ConnectionsRepository +class FakeKeyring: + def __init__(self) -> None: + self._store: Dict[str, Dict[str, str]] = {} + + def get_password(self, service: str, username: str) -> Optional[str]: + return self._store.get(service, {}).get(username) + + def set_password(self, service: str, username: str, password: str) -> None: + self._store.setdefault(service, {})[username] = password + + def delete_password(self, service: str, username: str) -> None: + service_store = self._store.get(service) + if service_store and username in service_store: + del service_store[username] + + +def _secret_key(secret_id: str, kind: str) -> str: + return f"connection:{secret_id}:{kind}" + + +@pytest.fixture +def fake_keyring(monkeypatch): + keyring = FakeKeyring() + monkeypatch.setattr("structures.secrets.keyring", keyring) + return keyring + + class TestConnectionsRepository: @pytest.fixture def temp_yaml(self): @@ -25,7 +58,7 @@ def temp_yaml(self): os.unlink(tmp_path) @pytest.fixture - def repo(self, temp_yaml, monkeypatch): + def repo(self, temp_yaml, monkeypatch, fake_keyring): """Create a ConnectionsRepository instance with a temporary YAML file.""" return ConnectionsRepository(config_file=str(temp_yaml)) @@ -38,7 +71,7 @@ def test_load_empty_yaml(self, temp_yaml, repo): connections = repo.load() assert connections == [] - def test_load_connections_from_yaml(self, temp_yaml, repo): + def test_load_connections_from_yaml(self, temp_yaml, repo, fake_keyring): """Test loading connections from YAML.""" data = [ { @@ -82,6 +115,8 @@ def test_load_connections_from_yaml(self, temp_yaml, repo): assert isinstance(conn1.configuration, SourceConfiguration) assert conn1.configuration.filename == ":memory:" assert conn1.comments == "Test connection" + assert conn1.secret_id is not None + assert re.fullmatch(r"[0-9a-f-]{36}", conn1.secret_id) # Check second connection conn2 = connections[1] @@ -95,6 +130,8 @@ def test_load_connections_from_yaml(self, temp_yaml, repo): assert conn2.configuration.password == "pass" assert conn2.ssh_tunnel.enabled is True assert conn2.ssh_tunnel.hostname == "remote.host" + assert conn2.secret_id is not None + assert re.fullmatch(r"[0-9a-f-]{36}", conn2.secret_id) def test_load_directories_from_yaml(self, temp_yaml, repo): """Test loading directories with nested connections.""" @@ -155,7 +192,7 @@ def test_load_directories_from_yaml(self, temp_yaml, repo): assert conn2.name == "Dev DB" assert conn2.engine == ConnectionEngine.SQLITE - def test_add_connection(self, temp_yaml, repo): + def test_add_connection(self, temp_yaml, repo, fake_keyring): """Test adding a new connection.""" config = SourceConfiguration(filename="test.db") connection = Connection( @@ -175,7 +212,7 @@ def test_add_connection(self, temp_yaml, repo): assert data[0]["name"] == "New Connection" assert data[0]["id"] == 0 - def test_save_connection(self, temp_yaml, repo): + def test_save_connection(self, temp_yaml, repo, fake_keyring): """Test saving/updating an existing connection.""" # Start with a connection data = [ @@ -202,7 +239,7 @@ def test_save_connection(self, temp_yaml, repo): updated_data = yaml.safe_load(f) assert updated_data[0]["name"] == "Updated Name" - def test_delete_connection(self, temp_yaml, repo): + def test_delete_connection(self, temp_yaml, repo, fake_keyring): """Test deleting a connection.""" # Start with connections data = [ @@ -233,7 +270,7 @@ def test_delete_connection(self, temp_yaml, repo): assert len(updated_data) == 1 assert updated_data[0]["name"] == "Conn2" - def test_add_directory(self, temp_yaml, repo): + def test_add_directory(self, temp_yaml, repo, fake_keyring): """Test adding a new directory.""" with open(temp_yaml, "w") as f: f.write("[]") @@ -249,7 +286,7 @@ def test_add_directory(self, temp_yaml, repo): assert data[0]["type"] == "directory" assert data[0]["name"] == "New Directory" - def test_delete_directory(self, temp_yaml, repo): + def test_delete_directory(self, temp_yaml, repo, fake_keyring): """Test deleting a directory.""" data = [ {"type": "directory", "name": "Dir1", "children": []}, @@ -258,13 +295,141 @@ def test_delete_directory(self, temp_yaml, repo): with open(temp_yaml, "w") as f: yaml.dump(data, f) - # Load and delete first directory - items = repo.load() - dir_to_delete = items[0] - repo.delete_directory(dir_to_delete) + def test_save_connection_moves_plaintext_password_to_keyring( + self, temp_yaml, repo, fake_keyring + ): + data = [ + { + "id": 1, + "name": "Legacy MySQL", + "engine": "MySQL", + "configuration": { + "hostname": "localhost", + "port": 3306, + "username": "user", + "password": "plaintext", + }, + "ssh_tunnel": { + "enabled": True, + "hostname": "remote.host", + "port": 22, + "username": "sshuser", + "password": "sshplain", + "local_port": 3307, + }, + } + ] + with open(temp_yaml, "w") as f: + yaml.dump(data, f) + + connections = repo.load() + connection = connections[0] + repo.save_connection(connection) - # Check only one directory remains with open(temp_yaml, "r") as f: - updated_data = yaml.safe_load(f) - assert len(updated_data) == 1 - assert updated_data[0]["name"] == "Dir2" + saved_data = yaml.safe_load(f) + + assert saved_data[0]["configuration"].get("password") is None + assert "password_keyring_id" not in saved_data[0]["configuration"] + assert saved_data[0]["ssh_tunnel"].get("password") is None + assert "password_keyring_id" not in saved_data[0]["ssh_tunnel"] + assert saved_data[0]["secret_id"] == connection.secret_id + assert fake_keyring.get_password("PeterSQL", _secret_key(connection.secret_id, "database_password")) == "plaintext" + assert fake_keyring.get_password("PeterSQL", _secret_key(connection.secret_id, "ssh_password")) == "sshplain" + + def test_delete_connection_removes_keyring_entries( + self, temp_yaml, repo, fake_keyring + ): + connection = Connection( + id=1, + name="ToDelete", + engine=ConnectionEngine.MYSQL, + configuration=CredentialsConfiguration( + hostname="localhost", + username="user", + password="secret", + port=3306, + ), + ssh_tunnel=SSHTunnelConfiguration( + enabled=True, + executable="ssh", + hostname="remote.host", + port=22, + username="sshuser", + password="sshsecret", + local_port=3307, + ), + ) + repo.add_connection(connection) + assert fake_keyring.get_password("PeterSQL", _secret_key(connection.secret_id, "database_password")) == "secret" + assert fake_keyring.get_password("PeterSQL", _secret_key(connection.secret_id, "ssh_password")) == "sshsecret" + + repo.delete_connection(connection) + + assert fake_keyring.get_password("PeterSQL", _secret_key(connection.secret_id, "database_password")) is None + assert fake_keyring.get_password("PeterSQL", _secret_key(connection.secret_id, "ssh_password")) is None + + def test_load_migrates_legacy_numeric_keyring_ids(self, temp_yaml, repo, fake_keyring): + secret_id = str(uuid.uuid4()) + legacy_db_key = _secret_key("1", "database_password") + legacy_ssh_key = _secret_key("1", "ssh_password") + fake_keyring.set_password("PeterSQL", legacy_db_key, "legacy-db") + fake_keyring.set_password("PeterSQL", legacy_ssh_key, "legacy-ssh") + fake_keyring.set_password("PeterSQL", _secret_key(secret_id, "database_password"), "new-db") + fake_keyring.set_password("PeterSQL", _secret_key(secret_id, "ssh_password"), "new-ssh") + + data = [ + { + "id": 1, + "name": "Legacy Numeric", + "engine": "MySQL", + "configuration": { + "hostname": "localhost", + "port": 3306, + "username": "user", + "password_keyring_id": "1", + }, + "ssh_tunnel": { + "enabled": True, + "hostname": "remote.host", + "port": 22, + "username": "sshuser", + "password_keyring_id": "1", + "local_port": 3307, + }, + "secret_id": secret_id, + } + ] + with open(temp_yaml, "w") as f: + yaml.dump(data, f) + + connections = repo.load() + connection = connections[0] + assert connection.configuration.password == "new-db" + assert connection.ssh_tunnel.password == "new-ssh" + assert connection.secret_id == secret_id + assert fake_keyring.get_password("PeterSQL", legacy_db_key) is None + assert fake_keyring.get_password("PeterSQL", legacy_ssh_key) is None + assert fake_keyring.get_password("PeterSQL", _secret_key(secret_id, "database_password")) == "new-db" + assert fake_keyring.get_password("PeterSQL", _secret_key(secret_id, "ssh_password")) == "new-ssh" + + def test_save_connection_persists_uuid_secret_id(self, temp_yaml, repo, fake_keyring): + connection = Connection( + id=1, + name="UUID Secret", + engine=ConnectionEngine.MYSQL, + configuration=CredentialsConfiguration( + hostname="localhost", + username="user", + password="uuid-secret", + port=3306, + ), + ) + repo.add_connection(connection) + + with open(temp_yaml, "r") as f: + saved_data = yaml.safe_load(f) + + assert re.fullmatch(r"[0-9a-f-]{36}", saved_data[0]["secret_id"]) + assert "password_keyring_id" not in saved_data[0]["configuration"] + assert fake_keyring.get_password("PeterSQL", _secret_key(saved_data[0]["secret_id"], "database_password")) == "uuid-secret" diff --git a/tests/core/test_engines_init.py b/tests/core/test_engines_init.py index c334868..174ebae 100644 --- a/tests/core/test_engines_init.py +++ b/tests/core/test_engines_init.py @@ -28,11 +28,22 @@ def test_merge_original_modified_columns(self): def test_merge_original_modified_indexes(self): mock_table = Mock() mock_type = Mock() + # Define a minimal concrete implementation of SQLIndex for testing. + class DummyIndex(SQLIndex): + def raw_create(self) -> str: # pragma: no cover + return "" + def create(self) -> bool: # pragma: no cover + return True + def drop(self) -> bool: # pragma: no cover + return True + def alter(self, original_index: Self) -> bool: # pragma: no cover + return True + original = [ - SQLIndex(id=1, name='idx1', type=mock_type, columns=[], table=mock_table), + DummyIndex(id=1, name='idx1', type=mock_type, columns=[], table=mock_table), ] current = [ - SQLIndex(id=1, name='idx1_updated', type=mock_type, columns=[], table=mock_table), + DummyIndex(id=1, name='idx1_updated', type=mock_type, columns=[], table=mock_table), ] result = merge_original_current(original, current) assert len(result) == 1 diff --git a/tests/core/test_session.py b/tests/core/test_session.py index 58c5045..0395bf2 100644 --- a/tests/core/test_session.py +++ b/tests/core/test_session.py @@ -88,3 +88,34 @@ def test_session_has_enabled_tunnel(self): session = Session(connection=conn) assert session.has_enabled_tunnel() is False + + def test_session_with_comments(self): + config = SourceConfiguration(filename="test.db") + conn = Connection(id=4, name="session_with_comments", engine=ConnectionEngine.SQLITE, + configuration=config, comments="Test session") + session = Session(connection=conn) + + assert session.connection.comments == "Test session" + + def test_session_with_ssh_tunnel(self): + from structures.configurations import SSHTunnelConfiguration + + config = SourceConfiguration(filename="test.db") + ssh_config = SSHTunnelConfiguration( + enabled=True, executable="ssh", hostname="remote.host", + port=22, username="sshuser", password="sshpwd", local_port=3307, + ) + conn = Connection(id=5, name="session_with_ssh", engine=ConnectionEngine.SQLITE, + configuration=config, ssh_tunnel=ssh_config) + session = Session(connection=conn) + + assert session.connection.ssh_tunnel == ssh_config + + def test_session_repr(self): + config = SourceConfiguration(filename="test.db") + conn = Connection(id=1, name="test", engine=ConnectionEngine.SQLITE, configuration=config) + session = Session(connection=conn) + + repr_str = repr(session) + assert "Session" in repr_str + assert "test" in repr_str diff --git a/tests/engines/base_database_tests.py b/tests/engines/base_database_tests.py index a703367..3a06c2d 100644 --- a/tests/engines/base_database_tests.py +++ b/tests/engines/base_database_tests.py @@ -1,4 +1,5 @@ import uuid +import pytest class BaseDatabaseCreateAlterTests: @@ -109,11 +110,43 @@ def _build_unique_database_name() -> str: unique_name = str(uuid.uuid4()).replace("-", "")[:12] return f"db_{unique_name}" - def test_database_create_returns_false(self, session, database): + def test_database_create_raises_not_implemented(self, session, database): name = self._build_unique_database_name() new_database = self._build_new_database(database, session, name) - assert new_database.create() is False + with pytest.raises(NotImplementedError) as exc_info: + new_database.create() - def test_database_alter_returns_false(self, database): - assert database.alter() is False \ No newline at end of file + assert "SQLite databases are files" in str(exc_info.value) + + def test_database_alter_raises_not_implemented(self, database): + with pytest.raises(NotImplementedError) as exc_info: + database.alter() + + assert "SQLite databases are files" in str(exc_info.value) + + def test_database_drop_raises_not_implemented(self, database): + with pytest.raises(NotImplementedError) as exc_info: + database.drop() + + assert "SQLite databases are files" in str(exc_info.value) + + def test_database_equality_same(self, session): + """Test that two databases with same attributes are equal.""" + db1 = self._build_new_database(None, session, "test_db") + db2 = self._build_new_database(None, session, "test_db") + assert db1 == db2 + + def test_database_equality_different_name(self, session): + """Test that two databases with different names are not equal.""" + db1 = self._build_new_database(None, session, "test_db_1") + db2 = self._build_new_database(None, session, "test_db_2") + assert db1 != db2 + + def test_database_equality_different_id(self, session): + """Test that two databases with different IDs are not equal.""" + db1 = self._build_new_database(None, session, "test_db") + db1.id = 1 + db2 = self._build_new_database(None, session, "test_db") + db2.id = 2 + assert db1 != db2 \ No newline at end of file diff --git a/tests/engines/base_foreignkey_tests.py b/tests/engines/base_foreignkey_tests.py index 694d822..0cce5aa 100644 --- a/tests/engines/base_foreignkey_tests.py +++ b/tests/engines/base_foreignkey_tests.py @@ -74,3 +74,105 @@ def get_indextype_class(self): def get_primary_key_name(self) -> str: raise NotImplementedError("Subclasses must implement get_primary_key_name()") + + def test_foreignkey_equality_same(self, session, database, create_users_table): + """Test that two foreign keys with same attributes are equal.""" + users_table = create_users_table(database, session) + posts_table = session.context.build_empty_table(database, name="posts") + + user_id_column = session.context.build_empty_column( + posts_table, + self.get_datatype_class().INTEGER, + name="user_id", + is_nullable=False, + ) + + fk1 = session.context.build_empty_foreign_key( + posts_table, + ["user_id"], + name="fk_posts_users", + ) + fk1.reference_table = "users" + fk1.reference_columns = ["id"] + fk1.on_delete = "CASCADE" + fk1.on_update = "CASCADE" + + fk2 = session.context.build_empty_foreign_key( + posts_table, + ["user_id"], + name="fk_posts_users", + ) + fk2.reference_table = "users" + fk2.reference_columns = ["id"] + fk2.on_delete = "CASCADE" + fk2.on_update = "CASCADE" + + assert fk1 == fk2 + + def test_foreignkey_equality_different_name(self, session, database, create_users_table): + """Test that two foreign keys with different names are not equal.""" + users_table = create_users_table(database, session) + posts_table = session.context.build_empty_table(database, name="posts") + + user_id_column = session.context.build_empty_column( + posts_table, + self.get_datatype_class().INTEGER, + name="user_id", + is_nullable=False, + ) + + fk1 = session.context.build_empty_foreign_key( + posts_table, + ["user_id"], + name="fk_posts_users_1", + ) + fk1.reference_table = "users" + fk1.reference_columns = ["id"] + fk1.on_delete = "CASCADE" + fk1.on_update = "CASCADE" + + fk2 = session.context.build_empty_foreign_key( + posts_table, + ["user_id"], + name="fk_posts_users_2", + ) + fk2.reference_table = "users" + fk2.reference_columns = ["id"] + fk2.on_delete = "CASCADE" + fk2.on_update = "CASCADE" + + assert fk1 != fk2 + + def test_foreignkey_equality_different_columns(self, session, database, create_users_table): + """Test that two foreign keys with different columns are not equal.""" + users_table = create_users_table(database, session) + posts_table = session.context.build_empty_table(database, name="posts") + + user_id_column = session.context.build_empty_column( + posts_table, + self.get_datatype_class().INTEGER, + name="user_id", + is_nullable=False, + ) + + fk1 = session.context.build_empty_foreign_key( + posts_table, + ["user_id"], + name="fk_posts_users", + ) + fk1.reference_table = "users" + fk1.reference_columns = ["id"] + fk1.on_delete = "CASCADE" + fk1.on_update = "CASCADE" + + fk2 = session.context.build_empty_foreign_key( + posts_table, + ["other_id"], + name="fk_posts_users", + ) + fk2.reference_table = "users" + fk2.reference_columns = ["id"] + fk2.on_delete = "CASCADE" + fk2.on_update = "CASCADE" + + assert fk1 != fk2 diff --git a/tests/engines/base_function_tests.py b/tests/engines/base_function_tests.py index 9eefeb5..cb3c354 100644 --- a/tests/engines/base_function_tests.py +++ b/tests/engines/base_function_tests.py @@ -15,16 +15,20 @@ def get_function_parameters(self) -> str: def get_function_returns(self) -> str: return "integer" + def get_function_deterministic(self) -> bool: + return False + def get_updated_function_statement(self) -> str: return self.get_function_statement() - + def test_function_create_and_drop(self, session: Session, database: SQLDatabase): function = session.context.build_empty_function( database, name="test_function", parameters=self.get_function_parameters(), returns=self.get_function_returns(), - statement=self.get_function_statement() + statement=self.get_function_statement(), + deterministic=self.get_function_deterministic(), ) assert function.is_new is True @@ -50,6 +54,7 @@ def test_function_alter(self, session: Session, database: SQLDatabase): parameters=self.get_function_parameters(), returns=self.get_function_returns(), statement=self.get_function_statement(), + deterministic=self.get_function_deterministic(), ) assert function.create() is True diff --git a/tests/engines/base_readonly_tests.py b/tests/engines/base_readonly_tests.py new file mode 100644 index 0000000..5c5a16c --- /dev/null +++ b/tests/engines/base_readonly_tests.py @@ -0,0 +1,82 @@ +import pytest + + +class BaseReadOnlyTests: + """Tests verifying that read-only connections block all write operations.""" + + def test_read_only_blocks_insert(self, session, database, create_users_table): + table = create_users_table(database, session) + + session.connection.read_only = True + try: + record = session.context.build_empty_record(table, values={"name": "Blocked"}) + with pytest.raises(PermissionError): + record.insert() + finally: + session.connection.read_only = False + table.drop() + + def test_read_only_blocks_update(self, session, database, create_users_table): + table = create_users_table(database, session) + record = session.context.build_empty_record(table, values={"name": "Before"}) + record.insert() + table.load_records() + record = table.records.get_value()[0] + + session.connection.read_only = True + try: + record.values["name"] = "After" + with pytest.raises(PermissionError): + record.update() + finally: + session.connection.read_only = False + table.drop() + + def test_read_only_blocks_delete(self, session, database, create_users_table): + table = create_users_table(database, session) + record = session.context.build_empty_record(table, values={"name": "ToDelete"}) + record.insert() + table.load_records() + record = table.records.get_value()[0] + + session.connection.read_only = True + try: + with pytest.raises(PermissionError): + record.delete() + finally: + session.connection.read_only = False + table.drop() + + def test_read_only_blocks_create_table(self, session, database): + session.connection.read_only = True + try: + with pytest.raises(PermissionError): + session.context.execute("CREATE TABLE _ro_test (id INTEGER);") + finally: + session.connection.read_only = False + + def test_read_only_blocks_drop_table(self, session, database, create_users_table): + table = create_users_table(database, session) + + session.connection.read_only = True + try: + with pytest.raises(PermissionError): + session.context.execute(f"DROP TABLE users;") + finally: + session.connection.read_only = False + table.drop() + + def test_read_only_allows_select(self, session, database, create_users_table): + table = create_users_table(database, session) + record = session.context.build_empty_record(table, values={"name": "Readable"}) + record.insert() + + session.connection.read_only = True + try: + table.load_records() + records = table.records.get_value() + assert len(records) == 1 + assert records[0].values["name"] == "Readable" + finally: + session.connection.read_only = False + table.drop() \ No newline at end of file diff --git a/tests/engines/base_view_tests.py b/tests/engines/base_view_tests.py index deb3dd6..6fada98 100644 --- a/tests/engines/base_view_tests.py +++ b/tests/engines/base_view_tests.py @@ -1,24 +1,125 @@ import pytest +class BaseViewCreateDropTests: + + def test_view_create(self, session, database): + view = session.context.build_empty_view( + database, + name="test_create_view", + statement=self.get_simple_view_statement(), + ) + assert view.is_new is True + + result = view.create() + assert result is True + + database.views.refresh() + assert any(v.name == "test_create_view" for v in database.views.get_value()) + + view.drop() + + def test_view_drop(self, session, database): + view = session.context.build_empty_view( + database, + name="test_drop_view", + statement=self.get_simple_view_statement(), + ) + view.create() + database.views.refresh() + + created = next((v for v in database.views.get_value() if v.name == "test_drop_view"), None) + assert created is not None + + result = created.drop() + assert result is True + + database.views.refresh() + assert not any(v.name == "test_drop_view" for v in database.views.get_value()) + + def test_view_list_via_database_observable(self, session, database): + view = session.context.build_empty_view( + database, + name="test_list_view", + statement=self.get_simple_view_statement(), + ) + view.create() + database.views.refresh() + + views = database.views.get_value() + assert any(v.name == "test_list_view" for v in views) + + view.drop() + + def test_view_list_has_name_and_statement(self, session, database): + view = session.context.build_empty_view( + database, + name="test_fields_view", + statement=self.get_simple_view_statement(), + ) + view.create() + database.views.refresh() + + found = next((v for v in database.views.get_value() if v.name == "test_fields_view"), None) + assert found is not None + assert found.name == "test_fields_view" + assert found.statement + + found.drop() + + def get_simple_view_statement(self) -> str: + raise NotImplementedError("Subclasses must implement get_simple_view_statement()") + + +class BaseViewAlterTests: + + def test_view_alter_changes_statement(self, session, database): + view = session.context.build_empty_view( + database, + name="test_alter_direct_view", + statement=self.get_simple_view_statement(), + ) + view.create() + database.views.refresh() + + created = next((v for v in database.views.get_value() if v.name == "test_alter_direct_view"), None) + assert created is not None + + created.statement = self.get_updated_view_statement() + result = created.alter() + assert result is True + + database.views.refresh() + updated = next((v for v in database.views.get_value() if v.name == "test_alter_direct_view"), None) + assert updated is not None + + created.drop() + + def get_simple_view_statement(self) -> str: + raise NotImplementedError("Subclasses must implement get_simple_view_statement()") + + def get_updated_view_statement(self) -> str: + raise NotImplementedError("Subclasses must implement get_updated_view_statement()") + + class BaseViewSaveTests: - + def test_save_creates_new_view_and_refreshes_database(self, session, database): view = session.context.build_empty_view( database, name="test_save_view", statement=self.get_view_statement(), ) - + assert view.is_new is True - + result = view.save() - + assert result is True database.views.refresh() views = database.views.get_value() assert any(v.name == "test_save_view" for v in views) - + view.drop() def test_save_alters_existing_view_and_refreshes_database(self, session, database): @@ -28,22 +129,22 @@ def test_save_alters_existing_view_and_refreshes_database(self, session, databas statement=self.get_simple_view_statement(), ) view.create() - + database.views.refresh() views = database.views.get_value() created_view = next((v for v in views if v.name == "test_alter_view"), None) assert created_view is not None - + created_view.statement = self.get_updated_view_statement() - + result = created_view.save() - + assert result is True database.views.refresh() views = database.views.get_value() updated_view = next((v for v in views if v.name == "test_alter_view"), None) assert updated_view is not None - + created_view.drop() def get_view_statement(self) -> str: @@ -57,14 +158,14 @@ def get_updated_view_statement(self) -> str: class BaseViewIsNewTests: - + def test_is_new_returns_true_for_new_view(self, session, database): view = session.context.build_empty_view( database, name="new_view", statement=self.get_simple_view_statement(), ) - + assert view.is_new is True def test_is_new_returns_false_for_existing_view(self, session, database): @@ -74,25 +175,268 @@ def test_is_new_returns_false_for_existing_view(self, session, database): statement=self.get_simple_view_statement(), ) view.create() - + database.views.refresh() views = database.views.get_value() existing_view = next((v for v in views if v.name == "existing_view"), None) - + assert existing_view is not None assert existing_view.is_new is False - + existing_view.drop() def get_simple_view_statement(self) -> str: raise NotImplementedError("Subclasses must implement get_simple_view_statement()") +class BaseViewColumnsTests: + + def test_view_columns_are_loadable(self, session, database, create_users_table): + create_users_table(database, session) + view = session.context.build_empty_view( + database, + name="test_cols_view", + statement=self.get_users_view_statement(), + ) + view.create() + database.views.refresh() + + view = next(v for v in database.views.get_value() if v.name == "test_cols_view") + columns = list(view.columns) + + assert len(columns) > 0 + + view.drop() + + def test_view_columns_have_name_attribute(self, session, database, create_users_table): + create_users_table(database, session) + view = session.context.build_empty_view( + database, + name="test_colnames_view", + statement=self.get_users_view_statement(), + ) + view.create() + database.views.refresh() + + view = next(v for v in database.views.get_value() if v.name == "test_colnames_view") + columns = list(view.columns) + + assert all(hasattr(c, "name") and c.name for c in columns) + + view.drop() + + def test_view_columns_include_expected_names(self, session, database, create_users_table): + create_users_table(database, session) + view = session.context.build_empty_view( + database, + name="test_expected_cols_view", + statement=self.get_users_view_statement(), + ) + view.create() + database.views.refresh() + + view = next(v for v in database.views.get_value() if v.name == "test_expected_cols_view") + column_names = [c.name for c in view.columns] + + assert "name" in column_names + + view.drop() + + def test_view_columns_have_datatype(self, session, database, create_users_table): + create_users_table(database, session) + view = session.context.build_empty_view( + database, + name="test_datatype_cols_view", + statement=self.get_users_view_statement(), + ) + view.create() + database.views.refresh() + + view = next(v for v in database.views.get_value() if v.name == "test_datatype_cols_view") + columns = list(view.columns) + + assert all(hasattr(c, "datatype") and c.datatype is not None for c in columns) + + view.drop() + + def get_users_view_statement(self) -> str: + raise NotImplementedError("Subclasses must implement get_users_view_statement()") + + +class BaseViewRecordsTests: + + def test_view_load_records_empty_when_table_empty(self, session, database, create_users_table): + create_users_table(database, session) + view = session.context.build_empty_view( + database, + name="test_empty_records_view", + statement=self.get_users_view_statement(), + ) + view.create() + database.views.refresh() + + view = next(v for v in database.views.get_value() if v.name == "test_empty_records_view") + view.load_records() + + assert len(list(view.records)) == 0 + + view.drop() + + def test_view_load_records_with_data(self, session, database, create_users_table): + table = create_users_table(database, session) + record = session.context.build_empty_record(table, values={"name": "Alice"}) + record.insert() + + view = session.context.build_empty_view( + database, + name="test_data_records_view", + statement=self.get_users_view_statement(), + ) + view.create() + database.views.refresh() + + view = next(v for v in database.views.get_value() if v.name == "test_data_records_view") + view.load_records() + records = list(view.records) + + assert len(records) == 1 + assert records[0].values.get("name") == "Alice" + + view.drop() + + def test_view_load_records_reflects_table_data(self, session, database, create_users_table): + table = create_users_table(database, session) + for name in ["Alice", "Bob", "Charlie"]: + session.context.build_empty_record(table, values={"name": name}).insert() + + view = session.context.build_empty_view( + database, + name="test_multi_records_view", + statement=self.get_users_view_statement(), + ) + view.create() + database.views.refresh() + + view = next(v for v in database.views.get_value() if v.name == "test_multi_records_view") + view.load_records() + + assert len(list(view.records)) == 3 + + view.drop() + + def test_view_load_records_with_limit(self, session, database, create_users_table): + table = create_users_table(database, session) + for name in ["Alice", "Bob", "Charlie"]: + session.context.build_empty_record(table, values={"name": name}).insert() + + view = session.context.build_empty_view( + database, + name="test_limit_records_view", + statement=self.get_users_view_statement(), + ) + view.create() + database.views.refresh() + + view = next(v for v in database.views.get_value() if v.name == "test_limit_records_view") + view.load_records(limit=2) + + assert len(list(view.records)) == 2 + + view.drop() + + def test_view_load_records_with_offset(self, session, database, create_users_table): + table = create_users_table(database, session) + for name in ["Alice", "Bob", "Charlie"]: + session.context.build_empty_record(table, values={"name": name}).insert() + + view = session.context.build_empty_view( + database, + name="test_offset_records_view", + statement=self.get_users_view_statement(), + ) + view.create() + database.views.refresh() + + view = next(v for v in database.views.get_value() if v.name == "test_offset_records_view") + view.load_records(limit=10, offset=2) + + assert len(list(view.records)) == 1 + + view.drop() + + def get_users_view_statement(self) -> str: + raise NotImplementedError("Subclasses must implement get_users_view_statement()") + + +class BaseViewCopyTests: + + def test_view_copy_creates_independent_instance(self, session, database): + view = session.context.build_empty_view( + database, + name="original_view", + statement=self.get_simple_view_statement(), + ) + + copy = view.copy() + + assert copy is not view + assert copy.name == view.name + assert copy.statement == view.statement + + def test_view_copy_shares_database_reference(self, session, database): + view = session.context.build_empty_view( + database, + name="db_ref_view", + statement=self.get_simple_view_statement(), + ) + + copy = view.copy() + + assert copy.database is view.database + + def test_view_copy_preserves_handlers(self, session, database): + view = session.context.build_empty_view( + database, + name="handlers_view", + statement=self.get_simple_view_statement(), + ) + + copy = view.copy() + + assert copy.get_columns_handler == view.get_columns_handler + assert copy.get_records_handler == view.get_records_handler + + def test_view_copy_has_independent_columns_observable(self, session, database): + view = session.context.build_empty_view( + database, + name="lazy_cols_view", + statement=self.get_simple_view_statement(), + ) + + copy = view.copy() + + assert copy.columns is not view.columns + + def test_view_copy_is_new_matches_original(self, session, database): + view = session.context.build_empty_view( + database, + name="is_new_copy_view", + statement=self.get_simple_view_statement(), + ) + assert view.is_new is True + + copy = view.copy() + assert copy.is_new is True + + def get_simple_view_statement(self) -> str: + raise NotImplementedError("Subclasses must implement get_simple_view_statement()") + + class BaseViewDefinerTests: - + def test_get_definers_returns_list(self, session): definers = session.context.get_definers() - + assert isinstance(definers, list) assert len(definers) > 0 - assert all('@' in definer for definer in definers) + assert all('@' in definer for definer in definers) \ No newline at end of file diff --git a/tests/engines/mariadb/test_integration_suite.py b/tests/engines/mariadb/test_integration_suite.py index 92da41b..bf3094d 100644 --- a/tests/engines/mariadb/test_integration_suite.py +++ b/tests/engines/mariadb/test_integration_suite.py @@ -9,9 +9,20 @@ from tests.engines.base_index_tests import BaseIndexTests from tests.engines.base_foreignkey_tests import BaseForeignKeyTests from tests.engines.base_check_tests import BaseCheckTests +from tests.engines.base_function_tests import BaseFunctionTests from tests.engines.base_procedure_tests import BaseProcedureTests from tests.engines.base_trigger_tests import BaseTriggerTests -from tests.engines.base_view_tests import BaseViewSaveTests, BaseViewIsNewTests, BaseViewDefinerTests +from tests.engines.base_readonly_tests import BaseReadOnlyTests +from tests.engines.base_view_tests import ( + BaseViewCreateDropTests, + BaseViewAlterTests, + BaseViewSaveTests, + BaseViewIsNewTests, + BaseViewColumnsTests, + BaseViewRecordsTests, + BaseViewCopyTests, + BaseViewDefinerTests, +) @pytest.mark.integration @@ -58,6 +69,26 @@ class TestMariaDBCheck(BaseCheckTests): pass +@pytest.mark.integration +@pytest.mark.xdist_group("mariadb") +class TestMariaDBFunction(BaseFunctionTests): + + def get_function_parameters(self) -> str: + return "x INT" + + def get_function_returns(self) -> str: + return "INT" + + def get_function_deterministic(self) -> bool: + return True + + def get_function_statement(self) -> str: + return "RETURN x + 1" + + def get_updated_function_statement(self) -> str: + return "RETURN x + 2" + + @pytest.mark.integration @pytest.mark.xdist_group("mariadb") class TestMariaDBProcedure(BaseProcedureTests): @@ -77,10 +108,29 @@ def get_trigger_statement(self, db_name: str, table_name: str) -> str: return f"AFTER INSERT ON {db_name}.{table_name} FOR EACH ROW BEGIN END" +@pytest.mark.integration +@pytest.mark.xdist_group("mariadb") +class TestMariaDBViewCreateDrop(BaseViewCreateDropTests): + + def get_simple_view_statement(self) -> str: + return "SELECT 1 as id, 'test' as name" + + +@pytest.mark.integration +@pytest.mark.xdist_group("mariadb") +class TestMariaDBViewAlter(BaseViewAlterTests): + + def get_simple_view_statement(self) -> str: + return "SELECT 1 as id" + + def get_updated_view_statement(self) -> str: + return "SELECT 1 as id, 'updated' as name" + + @pytest.mark.integration @pytest.mark.xdist_group("mariadb") class TestMariaDBViewSave(BaseViewSaveTests): - + def get_view_statement(self) -> str: return "SELECT 1 as id, 'test' as name" @@ -94,7 +144,31 @@ def get_updated_view_statement(self) -> str: @pytest.mark.integration @pytest.mark.xdist_group("mariadb") class TestMariaDBViewIsNew(BaseViewIsNewTests): - + + def get_simple_view_statement(self) -> str: + return "SELECT 1 as id" + + +@pytest.mark.integration +@pytest.mark.xdist_group("mariadb") +class TestMariaDBViewColumns(BaseViewColumnsTests): + + def get_users_view_statement(self) -> str: + return "SELECT id, name FROM users" + + +@pytest.mark.integration +@pytest.mark.xdist_group("mariadb") +class TestMariaDBViewRecords(BaseViewRecordsTests): + + def get_users_view_statement(self) -> str: + return "SELECT id, name FROM users" + + +@pytest.mark.integration +@pytest.mark.xdist_group("mariadb") +class TestMariaDBViewCopy(BaseViewCopyTests): + def get_simple_view_statement(self) -> str: return "SELECT 1 as id" @@ -120,3 +194,9 @@ def get_alter_options(self) -> dict[str, str]: "character_set": "utf8mb4", "default_collation": "utf8mb4_general_ci", } + + +@pytest.mark.integration +@pytest.mark.xdist_group("mariadb") +class TestMariaDBReadOnly(BaseReadOnlyTests): + pass diff --git a/tests/engines/mysql/test_integration_suite.py b/tests/engines/mysql/test_integration_suite.py index e0a54c6..7453298 100644 --- a/tests/engines/mysql/test_integration_suite.py +++ b/tests/engines/mysql/test_integration_suite.py @@ -9,9 +9,20 @@ from tests.engines.base_index_tests import BaseIndexTests from tests.engines.base_foreignkey_tests import BaseForeignKeyTests from tests.engines.base_check_tests import BaseCheckTests +from tests.engines.base_function_tests import BaseFunctionTests from tests.engines.base_procedure_tests import BaseProcedureTests from tests.engines.base_trigger_tests import BaseTriggerTests -from tests.engines.base_view_tests import BaseViewSaveTests, BaseViewIsNewTests, BaseViewDefinerTests +from tests.engines.base_readonly_tests import BaseReadOnlyTests +from tests.engines.base_view_tests import ( + BaseViewCreateDropTests, + BaseViewAlterTests, + BaseViewSaveTests, + BaseViewIsNewTests, + BaseViewColumnsTests, + BaseViewRecordsTests, + BaseViewCopyTests, + BaseViewDefinerTests, +) @pytest.mark.integration @@ -58,6 +69,26 @@ class TestMySQLCheck(BaseCheckTests): pass +@pytest.mark.integration +@pytest.mark.xdist_group("mysql") +class TestMySQLFunction(BaseFunctionTests): + + def get_function_parameters(self) -> str: + return "x INT" + + def get_function_returns(self) -> str: + return "INT" + + def get_function_deterministic(self) -> bool: + return True + + def get_function_statement(self) -> str: + return "RETURN x + 1" + + def get_updated_function_statement(self) -> str: + return "RETURN x + 2" + + @pytest.mark.integration @pytest.mark.xdist_group("mysql") class TestMySQLProcedure(BaseProcedureTests): @@ -77,6 +108,25 @@ def get_trigger_statement(self, db_name: str, table_name: str) -> str: return f"AFTER INSERT ON {db_name}.{table_name} FOR EACH ROW BEGIN END" +@pytest.mark.integration +@pytest.mark.xdist_group("mysql") +class TestMySQLViewCreateDrop(BaseViewCreateDropTests): + + def get_simple_view_statement(self) -> str: + return "SELECT 1 as id, 'test' as name" + + +@pytest.mark.integration +@pytest.mark.xdist_group("mysql") +class TestMySQLViewAlter(BaseViewAlterTests): + + def get_simple_view_statement(self) -> str: + return "SELECT 1 as id" + + def get_updated_view_statement(self) -> str: + return "SELECT 1 as id, 'updated' as name" + + @pytest.mark.integration @pytest.mark.xdist_group("mysql") class TestMySQLViewSave(BaseViewSaveTests): @@ -99,6 +149,30 @@ def get_simple_view_statement(self) -> str: return "SELECT 1 as id" +@pytest.mark.integration +@pytest.mark.xdist_group("mysql") +class TestMySQLViewColumns(BaseViewColumnsTests): + + def get_users_view_statement(self) -> str: + return "SELECT id, name FROM users" + + +@pytest.mark.integration +@pytest.mark.xdist_group("mysql") +class TestMySQLViewRecords(BaseViewRecordsTests): + + def get_users_view_statement(self) -> str: + return "SELECT id, name FROM users" + + +@pytest.mark.integration +@pytest.mark.xdist_group("mysql") +class TestMySQLViewCopy(BaseViewCopyTests): + + def get_simple_view_statement(self) -> str: + return "SELECT 1 as id" + + @pytest.mark.integration @pytest.mark.xdist_group("mysql") class TestMySQLViewDefiner(BaseViewDefinerTests): @@ -120,3 +194,10 @@ def get_alter_options(self) -> dict[str, str]: "character_set": "utf8mb4", "default_collation": "utf8mb4_general_ci", } + + + +@pytest.mark.integration +@pytest.mark.xdist_group("mysql") +class TestMySQLReadOnly(BaseReadOnlyTests): + pass diff --git a/tests/engines/postgresql/test_integration_suite.py b/tests/engines/postgresql/test_integration_suite.py index b016e9c..4220d06 100644 --- a/tests/engines/postgresql/test_integration_suite.py +++ b/tests/engines/postgresql/test_integration_suite.py @@ -21,7 +21,16 @@ from tests.engines.base_trigger_tests import BaseTriggerTests from tests.engines.base_function_tests import BaseFunctionTests from tests.engines.base_procedure_tests import BaseProcedureTests -from tests.engines.base_view_tests import BaseViewSaveTests, BaseViewIsNewTests +from tests.engines.base_readonly_tests import BaseReadOnlyTests +from tests.engines.base_view_tests import ( + BaseViewCreateDropTests, + BaseViewAlterTests, + BaseViewSaveTests, + BaseViewIsNewTests, + BaseViewColumnsTests, + BaseViewRecordsTests, + BaseViewCopyTests, +) @pytest.mark.integration @@ -86,10 +95,29 @@ def get_trigger_statement(self, db_name: str, table_name: str) -> str: """ +@pytest.mark.integration +@pytest.mark.xdist_group("postgresql") +class TestPostgreSQLViewCreateDrop(BaseViewCreateDropTests): + + def get_simple_view_statement(self) -> str: + return "SELECT 1 as id, 'test' as name" + + +@pytest.mark.integration +@pytest.mark.xdist_group("postgresql") +class TestPostgreSQLViewAlter(BaseViewAlterTests): + + def get_simple_view_statement(self) -> str: + return "SELECT 1 as id" + + def get_updated_view_statement(self) -> str: + return "SELECT 1 as id, 'updated' as name" + + @pytest.mark.integration @pytest.mark.xdist_group("postgresql") class TestPostgreSQLViewSave(BaseViewSaveTests): - + def get_view_statement(self) -> str: return "SELECT 1 as id, 'test' as name" @@ -100,6 +128,38 @@ def get_updated_view_statement(self) -> str: return "SELECT 1 as id, 'updated' as name" +@pytest.mark.integration +@pytest.mark.xdist_group("postgresql") +class TestPostgreSQLViewIsNew(BaseViewIsNewTests): + + def get_simple_view_statement(self) -> str: + return "SELECT 1 as id" + + +@pytest.mark.integration +@pytest.mark.xdist_group("postgresql") +class TestPostgreSQLViewColumns(BaseViewColumnsTests): + + def get_users_view_statement(self) -> str: + return "SELECT id, name FROM public.users" + + +@pytest.mark.integration +@pytest.mark.xdist_group("postgresql") +class TestPostgreSQLViewRecords(BaseViewRecordsTests): + + def get_users_view_statement(self) -> str: + return "SELECT id, name FROM public.users" + + +@pytest.mark.integration +@pytest.mark.xdist_group("postgresql") +class TestPostgreSQLViewCopy(BaseViewCopyTests): + + def get_simple_view_statement(self) -> str: + return "SELECT 1 as id" + + @pytest.mark.integration @pytest.mark.xdist_group("postgresql") class TestPostgreSQLFunction(BaseFunctionTests): @@ -150,3 +210,9 @@ def get_alter_options(self) -> dict[str, int]: def requires_autocommit_for_database_ddl(self) -> bool: return True + + +@pytest.mark.integration +@pytest.mark.xdist_group("postgresql") +class TestPostgreSQLReadOnly(BaseReadOnlyTests): + pass diff --git a/tests/engines/sqlite/test_integration_suite.py b/tests/engines/sqlite/test_integration_suite.py index bd4b6d5..d121478 100644 --- a/tests/engines/sqlite/test_integration_suite.py +++ b/tests/engines/sqlite/test_integration_suite.py @@ -10,7 +10,16 @@ from tests.engines.base_foreignkey_tests import BaseForeignKeyTests from tests.engines.base_check_tests import BaseCheckTests from tests.engines.base_trigger_tests import BaseTriggerTests -from tests.engines.base_view_tests import BaseViewSaveTests, BaseViewIsNewTests +from tests.engines.base_readonly_tests import BaseReadOnlyTests +from tests.engines.base_view_tests import ( + BaseViewCreateDropTests, + BaseViewAlterTests, + BaseViewSaveTests, + BaseViewIsNewTests, + BaseViewColumnsTests, + BaseViewRecordsTests, + BaseViewCopyTests, +) @pytest.mark.integration @@ -34,9 +43,8 @@ class TestSQLiteIndex(BaseIndexTests): @pytest.mark.integration -@pytest.mark.skip(reason="SQLite requires foreign keys to be defined inline in CREATE TABLE statement") class TestSQLiteForeignKey(BaseForeignKeyTests): - + def get_datatype_class(self): return SQLiteDataType @@ -46,6 +54,74 @@ def get_indextype_class(self): def get_primary_key_name(self) -> str: return "PRIMARY" + def test_foreignkey_create_and_drop(self, session, database, create_users_table): + pytest.skip( + "SQLite does not support add/drop foreign key constraints after table creation" + ) + + def test_build_empty_foreign_key_with_append_is_persisted_on_create( + self, + session, + database, + create_users_table, + ): + create_users_table(database, session) + + posts_table = session.context.build_empty_table(database, name="posts") + id_column = session.context.build_empty_column( + posts_table, + self.get_datatype_class().INTEGER, + name="id", + is_auto_increment=True, + is_nullable=False, + ) + user_id_column = session.context.build_empty_column( + posts_table, + self.get_datatype_class().INTEGER, + name="user_id", + is_nullable=False, + ) + + posts_table.columns.append(id_column) + posts_table.columns.append(user_id_column) + + primary_index = session.context.build_empty_index( + posts_table, + self.get_indextype_class().PRIMARY, + ["id"], + name=self.get_primary_key_name(), + ) + posts_table.indexes.append(primary_index) + + fk = session.context.build_empty_foreign_key( + posts_table, + ["user_id"], + name="fk_posts_users", + ) + fk.reference_table = "users" + fk.reference_columns = ["id"] + fk.on_delete = "CASCADE" + fk.on_update = "CASCADE" + posts_table.foreign_keys.append(fk) + + assert posts_table.create() is True + + database.tables.refresh() + created_posts_table = next( + table for table in database.tables.get_value() if table.name == "posts" + ) + created_posts_table.foreign_keys.refresh() + + foreign_keys = created_posts_table.foreign_keys.get_value() + assert len(foreign_keys) == 1 + + created_foreign_key = foreign_keys[0] + assert created_foreign_key.columns == ["user_id"] + assert created_foreign_key.reference_table == "users" + assert created_foreign_key.reference_columns == ["id"] + + created_posts_table.drop() + @pytest.mark.integration class TestSQLiteCheck(BaseCheckTests): @@ -59,9 +135,26 @@ def get_trigger_statement(self, db_name: str, table_name: str) -> str: return f"AFTER INSERT ON {table_name} BEGIN SELECT 1; END" +@pytest.mark.integration +class TestSQLiteViewCreateDrop(BaseViewCreateDropTests): + + def get_simple_view_statement(self) -> str: + return "SELECT id, name FROM users" + + +@pytest.mark.integration +class TestSQLiteViewAlter(BaseViewAlterTests): + + def get_simple_view_statement(self) -> str: + return "SELECT id FROM users" + + def get_updated_view_statement(self) -> str: + return "SELECT id, name FROM users" + + @pytest.mark.integration class TestSQLiteViewSave(BaseViewSaveTests): - + def get_view_statement(self) -> str: return "SELECT id, name FROM users WHERE id > 0" @@ -74,11 +167,37 @@ def get_updated_view_statement(self) -> str: @pytest.mark.integration class TestSQLiteViewIsNew(BaseViewIsNewTests): - + def get_simple_view_statement(self) -> str: return "SELECT * FROM users" +@pytest.mark.integration +class TestSQLiteViewColumns(BaseViewColumnsTests): + + def get_users_view_statement(self) -> str: + return "SELECT id, name FROM users" + + +@pytest.mark.integration +class TestSQLiteViewRecords(BaseViewRecordsTests): + + def get_users_view_statement(self) -> str: + return "SELECT id, name FROM users" + + +@pytest.mark.integration +class TestSQLiteViewCopy(BaseViewCopyTests): + + def get_simple_view_statement(self) -> str: + return "SELECT id, name FROM users" + + @pytest.mark.integration class TestSQLiteDatabase(BaseDatabaseUnsupportedTests): pass + + +@pytest.mark.integration +class TestSQLiteReadOnly(BaseReadOnlyTests): + pass diff --git a/tests/engines/test_connection_lost.py b/tests/engines/test_connection_lost.py new file mode 100644 index 0000000..4b3387d --- /dev/null +++ b/tests/engines/test_connection_lost.py @@ -0,0 +1,250 @@ +import sqlite3 +import threading +from unittest.mock import Mock, patch + +import psycopg2 +import pymysql + +import pytest + +from structures.engines.context import ConnectionLostError, AbstractContext +from structures.connection import Connection, ConnectionEngine +from structures.configurations import CredentialsConfiguration, SourceConfiguration +from structures.session import Session + + +# --------------------------------------------------------------------------- +# _is_connection_lost() — positive and negative cases +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize( + "exc, expected", + [ + # PyMySQL InterfaceError → always True + (pymysql.err.InterfaceError(0, ""), True), + # PyMySQL OperationalError with disconnect code → True + (pymysql.err.OperationalError(2006, "MySQL server has gone away"), True), + (pymysql.err.OperationalError(2013, "Lost connection to MySQL server"), True), + (pymysql.err.OperationalError(2055, "Lost connection to MySQL server at 'localhost'"), True), + # PyMySQL OperationalError with disconnect message fragment → True + (pymysql.err.OperationalError(9999, "server has gone away after reboot"), True), + # PyMySQL OperationalError with non-disconnect code and no disconnect message → False + (pymysql.err.OperationalError(1049, "Unknown database 'foo'"), False), + (pymysql.err.OperationalError(1045, "Access denied for user"), False), + (pymysql.err.OperationalError(1064, "SQL syntax error"), False), + # psycopg2 OperationalError with pgcode=None (connection-level) → True + (psycopg2.OperationalError("server closed the connection unexpectedly"), True), + # psycopg2 InterfaceError → always True + (psycopg2.InterfaceError("connection already closed"), True), + # SQLite disk-level errors → True + (sqlite3.OperationalError("database is locked"), True), + (sqlite3.OperationalError("disk I/O error"), True), + (sqlite3.OperationalError("unable to open database file"), True), + # SQLite ordinary SQL error → False + (sqlite3.OperationalError("no such table: foo"), False), + # Unrelated exception → False + (ValueError("unexpected"), False), + ], +) +def test_is_connection_lost_detection(exc, expected): + assert AbstractContext._is_connection_lost(exc) is expected + + +def test_psycopg2_operational_error_with_pgcode_is_not_connection_lost(): + """A psycopg2.OperationalError with a pgcode is a server-side SQL error, not + a lost connection — it must not trigger the reconnection flow.""" + + # psycopg2.Error.pgcode is a read-only C property that defaults to None. + # Use a subclass that overrides it to simulate a server-side error. + class _AuthError(psycopg2.OperationalError): + @property + def pgcode(self) -> str: + return "28000" # INVALID_AUTHORIZATION_SPECIFICATION + + exc = _AuthError("FATAL: role \"foo\" does not exist") + assert AbstractContext._is_connection_lost(exc) is False + + +def test_pymysql_operational_error_non_disconnect_not_connection_lost(): + """Ordinary PyMySQL OperationalError without disconnect code/message is + not treated as connection loss.""" + exc = pymysql.err.OperationalError(1146, "Table 'db.missing_table' doesn't exist") + assert AbstractContext._is_connection_lost(exc) is False + + +# --------------------------------------------------------------------------- +# execute() integration with the detection logic +# --------------------------------------------------------------------------- + +def test_execute_raises_connection_lost_for_sqlite_disk_io(): + config = SourceConfiguration(filename=":memory:") + connection = Connection( + id=2, + name="sqlite_test", + engine=ConnectionEngine.SQLITE, + configuration=config, + ) + + context = Mock(spec=AbstractContext) + context.connection = connection + context.connection.read_only = False + context.cursor = Mock() + context.cursor.execute.side_effect = sqlite3.OperationalError("disk I/O error") + + with pytest.raises(ConnectionLostError): + AbstractContext.execute(context, "SELECT 1") + + +def test_execute_reraises_non_connection_error(): + config = SourceConfiguration(filename=":memory:") + connection = Connection( + id=3, + name="sqlite_test", + engine=ConnectionEngine.SQLITE, + configuration=config, + ) + + context = Mock(spec=AbstractContext) + context.connection = connection + context.connection.read_only = False + context.cursor = Mock() + context.cursor.execute.side_effect = ValueError("syntax error") + context._is_connection_lost = Mock(return_value=False) + + with pytest.raises(ValueError, match="syntax error"): + AbstractContext.execute(context, "SELECT 1") + + +def test_execute_reraises_pymysql_ordinary_operational_error(): + """A non-disconnect PyMySQL OperationalError must not trigger ConnectionLostError.""" + config = SourceConfiguration(filename=":memory:") + connection = Connection( + id=6, + name="sqlite_test", + engine=ConnectionEngine.SQLITE, + configuration=config, + ) + + context = Mock(spec=AbstractContext) + context.connection = connection + context.connection.read_only = False + context.cursor = Mock() + exc = pymysql.err.OperationalError(1146, "Table 'x' doesn't exist") + context.cursor.execute.side_effect = exc + # Use the real static method for detection + context._is_connection_lost = AbstractContext._is_connection_lost + + with pytest.raises(pymysql.err.OperationalError): + AbstractContext.execute(context, "SELECT 1") + + +def test_global_connection_lost_handler_is_invoked(): + config = SourceConfiguration(filename=":memory:") + connection = Connection( + id=4, + name="sqlite_test", + engine=ConnectionEngine.SQLITE, + configuration=config, + ) + + session = Session(connection) + session.connect() + + handler = Mock() + session.context.set_connection_lost_handler(handler) + + original_cursor = session.context._cursor + mock_cursor = Mock() + mock_cursor.execute.side_effect = sqlite3.OperationalError("disk I/O error") + session.context._cursor = mock_cursor + + with pytest.raises(ConnectionLostError): + AbstractContext.execute(session.context, "SELECT 1") + + handler.assert_called_once_with(session.context, "Database connection lost: disk I/O error") + session.context._cursor = original_cursor + session.disconnect() + + +def test_global_connection_lost_handler_is_not_invoked_for_non_connection_error(): + config = SourceConfiguration(filename=":memory:") + connection = Connection( + id=5, + name="sqlite_test", + engine=ConnectionEngine.SQLITE, + configuration=config, + ) + + session = Session(connection) + session.connect() + + handler = Mock() + session.context.set_connection_lost_handler(handler) + + original_cursor = session.context._cursor + mock_cursor = Mock() + mock_cursor.execute.side_effect = ValueError("syntax error") + session.context._cursor = mock_cursor + + with pytest.raises(ValueError, match="syntax error"): + AbstractContext.execute(session.context, "SELECT 1") + + handler.assert_not_called() + session.context._cursor = original_cursor + session.disconnect() + + +# --------------------------------------------------------------------------- +# QueryExecutor concurrency guard +# --------------------------------------------------------------------------- + +def test_executor_refuses_second_start_while_running(): + """execute_statements() must return early if a worker thread is already alive, + without mutating _cancel_requested or _loader_context.""" + from windows.main.query.executor import QueryExecutor + + fake_session = Mock() + executor = QueryExecutor(fake_session) + + # Simulate a running thread + alive_thread = Mock(spec=threading.Thread) + alive_thread.is_alive.return_value = True + executor._current_thread = alive_thread + + on_stmt = Mock() + on_all = Mock() + + # Should return without starting a new thread or touching loader state + with patch("windows.main.query.executor.Loader") as mock_loader: + executor.execute_statements( + statements=[], + on_statement_complete=on_stmt, + on_all_complete=on_all, + ) + mock_loader.cursor_wait.assert_not_called() + + # Callbacks never called + on_stmt.assert_not_called() + on_all.assert_not_called() + # The existing thread reference is unchanged + assert executor._current_thread is alive_thread + + +def test_executor_is_running_reflects_thread_state(): + """is_running() returns True only when _current_thread is alive.""" + from windows.main.query.executor import QueryExecutor + + fake_session = Mock() + executor = QueryExecutor(fake_session) + + assert executor.is_running() is False + + dead_thread = Mock(spec=threading.Thread) + dead_thread.is_alive.return_value = False + executor._current_thread = dead_thread + assert executor.is_running() is False + + alive_thread = Mock(spec=threading.Thread) + alive_thread.is_alive.return_value = True + executor._current_thread = alive_thread + assert executor.is_running() is True diff --git a/tests/test_engines_init.py b/tests/test_engines_init.py deleted file mode 100644 index eb60b09..0000000 --- a/tests/test_engines_init.py +++ /dev/null @@ -1,53 +0,0 @@ -import pytest -from unittest.mock import Mock - -from structures.helpers import merge_original_current -from structures.engines.database import SQLColumn, SQLIndex, SQLForeignKey - - -class TestMergeOriginalCurrent: - def test_merge_original_current_columns(self): - mock_table = Mock() - original = [ - SQLColumn(id=1, name='col1', table=mock_table, datatype=Mock()), - SQLColumn(id=2, name='col2', table=mock_table, datatype=Mock()), - ] - current = [ - SQLColumn(id=1, name='col1_updated', table=mock_table, datatype=Mock()), - SQLColumn(id=3, name='col3', table=mock_table, datatype=Mock()), - ] - result = merge_original_current(original, current) - assert len(result) == 3 - assert result[0][0].id == 1 # original col1 - assert result[0][1].id == 1 # current col1 - assert result[1][0] is None # no original for col3 - assert result[1][1].id == 3 # current col3 - assert result[2][0].id == 2 # original col2 - assert result[2][1] is None # no current for col2 - - def test_merge_original_current_indexes(self): - mock_table = Mock() - mock_type = Mock() - original = [ - SQLIndex(id=1, name='idx1', type=mock_type, columns=[], table=mock_table), - ] - current = [ - SQLIndex(id=1, name='idx1_updated', type=mock_type, columns=[], table=mock_table), - ] - result = merge_original_current(original, current) - assert len(result) == 1 - assert result[0][0].id == 1 - assert result[0][1].id == 1 - - def test_merge_original_current_foreign_keys(self): - mock_table = Mock() - original = [ - SQLForeignKey(id=1, name='fk1', table=mock_table, columns=[], reference_table='ref1', reference_columns=[], on_update=None, on_delete=None), - ] - current = [ - SQLForeignKey(id=1, name='fk1_updated', table=mock_table, columns=[], reference_table='ref1', reference_columns=[], on_update=None, on_delete=None), - ] - result = merge_original_current(original, current) - assert len(result) == 1 - assert result[0][0].id == 1 - assert result[0][1].id == 1 diff --git a/tests/test_observables.py b/tests/test_observables.py deleted file mode 100644 index 240041d..0000000 --- a/tests/test_observables.py +++ /dev/null @@ -1,73 +0,0 @@ -import pytest - -from helpers.observables import Observable, ObservableList, ObservableLazyList, CallbackEvent - - -class TestObservable: - def test_observable_creation(self): - obs = Observable(initial=10) - assert obs._value == 10 - - def test_observable_get_value(self): - obs = Observable(initial=5) - assert obs.get_value() == 5 - - def test_observable_set_value(self): - obs = Observable() - obs.set_value(20) - assert obs.get_value() == 20 - - def test_observable_callbacks(self): - obs = Observable() - called = [] - - def callback(value): - called.append(True) - - obs.subscribe(callback, CallbackEvent.AFTER_CHANGE) - obs.set_value(30) - assert len(called) == 1 - - -class TestObservableList: - def test_observable_list_creation(self): - obs_list = ObservableList([1, 2, 3]) - assert obs_list.get_value() == [1, 2, 3] - - def test_observable_list_append(self): - obs_list = ObservableList() - obs_list.append(4) - assert obs_list.get_value() == [4] - - def test_observable_list_extend(self): - obs_list = ObservableList([1]) - obs_list.extend([2, 3]) - assert obs_list.get_value() == [1, 2, 3] - - def test_observable_list_remove(self): - obs_list = ObservableList([1, 2, 3]) - obs_list.remove(2) - assert obs_list.get_value() == [1, 3] - - -class TestObservableLazyList: - def test_observable_lazy_list_creation(self): - def loader(): - return [1, 2, 3] - - obs_lazy = ObservableLazyList(loader) - assert not obs_lazy.is_loaded - assert obs_lazy.get_value() == [1, 2, 3] - assert obs_lazy.is_loaded - - def test_observable_lazy_list_refresh(self): - call_count = [0] - def loader(): - call_count[0] += 1 - return [call_count[0]] - - obs_lazy = ObservableLazyList(loader) - obs_lazy.get_value() # loads [1] - obs_lazy.refresh() - obs_lazy.get_value() # loads [2] - assert obs_lazy.get_value() == [2] diff --git a/tests/test_session.py b/tests/test_session.py deleted file mode 100644 index dd9eb65..0000000 --- a/tests/test_session.py +++ /dev/null @@ -1,77 +0,0 @@ -from structures.connection import Connection, ConnectionEngine -from structures.configurations import CredentialsConfiguration, SourceConfiguration -from structures.session import Session - - -class TestConnection: - def test_sqlite_session_creation(self): - config = SourceConfiguration(filename=':memory:') - connection = Connection(id=1, name='test_session', engine=ConnectionEngine.SQLITE, configuration=config) - session = Session(connection) - - assert session.id == 1 - assert session.name == 'test_session' - assert session.engine == ConnectionEngine.SQLITE - assert session.configuration == config - assert session.context is not None - - def test_session_equality(self): - config1 = SourceConfiguration(filename='test1.db') - config2 = SourceConfiguration(filename='test2.db') - - connection1 = Connection(id=1, name='session1', engine=ConnectionEngine.SQLITE, configuration=config1) - connection2 = Connection(id=1, name='session1', engine=ConnectionEngine.SQLITE, configuration=config1) - connection3 = Connection(id=2, name='session2', engine=ConnectionEngine.SQLITE, configuration=config2) - - session1 = Session(connection1) - session2 = Session(connection2) - session3 = Session(connection3) - - assert session1.connection == session2.connection - assert session1.connection != session3.connection - - def test_session_with_comments(self): - config = SourceConfiguration(filename='test.db') - connection = Connection( - id=4, - name='session_with_comments', - engine=ConnectionEngine.SQLITE, - configuration=config, - comments='Test session' - ) - session = Session(connection) - - assert session.connection.comments == 'Test session' - - def test_session_with_ssh_tunnel(self): - from structures.configurations import SSHTunnelConfiguration - - config = SourceConfiguration(filename='test.db') - ssh_config = SSHTunnelConfiguration( - enabled=True, - executable='ssh', - hostname='remote.host', - port=22, - username='sshuser', - password='sshpwd', - local_port=3307 - ) - connection = Connection( - id=5, - name='session_with_ssh', - engine=ConnectionEngine.SQLITE, - configuration=config, - ssh_tunnel=ssh_config - ) - session = Session(connection) - - assert session.connection.ssh_tunnel == ssh_config - - def test_session_repr(self): - config = SourceConfiguration(filename='test.db') - connection = Connection(id=1, name='test', engine=ConnectionEngine.SQLITE, configuration=config) - session = Session(connection) - # Test that repr doesn't crash - repr_str = repr(session) - assert 'Session' in repr_str - assert 'test' in repr_str diff --git a/tests/ui/README.md b/tests/ui/README.md new file mode 100644 index 0000000..872309a --- /dev/null +++ b/tests/ui/README.md @@ -0,0 +1,42 @@ +# UI Scenario Tests + +This directory includes UI scenario tests that validate key interaction flows and can refresh screenshots. +Current scenarios use `wx.UIActionSimulator` for user-like typing/clicking on the Connection Dialog. + +## Scenarios + +- `test_scenario_a_connection_dialog_root_flow` + - Create and save a root connection + - Update connection values and save again + - Create and delete a root directory + - Delete the root connection and verify cleanup + +- `test_scenario_b_connection_dialog_nested_flow` + - Create a directory + - Keep nested directory selected and expanded + - Persist expanded directory path in settings + - Create and save a connection inside that directory + - Rename and update the nested connection + - Create a third connection and enable **Use SSH Tunnel** (SSH panel visible) + - Optional final screenshot (`connection_dialog_configured.png`) + - Optional SSH screenshot (`connection_dialog_ssh_tunnel.png`) + +## Run in test mode + +Use the project test runner with the dedicated UI suite: + +```bash +PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:$PATH" xvfb-run -a uv run ./scripts/runtest.py --suite ui +``` + +The UI suite is forced to run with xdist single worker (`-n 1`) inside `scripts/runtest.py` for clearer one-failure-at-a-time diagnostics. + +## Refresh screenshots + +Use the same suite with `--refresh-screenshots`: + +```bash +PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:$PATH" xvfb-run -a uv run ./scripts/runtest.py --suite ui --refresh-screenshots +``` + +Generated files are saved under `screenshot/`. diff --git a/tests/ui/scenario_helpers.py b/tests/ui/scenario_helpers.py new file mode 100644 index 0000000..48df2ce --- /dev/null +++ b/tests/ui/scenario_helpers.py @@ -0,0 +1,184 @@ +from pathlib import Path +import time + +import pyautogui +import wx + + +def pump_ui(iterations: int = 10) -> None: + for _ in range(iterations): + wx.YieldIfNeeded() + + +# def _is_likely_black(bitmap: wx.Bitmap) -> bool: +# image = bitmap.ConvertToImage() +# if not image.IsOk(): +# return True +# +# data = image.GetDataBuffer() +# if data is None or len(data) == 0: +# return True +# +# total = len(data) +# bright = 0 +# dark = 0 +# bright_threshold = 24 +# dark_threshold = 6 +# +# for idx in range(0, total, 3): +# r = data[idx] +# g = data[idx + 1] +# b = data[idx + 2] +# if r > bright_threshold or g > bright_threshold or b > bright_threshold: +# bright += 1 +# if r < dark_threshold and g < dark_threshold and b < dark_threshold: +# dark += 1 +# +# pixel_count = total // 3 +# if bright == 0: +# return True +# +# # Treat as invalid only when the bitmap is almost entirely near-black. +# # This avoids false negatives on dark themes while still rejecting empty +# # ScreenDC captures under Xvfb. +# return dark >= int(pixel_count * 0.995) +# +# +# def _bitmap_quality(bitmap: wx.Bitmap) -> int: +# image = bitmap.ConvertToImage() +# if not image.IsOk(): +# return -1 +# +# data = image.GetDataBuffer() +# if data is None or len(data) == 0: +# return -1 +# +# bright = 0 +# threshold = 24 +# total = len(data) +# for idx in range(0, total, 3): +# if data[idx] > threshold or data[idx + 1] > threshold or data[idx + 2] > threshold: +# bright += 1 +# +# return bright + + +def capture_window_bitmap(window: wx.TopLevelWindow) -> wx.Bitmap: + window.Show() + window.Raise() + window.SetFocus() + window.Layout() + window.Refresh() + window.Update() + pump_ui() + + rect = window.GetScreenRect() + + bitmap = wx.Bitmap(rect.width, rect.height) + memory_dc = wx.MemoryDC(bitmap) + screen_dc = wx.ScreenDC() + + try: + memory_dc.SetBackground(wx.Brush(wx.Colour(0, 0, 0))) + memory_dc.Clear() + memory_dc.Blit( + 0, + 0, + rect.width, + rect.height, + screen_dc, + rect.x, + rect.y, + ) + finally: + memory_dc.SelectObject(wx.NullBitmap) + + return bitmap + + +def _trim_black_edges(bitmap: wx.Bitmap) -> wx.Bitmap: + image = bitmap.ConvertToImage() + if not image.IsOk(): + return bitmap + + width = image.GetWidth() + height = image.GetHeight() + if width <= 1 or height <= 1: + return bitmap + + data = image.GetDataBuffer() + if data is None or len(data) == 0: + return bitmap + + threshold = 20 + + def _is_dark_pixel(x: int, y: int) -> bool: + idx = (y * width + x) * 3 + return data[idx] < threshold and data[idx + 1] < threshold and data[idx + 2] < threshold + + def _is_dark_row(y: int) -> bool: + dark = 0 + for x in range(width): + if _is_dark_pixel(x, y): + dark += 1 + return dark >= int(width * 0.98) + + def _is_dark_column(x: int) -> bool: + dark = 0 + for y in range(height): + if _is_dark_pixel(x, y): + dark += 1 + return dark >= int(height * 0.98) + + top = 0 + while top < height - 1 and _is_dark_row(top): + top += 1 + + bottom = height - 1 + while bottom > top and _is_dark_row(bottom): + bottom -= 1 + + left = 0 + while left < width - 1 and _is_dark_column(left): + left += 1 + + right = width - 1 + while right > left and _is_dark_column(right): + right -= 1 + + cropped_width = right - left + 1 + cropped_height = bottom - top + 1 + if cropped_width <= 0 or cropped_height <= 0: + return bitmap + + if cropped_width == width and cropped_height == height: + return bitmap + + return bitmap.GetSubBitmap(wx.Rect(left, top, cropped_width, cropped_height)) + + +def capture_window_screenshot(window: wx.TopLevelWindow, target_path: Path) -> None: + window.Show() + window.Raise() + window.SetFocus() + window.Layout() + window.Refresh() + window.Update() + + for _ in range(20): + wx.GetApp().ProcessPendingEvents() + wx.YieldIfNeeded() + wx.MilliSleep(50) + + width, height = window.GetClientSize() + + bitmap = wx.Bitmap(width, height) + memory_dc = wx.MemoryDC(bitmap) + + try: + source_dc = wx.ClientDC(window) + memory_dc.Blit(0, 0, width, height, source_dc, 0, 0) + finally: + memory_dc.SelectObject(wx.NullBitmap) + + bitmap.SaveFile(str(target_path), wx.BITMAP_TYPE_PNG) diff --git a/tests/ui/test_bindings.py b/tests/ui/test_bindings.py index eafcb70..f978eb1 100644 --- a/tests/ui/test_bindings.py +++ b/tests/ui/test_bindings.py @@ -143,6 +143,8 @@ class TestAbstractModel: def test_model_bind_control(self, wx_app): """Test binding control to model.""" class TestModel(AbstractModel): + super().__init__() + name = Observable[str]() frame = wx.Frame(None) @@ -158,6 +160,7 @@ class TestModel(AbstractModel): def test_model_bind_controls(self, wx_app): """Test binding multiple controls.""" class TestModel(AbstractModel): + super().__init__() name = Observable[str]() age = Observable[int]() diff --git a/tests/ui/test_database_list.py b/tests/ui/test_database_list.py new file mode 100644 index 0000000..5e6c58e --- /dev/null +++ b/tests/ui/test_database_list.py @@ -0,0 +1,233 @@ +import pytest +from unittest.mock import Mock, patch + +from windows.main.database.list import ( + ListDatabaseTable, + ListDatabaseView, + ListDatabaseProcedure, + _truncate_statement, +) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_list_table(): + with patch('windows.main.database.list.CURRENT_DATABASE') as mock_db, \ + patch('windows.main.database.list.CURRENT_TABLE'): + mock_db.get_value.return_value = None + controller = ListDatabaseTable(Mock()) + controller.model = Mock() + controller._app = Mock() # class attr is None at import time (before wx.App) + return controller + + +def _make_list_view(): + with patch('windows.main.database.list.CURRENT_DATABASE') as mock_db, \ + patch('windows.main.database.list.CURRENT_VIEW'): + mock_db.get_value.return_value = None + controller = ListDatabaseView(Mock()) + controller.model = Mock() + controller._app = Mock() + return controller + + +def _make_list_procedure(): + with patch('windows.main.database.list.CURRENT_DATABASE') as mock_db, \ + patch('windows.main.database.list.CURRENT_PROCEDURE'): + mock_db.get_value.return_value = None + controller = ListDatabaseProcedure(Mock()) + controller.model = Mock() + return controller + + +# --------------------------------------------------------------------------- +# _truncate_statement +# --------------------------------------------------------------------------- + +def test_truncate_statement_short_returns_as_is(): + assert _truncate_statement("SELECT 1") == "SELECT 1" + + +def test_truncate_statement_long_appends_ellipsis(): + result = _truncate_statement("A" * 200) + assert result.endswith("...") + assert len(result) == 123 # 120 + 3 + + +def test_truncate_statement_empty_returns_empty(): + assert _truncate_statement("") == "" + + +def test_truncate_statement_collapses_whitespace(): + result = _truncate_statement("SELECT * FROM t") + assert " " not in result + + +# --------------------------------------------------------------------------- +# ListDatabaseTable +# --------------------------------------------------------------------------- + +def test_list_table_load_database_calls_set_observable(): + controller = _make_list_table() + mock_db = Mock() + + with patch('wx.IsMainThread', return_value=True): + controller._load_database(mock_db) + + controller.model.set_observable.assert_called_once_with(mock_db.tables) + + +def test_list_table_load_database_none_does_nothing(): + controller = _make_list_table() + + with patch('wx.IsMainThread', return_value=True): + controller._load_database(None) + + controller.model.set_observable.assert_not_called() + + +def test_list_table_load_database_off_thread_reschedules(): + controller = _make_list_table() + mock_db = Mock() + + with patch('wx.IsMainThread', return_value=False), \ + patch('wx.CallAfter') as mock_call_after: + controller._load_database(mock_db) + + mock_call_after.assert_called_once_with(controller._load_database, mock_db) + controller.model.set_observable.assert_not_called() + + +def test_list_table_item_activated_sets_current_table(): + controller = _make_list_table() + + mock_table = Mock() + mock_table.copy.return_value = mock_table + controller.model.get_data_by_item.return_value = mock_table + + item = Mock() + item.IsOk.return_value = True + event = Mock() + event.GetItem.return_value = item + + with patch('windows.main.database.list.CURRENT_TABLE') as mock_ct, \ + patch('windows.main.database.list.CURRENT_VIEW') as mock_cv: + controller._on_item_activated(event) + + mock_cv.set_value.assert_called_once_with(None) + mock_ct.set_value.assert_called_once_with(mock_table) + + +def test_list_table_item_activated_invalid_item_does_nothing(): + controller = _make_list_table() + + item = Mock() + item.IsOk.return_value = False + event = Mock() + event.GetItem.return_value = item + + with patch('windows.main.database.list.CURRENT_TABLE') as mock_ct: + controller._on_item_activated(event) + + mock_ct.set_value.assert_not_called() + + +# --------------------------------------------------------------------------- +# ListDatabaseView +# --------------------------------------------------------------------------- + +def test_list_view_load_database_calls_set_observable(): + controller = _make_list_view() + mock_db = Mock() + + with patch('wx.IsMainThread', return_value=True): + controller._load_database(mock_db) + + controller.model.set_observable.assert_called_once_with(mock_db.views) + + +def test_list_view_load_database_none_does_nothing(): + controller = _make_list_view() + + with patch('wx.IsMainThread', return_value=True): + controller._load_database(None) + + controller.model.set_observable.assert_not_called() + + +def test_list_view_selection_changed_sets_current_view(): + controller = _make_list_view() + + mock_view = Mock() + mock_view.copy.return_value = mock_view + controller.model.get_data_by_item.return_value = mock_view + + item = Mock() + item.IsOk.return_value = True + event = Mock() + event.GetItem.return_value = item + + with patch('windows.main.database.list.CURRENT_VIEW') as mock_cv, \ + patch('windows.main.database.list.CURRENT_TABLE') as mock_ct: + controller._on_selection_changed(event) + + mock_ct.set_value.assert_called_once_with(None) + mock_cv.set_value.assert_called_once_with(mock_view) + + +# --------------------------------------------------------------------------- +# ListDatabaseProcedure +# --------------------------------------------------------------------------- + +def test_list_procedure_load_database_calls_set_observable(): + controller = _make_list_procedure() + mock_db = Mock(spec=['procedures']) + mock_db.procedures = Mock() + + with patch('wx.IsMainThread', return_value=True): + controller._load_database(mock_db) + + controller.model.set_observable.assert_called_once_with(mock_db.procedures) + + +def test_list_procedure_load_database_without_procedures_attr_does_nothing(): + controller = _make_list_procedure() + mock_db = Mock(spec=[]) # No 'procedures' attribute + + with patch('wx.IsMainThread', return_value=True): + controller._load_database(mock_db) + + controller.model.set_observable.assert_not_called() + + +def test_list_procedure_load_database_none_does_nothing(): + controller = _make_list_procedure() + + with patch('wx.IsMainThread', return_value=True): + controller._load_database(None) + + controller.model.set_observable.assert_not_called() + + +def test_list_procedure_selection_changed_sets_current_procedure(): + controller = _make_list_procedure() + + mock_proc = Mock() + mock_proc.copy.return_value = mock_proc + controller.model.get_data_by_item.return_value = mock_proc + + item = Mock() + item.IsOk.return_value = True + event = Mock() + event.GetItem.return_value = item + + with patch('windows.main.database.list.CURRENT_PROCEDURE') as mock_cp, \ + patch('windows.main.database.list.CURRENT_TABLE') as mock_ct, \ + patch('windows.main.database.list.CURRENT_VIEW') as mock_cv: + controller._on_selection_changed(event) + + mock_ct.set_value.assert_called_once_with(None) + mock_cv.set_value.assert_called_once_with(None) + mock_cp.set_value.assert_called_once_with(mock_proc) diff --git a/tests/ui/test_explorer.py b/tests/ui/test_explorer.py new file mode 100644 index 0000000..7bc00ed --- /dev/null +++ b/tests/ui/test_explorer.py @@ -0,0 +1,173 @@ +import pytest +from unittest.mock import Mock, patch + +from structures.engines.sqlite.database import SQLiteDatabase, SQLiteTable, SQLiteView +from windows.main.explorer import TreeExplorerController + + +def _make_explorer(sessions=None): + tree_ctrl = Mock() + tree_ctrl.AddRoot.return_value = Mock() + tree_ctrl.GetRootItem.return_value = Mock() + + app_mock = Mock() + app_mock.icon_registry_16.imagelist = Mock() + app_mock.icon_registry_16.get_index.return_value = 0 + + with patch('wx.GetApp', return_value=app_mock), \ + patch('windows.main.explorer.SESSIONS_LIST') as mock_sl, \ + patch('windows.main.explorer.NEW_TABLE') as mock_nt: + mock_sl.get_value.return_value = sessions or [] + mock_nt.get_value.return_value = None + controller = TreeExplorerController(tree_ctrl) + + return controller, tree_ctrl + + +# --------------------------------------------------------------------------- +# reset_current_objects +# --------------------------------------------------------------------------- + +@patch('windows.main.explorer.CURRENT_FUNCTION') +@patch('windows.main.explorer.CURRENT_EVENT') +@patch('windows.main.explorer.CURRENT_PROCEDURE') +@patch('windows.main.explorer.CURRENT_TRIGGER') +@patch('windows.main.explorer.CURRENT_VIEW') +@patch('windows.main.explorer.CURRENT_TABLE') +def test_reset_current_objects_clears_all_observables( + mock_table, mock_view, mock_trigger, mock_proc, mock_event, mock_func): + controller, _ = _make_explorer() + + controller.reset_current_objects() + + mock_table.set_value.assert_called_once_with(None) + mock_view.set_value.assert_called_once_with(None) + mock_trigger.set_value.assert_called_once_with(None) + mock_proc.set_value.assert_called_once_with(None) + mock_event.set_value.assert_called_once_with(None) + mock_func.set_value.assert_called_once_with(None) + + +# --------------------------------------------------------------------------- +# select_session +# --------------------------------------------------------------------------- + +@patch('windows.main.explorer.CURRENT_CONNECTION') +@patch('windows.main.explorer.CURRENT_SESSION') +@patch('windows.main.explorer.CURRENT_DATABASE') +def test_select_session_sets_session_and_connection(mock_db, mock_session, mock_conn, sqlite_session): + controller, _ = _make_explorer() + + mock_session.get_value.return_value = None + mock_db.get_value.return_value = None + + event = Mock() + controller.select_session(sqlite_session, event) + + mock_session.set_value.assert_called_once_with(sqlite_session) + mock_conn.set_value.assert_called_once_with(sqlite_session.connection) + + +@patch('windows.main.explorer.CURRENT_CONNECTION') +@patch('windows.main.explorer.CURRENT_SESSION') +@patch('windows.main.explorer.CURRENT_DATABASE') +def test_select_session_skips_if_same_session_and_database(mock_db, mock_session, mock_conn, sqlite_session): + controller, _ = _make_explorer() + + mock_session.get_value.return_value = sqlite_session + mock_db.get_value.return_value = Mock() # Database already selected + + event = Mock() + controller.select_session(sqlite_session, event) + + mock_session.set_value.assert_not_called() + event.Skip.assert_called_once() + + +# --------------------------------------------------------------------------- +# select_sql_object +# --------------------------------------------------------------------------- + +@patch('windows.main.explorer.CURRENT_SESSION') +@patch('windows.main.explorer.CURRENT_DATABASE') +@patch('windows.main.explorer.CURRENT_CONNECTION') +@patch('windows.main.explorer.CURRENT_TABLE') +@patch('windows.main.explorer.CURRENT_VIEW') +def test_select_sql_object_table_sets_current_table( + mock_view, mock_table, mock_conn, mock_db, mock_session, sqlite_session): + controller, _ = _make_explorer() + + database = SQLiteDatabase(id=1, name="db", context=sqlite_session.context) + table = SQLiteTable(id=1, name="users", database=database) + table.copy = Mock(return_value=table) + + mock_session.get_value.return_value = Mock(connection=None) + mock_conn.get_value.return_value = None + mock_db.get_value.return_value = database + mock_table.get_value.return_value = None + + controller.select_sql_object(table) + + mock_table.set_value.assert_called_once_with(table) + mock_view.set_value.assert_not_called() + + +@patch('windows.main.explorer.CURRENT_SESSION') +@patch('windows.main.explorer.CURRENT_DATABASE') +@patch('windows.main.explorer.CURRENT_CONNECTION') +@patch('windows.main.explorer.CURRENT_TABLE') +@patch('windows.main.explorer.CURRENT_VIEW') +def test_select_sql_object_view_sets_current_view( + mock_view, mock_table, mock_conn, mock_db, mock_session, sqlite_session): + controller, _ = _make_explorer() + + database = SQLiteDatabase(id=1, name="db", context=sqlite_session.context) + view = SQLiteView(id=1, name="vw_users", database=database, statement="SELECT 1") + view.copy = Mock(return_value=view) + + mock_session.get_value.return_value = Mock(connection=None) + mock_conn.get_value.return_value = None + mock_db.get_value.return_value = database + mock_view.get_value.return_value = None + + controller.select_sql_object(view) + + mock_view.set_value.assert_called_once_with(view) + mock_table.set_value.assert_not_called() + + +@patch('windows.main.explorer.CURRENT_SESSION') +@patch('windows.main.explorer.CURRENT_DATABASE') +@patch('windows.main.explorer.CURRENT_CONNECTION') +@patch('windows.main.explorer.CURRENT_TABLE') +def test_select_sql_object_different_db_updates_current_database( + mock_table, mock_conn, mock_db, mock_session, sqlite_session): + controller, _ = _make_explorer() + + old_db = SQLiteDatabase(id=1, name="old_db", context=sqlite_session.context) + new_db = SQLiteDatabase(id=2, name="new_db", context=sqlite_session.context) + table = SQLiteTable(id=1, name="users", database=new_db) + table.copy = Mock(return_value=table) + + session_mock = Mock() + session_mock.connection = None + session_mock.context.set_database = Mock() + mock_session.get_value.return_value = session_mock + mock_conn.get_value.return_value = None + mock_db.get_value.return_value = old_db + mock_table.get_value.return_value = None + + controller.select_sql_object(table) + + mock_db.set_value.assert_called_once_with(new_db) + + +# --------------------------------------------------------------------------- +# populate_tree +# --------------------------------------------------------------------------- + +def test_populate_tree_with_no_sessions_does_not_crash(): + controller, tree_ctrl = _make_explorer(sessions=[]) + + tree_ctrl.DeleteAllItems.assert_called() + tree_ctrl.AddRoot.assert_called_once_with("") diff --git a/tests/ui/test_foreign_key_controller.py b/tests/ui/test_foreign_key_controller.py new file mode 100644 index 0000000..a9b1463 --- /dev/null +++ b/tests/ui/test_foreign_key_controller.py @@ -0,0 +1,133 @@ +import pytest +from unittest.mock import Mock, patch + +from structures.engines.sqlite.database import SQLiteDatabase, SQLiteTable +from windows.main.table.foreign_key import TableForeignKeyController + + +@pytest.fixture +def mock_table(sqlite_session): + database = SQLiteDatabase(id=1, name="test_db", context=sqlite_session.context) + table = SQLiteTable(id=1, name="orders", database=database) + table.foreign_keys = Mock() + table.foreign_keys.get_value.return_value = [] + table.foreign_keys.__iter__ = Mock(return_value=iter([])) + table.foreign_keys.__len__ = Mock(return_value=0) + table.copy = Mock(return_value=table) + return table + + +def _make_fk_controller(): + list_ctrl = Mock() + with patch('wx.GetApp') as mock_app, \ + patch('windows.main.table.foreign_key.CURRENT_TABLE') as mock_ct, \ + patch('windows.main.table.foreign_key.NEW_TABLE') as mock_nt, \ + patch('windows.main.table.foreign_key.CURRENT_FOREIGN_KEY'), \ + patch('windows.main.table.foreign_key.CURRENT_SESSION'): + mock_app.return_value = Mock() + mock_ct.get_value.return_value = None + mock_nt.get_value.return_value = None + controller = TableForeignKeyController(list_ctrl) + controller.model = Mock() + return controller, list_ctrl + + +@patch('windows.main.table.foreign_key.CURRENT_TABLE') +@patch('windows.main.table.foreign_key.NEW_TABLE') +@patch('windows.main.table.foreign_key.CURRENT_FOREIGN_KEY') +@patch('windows.main.table.foreign_key.CURRENT_SESSION') +def test_on_selection_changed_sets_current_fk(mock_session, mock_cur_fk, mock_new_table, mock_cur_table, mock_table): + controller, _ = _make_fk_controller() + mock_cur_table.get_value.return_value = None + mock_new_table.get_value.return_value = None + + fake_fk = Mock() + controller.model.get_data_by_item.return_value = fake_fk + + item = Mock() + item.IsOk.return_value = True + event = Mock() + event.GetItem.return_value = item + + controller._on_selection_changed(event) + + mock_cur_fk.set_value.assert_any_call(None) + mock_cur_fk.set_value.assert_any_call(fake_fk) + + +@patch('windows.main.table.foreign_key.CURRENT_TABLE') +@patch('windows.main.table.foreign_key.NEW_TABLE') +@patch('windows.main.table.foreign_key.CURRENT_FOREIGN_KEY') +@patch('windows.main.table.foreign_key.CURRENT_SESSION') +def test_on_selection_changed_invalid_item_only_clears_fk(mock_session, mock_cur_fk, mock_new_table, mock_cur_table): + controller, _ = _make_fk_controller() + + item = Mock() + item.IsOk.return_value = False + event = Mock() + event.GetItem.return_value = item + + controller._on_selection_changed(event) + + mock_cur_fk.set_value.assert_called_once_with(None) + controller.model.get_data_by_item.assert_not_called() + + +@patch('windows.main.table.foreign_key.CURRENT_TABLE') +@patch('windows.main.table.foreign_key.NEW_TABLE') +@patch('windows.main.table.foreign_key.CURRENT_FOREIGN_KEY') +@patch('windows.main.table.foreign_key.CURRENT_SESSION') +def test_on_fk_delete_removes_fk_from_table(mock_session, mock_cur_fk, mock_new_table, mock_cur_table, mock_table): + controller, list_ctrl = _make_fk_controller() + mock_cur_table.get_value.return_value = mock_table + mock_new_table.get_value.return_value = None + + fake_fk = Mock() + controller.model.GetRow.return_value = 0 + controller.model.get_data_by_row.return_value = fake_fk + + selected = Mock() + selected.IsOk.return_value = True + list_ctrl.GetSelection.return_value = selected + + mock_table.foreign_keys.__contains__ = Mock(return_value=True) + mock_table.foreign_keys.remove = Mock() + + controller.on_foreign_key_delete(Mock()) + + mock_table.foreign_keys.remove.assert_called_once_with(fake_fk) + mock_new_table.set_value.assert_called_once_with(mock_table) + + +@patch('windows.main.table.foreign_key.CURRENT_TABLE') +@patch('windows.main.table.foreign_key.NEW_TABLE') +@patch('windows.main.table.foreign_key.CURRENT_FOREIGN_KEY') +@patch('windows.main.table.foreign_key.CURRENT_SESSION') +def test_on_fk_delete_does_nothing_without_selection(mock_session, mock_cur_fk, mock_new_table, mock_cur_table): + controller, list_ctrl = _make_fk_controller() + + selected = Mock() + selected.IsOk.return_value = False + list_ctrl.GetSelection.return_value = selected + + controller.on_foreign_key_delete(Mock()) + + mock_new_table.set_value.assert_not_called() + + +@patch('windows.main.table.foreign_key.CURRENT_TABLE') +@patch('windows.main.table.foreign_key.NEW_TABLE') +@patch('windows.main.table.foreign_key.CURRENT_FOREIGN_KEY') +@patch('windows.main.table.foreign_key.CURRENT_SESSION') +def test_on_fk_clear_empties_table_foreign_keys(mock_session, mock_cur_fk, mock_new_table, mock_cur_table, mock_table): + controller, _ = _make_fk_controller() + mock_cur_table.get_value.return_value = mock_table + mock_new_table.get_value.return_value = None + + mock_table.foreign_keys.clear = Mock() + + controller.on_foreign_key_clear(Mock()) + + controller.model.clear.assert_called_once() + mock_table.foreign_keys.clear.assert_called_once() + mock_new_table.set_value.assert_called_once_with(mock_table) diff --git a/tests/ui/test_main_exception_hook.py b/tests/ui/test_main_exception_hook.py new file mode 100644 index 0000000..795c129 --- /dev/null +++ b/tests/ui/test_main_exception_hook.py @@ -0,0 +1,18 @@ +import main + +from main import PeterSQL + + +class TestPeterSQLExceptionHook: + def test_on_exception_in_main_loop_logs_and_returns_true(self, monkeypatch): + messages = [] + + def fake_exception(message): + messages.append(message) + + monkeypatch.setattr(main.logger, "exception", fake_exception) + + result = PeterSQL.OnExceptionInMainLoop(object()) + + assert result is True + assert messages == ["Unhandled exception raised inside wx main loop"] \ No newline at end of file diff --git a/tests/ui/test_query_history.py b/tests/ui/test_query_history.py new file mode 100644 index 0000000..02235bb --- /dev/null +++ b/tests/ui/test_query_history.py @@ -0,0 +1,142 @@ +import os +import tempfile +from unittest.mock import Mock, patch + +from windows.main.query.history import QueryHistoryController + + +def test_build_query_preview_returns_first_non_empty_line(): + result = QueryHistoryController._build_query_preview("SELECT * FROM users") + assert result == "SELECT * FROM users" + + +def test_build_query_preview_skips_blank_lines(): + result = QueryHistoryController._build_query_preview("\n\n \nSELECT 1") + assert result == "SELECT 1" + + +def test_build_query_preview_truncates_at_120_chars(): + long_query = "SELECT " + "x" * 200 + result = QueryHistoryController._build_query_preview(long_query) + assert len(result) == 120 + + +def test_build_query_preview_empty_content_returns_placeholder(): + result = QueryHistoryController._build_query_preview("") + assert "(empty query)" in result + + +def test_build_query_preview_only_whitespace_returns_placeholder(): + result = QueryHistoryController._build_query_preview(" \n ") + assert "(empty query)" in result + + +def test_group_query_paths_by_date_groups_correctly(): + with tempfile.NamedTemporaryFile(suffix=".sql", delete=False) as f1, \ + tempfile.NamedTemporaryFile(suffix=".sql", delete=False) as f2: + path1, path2 = f1.name, f2.name + + try: + grouped = QueryHistoryController._group_query_paths_by_date([path1, path2]) + all_paths = [p for paths in grouped.values() for p in paths] + assert path1 in all_paths + assert path2 in all_paths + finally: + os.unlink(path1) + os.unlink(path2) + + +def test_group_query_paths_by_date_empty_list_returns_empty(): + grouped = QueryHistoryController._group_query_paths_by_date([]) + assert grouped == {} + + +def test_refresh_populates_tree_with_sql_files(): + tree_ctrl = Mock() + controller = QueryHistoryController(tree_ctrl, Mock()) + + with tempfile.TemporaryDirectory() as tmpdir: + sql_file = os.path.join(tmpdir, "q.sql") + with open(sql_file, "w") as f: + f.write("SELECT 1") + + with patch.object(controller, '_list_query_paths', return_value=[sql_file]): + controller.refresh() + + tree_ctrl.DeleteAllItems.assert_called_once() + tree_ctrl.AppendContainer.assert_called_once() + tree_ctrl.AppendItem.assert_called_once() + + +def test_refresh_empty_history_only_clears_tree(): + tree_ctrl = Mock() + controller = QueryHistoryController(tree_ctrl, Mock()) + + with patch.object(controller, '_list_query_paths', return_value=[]): + controller.refresh() + + tree_ctrl.DeleteAllItems.assert_called_once() + tree_ctrl.AppendContainer.assert_not_called() + + +def test_refresh_expands_first_date_group(): + tree_ctrl = Mock() + controller = QueryHistoryController(tree_ctrl, Mock()) + + with tempfile.TemporaryDirectory() as tmpdir: + sql_file = os.path.join(tmpdir, "q.sql") + with open(sql_file, "w") as f: + f.write("SELECT 1") + + with patch.object(controller, '_list_query_paths', return_value=[sql_file]): + controller.refresh() + + tree_ctrl.Expand.assert_called_once() + + +def test_open_history_item_calls_callback_for_valid_file(): + on_open = Mock() + tree_ctrl = Mock() + controller = QueryHistoryController(tree_ctrl, on_open) + + with tempfile.NamedTemporaryFile(suffix=".sql", delete=False) as f: + path = f.name + + try: + item = Mock() + item.IsOk.return_value = True + tree_ctrl.GetItemData.return_value = path + + controller._open_history_item(item) + + on_open.assert_called_once_with(path) + finally: + os.unlink(path) + + +def test_open_history_item_does_nothing_for_missing_file(): + on_open = Mock() + tree_ctrl = Mock() + controller = QueryHistoryController(tree_ctrl, on_open) + + item = Mock() + item.IsOk.return_value = True + tree_ctrl.GetItemData.return_value = "/nonexistent/path.sql" + + controller._open_history_item(item) + + on_open.assert_not_called() + + +def test_open_history_item_does_nothing_when_data_is_not_string(): + on_open = Mock() + tree_ctrl = Mock() + controller = QueryHistoryController(tree_ctrl, on_open) + + item = Mock() + item.IsOk.return_value = True + tree_ctrl.GetItemData.return_value = 42 + + controller._open_history_item(item) + + on_open.assert_not_called() diff --git a/tests/ui/test_scenarios.py b/tests/ui/test_scenarios.py new file mode 100644 index 0000000..cbe43b6 --- /dev/null +++ b/tests/ui/test_scenarios.py @@ -0,0 +1,537 @@ +from pathlib import Path +import uuid + +import pytest +import wx +from testcontainers.mysql import MySqlContainer + +pytestmark = pytest.mark.xdist_group("ui_scenarios") + +from constants import WORKDIR +from icons import IconRegistry + +from helpers.settings import SettingsRepository + +from structures.configurations import CredentialsConfiguration +from structures.connection import ConnectionEngine +from structures.session import Session + +from windows.components.stc.profiles import BASE64, CSV, HTML, JSON, MARKDOWN, REGEX, SQL, TEXT, XML, YAML +from windows.components.stc.registry import SyntaxRegistry +from windows.components.stc.styles import apply_stc_theme +from windows.components.stc.styles import set_theme_loader +from windows.components.stc.theme_loader import ThemeLoader +from windows.components.stc.themes import ThemeManager + +from windows.dialogs.connections import CURRENT_CONNECTION +from windows.dialogs.connections import CURRENT_DIRECTORY +from windows.dialogs.connections import PENDING_CONNECTION +from windows.dialogs.connections import Connection +from windows.dialogs.connections import ConnectionDirectory +from windows.dialogs.connections.view import ConnectionsManager +from windows.main import CURRENT_COLUMN +from windows.main import CURRENT_DATABASE +from windows.main import CURRENT_EVENT +from windows.main import CURRENT_FOREIGN_KEY +from windows.main import CURRENT_FUNCTION +from windows.main import CURRENT_INDEX +from windows.main import CURRENT_PROCEDURE +from windows.main import CURRENT_RECORDS +from windows.main import CURRENT_SESSION +from windows.main import CURRENT_TABLE +from windows.main import CURRENT_TRIGGER +from windows.main import CURRENT_VIEW +from windows.main import SESSIONS_LIST +from windows.main.controller import MainFrameController + +from tests.ui.scenario_helpers import capture_window_screenshot +from tests.ui.scenario_helpers import pump_ui + + +@pytest.fixture(scope="module") +def scenario_environment(tmp_path_factory): + import windows.dialogs.connections.repository as repository_module + + monkeypatch = pytest.MonkeyPatch() + config_root = tmp_path_factory.mktemp("ui_scenarios") + config_file = config_root / "connections.yml" + monkeypatch.setattr( + repository_module, + "CONNECTIONS_CONFIG_FILE", + config_file, + ) + + app = wx.GetApp() + if app is None: + raise RuntimeError("wx application is not initialized") + + settings_file = config_root / "settings.yml" + app.settings_repository = SettingsRepository(settings_file) + app.settings = app.settings_repository.load() + + app.icon_registry_16 = IconRegistry(str(WORKDIR / "icons"), 16) + + original_message_dialog = wx.MessageDialog + + class _AutoConfirmDialog: + def __init__(self, *args, **kwargs): + self._dialog = original_message_dialog(*args, **kwargs) + + def ShowModal(self): + return wx.ID_YES + + def Destroy(self): + self._dialog.Destroy() + + monkeypatch.setattr(wx, "MessageDialog", _AutoConfirmDialog) + + yield + + monkeypatch.undo() + + +@pytest.fixture(autouse=True) +def cleanup_observables_after_test(): + yield + + # After dialog.Destroy() the C++ widgets are gone but Python callbacks can + # still be subscribed, causing runtime errors in the next test. + for obs in ( + CURRENT_DIRECTORY, + CURRENT_CONNECTION, + PENDING_CONNECTION, + CURRENT_SESSION, + CURRENT_DATABASE, + CURRENT_TABLE, + CURRENT_COLUMN, + CURRENT_INDEX, + CURRENT_FOREIGN_KEY, + CURRENT_RECORDS, + CURRENT_VIEW, + CURRENT_TRIGGER, + CURRENT_PROCEDURE, + CURRENT_FUNCTION, + CURRENT_EVENT, + ): + if obs is CURRENT_RECORDS: + obs._value = [] + else: + obs._value = None + for event_callbacks in obs.callbacks.values(): + event_callbacks.clear() + + SESSIONS_LIST.set_value([]) + for event_callbacks in SESSIONS_LIST.callbacks.values(): + event_callbacks.clear() + + +def _type_text_with_simulator(control: wx.TextCtrl, value: str) -> None: + control.SetFocus() + control.SetValue(value) + text_event = wx.CommandEvent(wx.EVT_TEXT.typeId, control.GetId()) + text_event.SetEventObject(control) + control.GetEventHandler().ProcessEvent(text_event) + pump_ui(5) + + +def _click_button_with_simulator(button: wx.Button) -> None: + click_event = wx.CommandEvent(wx.EVT_BUTTON.typeId, button.GetId()) + click_event.SetEventObject(button) + button.GetEventHandler().ProcessEvent(click_event) + pump_ui(10) + + +def _click_tool_with_simulator(frame: wx.Frame, tool_id: int) -> None: + tool_event = wx.CommandEvent(wx.EVT_TOOL.typeId, tool_id) + tool_event.SetEventObject(frame) + frame.GetEventHandler().ProcessEvent(tool_event) + pump_ui(10) + + +def _set_choice_with_event(control: wx.Choice, value: str) -> None: + if not control.SetStringSelection(value): + raise AssertionError(f"Choice value not found: {value}") + + choice_event = wx.CommandEvent(wx.EVT_CHOICE.typeId, control.GetId()) + choice_event.SetEventObject(control) + control.GetEventHandler().ProcessEvent(choice_event) + pump_ui(5) + + +def _set_checkbox_value(checkbox: wx.CheckBox, checked: bool) -> None: + checkbox.SetFocus() + checkbox.SetValue(checked) + + check_event = wx.CommandEvent(wx.EVT_CHECKBOX.typeId, checkbox.GetId()) + check_event.SetEventObject(checkbox) + check_event.SetInt(1 if checked else 0) + checkbox.GetEventHandler().ProcessEvent(check_event) + pump_ui(10) + + +def _rename_current_selection(dialog: ConnectionsManager, target_name: str) -> None: + item = dialog.connections_tree_ctrl.GetSelection() + assert item.IsOk() + obj = dialog.connections_tree_controller.model.ItemToObject(item) + if isinstance(obj, ConnectionDirectory): + obj.name = target_name + else: + obj.name = target_name + dialog._on_item_renamed(obj) + + +def _clear_tree_selection(dialog: ConnectionsManager) -> None: + dialog.connections_tree_ctrl.UnselectAll() + CURRENT_DIRECTORY(None) + CURRENT_CONNECTION(None) + pump_ui(5) + + +def _select_directory_by_name(dialog: ConnectionsManager, directory_name: str) -> ConnectionDirectory: + nodes = dialog._repository.connections.get_value() + directory = next( + node + for node in nodes + if isinstance(node, ConnectionDirectory) and node.name == directory_name + ) + item = dialog.connections_tree_controller.model.ObjectToItem(directory) + assert item.IsOk() + dialog.connections_tree_ctrl.Select(item) + dialog.connections_tree_ctrl.EnsureVisible(item) + CURRENT_DIRECTORY(directory) + pump_ui(5) + return directory + + +def _expand_directory(dialog: ConnectionsManager, directory: ConnectionDirectory) -> None: + item = dialog.connections_tree_controller.model.ObjectToItem(directory) + assert item.IsOk() + dialog.connections_tree_ctrl.Expand(item) + dialog.connections_tree_ctrl.Select(item) + dialog.connections_tree_ctrl.EnsureVisible(item) + CURRENT_DIRECTORY(directory) + pump_ui(5) + + +def _is_directory_expanded(dialog: ConnectionsManager, directory: ConnectionDirectory) -> bool: + item = dialog.connections_tree_controller.model.ObjectToItem(directory) + assert item.IsOk() + return bool(dialog.connections_tree_ctrl.IsExpanded(item)) + + +def _get_expanded_paths_from_settings(dialog: ConnectionsManager) -> list[list[int]]: + value = dialog._app.settings.get_value( + "ui", + "dialogs", + "connections", + "expanded_directories", + default=[], + ) + assert isinstance(value, list) + return value + + +def _select_notebook_tab_by_title(notebook: wx.Notebook, title: str) -> None: + page_count = notebook.GetPageCount() + for idx in range(page_count): + if notebook.GetPageText(idx) == title: + for _ in range(8): + notebook.SetSelection(idx) + notebook.ChangeSelection(idx) + notebook.SetFocus() + pump_ui(5) + + selected_index = notebook.GetSelection() + if selected_index == idx and notebook.GetPageText(selected_index) == title: + return + + raise AssertionError(f"Failed to keep notebook tab selected: {title}") + + raise AssertionError(f"Notebook tab not found: {title}") + + +def _find_connection_by_id(dialog: ConnectionsManager, connection_id: int) -> Connection: + nodes = dialog._repository.connections.get_value() + return next( + node + for node in nodes + if isinstance(node, Connection) and node.id == connection_id + ) + + +def _find_child_connection_by_id( + parent: ConnectionDirectory, + connection_id: int, +) -> Connection: + return next( + child + for child in parent.children + if isinstance(child, Connection) and child.id == connection_id + ) + + +def _select_connection(dialog: ConnectionsManager, connection: Connection) -> None: + item = dialog.connections_tree_controller.model.ObjectToItem(connection) + assert item.IsOk() + dialog.connections_tree_ctrl.Select(item) + dialog.connections_tree_ctrl.EnsureVisible(item) + CURRENT_CONNECTION(connection) + pump_ui(5) + + +def _prepare_dialog() -> ConnectionsManager: + CURRENT_DIRECTORY(None) + CURRENT_CONNECTION(None) + PENDING_CONNECTION(None) + + dialog = ConnectionsManager(None) + dialog.SetSize(wx.Size(1100, 780)) + dialog.Show() + dialog.Raise() + dialog.SetFocus() + pump_ui(10) + return dialog + + +def test_scenario_a_connection_dialog_root_flow(scenario_environment): + dialog = _prepare_dialog() + try: + _clear_tree_selection(dialog) + + _click_button_with_simulator(dialog.btn_create) + pump_ui() + + root_connection = CURRENT_CONNECTION() + assert root_connection is not None + _type_text_with_simulator(dialog.name, "Scenario Root Connection") + _type_text_with_simulator(dialog.hostname, "localhost") + _type_text_with_simulator(dialog.username, "root") + _type_text_with_simulator(dialog.password, "root") + _click_button_with_simulator(dialog.btn_save) + pump_ui(10) + + saved_root_connection = CURRENT_CONNECTION() + assert saved_root_connection is not None + root_connection = _find_connection_by_id(dialog, saved_root_connection.id) + _select_connection(dialog, root_connection) + + _type_text_with_simulator(dialog.hostname, "127.0.0.1") + _click_button_with_simulator(dialog.btn_save) + pump_ui(10) + + _click_button_with_simulator(dialog.btn_create_directory) + pump_ui(10) + _rename_current_selection(dialog, "Scenario Root Directory") + pump_ui() + + _select_directory_by_name(dialog, "Scenario Root Directory") + _click_button_with_simulator(dialog.btn_delete) + pump_ui(10) + + root_connection = _find_connection_by_id(dialog, root_connection.id) + _select_connection(dialog, root_connection) + _click_button_with_simulator(dialog.btn_delete) + pump_ui(10) + + nodes = dialog._repository.connections.get_value() + assert not any( + isinstance(node, ConnectionDirectory) and node.name == "Scenario Root Directory" + for node in nodes + ) + assert not any( + isinstance(node, Connection) and node.name == "Scenario Root Connection" + for node in nodes + ) + finally: + dialog.Destroy() + + +def test_scenario_b_connection_dialog_nested_flow(refresh_screenshots, scenario_environment): + dialog = _prepare_dialog() + try: + _click_button_with_simulator(dialog.btn_create_directory) + pump_ui(10) + _rename_current_selection(dialog, "Scenario Nested Directory") + pump_ui() + + directory = _select_directory_by_name(dialog, "Scenario Nested Directory") + + _click_button_with_simulator(dialog.btn_create) + pump_ui() + + connection = CURRENT_CONNECTION() + assert connection is not None + _type_text_with_simulator(dialog.name, "Scenario Nested Connection") + _type_text_with_simulator(dialog.hostname, "localhost") + _type_text_with_simulator(dialog.username, "root") + _type_text_with_simulator(dialog.password, "root") + pump_ui() + + _click_button_with_simulator(dialog.btn_save) + saved = CURRENT_CONNECTION() is not None and PENDING_CONNECTION() is None + assert saved is True + + reloaded_directory = _select_directory_by_name(dialog, "Scenario Nested Directory") + _expand_directory(dialog, reloaded_directory) + assert _is_directory_expanded(dialog, reloaded_directory) is True + dialog._save_expanded_directory_paths_to_settings() + expanded_paths = _get_expanded_paths_from_settings(dialog) + assert [reloaded_directory.id] in expanded_paths + + saved_nested_connection = CURRENT_CONNECTION() + assert saved_nested_connection is not None + nested_connection = _find_child_connection_by_id( + directory, + saved_nested_connection.id, + ) + _select_connection(dialog, nested_connection) + nested_connection.name = "Scenario Nested Connection Renamed" + dialog._on_item_renamed(nested_connection) + pump_ui(10) + + # Reload the renamed connection from repository-backed tree state. + reloaded_directory = _select_directory_by_name(dialog, "Scenario Nested Directory") + nested_connection = _find_child_connection_by_id( + reloaded_directory, + nested_connection.id, + ) + _select_connection(dialog, nested_connection) + + _type_text_with_simulator(dialog.hostname, "127.0.0.1") + _click_button_with_simulator(dialog.btn_save) + pump_ui(10) + + reloaded_directory = _select_directory_by_name(dialog, "Scenario Nested Directory") + _expand_directory(dialog, reloaded_directory) + + _click_button_with_simulator(dialog.btn_create) + pump_ui() + + ssh_connection = CURRENT_CONNECTION() + assert ssh_connection is not None + _type_text_with_simulator(dialog.name, "Scenario SSH Tunnel Connection") + _type_text_with_simulator(dialog.hostname, "localhost") + _type_text_with_simulator(dialog.username, "root") + _type_text_with_simulator(dialog.password, "root") + _set_checkbox_value(dialog.ssh_tunnel_enabled, True) + assert dialog.connections_model.ssh_tunnel_enabled() is True + assert dialog.panel_ssh_tunnel.IsEnabled() is True + _select_notebook_tab_by_title(dialog.m_notebook4, "SSH Tunnel") + + _click_button_with_simulator(dialog.btn_save) + pump_ui(10) + + if refresh_screenshots: + _select_notebook_tab_by_title(dialog.m_notebook4, "Settings") + capture_window_screenshot( + dialog, + Path("screenshot") / "connection_dialog_configured.png", + ) + + _select_notebook_tab_by_title(dialog.m_notebook4, "SSH Tunnel") + capture_window_screenshot( + dialog, + Path("screenshot") / "connection_dialog_ssh_tunnel.png", + ) + + pump_ui() + + reloaded_directory = _select_directory_by_name(dialog, "Scenario Nested Directory") + renamed_nested_connection = _find_child_connection_by_id( + reloaded_directory, + nested_connection.id, + ) + assert renamed_nested_connection.configuration is not None + assert renamed_nested_connection.configuration.hostname == "127.0.0.1" + finally: + dialog.Destroy() + + +def test_scenario_c_main_window_mysql_container_screenshot(refresh_screenshots, scenario_environment): + container_name = f"petersql_ui_mysql_{uuid.uuid4().hex[:8]}" + container = MySqlContainer( + "mysql:8", + name=container_name, + mem_limit="768m", + memswap_limit="1g", + nano_cpus=1_000_000_000, + shm_size="256m", + ) + + with container: + config = CredentialsConfiguration( + hostname=container.get_container_host_ip(), + username="root", + password=container.root_password, + port=int(container.get_exposed_port(3306)), + ) + connection = Connection( + id=1001, + name="Scenario MySQL Main Window", + engine=ConnectionEngine.MYSQL, + configuration=config, + ) + session = Session(connection=connection) + session.connect() + + app = wx.GetApp() + if app is None: + raise RuntimeError("wx application is not initialized") + + app.theme_loader = ThemeLoader(WORKDIR / "themes") + app.theme_loader.load_theme("petersql") + set_theme_loader(app.theme_loader) + + app.theme_manager = ThemeManager(apply_function=apply_stc_theme) + app.syntax_registry = SyntaxRegistry([JSON, SQL, XML, YAML, MARKDOWN, HTML, REGEX, CSV, BASE64, TEXT]) + + main_frame = MainFrameController() + main_frame.SetSize(wx.Size(1440, 900)) + main_frame.Show() + main_frame.Raise() + main_frame.SetFocus() + + try: + SESSIONS_LIST.set_value([session]) + CURRENT_SESSION.set_value(session) + CURRENT_CONNECTION.set_value(connection) + + session.context.databases.refresh() + first_database = next(iter(session.context.databases.get_value()), None) + if first_database is not None: + session.context.set_database(first_database) + CURRENT_DATABASE.set_value(first_database) + + _click_tool_with_simulator(main_frame, main_frame.tool_add_database.GetId()) + + database_name = "ui_mysql_scenario_db" + _type_text_with_simulator(main_frame.database_name, database_name) + + collation_values = [main_frame.database_collation.GetString(i) for i in range(main_frame.database_collation.GetCount())] + utf8mb4_collation = next( + value + for value in collation_values + if value.startswith("utf8mb4_general_ci") + ) + _set_choice_with_event(main_frame.database_collation, utf8mb4_collation) + + _click_button_with_simulator(main_frame.btn_apply_database) + pump_ui(20) + + session.context.databases.refresh() + database = next(db for db in session.context.databases.get_value() if db.name == database_name) + + if refresh_screenshots: + capture_window_screenshot( + main_frame, + Path("screenshot") / "mysql_main_window_add_database.png", + ) + + assert CURRENT_SESSION.get_value() is session + current_database = CURRENT_DATABASE.get_value() + assert current_database is not None + assert current_database.id == database.id + assert current_database.name == database.name + finally: + main_frame.Destroy() + session.disconnect() diff --git a/tests/ui/test_view_editor.py b/tests/ui/test_view_editor.py new file mode 100644 index 0000000..b09b2c3 --- /dev/null +++ b/tests/ui/test_view_editor.py @@ -0,0 +1,139 @@ +import pytest +from unittest.mock import Mock, patch, call + +from structures.engines.sqlite.database import SQLiteDatabase, SQLiteView +from windows.main.database.view import ViewEditorController + + +@pytest.fixture +def mock_parent(): + parent = Mock() + for name in [ + "rad_view_algorithm_undefined", + "rad_view_algorithm_merge", + "rad_view_algorithm_temptable", + "rad_view_constraint_none", + "rad_view_constraint_local", + "rad_view_constraint_cascaded", + "rad_view_constraint_check_only", + "rad_view_constraint_read_only", + ]: + radio = Mock() + radio.GetValue.return_value = False + radio.IsShown.return_value = True + setattr(parent, name, radio) + return parent + + +@pytest.fixture +def new_view(sqlite_session): + database = SQLiteDatabase(id=1, name="test_db", context=sqlite_session.context) + return SQLiteView(id=-1, name="new_view", database=database, statement="SELECT 1") + + +@pytest.fixture +def existing_view(sqlite_session): + database = SQLiteDatabase(id=1, name="test_db", context=sqlite_session.context) + return SQLiteView(id=10, name="existing_view", database=database, statement="SELECT 1") + + +def _make_controller(mock_parent): + with patch("windows.main.database.view.CURRENT_VIEW") as mock_view_obs, \ + patch("windows.main.database.view.CURRENT_SESSION"), \ + patch("windows.main.database.view.CURRENT_DATABASE"): + mock_view_obs.get_value.return_value = None + controller = ViewEditorController(mock_parent) + return controller + + +@patch("windows.main.database.view.CURRENT_SESSION") +@patch("windows.main.database.view.CURRENT_DATABASE") +@patch("windows.main.database.view.CURRENT_VIEW") +def test_new_view_has_changes_immediately(mock_view_obs, mock_db, mock_session, mock_parent, new_view): + mock_view_obs.get_value.return_value = None + controller = _make_controller(mock_parent) + assert controller._has_changes(new_view) is True + + +@patch("windows.main.database.view.CURRENT_SESSION") +@patch("windows.main.database.view.CURRENT_DATABASE") +@patch("windows.main.database.view.CURRENT_VIEW") +def test_new_view_delete_disabled(mock_view_obs, mock_db, mock_session, mock_parent, new_view): + mock_view_obs.get_value.return_value = new_view + controller = _make_controller(mock_parent) + + with patch("windows.main.database.view.CURRENT_VIEW") as pv: + pv.get_value.return_value = new_view + controller.update_button_states() + + mock_parent.btn_delete_view.Enable.assert_called_with(False) + + +@patch("windows.main.database.view.CURRENT_SESSION") +@patch("windows.main.database.view.CURRENT_DATABASE") +@patch("windows.main.database.view.CURRENT_VIEW") +def test_existing_view_delete_enabled(mock_view_obs, mock_db, mock_session, mock_parent, existing_view): + mock_view_obs.get_value.return_value = None + controller = _make_controller(mock_parent) + + with patch("windows.main.database.view.CURRENT_VIEW") as pv, \ + patch("windows.main.database.view.CURRENT_DATABASE") as pd: + pv.get_value.return_value = existing_view + mock_db_instance = Mock() + mock_db_instance.views = [] + pd.get_value.return_value = mock_db_instance + controller.update_button_states() + + mock_parent.btn_delete_view.Enable.assert_called_with(True) + + +@patch("wx.MessageBox") +@patch("windows.main.database.view.CURRENT_SESSION") +@patch("windows.main.database.view.CURRENT_DATABASE") +@patch("windows.main.database.view.CURRENT_VIEW") +def test_save_new_view_calls_save(mock_view_obs, mock_db, mock_session, mock_msgbox, mock_parent, new_view): + mock_view_obs.get_value.return_value = None + controller = _make_controller(mock_parent) + + new_view.save = Mock(return_value=True) + saved = Mock() + saved.name = new_view.name + mock_database = Mock() + mock_database.views = [saved] + + with patch("windows.main.database.view.CURRENT_VIEW") as pv, \ + patch("windows.main.database.view.CURRENT_DATABASE") as pd, \ + patch("windows.main.database.view.CURRENT_SESSION") as ps: + pv.get_value.return_value = new_view + pd.get_value.return_value = mock_database + ps.get_value.return_value = Mock() + controller.do_save_view() + + new_view.save.assert_called_once() + + +@patch("wx.MessageBox") +@patch("windows.main.database.view.CURRENT_SESSION") +@patch("windows.main.database.view.CURRENT_DATABASE") +@patch("windows.main.database.view.CURRENT_VIEW") +def test_save_new_view_reloads_from_db(mock_view_obs, mock_db, mock_session, mock_msgbox, mock_parent, new_view): + mock_view_obs.get_value.return_value = None + controller = _make_controller(mock_parent) + + new_view.save = Mock(return_value=True) + saved = Mock() + saved.name = new_view.name + mock_database = Mock() + mock_database.views = [saved] + + with patch("windows.main.database.view.CURRENT_VIEW") as pv, \ + patch("windows.main.database.view.CURRENT_DATABASE") as pd, \ + patch("windows.main.database.view.CURRENT_SESSION") as ps: + pv.get_value.return_value = new_view + pd.get_value.return_value = mock_database + ps.get_value.return_value = Mock() + controller.do_save_view() + + assert pv.set_value.call_count == 2 + pv.set_value.assert_any_call(None) + pv.set_value.assert_any_call(saved) diff --git a/uv.lock b/uv.lock index 6ed37a4..619e500 100644 --- a/uv.lock +++ b/uv.lock @@ -256,6 +256,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/cf/ea4ef2920830dea3f5ab2ea4da6fb67724e6dca80ee2553788c3607243d0/jaraco_functools-4.5.0.tar.gz", hash = "sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03", size = 20272, upload-time = "2026-05-15T21:34:10.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl", hash = "sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4", size = 10594, upload-time = "2026-05-15T21:34:08.595Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + [[package]] name = "librt" version = "0.8.1" @@ -290,6 +361,131 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "memray" +version = "1.19.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "rich" }, + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/04/5b886a36df947599e0f37cd46e6e44e565299815f044e2303ab2ae9f8870/memray-1.19.3.tar.gz", hash = "sha256:4e0fb29ff0a50c0ec9dc84294d8f2c83419feba561a37628b304c2ae4fe73d03", size = 2417089, upload-time = "2026-04-08T18:49:32.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/d5/36ae02ef5c55cc9d9febb89fb39b78135eaf76d71458703d91cde401142f/memray-1.19.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f62ca641421c0856b38b95c2bc39bdd1493330a94d526efd817d2135a2c1600b", size = 2186101, upload-time = "2026-04-08T18:48:32.663Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e2/b08b27e84fff1ea039508baaac252c3facbd3a81930e1a8108fd3db1dfcb/memray-1.19.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4335e51dac85891434de4de603f58be1296c2fa463dbf4075b7230820c2a58b", size = 2164551, upload-time = "2026-04-08T18:48:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/34/48/3a0422d71f43a361782517f6c0b93bdc32e4b211aaa7290c3038a91f991b/memray-1.19.3-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:03f0e1c3584e19951471413f858b70ecb4f1b5d2e3e4bf0c147c104ac79a8548", size = 9743615, upload-time = "2026-04-08T18:48:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/4b/52/f9ee7911819085e174fe07d1bab27691dc1486be39bded35baebda792069/memray-1.19.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:90e634b79be3c332d0d0ab3e1da6ccce6acd20c5fa847f505230ff49d9450a62", size = 9994310, upload-time = "2026-04-08T18:48:39.588Z" }, + { url = "https://files.pythonhosted.org/packages/aa/64/a1f75b5a105cc325b7719a4ed1690c45e751098d17a45902668fd9e0af67/memray-1.19.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4be4535a98c2c93c3195a769c342da12564c8bfd4f5056c3527d237845fd354", size = 9386965, upload-time = "2026-04-08T18:48:41.849Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ad/01178b4c2f61b1b64995f0de9ada899f91abe4de67db793df7d428834456/memray-1.19.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e3c6433d246063cc33d1ae25104120643492f0750ed5f7ac660d92aba064da6", size = 12238366, upload-time = "2026-04-08T18:48:44.14Z" }, + { url = "https://files.pythonhosted.org/packages/e8/21/8034941ba5aeb9f73e05becc40661af54d548c2047e4058948a3d94acd5d/memray-1.19.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e034252330961dadfe83ac0f111c9a1d3d8fb8f1358c9b992ed664b6a8b2d2a9", size = 2202163, upload-time = "2026-04-08T18:48:46.689Z" }, + { url = "https://files.pythonhosted.org/packages/c2/62/ba34b1cf5a8c89f6f4b24e8471a6e6e011e50a86a0d697407320eeaf5b65/memray-1.19.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6ee43e6132fd457d0865b6a1d0de3acdb220e897625a32a0d609beaebb8abd20", size = 2181584, upload-time = "2026-04-08T18:48:48.516Z" }, + { url = "https://files.pythonhosted.org/packages/89/f7/901e83b6bf71d968bcfdcd5b081d8c5940ec7b5b9d28d67d5d4027d72529/memray-1.19.3-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:78eacb4e1c1b66cf7c4dd6dd27b0c88fb4598b63f3096e014d0a2b91ab70fb6f", size = 9706760, upload-time = "2026-04-08T18:48:50.161Z" }, + { url = "https://files.pythonhosted.org/packages/9e/18/a556d0e93d69786edd5c62b8d71bc04f8454b7655b23351a3d95a6d0bc62/memray-1.19.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c33442360bba89fd34c4b985cd505abbf2bbec6a40bdfdf50e1981a4afd9625", size = 9958651, upload-time = "2026-04-08T18:48:52.88Z" }, + { url = "https://files.pythonhosted.org/packages/0a/20/f9fea6a3bb8cbc835567ddf89eb524c6fb4c01130b9a4a2b88e65d0c7aca/memray-1.19.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63868a883a6ca926ed1e9200a0d8f3bedde6e1af2a8d4b6ef0cf85160a3b28cd", size = 9382102, upload-time = "2026-04-08T18:48:55.787Z" }, + { url = "https://files.pythonhosted.org/packages/41/4a/035e5bdeddbd51a2ba1be53f1fc979268c8ce51640b042db4cbee305849b/memray-1.19.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:96ee0a76e4ca57e2ad48a8eccc28531503029884a791bb3556cc9ddac74e148d", size = 12162201, upload-time = "2026-04-08T18:48:57.952Z" }, +] + +[[package]] +name = "more-itertools" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, +] + +[[package]] +name = "mouseinfo" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyperclip" }, + { name = "python3-xlib", marker = "sys_platform == 'linux'" }, + { name = "rubicon-objc", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/fa/b2ba8229b9381e8f6381c1dcae6f4159a7f72349e414ed19cfbbd1817173/MouseInfo-0.1.3.tar.gz", hash = "sha256:2c62fb8885062b8e520a3cce0a297c657adcc08c60952eb05bc8256ef6f7f6e7", size = 10850, upload-time = "2020-03-27T21:20:10.136Z" } + [[package]] name = "mypy" version = "1.19.1" @@ -370,6 +566,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "babel" }, + { name = "keyring" }, { name = "oracledb" }, { name = "psutil" }, { name = "psycopg2-binary" }, @@ -381,8 +578,12 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "memray" }, { name = "mypy" }, + { name = "pillow" }, { name = "pre-commit" }, + { name = "pyautogui" }, + { name = "pyscreeze" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-mock" }, @@ -397,12 +598,17 @@ dev = [ [package.metadata] requires-dist = [ { name = "babel", specifier = ">=2.18.0" }, + { name = "keyring", specifier = ">=25.0.0" }, + { name = "memray", marker = "extra == 'dev'", specifier = ">=1.19.3" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.19.1" }, { name = "oracledb", specifier = ">=3.4.2" }, + { name = "pillow", marker = "extra == 'dev'", specifier = ">=12.2.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.5.1" }, { name = "psutil", specifier = ">=7.2.2" }, { name = "psycopg2-binary", specifier = ">=2.9.11" }, + { name = "pyautogui", marker = "extra == 'dev'", specifier = ">=0.9.54" }, { name = "pymysql", specifier = ">=1.1.2" }, + { name = "pyscreeze", marker = "extra == 'dev'", specifier = ">=1.0.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.15.1" }, @@ -418,6 +624,39 @@ requires-dist = [ ] provides-extras = ["dev"] +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + [[package]] name = "platformdirs" version = "4.9.2" @@ -493,6 +732,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, ] +[[package]] +name = "pyautogui" +version = "0.9.54" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mouseinfo" }, + { name = "pygetwindow" }, + { name = "pymsgbox" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, + { name = "pyscreeze" }, + { name = "python3-xlib", marker = "sys_platform == 'linux'" }, + { name = "pytweening" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/ff/cdae0a8c2118a0de74b6cf4cbcdcaf8fd25857e6c3f205ce4b1794b27814/PyAutoGUI-0.9.54.tar.gz", hash = "sha256:dd1d29e8fd118941cb193f74df57e5c6ff8e9253b99c7b04f39cfc69f3ae04b2", size = 61236, upload-time = "2023-05-24T20:11:32.972Z" } + [[package]] name = "pycparser" version = "3.0" @@ -502,6 +757,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +name = "pygetwindow" +version = "0.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyrect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/70/c7a4f46dbf06048c6d57d9489b8e0f9c4c3d36b7479f03c5ca97eaa2541d/PyGetWindow-0.0.9.tar.gz", hash = "sha256:17894355e7d2b305cd832d717708384017c1698a90ce24f6f7fbf0242dd0a688", size = 9699, upload-time = "2020-10-04T02:12:50.806Z" } + [[package]] name = "pygments" version = "2.19.2" @@ -511,6 +775,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pymsgbox" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/6a/e80da7594ee598a776972d09e2813df2b06b3bc29218f440631dfa7c78a8/pymsgbox-2.0.1.tar.gz", hash = "sha256:98d055c49a511dcc10fa08c3043e7102d468f5e4b3a83c6d3c61df722c7d798d", size = 20768, upload-time = "2025-09-09T00:38:56.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/3e/08c8cac81b2b2f7502746e6b9c8e5b0ec6432cd882c605560fc409aaf087/pymsgbox-2.0.1-py3-none-any.whl", hash = "sha256:5de8ec19bca2ca7e6c09d39c817c83f17c75cee80275235f43a9931db699f73b", size = 9994, upload-time = "2025-09-09T00:38:55.672Z" }, +] + [[package]] name = "pymysql" version = "1.1.2" @@ -520,6 +793,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, ] +[[package]] +name = "pyobjc-core" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, + { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, +] + +[[package]] +name = "pyobjc-framework-quartz" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558, upload-time = "2025-11-14T10:00:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + +[[package]] +name = "pyrect" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/04/2ba023d5f771b645f7be0c281cdacdcd939fe13d1deb331fc5ed1a6b3a98/PyRect-0.2.0.tar.gz", hash = "sha256:f65155f6df9b929b67caffbd57c0947c5ae5449d3b580d178074bffb47a09b78", size = 17219, upload-time = "2022-03-16T04:45:52.36Z" } + +[[package]] +name = "pyscreeze" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/f0/cb456ac4f1a73723d5b866933b7986f02bacea27516629c00f8e7da94c2d/pyscreeze-1.0.1.tar.gz", hash = "sha256:cf1662710f1b46aa5ff229ee23f367da9e20af4a78e6e365bee973cad0ead4be", size = 27826, upload-time = "2024-08-20T23:03:07.291Z" } + [[package]] name = "pytest" version = "9.0.2" @@ -597,6 +928,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] +[[package]] +name = "python3-xlib" +version = "0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c6/2c5999de3bb1533521f1101e8fe56fd9c266732f4d48011c7c69b29d12ae/python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8", size = 132828, upload-time = "2014-05-31T12:28:59.603Z" } + +[[package]] +name = "pytweening" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/0c/c16bc93ac2755bac0066a8ecbd2a2931a1735a6fffd99a2b9681c7e83e90/pytweening-1.2.0.tar.gz", hash = "sha256:243318b7736698066c5f362ec5c2b6434ecf4297c3c8e7caa8abfe6af4cac71b", size = 171241, upload-time = "2024-02-20T03:37:56.809Z" } + [[package]] name = "pywin32" version = "311" @@ -607,6 +950,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -648,6 +1000,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rubicon-objc" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/d2/d39ecd205661a5c14c90dbd92a722a203848a3621785c9783716341de427/rubicon_objc-0.5.3.tar.gz", hash = "sha256:74c25920c5951a05db9d3a1aac31d23816ec7dacc841a5b124d911b99ea71b9a", size = 171512, upload-time = "2025-12-03T03:51:10.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/ab/e834c01138c272fb2e37d2f3c7cba708bc694dbc7b3f03b743f29ceb92d5/rubicon_objc-0.5.3-py3-none-any.whl", hash = "sha256:31dedcda9be38435f5ec067906e1eea5d0ddb790330e98a22e94ff424758b415", size = 64414, upload-time = "2025-12-03T03:51:09.082Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + [[package]] name = "sqlglot" version = "29.0.1" @@ -673,6 +1060,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/31/5e7b23f9e43ff7fd46d243808d70c5e8daf3bc08ecf5a7fb84d5e38f7603/testcontainers-4.14.1-py3-none-any.whl", hash = "sha256:03dfef4797b31c82e7b762a454b6afec61a2a512ad54af47ab41e4fa5415f891", size = 125640, upload-time = "2026-01-31T23:13:45.464Z" }, ] +[[package]] +name = "textual" +version = "8.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/b3/b62658f6cf808d28e4d16a07509728a7b17824f55a6d3533f017fd4566b0/textual-8.2.6.tar.gz", hash = "sha256:cef3714498a120a99278b98d4c165c278844e73db50f1db039aaabd89f2d1b63", size = 1856990, upload-time = "2026-05-13T09:56:12.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/b4/c2b876f445e52522824cb900f2c7db3a7c24f89d20449ef278b4195d0ecb/textual-8.2.6-py3-none-any.whl", hash = "sha256:17c92bec7ff1617bd7db2a3d9734b0c3b7d2c274c67d5eba94371ea2f99a63fd", size = 729855, upload-time = "2026-05-13T09:56:14.687Z" }, +] + [[package]] name = "toml" version = "0.10.2" @@ -718,6 +1122,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + [[package]] name = "urllib3" version = "2.6.3" diff --git a/windows/components/__init__.py b/windows/components/__init__.py index 60bcf7d..4709ebb 100644 --- a/windows/components/__init__.py +++ b/windows/components/__init__.py @@ -206,19 +206,32 @@ def edit_item(self, item, column): # For editable cells, use EditItem wx.CallAfter(self.EditItem, item, column) - def calculate_column_width(self, text, col=None): - w = 0 - cw = 0 + def autosize_columns_from_content(self, sample_rows: int = 30): + if not (model := self.GetModel()): + return + n_rows = min(model.GetCount(), sample_rows) - if view := self.GetParent(): - dc = wx.ClientDC(view) - w, h = dc.GetTextExtent(str(text)) - if col: - cw = self.GetCurrentColumn().GetWidth() + dc = wx.ClientDC(self) + dc.SetFont(self.GetFont()) + + for col_idx, col in enumerate(self.GetColumns()): + # header + max_width = self.measure_text(col.GetTitle()) + + # sample rows + for row in range(n_rows): + row_value = model.GetValueByRow(row, col_idx) + if row_value is None: + continue + row_width = self.measure_text(str(row_value)) + if row_width > max_width: + max_width = row_width - return max(cw, w + 20) + # width = max_w + 24 + width = max(60, min(max_width, 360)) + col.SetWidth(width) - def measure_text(self, text: str, padding: int = 24) -> int: + def measure_text(self, text: str, padding: int = 32) -> int: dc = wx.ClientDC(self) dc.SetFont(self.GetFont()) width, _ = dc.GetTextExtent(str(text)) diff --git a/windows/components/dataview.py b/windows/components/dataview.py index c96ba88..e9adff4 100644 --- a/windows/components/dataview.py +++ b/windows/components/dataview.py @@ -17,7 +17,7 @@ from windows.components.popup import PopupColumnDatatype, PopupColumnDefault, PopupCheckList, PopupChoice, PopupCalendar, PopupCalendarTime from windows.components.renders import PopupRenderer, LengthSetRender, TimeRenderer, FloatRenderer, IntegerRenderer, TextRenderer, AdvancedTextRenderer -from windows.state import CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE, NEW_TABLE +from windows.state import CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE, CURRENT_VIEW, NEW_TABLE class _SQLiteTableColumnsDataViewCtrl: @@ -103,6 +103,8 @@ class TableColumnsDataViewCtrl(BaseDataViewCtrl): on_finish_editing: Callable[[...], Optional[bool]] = None + _BASE_COLUMN_COUNT = 4 + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -129,17 +131,29 @@ def __init__(self, *args, **kwargs): _MariaDBMySQLTableColumnsDataViewCtrl, _PostgreSQLTableColumnsDataViewCtrl ] = None + self._current_engine = None CURRENT_SESSION.subscribe(self._load_session) def _load_session(self, session: Session): - if not self._current_dataview and session: - if session.engine == ConnectionEngine.SQLITE: - self._current_dataview = _SQLiteTableColumnsDataViewCtrl(self) - elif session.engine in [ConnectionEngine.MYSQL, ConnectionEngine.MARIADB]: - self._current_dataview = _MariaDBMySQLTableColumnsDataViewCtrl(self) - elif session.engine == ConnectionEngine.POSTGRESQL: - self._current_dataview = _PostgreSQLTableColumnsDataViewCtrl(self) + if session is None: + return + + engine = session.engine + if engine == self._current_engine: + return + + while self.GetColumnCount() > self._BASE_COLUMN_COUNT: + self.DeleteColumn(self.GetColumn(self.GetColumnCount() - 1)) + + self._current_engine = engine + + if engine == ConnectionEngine.SQLITE: + self._current_dataview = _SQLiteTableColumnsDataViewCtrl(self) + elif engine in [ConnectionEngine.MYSQL, ConnectionEngine.MARIADB]: + self._current_dataview = _MariaDBMySQLTableColumnsDataViewCtrl(self) + elif engine == ConnectionEngine.POSTGRESQL: + self._current_dataview = _PostgreSQLTableColumnsDataViewCtrl(self) def _on_context_menu(self, event): session = CURRENT_SESSION() @@ -153,13 +167,13 @@ def _on_context_menu(self, event): menu = wx.Menu() add_item = wx.MenuItem(menu, wx.ID_ANY, _("Add column\tCTRL+INS"), wx.EmptyString, wx.ITEM_NORMAL) - add_item.SetBitmap(wx.GetApp().icon_registry_16.get_bitmap(IconList.ADD)) + add_item.SetBitmap(self.app.icon_registry_16.get_bitmap(IconList.ADD)) menu.Append(add_item) self.Bind(wx.EVT_MENU, lambda e: wx.CallAfter(self.on_column_insert, e), add_item) delete_item = wx.MenuItem(menu, wx.ID_ANY, _("Remove column\tCTRL+DEL"), wx.EmptyString, wx.ITEM_NORMAL) - delete_item.SetBitmap(wx.GetApp().icon_registry_16.get_bitmap(IconList.DELETE)) + delete_item.SetBitmap(self.app.icon_registry_16.get_bitmap(IconList.DELETE)) # delete_item.Enable(selected.IsOk()) menu.Append(delete_item) menu.Enable(delete_item.GetId(), selected.IsOk()) @@ -167,14 +181,14 @@ def _on_context_menu(self, event): self.Bind(wx.EVT_MENU, self.on_column_delete, delete_item) move_up_item = wx.MenuItem(menu, wx.ID_ANY, _("Move up\tCTRL+UP"), wx.EmptyString, wx.ITEM_NORMAL) - move_up_item.SetBitmap(wx.GetApp().icon_registry_16.get_bitmap(IconList.ARROW_UP)) + move_up_item.SetBitmap(self.app.icon_registry_16.get_bitmap(IconList.ARROW_UP)) menu.Append(move_up_item) menu.Enable(move_up_item.GetId(), selected.IsOk()) self.Bind(wx.EVT_MENU, self.on_column_move_up, move_up_item) move_down_item = wx.MenuItem(menu, wx.ID_ANY, _("Move down\tCTRL+D"), wx.EmptyString, wx.ITEM_NORMAL) - move_down_item.SetBitmap(wx.GetApp().icon_registry_16.get_bitmap(IconList.ARROW_DOWN)) + move_down_item.SetBitmap(self.app.icon_registry_16.get_bitmap(IconList.ARROW_DOWN)) menu.Append(move_down_item) menu.Enable(move_down_item.GetId(), selected.IsOk()) @@ -186,7 +200,8 @@ def _on_context_menu(self, event): for index_type in session.context.INDEXTYPE.get_all(): item = wx.MenuItem(create_index_menu, wx.ID_ANY, index_type.name, wx.EmptyString, wx.ITEM_NORMAL) - item.SetBitmap(index_type.bitmap) + + item.SetBitmap(self.app.icon_registry_16.get_bitmap(index_type.bitmap)) create_index_menu.Append(item) if index_type.name == "PRIMARY" and len([pk for pk in list(table.indexes) if pk.type == StandardIndexType.PRIMARY]) > 0: @@ -202,7 +217,7 @@ def _on_context_menu(self, event): for index in list(table.indexes): if column.name not in index.columns: item = wx.MenuItem(append_index_menu, wx.ID_ANY, index.name, wx.EmptyString, wx.ITEM_NORMAL) - item.SetBitmap(index.type.bitmap) + item.SetBitmap(self.app.icon_registry_16.get_bitmap(index.type.bitmap)) append_index_menu.Append(item) if not index.type.enable_append: @@ -328,8 +343,9 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) CURRENT_TABLE.subscribe(self._load_table) + CURRENT_VIEW.subscribe(self._load_view) - def make_advanced_dialog(self, parent, value: str, read_only : bool = False): + 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) @@ -408,38 +424,19 @@ def _load_table(self, table: SQLTable): col = wx.dataview.DataViewColumn(column.name, renderer, i, width=self.measure_text(column.name), flags=wx.dataview.DATAVIEW_COL_RESIZABLE) self.AppendColumn(col) - wx.CallAfter(self.autosize_columns_from_content) - - def autosize_columns_from_content(self, sample_rows: int = 30): - model = self.GetModel() - if not model: - return - - n_cols = self.GetColumnCount() - n_rows = min(model.GetCount(), sample_rows) - - dc = wx.ClientDC(self) - dc.SetFont(self.GetFont()) - - for col_idx in range(n_cols): - col = self.GetColumn(col_idx) - - # header - max_w, _ = dc.GetTextExtent(col.GetTitle()) + def _load_view(self, view): + while self.GetColumnCount() > 0: + self.DeleteColumn(self.GetColumn(0)) - # sample rows - for row in range(n_rows): - v = model.GetValueByRow(row, col_idx) - if v is None: - continue - w, _ = dc.GetTextExtent(str(v)) - if w > max_w: - max_w = w + if view is not None and not view.is_new: + for i, column in enumerate(view.columns): + renderer = TextRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) + col = wx.dataview.DataViewColumn(column.name, renderer, i, width=self.measure_text(column.name), flags=wx.dataview.DATAVIEW_COL_RESIZABLE) + self.AppendColumn(col) - # clamp - width = max_w + 24 - width = max(60, min(width, 360)) # max 360 evita colonne infinite - col.SetWidth(width) + def AssociateModel(self, model): + super().AssociateModel(model) + wx.CallAfter(self.autosize_columns_from_content) class QueryEditorResultsDataViewCtrl(TableRecordsDataViewCtrl): diff --git a/windows/components/renders.py b/windows/components/renders.py index d2304f0..2289b16 100644 --- a/windows/components/renders.py +++ b/windows/components/renders.py @@ -20,6 +20,8 @@ def __init__(self, popup_class: Type[BasePopup], on_open: Optional[Callable[[Bas def GetSize(self): view = self.GetView() + if view is None: + return wx.Size(50, 20) value = self._value.strip() or getattr(self.popup_class, "default_value", "") if not value: diff --git a/windows/components/stc/autocomplete/auto_complete.py b/windows/components/stc/autocomplete/auto_complete.py index c5fd783..93eca16 100644 --- a/windows/components/stc/autocomplete/auto_complete.py +++ b/windows/components/stc/autocomplete/auto_complete.py @@ -14,6 +14,8 @@ CompletionResult, ) from windows.components.stc.autocomplete.context_detector import ContextDetector +from windows.components.stc.autocomplete.query_scope import TableReference +from windows.components.stc.autocomplete.sql_context import SQLContext from windows.components.stc.autocomplete.dot_completion_handler import ( DotCompletionHandler, ) @@ -76,6 +78,15 @@ def get( ) scope.current_table = self._get_current_table() + if self._is_filter_editor: + context = SQLContext.WHERE_CLAUSE + if scope.current_table and not scope.from_tables: + scope.from_tables = [TableReference( + name=scope.current_table.name, + alias=None, + table=scope.current_table, + )] + if self._dot_handler: self._dot_handler.refresh(database, scope) if self._dot_handler.is_dot_completion(statement, relative_pos): @@ -152,6 +163,10 @@ def __init__( self._editor.Bind(wx.stc.EVT_STC_CHARADDED, self._on_char_added) self._editor.Bind(wx.EVT_KEY_DOWN, self._on_key_down) + self._editor.Bind(wx.EVT_KILL_FOCUS, self._on_editor_kill_focus) + + top_level = wx.GetTopLevelParent(editor) + top_level.Bind(wx.EVT_MOVE, self._on_parent_move) def set_enabled(self, is_enabled: bool) -> None: self._is_enabled = is_enabled @@ -335,6 +350,41 @@ def _on_key_down(self, event: wx.KeyEvent) -> None: self._on_item_completed(selected_item) return + if key_code == wx.WXK_UP and self._popup and self._popup.IsShown(): + self._popup.select_prev() + return + + if key_code == wx.WXK_DOWN and self._popup and self._popup.IsShown(): + self._popup.select_next() + return + + if key_code == wx.WXK_PAGEUP and self._popup and self._popup.IsShown(): + self._popup.select_prev_page() + return + + if key_code == wx.WXK_PAGEDOWN and self._popup and self._popup.IsShown(): + self._popup.select_next_page() + return + + event.Skip() + + def _on_editor_kill_focus(self, event: wx.FocusEvent) -> None: + wx.CallAfter(self._hide_if_focus_outside_popup) + event.Skip() + + def _hide_if_focus_outside_popup(self) -> None: + if not (self._popup and self._popup.IsShown()): + return + focused = wx.Window.FindFocus() + current = focused + while current is not None: + if current is self._popup: + return + current = current.GetParent() + self._hide_popup() + + def _on_parent_move(self, event: wx.MoveEvent) -> None: + self._hide_popup() event.Skip() def _on_char_added(self, event: wx.stc.StyledTextEvent) -> None: diff --git a/windows/components/stc/autocomplete/autocomplete_popup.py b/windows/components/stc/autocomplete/autocomplete_popup.py index 671bbb9..1cc89ae 100644 --- a/windows/components/stc/autocomplete/autocomplete_popup.py +++ b/windows/components/stc/autocomplete/autocomplete_popup.py @@ -50,7 +50,6 @@ def _create_ui(self) -> None: 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 @@ -80,7 +79,6 @@ def show_items(self, items: list[CompletionItem], position: wx.Point) -> None: 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 = { @@ -146,28 +144,44 @@ def _on_key_down(self, event: wx.KeyEvent) -> None: 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 select_next(self) -> None: + current = self._list_ctrl.GetFirstSelected() + new_index = (current + 1) if current != wx.NOT_FOUND else 0 + new_index = min(new_index, len(self._items) - 1) + self._list_ctrl.Select(new_index) + self._list_ctrl.Focus(new_index) + self._list_ctrl.EnsureVisible(new_index) + + def select_prev(self) -> None: + current = self._list_ctrl.GetFirstSelected() + if current == wx.NOT_FOUND: + return + new_index = max(current - 1, 0) + self._list_ctrl.Select(new_index) + self._list_ctrl.Focus(new_index) + self._list_ctrl.EnsureVisible(new_index) + + def select_next_page(self) -> None: + current = self._list_ctrl.GetFirstSelected() + if current != wx.NOT_FOUND: + new_index = min(current + self._popup_max_height, len(self._items) - 1) + self._list_ctrl.Select(new_index) + self._list_ctrl.Focus(new_index) + self._list_ctrl.EnsureVisible(new_index) + + def select_prev_page(self) -> None: + current = self._list_ctrl.GetFirstSelected() + if current != wx.NOT_FOUND: + new_index = max(current - self._popup_max_height, 0) + self._list_ctrl.Select(new_index) + self._list_ctrl.Focus(new_index) + self._list_ctrl.EnsureVisible(new_index) + def set_on_item_selected(self, callback: Callable) -> None: self._on_item_selected = callback diff --git a/windows/dialogs/connections/controller.py b/windows/dialogs/connections/controller.py index f955086..db9d8ce 100644 --- a/windows/dialogs/connections/controller.py +++ b/windows/dialogs/connections/controller.py @@ -77,6 +77,7 @@ def SetValue(self, variant, item, col): class ConnectionsTreeController: on_selection_chance: Callable[[Optional[Any]], Optional[Any]] = None on_item_activated: Callable[[Optional[Any]], Optional[Any]] = None + on_item_renamed: Callable[[Optional[Any]], None] = None def __init__( self, @@ -108,22 +109,20 @@ def __init__( CURRENT_CONNECTION.subscribe(self._on_current_connection) def _on_item_editing_done(self, event): - item = event.GetItem() + if event.IsEditCancelled(): + return + item = event.GetItem() if not item.IsOk(): return obj = self.model.ItemToObject(item) - if isinstance(obj, ConnectionDirectory): - self.repository.save_directory(obj) - CURRENT_DIRECTORY(obj) - - elif isinstance(obj, Connection): - self.repository.save_connection(obj) - CURRENT_CONNECTION(obj) - - self.model.ItemChanged(item) + if isinstance(obj, (ConnectionDirectory, Connection)): + # On GTK, EVT_DATAVIEW_ITEM_EDITING_DONE fires before SetValue is called, + # so obj.name still holds the old name at this point. wx.CallAfter defers + # the notification to the next event loop iteration, after SetValue has run. + wx.CallAfter(self.on_item_renamed, obj) def _on_item_start_editing(self, event): if not self._allow_next_edit: @@ -196,4 +195,4 @@ def _on_current_connection(self, connection: Optional[Connection]): item = self.model.ObjectToItem(connection) if not self.connections_tree_ctrl.IsSelected(item): self.connections_tree_ctrl.Select(item) - self.connections_tree_ctrl.EnsureVisible(item) + self.connections_tree_ctrl.EnsureVisible(item) \ No newline at end of file diff --git a/windows/dialogs/connections/model.py b/windows/dialogs/connections/model.py index 0df3922..2d21341 100644 --- a/windows/dialogs/connections/model.py +++ b/windows/dialogs/connections/model.py @@ -15,6 +15,7 @@ class ConnectionModel(AbstractModel): def __init__(self): + super().__init__() self.name = Observable[str]() self.engine = Observable[str](initial=ConnectionEngine.MYSQL.value.name) self.hostname = Observable[str]() @@ -37,13 +38,15 @@ def __init__(self): self.average_connection_time = Observable[str]("") self.most_recent_connection_duration = Observable[str]("") + self.read_only = Observable[bool](initial=False) + self.ssh_tunnel_enabled = Observable[bool](initial=False) self.ssh_tunnel_executable = Observable[str](initial="ssh") self.ssh_tunnel_hostname = Observable[str]() self.ssh_tunnel_port = Observable[int](initial=22) self.ssh_tunnel_username = Observable[str]() self.ssh_tunnel_password = Observable[str]() - self.ssh_tunnel_local_port = Observable[int](initial=3307) + self.ssh_tunnel_local_port = Observable[int](initial=0) self.ssh_tunnel_identity_file = Observable[str]() self.ssh_tunnel_remote_hostname = Observable[str]() self.ssh_tunnel_remote_port = Observable[int](initial=3306) @@ -63,6 +66,7 @@ def __init__(self): self.port, self.filename, self.comments, + self.read_only, self.created_at, self.last_connection_at, self.successful_connected, @@ -118,13 +122,14 @@ def clear(self, *args): self.total_connection_attempts: "0", self.average_connection_time: "", self.most_recent_connection_duration: "", + self.read_only: False, self.ssh_tunnel_enabled: False, self.ssh_tunnel_executable: "ssh", self.ssh_tunnel_hostname: None, self.ssh_tunnel_port: 22, self.ssh_tunnel_username: None, self.ssh_tunnel_password: None, - self.ssh_tunnel_local_port: 3307, + self.ssh_tunnel_local_port: 0, self.ssh_tunnel_identity_file: None, self.ssh_tunnel_remote_hostname: None, self.ssh_tunnel_remote_port: 3306, @@ -144,6 +149,7 @@ def apply(self, connection: Connection): self.engine(connection.engine.value.name) self.comments(connection.comments) + self.read_only(connection.read_only) self.created_at(connection.created_at or "") self.last_connection_at(connection.last_connection_at or "") self.successful_connected(str(connection.successful_connections)) @@ -234,6 +240,7 @@ def _build(self, *args): pending_connection.name = self.name() or "" pending_connection.engine = connection_engine pending_connection.comments = self.comments() + pending_connection.read_only = bool(self.read_only.get_value()) if connection_engine in [ ConnectionEngine.MYSQL, diff --git a/windows/dialogs/connections/repository.py b/windows/dialogs/connections/repository.py index 22f6bcc..efa0256 100644 --- a/windows/dialogs/connections/repository.py +++ b/windows/dialogs/connections/repository.py @@ -1,3 +1,4 @@ +import uuid from pathlib import Path from typing import Any, Optional, Union @@ -6,6 +7,16 @@ from helpers.observables import ObservableLazyList from helpers.repository import YamlRepository +from structures.secrets import ( + _is_legacy_numeric_id, + delete_database_password, + delete_ssh_password, + get_database_password, + get_ssh_password, + set_database_password, + set_ssh_password, +) + from windows.dialogs.connections import ConnectionDirectory from structures.connection import ( @@ -40,7 +51,7 @@ def _read(self) -> list[dict[str, Any]]: def _write(self) -> None: connections = self.connections.get_value() - payload = [item.to_dict() for item in connections] + payload = [self._prepare_connection_dict(item) for item in connections] self._write_yaml(payload) def load(self) -> list[Union[ConnectionDirectory, Connection]]: @@ -89,18 +100,45 @@ def _connection_from_dict( Union[CredentialsConfiguration, SourceConfiguration] ] = None + secret_id = data.get("secret_id") + if secret_id is None: + secret_id = str(uuid.uuid4()) + if data.get("configuration"): - config_data = data["configuration"] + config_data = dict(data["configuration"]) if engine in [ ConnectionEngine.MYSQL, ConnectionEngine.MARIADB, ConnectionEngine.POSTGRESQL, ]: + password_keyring_id = config_data.pop("password_keyring_id", None) + if password_keyring_id is not None: + if _is_legacy_numeric_id(password_keyring_id): + legacy_password = get_database_password(password_keyring_id) + if legacy_password is not None: + set_database_password(secret_id, legacy_password) + delete_database_password(password_keyring_id) + else: + set_database_password(secret_id, get_database_password(password_keyring_id)) + config_data["password"] = get_database_password(secret_id) configuration = self._build_credentials_configuration(config_data) elif engine == ConnectionEngine.SQLITE: configuration = SourceConfiguration(**config_data) - ssh_config = self._build_ssh_configuration(data.get("ssh_tunnel", {})) + ssh_tunnel_data = data.get("ssh_tunnel", {}) + ssh_password_keyring_id = ssh_tunnel_data.get("password_keyring_id") + if ssh_password_keyring_id is not None: + if _is_legacy_numeric_id(ssh_password_keyring_id): + legacy_ssh_password = get_ssh_password(ssh_password_keyring_id) + if legacy_ssh_password is not None: + set_ssh_password(secret_id, legacy_ssh_password) + delete_ssh_password(ssh_password_keyring_id) + else: + set_ssh_password(secret_id, get_ssh_password(ssh_password_keyring_id)) + ssh_tunnel_data = dict(ssh_tunnel_data) + ssh_tunnel_data.pop("password_keyring_id", None) + + ssh_config = self._build_ssh_configuration(ssh_tunnel_data) if data.get("id") is not None: self._id_counter = max(self._id_counter, data["id"] + 1) @@ -130,6 +168,7 @@ def _connection_from_dict( configuration=configuration, comments=comments, ssh_tunnel=ssh_config, + read_only=bool(data.get("read_only", False)), parent=parent, created_at=data.get("created_at"), last_connection_at=data.get("last_connection_at"), @@ -140,6 +179,7 @@ def _connection_from_dict( total_connection_attempts=total_connection_attempts, average_connection_time_ms=average_connection_time_ms, most_recent_connection_duration_ms=most_recent_connection_duration_ms, + secret_id=secret_id, ) def add_connection( @@ -150,6 +190,11 @@ def add_connection( if connection.is_new: connection.id = self._next_id() + if connection.secret_id is None: + connection.secret_id = str(uuid.uuid4()) + + self._persist_connection_secrets(connection) + if parent: parent.children.append(connection) connection.parent = parent @@ -180,6 +225,11 @@ def _find_and_replace( return True return False + if connection.secret_id is None: + connection.secret_id = str(uuid.uuid4()) + + self._persist_connection_secrets(connection) + if _find_and_replace(self.connections.get_value(), connection.id): self._write() self.connections.refresh() @@ -226,6 +276,7 @@ def add_directory( self.connections.append(directory) self._write() + self.connections.refresh() def delete_directory(self, directory: ConnectionDirectory): self.connections.get_value() @@ -303,6 +354,7 @@ def _find_and_delete(connections, target_id): return False if _find_and_delete(self.connections.get_value(), connection.id): + self._delete_connection_secrets(connection) self._write() self.connections.refresh() @@ -352,6 +404,59 @@ def _build_credentials_configuration( except (TypeError, ValueError): return None + def _prepare_connection_dict( + self, item: Union["ConnectionDirectory", "Connection"] + ) -> dict[str, Any]: + data = item.to_dict() + + if isinstance(item, ConnectionDirectory): + data["children"] = [ + self._prepare_connection_dict(child) for child in item.children + ] + return data + + secret_id = getattr(item, "secret_id", None) or str(item.id) + + configuration = data.get("configuration") + if configuration: + configuration = dict(configuration) + password = configuration.pop("password", None) + if password not in (None, ""): + configuration.pop("password_keyring_id", None) + set_database_password(secret_id, password) + elif "password_keyring_id" in configuration: + del configuration["password_keyring_id"] + data["configuration"] = configuration + + ssh_tunnel = data.get("ssh_tunnel") + if ssh_tunnel: + ssh_tunnel = dict(ssh_tunnel) + ssh_password = ssh_tunnel.pop("password", None) + if ssh_password not in (None, ""): + ssh_tunnel.pop("password_keyring_id", None) + set_ssh_password(secret_id, ssh_password) + elif "password_keyring_id" in ssh_tunnel: + del ssh_tunnel["password_keyring_id"] + data["ssh_tunnel"] = ssh_tunnel + + if secret_id is not None: + data["secret_id"] = secret_id + + return data + + def _persist_connection_secrets(self, connection: Connection) -> None: + secret_id = getattr(connection, "secret_id", None) or str(connection.id) + if isinstance(connection.configuration, CredentialsConfiguration): + set_database_password(secret_id, connection.configuration.password) + + if connection.ssh_tunnel: + set_ssh_password(secret_id, connection.ssh_tunnel.password) + + def _delete_connection_secrets(self, connection: Connection) -> None: + secret_id = getattr(connection, "secret_id", None) or str(connection.id) + delete_database_password(secret_id) + delete_ssh_password(secret_id) + @staticmethod def _normalize_ssh_extra_args(extra_args: Any) -> Optional[Union[str, list[str]]]: if isinstance(extra_args, str): diff --git a/windows/dialogs/connections/view.py b/windows/dialogs/connections/view.py index 4534ecb..7f2d826 100644 --- a/windows/dialogs/connections/view.py +++ b/windows/dialogs/connections/view.py @@ -48,6 +48,7 @@ def __init__(self, parent): self.connections_tree_controller.on_item_activated = ( lambda connection: self.on_connect(None) ) + self.connections_tree_controller.on_item_renamed = self._on_item_renamed self.connections_model = ConnectionModel() self.connections_model.bind_controls( @@ -71,6 +72,7 @@ def __init__(self, parent): total_connection_attempts=self.total_connection_attempts, average_connection_time=self.average_connection_time, most_recent_connection_duration=self.most_recent_connection_duration, + read_only=self.read_only, ssh_tunnel_enabled=self.ssh_tunnel_enabled, ssh_tunnel_executable=self.ssh_tunnel_executable, ssh_tunnel_hostname=self.ssh_tunnel_hostname, @@ -644,6 +646,20 @@ def _select_connection_in_tree( self.connections_tree_ctrl.Select(item) self.connections_tree_ctrl.EnsureVisible(item) + def _select_directory_in_tree(self, directory_id: int) -> None: + directory = self._find_directory_by_id(directory_id) + if directory is None: + return + + item = self.connections_tree_controller.model.ObjectToItem(directory) + if item is None or not item.IsOk(): + return + + self._expand_item_parents(item) + self.connections_tree_ctrl.Select(item) + self.connections_tree_ctrl.EnsureVisible(item) + CURRENT_DIRECTORY(directory) + def on_create_directory(self, event): if not self._confirm_save_pending_changes(): return @@ -653,10 +669,7 @@ def on_create_directory(self, event): new_dir = ConnectionDirectory(id=-1, name=_("New directory")) self._repository.add_directory(new_dir, parent) - item = self.connections_tree_controller.model.ObjectToItem(new_dir) - if item.IsOk(): - self.connections_tree_ctrl.Select(item) - self.connections_tree_controller.edit_item(item) + wx.CallAfter(self._select_directory_in_tree, new_dir.id) if parent: parent_item = self.connections_tree_controller.model.ObjectToItem(parent) @@ -716,6 +729,16 @@ def on_clone_connection(self, event): wx.CallAfter(self._restore_expanded_directory_paths, expanded_paths) wx.CallAfter(self._select_connection_in_tree, refreshed_connection) + def _on_item_renamed(self, obj): + expanded_paths = self._capture_expanded_directory_paths() + if isinstance(obj, ConnectionDirectory): + self._repository.save_directory(obj) + CURRENT_DIRECTORY(obj) + elif isinstance(obj, Connection): + self._repository.save_connection(obj) + CURRENT_CONNECTION(obj) + wx.CallAfter(self._restore_expanded_directory_paths, expanded_paths) + def on_rename(self, event): selected_item = self._get_action_item() if selected_item is None or not selected_item.IsOk(): diff --git a/windows/main/__init__.py b/windows/main/__init__.py index 8d7fd9e..af7ac31 100755 --- a/windows/main/__init__.py +++ b/windows/main/__init__.py @@ -14,6 +14,7 @@ CURRENT_TRIGGER, CURRENT_VIEW, SESSIONS_LIST, + WRITE_OVERRIDE, ) __all__ = [ @@ -32,4 +33,5 @@ "CURRENT_TRIGGER", "CURRENT_VIEW", "SESSIONS_LIST", + "WRITE_OVERRIDE", ] diff --git a/windows/main/controller.py b/windows/main/controller.py index a3d17b5..6770336 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -19,12 +19,12 @@ from helpers import bytes_to_human from helpers.loader import Loader from helpers.logger import logger -from helpers.observables import CallbackEvent +from helpers.observables import CallbackEvent, ObservableList from structures.session import Session from structures.connection import Connection, ConnectionEngine from structures.engines.context import QUERY_LOGS -from structures.engines.database import SQLTable, SQLColumn, SQLIndex, SQLForeignKey, SQLRecord, SQLView, SQLTrigger, SQLDatabase +from structures.engines.database import SQLTable, SQLColumn, SQLIndex, SQLForeignKey, SQLRecord, SQLView, SQLTrigger, SQLDatabase, SQLProcedure, SQLFunction from windows.views import MainFrameView @@ -33,12 +33,14 @@ from windows.components.stc.autocomplete.auto_complete import SQLAutoCompleteController, SQLCompletionProvider 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 import CURRENT_CONNECTION, CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE, CURRENT_COLUMN, CURRENT_INDEX, CURRENT_FOREIGN_KEY, CURRENT_RECORDS, AUTO_APPLY, CURRENT_VIEW, CURRENT_TRIGGER, CURRENT_PROCEDURE, CURRENT_FUNCTION, WRITE_OVERRIDE +from windows.state import SESSIONS_LIST from windows.main.explorer import TreeExplorerController -from windows.main.database.list import ListDatabaseTable +from windows.main.database.list import ListDatabaseTable, ListDatabaseView, ListDatabaseProcedure, ListDatabaseFunction, ListDatabaseTrigger, ListDatabaseEvent from windows.main.database.view import ViewEditorController +from windows.main.database.routine import RoutineController from windows.main.database.options import DatabaseOptionsController from windows.main.table.check import TableCheckController @@ -46,9 +48,11 @@ 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.database.options import DatabaseOptionsController, NEW_DATABASE from windows.main.table.foreign_key import TableForeignKeyController from windows.main.query.controller import QueryResultsController +from windows.main.query.history import QueryHistoryController class MainFrameController(MainFrameView): @@ -61,6 +65,10 @@ def __init__(self): self._query_pages: list[wx.Panel] = [] self._query_page_counter = 1 self._query_page_meta: dict[wx.Panel, dict[str, Any]] = {} + self._query_history_controller = QueryHistoryController( + self.tree_ctrl_query_history, + on_open_query=self._open_query_history_file, + ) self._query_shortcuts = self._load_query_shortcuts() self.edit_table_model = EditTableModel() @@ -75,6 +83,7 @@ def __init__(self): ) self.list_database_tables = ListDatabaseTable(self.list_ctrl_database_tables) + self.list_database_views = ListDatabaseView(self.list_ctrl_database_views) self.controller_database_options = DatabaseOptionsController(self) self.controller_tree_connections = TreeExplorerController(self.tree_ctrl_explorer) @@ -90,6 +99,11 @@ def __init__(self): self._setup_query_pages() self.controller_view_editor = ViewEditorController(self) + self.controller_routine_editor = RoutineController(self) + self.list_database_procedures = ListDatabaseProcedure(self.list_ctrl_database_procedure) + self.list_database_functions = ListDatabaseFunction(self.list_ctrl_database_function) + self.list_database_triggers = ListDatabaseTrigger(self.list_ctrl_database_trigger) + self.list_database_events = ListDatabaseEvent(self.list_ctrl_database_event) records_limit = self._load_records_limit_from_settings() self.limit_records.SetValue(records_limit) @@ -108,6 +122,12 @@ def __init__(self): self._setup_database_action_buttons_bindings() + # Write override state — must be initialized before _setup_subscribers() fires + self._write_override_timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self._on_write_override_tick, self._write_override_timer) + self._override_remaining_seconds: int = 0 + self._overridden_connection: Optional[Connection] = None + self._setup_subscribers() # Memory update timer @@ -117,24 +137,22 @@ def __init__(self): self.Bind(wx.EVT_SYS_COLOUR_CHANGED, self.on_sys_colour_changed) + self.sql_query_filters.Bind(wx.EVT_KEY_DOWN, self._on_filters_key_down) + + self._id_f5_refresh = wx.NewIdRef() + accel = wx.AcceleratorTable([(wx.ACCEL_NORMAL, wx.WXK_F5, self._id_f5_refresh)]) + self.SetAcceleratorTable(accel) + self.Bind(wx.EVT_MENU, self._on_f5_refresh, id=self._id_f5_refresh) + 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, + model.name, + model.collation, + model.encryption, + model.tablespace, + model.connection_limit, ] for observable in database_observables: @@ -143,6 +161,7 @@ def _setup_database_action_buttons_bindings(self) -> None: CURRENT_SESSION.subscribe(self._on_database_options_changed) def _on_database_options_changed(self, _=None) -> None: + logger.debug("ui trace: _on_database_options_changed") self._update_database_action_buttons() @staticmethod @@ -178,12 +197,18 @@ 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) + has_changes = self._database_has_changes(database) or NEW_DATABASE.get_value() is not None 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) + logger.debug( + "ui trace: _update_database_action_buttons has_database=%s has_changes=%s is_persisted=%s", + has_database, + has_changes, + is_persisted, + ) def on_sys_colour_changed(self, event): self._setup_query_editors() @@ -196,7 +221,8 @@ def _setup_query_editors(self): 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) + is_filter = styled_text_ctrl_name == "sql_query_filters" + self._setup_sql_editor(styled_text_ctrl, is_filter_editor=is_filter) editors.add(styled_text_ctrl) for meta in self._query_page_meta.values(): @@ -206,7 +232,7 @@ def _setup_query_editors(self): self._setup_sql_editor(styled_text_ctrl) - def _setup_sql_editor(self, styled_text_ctrl: wx.stc.StyledTextCtrl) -> None: + def _setup_sql_editor(self, styled_text_ctrl: wx.stc.StyledTextCtrl, *, is_filter_editor: bool = False) -> None: styled_text_ctrl.EmptyUndoBuffer() wx.GetApp().theme_manager.register(styled_text_ctrl, lambda: wx.GetApp().syntax_registry.get("sql")) @@ -216,6 +242,7 @@ def _setup_sql_editor(self, styled_text_ctrl: wx.stc.StyledTextCtrl) -> None: sql_completion_provider = SQLCompletionProvider( get_database=lambda: CURRENT_DATABASE.get_value(), get_current_table=lambda: CURRENT_TABLE.get_value(), + is_filter_editor=is_filter_editor, ) SQLAutoCompleteController( @@ -231,6 +258,20 @@ def _setup_sql_editor(self, styled_text_ctrl: wx.stc.StyledTextCtrl) -> None: get_current_table=lambda: CURRENT_TABLE.get_value(), ) + @staticmethod + def _apply_sql_keywords_to_editor( + styled_text_ctrl: wx.stc.StyledTextCtrl, + keywords: str, + colors_datatypes: defaultdict, + ) -> None: + styled_text_ctrl.SetKeyWords(0, keywords) + + for idx, (color, words) in enumerate(colors_datatypes.items(), start=1): + styled_text_ctrl.SetKeyWords(idx, " ".join(sorted(words))) + styled_text_ctrl.StyleSetForeground(wx.stc.STC_SQL_WORD + idx, wx.Colour(*color)) + + styled_text_ctrl.Colourise(0, -1) + 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) @@ -282,69 +323,6 @@ def _apply_query_toolbar_shortcuts(self, toolbar: wx.ToolBar, tool_ids: dict[str 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: @@ -375,6 +353,7 @@ def _register_query_page( 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), + on_connection_lost=self._on_query_connection_lost, ) self._query_pages.append(panel) self._query_page_meta[panel] = { @@ -444,14 +423,6 @@ def _update_query_page_title(self, page: wx.Panel) -> None: 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: @@ -484,6 +455,7 @@ def _setup_query_pages(self) -> None: 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() + self._query_history_controller.refresh() def _update_query_close_tools_state(self) -> None: can_close = self.notebook_query_editor.GetPageCount() > 1 @@ -496,7 +468,12 @@ 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() + 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) + self.notebook_query_editor.AddPage(panel, label, select=True) shared_tool_ids = { @@ -518,6 +495,17 @@ def _create_new_query_page(self) -> None: ) self._setup_sql_editor(editor) + + if session := CURRENT_SESSION.get_value(): + keywords = " ".join(k.lower() for k in session.context.KEYWORDS) + colors_datatypes = defaultdict(list) + + for datatype in session.context.DATATYPE.get_all(): + colors_datatypes[datatype.category.value.color].append(datatype.name.lower()) + colors_datatypes[datatype.category.value.color].extend([d.lower() for d in datatype.alias]) + + self._apply_sql_keywords_to_editor(editor, keywords, colors_datatypes) + self._update_query_close_tools_state() def _confirm_close_query_page(self, page: wx.Panel) -> bool: @@ -590,10 +578,32 @@ 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) + def _open_query_history_file(self, file_path: str) -> None: + page = self.notebook_query_editor.GetCurrentPage() + if page is None: + return + + meta = self._query_page_meta.get(page) + if meta is None: + return + + try: + with open(file_path, "r", encoding="utf-8") as file_obj: + content = file_obj.read() + except Exception as ex: + logger.error(str(ex), exc_info=True) + wx.MessageDialog(None, str(ex), _("Error"), wx.OK | wx.ICON_ERROR).ShowModal() + return + + editor = meta["editor"] + editor.SetText(content) + meta["file_path"] = file_path + meta["display_name"] = os.path.basename(file_path) + self._set_query_dirty(page, is_dirty=False) + @staticmethod def _get_query_autosave_path() -> str: - query_dir = os.path.join(os.getcwd(), ".queries") - os.makedirs(query_dir, exist_ok=True) + query_dir = QueryHistoryController.get_query_history_directory() 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: @@ -619,6 +629,7 @@ def _save_query_page(self, page: wx.Panel, force_save_as: bool) -> bool: meta["file_path"] = file_path meta["display_name"] = os.path.basename(file_path) self._set_query_dirty(page, is_dirty=False) + self._query_history_controller.refresh() QUERY_LOGS.append(_("-- Saved query to {file_path}").format(file_path=file_path)) return True @@ -644,6 +655,7 @@ def _autosave_query_page_before_execute(self, page: wx.Panel) -> bool: meta["file_path"] = file_path self._set_query_dirty(page, is_dirty=False) + self._query_history_controller.refresh() QUERY_LOGS.append(_("-- Autosaved query to {file_path}").format(file_path=file_path)) return True @@ -660,6 +672,9 @@ def _setup_subscribers(self): CURRENT_VIEW.subscribe(self._on_current_view) + CURRENT_PROCEDURE.subscribe(self._on_current_procedure) + CURRENT_FUNCTION.subscribe(self._on_current_function) + CURRENT_TRIGGER.subscribe(self._on_current_trigger) CURRENT_TABLE.subscribe(self._on_current_table) @@ -675,9 +690,12 @@ def _setup_subscribers(self): # SELECTED_TABLE.subscribe(self._on_selected_table) NEW_TABLE.subscribe(self._on_new_table) + NEW_DATABASE.subscribe(self._on_new_database) AUTO_APPLY.subscribe(self._on_auto_apply) + WRITE_OVERRIDE.subscribe(self._on_write_override) + # Initialize record toolbar states self._initialize_record_toolbar_states() @@ -685,6 +703,9 @@ def _setup_subscribers(self): self._initialize_column_toolbar_states() def _write_query_log(self, text: str): + wx.CallAfter(self._append_query_log, text) + + def _append_query_log(self, text: str): self.sql_query_logs.AppendText(f"{text}\n") self.sql_query_logs.GotoLine(self.sql_query_logs.GetLineCount() - 1) @@ -750,14 +771,21 @@ def on_open_settings(self, event): if controller.show_modal() == wx.ID_OK: wx.MessageBox(_("Settings saved successfully"), _("Settings"), wx.OK | wx.ICON_INFORMATION) - def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, SQLTrigger]] = None): + def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, SQLTrigger, SQLProcedure, SQLFunction]] = None): # self.MainFrameNotebook.SetSelection(0) + logger.debug( + "ui trace: toggle_panel current=%s", + type(current).__name__ if current is not None else "None", + ) current_session = CURRENT_SESSION.get_value() current_database = CURRENT_DATABASE.get_value() current_table = CURRENT_TABLE.get_value() current_view = CURRENT_VIEW.get_value() current_trigger = CURRENT_TRIGGER.get_value() + current_procedure = CURRENT_PROCEDURE.get_value() + current_function = CURRENT_FUNCTION.get_value() + routine_page_index = self.MainFrameNotebook.FindPage(self.panel_routine) total_pages = self.MainFrameNotebook.GetPageCount() @@ -772,14 +800,20 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S if not current_table: self.MainFrameNotebook.GetPage(2).Hide() - self.MainFrameNotebook.GetPage(5).Hide() + self.MainFrameNotebook.GetPage(6).Hide() if not current_view: self.MainFrameNotebook.GetPage(3).Hide() - self.MainFrameNotebook.GetPage(5).Hide() + self.MainFrameNotebook.GetPage(6).Hide() if not current_trigger: - self.MainFrameNotebook.GetPage(4).Hide() + self.MainFrameNotebook.GetPage(5).Hide() + + if not current_procedure: + self.MainFrameNotebook.GetPage(routine_page_index).Hide() + + if not current_function: + self.MainFrameNotebook.GetPage(routine_page_index).Hide() return @@ -789,7 +823,7 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S elif isinstance(current, SQLDatabase): self.MainFrameNotebook.GetPage(1).Show() - self.MainFrameNotebook.GetPage(6).Show() + self.MainFrameNotebook.GetPage(7).Show() self.MainFrameNotebook.SetSelection(1) elif isinstance(current, SQLTable) or isinstance(current, SQLView): @@ -803,15 +837,28 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S if self.MainFrameNotebook.GetSelection() < 3: self.MainFrameNotebook.SetSelection(3) - self.MainFrameNotebook.GetPage(5).Show() self.MainFrameNotebook.GetPage(6).Show() + logger.debug("ui trace: toggle_panel records page shown (load disabled isolation)") + self.MainFrameNotebook.GetPage(7).Show() elif isinstance(current, SQLTrigger): - self.MainFrameNotebook.GetPage(4).Show() - self.MainFrameNotebook.GetPage(6).Show() - if self.MainFrameNotebook.GetSelection() < 4: + self.MainFrameNotebook.GetPage(5).Show() + self.MainFrameNotebook.GetPage(7).Show() + if self.MainFrameNotebook.GetSelection() < 5: self.MainFrameNotebook.SetSelection(3) + elif isinstance(current, SQLProcedure): + self.MainFrameNotebook.GetPage(routine_page_index).Show() + self.MainFrameNotebook.GetPage(7).Show() + if self.MainFrameNotebook.GetSelection() != routine_page_index: + self.MainFrameNotebook.SetSelection(routine_page_index) + + elif isinstance(current, SQLFunction): + self.MainFrameNotebook.GetPage(routine_page_index).Show() + self.MainFrameNotebook.GetPage(7).Show() + if self.MainFrameNotebook.GetSelection() != routine_page_index: + self.MainFrameNotebook.SetSelection(routine_page_index) + def _get_records_filters(self) -> str: return (self.sql_query_filters.GetSelectedText() or self.sql_query_filters.GetText()).strip() @@ -1007,13 +1054,26 @@ def _on_records_count_complete( total_rows: int, error: Optional[str], ) -> None: + logger.debug( + "ui trace: records._on_records_count_complete start request_id=%s expected_request_id=%s total_key=%s total_rows=%s error=%s", + request_id, + self._records_total_request_id, + total_key, + total_rows, + error, + ) if request_id != self._records_total_request_id: + logger.debug("ui trace: records._on_records_count_complete stale request ignored") return self._records_total_is_loading = False if error: table = CURRENT_TABLE.get_value() + logger.debug( + "ui trace: records._on_records_count_complete error branch table=%s", + getattr(table, "name", None) if table is not None else None, + ) if table is not None: self._update_records_label(table) self._set_records_paging_buttons(table) @@ -1021,10 +1081,17 @@ def _on_records_count_complete( table = CURRENT_TABLE.get_value() if table is None: + logger.debug("ui trace: records._on_records_count_complete skip table=None") return filters = self._get_records_filters() if self._build_records_total_key(table, filters) != total_key: + logger.debug( + "ui trace: records._on_records_count_complete key mismatch table=%s current_key=%s callback_key=%s", + table.name, + self._build_records_total_key(table, filters), + total_key, + ) return self._records_total_rows = max(int(total_rows), 0) @@ -1032,6 +1099,12 @@ def _on_records_count_complete( if self._records_offset > last_offset: self._records_offset = last_offset + logger.debug( + "ui trace: records._on_records_count_complete offset clamp reload table=%s offset=%s last_offset=%s", + table.name, + self._records_offset, + last_offset, + ) try: self._load_records_page() except Exception as ex: @@ -1041,6 +1114,12 @@ def _on_records_count_complete( try: self._update_records_label(table) self._set_records_paging_buttons(table) + logger.debug( + "ui trace: records._on_records_count_complete end table=%s total_rows=%s offset=%s", + table.name, + self._records_total_rows, + self._records_offset, + ) except Exception as ex: logger.error(f"Error updating records label: {ex}", exc_info=True) @@ -1053,25 +1132,40 @@ def _get_records_last_offset(self, limit: int) -> int: def _load_records_page(self): table = CURRENT_TABLE.get_value() - if table is None: + view = CURRENT_VIEW.get_value() if table is None else None + obj = table or view + if obj 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) + self._refresh_records_total_rows(obj, 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() + logger.debug( + "ui trace: records._load_records_page start obj=%s limit=%s offset=%s filters=%s", + obj.name, + limit, + self._records_offset, + filters, + ) + obj.records = ObservableList() + self.controller_list_table_records.load_model_for(obj) + self.controller_list_table_records.load_records_async( + obj=obj, + filters=filters, + limit=limit, + offset=self._records_offset, + ) - self._update_records_label(table) - self._set_records_paging_buttons(table) + self._update_records_label(obj) + self._set_records_paging_buttons(obj) + logger.debug("ui trace: records._load_records_page end obj=%s", obj.name) def _update_records_label(self, table: SQLTable): rows_count = self._get_loaded_records_count(table) @@ -1116,22 +1210,103 @@ def _set_records_paging_buttons(self, table: SQLTable): 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(): + if int(event.Selection) == 6: + if CURRENT_TABLE.get_value() or CURRENT_VIEW.get_value(): self._records_offset = 0 self._load_records_page() + def on_toggle_read_only(self, event): + session = CURRENT_SESSION.get_value() + if not session: + self.m_toggleBtn1.SetValue(False) + return + WRITE_OVERRIDE.set_value(self.m_toggleBtn1.GetValue()) + + def _on_write_override(self, active: bool): + session = CURRENT_SESSION.get_value() + if not session: + return + + if active: + # 1. Python-level: allow execute() to pass write queries + self._overridden_connection = session.connection + session.connection.read_only = False + # 2. DB-level: SET SESSION TRANSACTION READ WRITE (MySQL/MariaDB/PG) + # or reconnect without ?mode=ro (SQLite) + try: + session.context.set_write_mode(True) + except Exception as ex: + logger.warning("set_write_mode(True) failed: %s", ex) + self._override_remaining_seconds = 120 + self._write_override_timer.Start(1000) + self.m_toggleBtn1.SetValue(True) + self.m_toggleBtn1.SetLabel(_("Write Mode (2:00)")) + else: + self._cancel_write_override() + + def _on_write_override_tick(self, event): + self._override_remaining_seconds -= 1 + if self._override_remaining_seconds <= 0: + WRITE_OVERRIDE.set_value(False) + else: + mins = self._override_remaining_seconds // 60 + secs = self._override_remaining_seconds % 60 + self.m_toggleBtn1.SetLabel(_(f"Write Mode ({mins}:{secs:02d})")) + + def _cancel_write_override(self): + self._write_override_timer.Stop() + if self._overridden_connection: + # 1. Python-level restored first — SQLite connect() needs this to pick ?mode=ro URI + self._overridden_connection.read_only = True + try: + session = CURRENT_SESSION.get_value() + if session and session.connection is self._overridden_connection: + # 2. DB-level: SET SESSION TRANSACTION READ ONLY (MySQL/MariaDB/PG) + # or reconnect with ?mode=ro (SQLite) + session.context.set_write_mode(False) + except Exception as ex: + logger.warning("set_write_mode(False) failed: %s", ex) + self._overridden_connection = None + self._override_remaining_seconds = 0 + self.m_toggleBtn1.SetValue(False) + self.m_toggleBtn1.SetLabel(_("Read Only")) + def _on_current_session(self, session: Session): + if not wx.IsMainThread(): + logger.debug("ui trace: _on_current_session rescheduled to main thread") + wx.CallAfter(self._on_current_session, session) + return + from structures.session import Session - self.toggle_panel(session.connection if session else None) + # Cancel any active write override when switching connections + if self._overridden_connection and ( + not session or session.connection is not self._overridden_connection + ): + self._cancel_write_override() + WRITE_OVERRIDE.set_value(False) + + # Sync toggle button state to the incoming session + if session: + is_read_only = session.connection.read_only + self.m_toggleBtn1.Enable(is_read_only) + self.m_toggleBtn1.SetValue(not is_read_only) + self.m_toggleBtn1.SetLabel(_("Read Only") if is_read_only else _("Write Mode")) + else: + self.m_toggleBtn1.Enable(False) + self.m_toggleBtn1.SetValue(False) + self.m_toggleBtn1.SetLabel(_("Read Only")) + + self.toggle_panel(session if session else None) if session: - wx.CallAfter(self.status_bar.SetStatusText, f"{_('Connection')}: {session.name}", 0) + wx.CallAfter(self.status_bar.SetStatusText, f"{_('Connection')}: {session.name}", 1) - wx.CallAfter(self.status_bar.SetStatusText, f"{_('Version')}: {session.context.server_version}", 1) + wx.CallAfter(self.status_bar.SetStatusText, f"{_('Version')}: {session.context.server_version}", 2) - wx.CallAfter(self.status_bar.SetStatusText, f"{_('Uptime')}: {self._format_server_uptime(session.context.get_server_uptime())}", 2) + wx.CallAfter(self.status_bar.SetStatusText, f"{_('Uptime')}: {self._format_server_uptime(session.context.get_server_uptime())}", 3) + + session.context.set_connection_lost_handler(self._on_global_connection_lost) keywords = " ".join(k.lower() for k in session.context.KEYWORDS) @@ -1143,17 +1318,22 @@ def _on_current_session(self, session: Session): for stc_name in self.styled_text_ctrls_name: stc_ctrl = getattr(self, stc_name) + self._apply_sql_keywords_to_editor(stc_ctrl, keywords, colors_datatypes) - stc_ctrl.SetKeyWords(0, keywords) - - for idx, (color, words) in enumerate(colors_datatypes.items(), start=1): - stc_ctrl.SetKeyWords(idx, " ".join(sorted(words))) - - stc_ctrl.StyleSetForeground(wx.stc.STC_SQL_WORD + idx, wx.Colour(*color)) - - stc_ctrl.Colourise(0, -1) + for meta in self._query_page_meta.values(): + stc_ctrl = meta["editor"] + self._apply_sql_keywords_to_editor(stc_ctrl, keywords, colors_datatypes) def _on_current_database(self, database: SQLDatabase): + if not wx.IsMainThread(): + logger.debug("ui trace: _on_current_database rescheduled to main thread") + wx.CallAfter(self._on_current_database, database) + return + + logger.debug( + "ui trace: _on_current_database database=%s", + getattr(database, "name", None) if database is not None else None, + ) self.toggle_panel(database) self._update_database_action_buttons() @@ -1176,6 +1356,17 @@ def _on_current_database(self, database: SQLDatabase): self.convert_data_collation.Enable(False) self.table_row_format.Enable(False) + def on_add_database(self, event): + session = CURRENT_SESSION.get_value() + if session is None: + return + + new_db = session.context.build_empty_database() + CURRENT_DATABASE.set_value(new_db) + self._toggle_panel(1, True) + self.MainFrameNotebook.SetSelection(1) + self.database_name.SetFocus() + def on_apply_database(self, event: wx.Event): database = CURRENT_DATABASE.get_value() session = CURRENT_SESSION.get_value() @@ -1183,15 +1374,56 @@ def on_apply_database(self, event: wx.Event): if database is None or session is None: return + logger.debug( + "ui trace: on_apply_database before_sync db_id=%s db_name=%r input_name=%r model_name=%r", + getattr(database, "id", None), + getattr(database, "name", None), + self.database_name.GetValue(), + self.controller_database_options.model.name.get_value(), + ) + + self.controller_database_options.model.sync() + database = CURRENT_DATABASE.get_value() + if database is None: + return + + logger.debug( + "ui trace: on_apply_database after_sync db_id=%s db_name=%r model_name=%r", + getattr(database, "id", None), + getattr(database, "name", None), + self.controller_database_options.model.name.get_value(), + ) + try: + db_name = database.name + + logger.debug( + "ui trace: on_apply_database before_save db_id=%s db_name=%r", + getattr(database, "id", None), + db_name, + ) + database.save() - session.context.databases.refresh() - database = next( - (d for d in session.context.databases.get_value() if d.id == database.id), - None, + logger.debug( + "ui trace: on_apply_database after_save db_id=%s db_name=%r", + getattr(database, "id", None), + getattr(database, "name", None), ) + session.context.databases.refresh() + + if database.is_new: + database = next( + (d for d in session.context.databases.get_value() if d.name == db_name), + None, + ) + else: + database = next( + (d for d in session.context.databases.get_value() if d.id == database.id), + None, + ) + if database is not None: CURRENT_DATABASE.set_value(None).set_value(database) session.context.set_database(database) @@ -1293,29 +1525,171 @@ def on_delete_database(self, event: wx.Event): # VIEW def _on_current_view(self, current: SQLView): + logger.debug( + "ui trace: _on_current_view view=%s is_new=%s", + getattr(current, "name", None) if current is not None else None, + getattr(current, "is_new", None) if current is not None else None, + ) self.toggle_panel(current) - self.btn_delete_view.Enable(current is not None) + if current and not current.is_new: + 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(current) + self._set_records_paging_buttons(current) + if self.MainFrameNotebook.GetSelection() == 6: + self._load_records_page() + + can_act = current is not None and not current.is_new + self.btn_delete_view.Enable(can_act) + self.m_toolBar5.EnableTool(self.tool_clone_view.GetId(), can_act) + + if current: + self._set_record_write_tools_enabled(False) + + def on_clone_view(self, event): + view = CURRENT_VIEW.get_value() + session = CURRENT_SESSION.get_value() + database = CURRENT_DATABASE.get_value() + if not view or not session or not database: + return + clone = session.context.build_empty_view(database, name=f"{view.name}_copy", statement=view.statement) + CURRENT_VIEW.set_value(None) + CURRENT_VIEW.set_value(clone) + self._toggle_panel(3, True) + self.MainFrameNotebook.SetSelection(3) + + def on_insert_view(self, event): + session = CURRENT_SESSION.get_value() + database = CURRENT_DATABASE.get_value() + if not session or not database: + return + CURRENT_VIEW.set_value(None) + new_view = session.context.build_empty_view(database) + CURRENT_VIEW.set_value(new_view) + self._toggle_panel(3, True) + self.MainFrameNotebook.SetSelection(3) # TRIGGER def _on_current_trigger(self, current: SQLTrigger): + logger.debug( + "ui trace: _on_current_trigger trigger=%s", + getattr(current, "name", None) if current is not None else None, + ) + self.toggle_panel(current) + + # PROCEDURE + def _on_current_procedure(self, current: SQLProcedure): + logger.debug( + "ui trace: _on_current_procedure procedure=%s is_new=%s", + getattr(current, "name", None) if current is not None else None, + getattr(current, "is_new", None) if current is not None else None, + ) + self.toggle_panel(current) + + def on_insert_procedure(self): + session = CURRENT_SESSION.get_value() + database = CURRENT_DATABASE.get_value() + if not session or not database: + return + CURRENT_PROCEDURE.set_value(None) + new_proc = session.context.build_empty_procedure(database) + CURRENT_PROCEDURE.set_value(new_proc) + routine_page_index = self.MainFrameNotebook.FindPage(self.panel_routine) + self._toggle_panel(routine_page_index, True) + self.MainFrameNotebook.SetSelection(routine_page_index) + + # FUNCTION + def _on_current_function(self, current: SQLFunction): + logger.debug( + "ui trace: _on_current_function function=%s is_new=%s", + getattr(current, "name", None) if current is not None else None, + getattr(current, "is_new", None) if current is not None else None, + ) self.toggle_panel(current) + def on_routine_save(self, event): + self.controller_routine_editor.do_save() + + def on_routine_delete(self, event): + self.controller_routine_editor.do_delete() + + def on_routine_cancel(self, event): + self.controller_routine_editor.do_cancel() + + def on_routine_parameters_insert(self, event): + self.controller_routine_editor.on_parameter_insert(event) + + def on_routine_parameters_delete(self, event): + self.controller_routine_editor.on_parameter_remove(event) + + def on_routine_parameters_clear(self, event): + self.controller_routine_editor.on_parameter_clear(event) + + def on_insert_function(self): + session = CURRENT_SESSION.get_value() + database = CURRENT_DATABASE.get_value() + if not session or not database: + return + CURRENT_FUNCTION.set_value(None) + new_func = session.context.build_empty_function(database) + CURRENT_FUNCTION.set_value(new_func) + routine_page_index = self.MainFrameNotebook.FindPage(self.panel_routine) + self._toggle_panel(routine_page_index, True) + self.MainFrameNotebook.SetSelection(routine_page_index) + + def on_clone_procedure(self): + procedure = CURRENT_PROCEDURE.get_value() + session = CURRENT_SESSION.get_value() + database = CURRENT_DATABASE.get_value() + if not procedure or not session or not database: + return + clone = session.context.build_empty_procedure( + database, + name=f"{procedure.name}_copy", + parameters=getattr(procedure, "parameters", ""), + statement=getattr(procedure, "statement", ""), + ) + CURRENT_PROCEDURE.set_value(None) + CURRENT_PROCEDURE.set_value(clone) + routine_page_index = self.MainFrameNotebook.FindPage(self.panel_routine) + self._toggle_panel(routine_page_index, True) + self.MainFrameNotebook.SetSelection(routine_page_index) + # TABLE def _on_current_table(self, table: SQLTable): + logger.debug( + "ui trace: _on_current_table table=%s", + getattr(table, "name", None) if table is not None else None, + ) if NEW_TABLE.get_value() and not self.on_cancel_table(None): return if table: + logger.debug( + "ui trace: _on_current_table reset records state table=%s selected_page=%s", + table.name, + self.MainFrameNotebook.GetSelection(), + ) 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.sql_query_filters.ClearAll() self._update_records_label(table) self.toggle_panel(table) self._set_records_paging_buttons(table) + self._set_record_write_tools_enabled(True) + logger.debug( + "ui trace: _on_current_table panel updated table=%s selected_page=%s", + table.name, + self.MainFrameNotebook.GetSelection(), + ) CURRENT_COLUMN.set_value(None) CURRENT_RECORDS.set_value([]) @@ -1331,11 +1705,24 @@ def _on_current_table(self, table: SQLTable): table.raw_create() ) - if self.MainFrameNotebook.GetSelection() == 5: + if self.MainFrameNotebook.GetSelection() == 6: + logger.debug( + "ui trace: _on_current_table triggering records page load table=%s", + table.name, + ) self._load_records_page() + else: + logger.debug( + "ui trace: _on_current_table skip records page load table=%s selected_page=%s", + table.name, + self.MainFrameNotebook.GetSelection(), + ) + + self.tool_clone_table.Enable(table is not None) + self.tool_delete_table.Enable(table is not None) - self.btn_clone_table.Enable(table is not None) - self.btn_delete_table.Enable(table is not None) + def _on_new_database(self, database) -> None: + self._update_database_action_buttons() def _on_new_table(self, table: SQLTable): self.btn_apply_table.Enable(bool(table is not None and table.is_valid)) @@ -1347,7 +1734,7 @@ def _on_new_table(self, table: SQLTable): ) # def _on_selected_table(self, table : SQLTable): - # self.btn_delete_table.Enable(table is not None) + # self.tool_delete_table.Enable(table is not None) def on_insert_table(self, event): session = CURRENT_SESSION.get_value() @@ -1503,7 +1890,7 @@ def on_move_down_column(self, event: wx.Event): # INDEXES def _on_current_index(self, index: SQLIndex): - self.btn_delete_index.Enable(index is not None) + self.m_toolBar12.EnableTool(self.m_tool43.GetId(), index is not None) def on_delete_index(self, event): self.controller_list_table_index.on_index_delete() @@ -1513,7 +1900,7 @@ def on_clear_index(self, event): # FOREIGN KEYS def _on_current_foreign_key(self, foreign_key: SQLForeignKey): - self.btn_delete_foreign_key.Enable(foreign_key is not None) + self.m_toolBar121.EnableTool(self.m_tool431.GetId(), foreign_key is not None) def on_insert_foreign_key(self, event: wx.Event): self.controller_list_table_foreign_key.on_foreign_key_insert(event) @@ -1526,12 +1913,26 @@ def on_clear_foreign_key(self, event: wx.Event): # RECORDS def _on_auto_apply(self, value: bool): + if CURRENT_VIEW.get_value() is not None: + return 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 _set_record_write_tools_enabled(self, enabled: bool): + self.m_toolBar3.EnableTool(self.tool_insert_record.GetId(), enabled) + self.m_toolBar3.EnableTool(self.tool_duplicate_record.GetId(), False) + self.m_toolBar3.EnableTool(self.tool_delete_record.GetId(), False) + self.chb_auto_apply.Enable(enabled) + if enabled: + auto_apply = self.chb_auto_apply.GetValue() + self.m_toolBar3.EnableTool(self.tool_apply_record.GetId(), not auto_apply) + self.m_toolBar3.EnableTool(self.tool_cancel_record.GetId(), not auto_apply) + else: + self.m_toolBar3.EnableTool(self.tool_apply_record.GetId(), False) + self.m_toolBar3.EnableTool(self.tool_cancel_record.GetId(), False) + self.m_toolBar3.Refresh() + def _initialize_record_toolbar_states(self): """Initialize toolbar states to ensure proper default behavior.""" # Initially disable duplicate and delete tools (no selection) @@ -1558,9 +1959,9 @@ def on_collapsible_pane_changed(self, event): event.Skip() def _on_current_records(self, records: list[SQLRecord]): - # 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) + if CURRENT_VIEW.get_value() is None: + 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() @@ -1574,6 +1975,22 @@ def on_insert_record(self, event): def on_refresh_records(self, event): self.controller_list_table_records.do_refresh_records() + def _on_filters_key_down(self, event: wx.KeyEvent): + if event.ControlDown() and event.GetKeyCode() in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER): + self._load_records_page() + else: + event.Skip() + + def _on_f5_refresh(self, event): + logger.debug("F5 refresh triggered, page=%s", self.MainFrameNotebook.GetSelection()) + with Loader.cursor_wait(): + self.controller_tree_connections.refresh_current_database() + page = self.MainFrameNotebook.GetSelection() + if page == 2: + self.controller_list_table_columns.do_refresh_columns() + elif page == 6: + self.controller_list_table_records.do_refresh_records() + def on_duplicate_record(self, event): self.controller_list_table_records.do_duplicate_record() @@ -1595,8 +2012,7 @@ def on_prev_records(self, event): self._load_records_page() def on_next_records(self, event): - table = CURRENT_TABLE.get_value() - if table is None: + if CURRENT_TABLE.get_value() is None and CURRENT_VIEW.get_value() is None: return self._records_offset = min( @@ -1606,8 +2022,7 @@ def on_next_records(self, event): self._load_records_page() def on_last_records(self, event): - table = CURRENT_TABLE.get_value() - if table is None: + if CURRENT_TABLE.get_value() is None and CURRENT_VIEW.get_value() is None: return self._records_offset = self._get_records_last_offset(self._records_limit) @@ -1625,6 +2040,13 @@ def on_apply_filters(self, event): self._records_offset = 0 self._load_records_page() + def on_clear_filters(self, event): + self.sql_query_filters.ClearAll() + self.m_collapsiblePane1.Collapse(True) + self.panel_records.Layout() + self._records_offset = 0 + self._load_records_page() + def on_new_query(self, event): self._create_new_query_page() @@ -1669,5 +2091,52 @@ def on_cancel_query_execution(self, event): self.controller_query_records = controller controller.cancel_execution(event) + def _on_query_connection_lost(self, session: Session, error: str) -> None: + self._on_global_connection_lost(session, error) + + def _on_global_connection_lost(self, session: Session, error: str) -> None: + if not wx.IsMainThread(): + wx.CallAfter(self._on_global_connection_lost, session, error) + return + + choice = wx.MessageDialog( + None, + message=_("Database connection lost. Do you want to reconnect?"), + caption=_("Connection lost"), + style=wx.YES_NO | wx.ICON_QUESTION, + ).ShowModal() + + if choice == wx.ID_YES: + try: + session.connect() + wx.MessageBox( + _("Connection restored successfully."), + _("Connection restored"), + wx.OK | wx.ICON_INFORMATION, + ) + except Exception as ex: + logger.error("Reconnection failed: %s", ex, exc_info=True) + wx.MessageBox( + _("Could not reconnect: {error}").format(error=str(ex)), + _("Reconnection failed"), + wx.OK | wx.ICON_ERROR, + ) + self._remove_session_from_explorer(session) + else: + self._remove_session_from_explorer(session) + + def _remove_session_from_explorer(self, session: Session) -> None: + try: + SESSIONS_LIST.remove(session) + except ValueError: + pass + + if CURRENT_SESSION.get_value() is session: + CURRENT_SESSION.set_value(None) + CURRENT_CONNECTION.set_value(None) + CURRENT_DATABASE.set_value(None) + + self.controller_tree_connections.populate_tree() + # def on_clear_record(self, event): # self.controller_list_table_records.on_row_clear() diff --git a/windows/main/database/list.py b/windows/main/database/list.py index 53fabd5..99557af 100644 --- a/windows/main/database/list.py +++ b/windows/main/database/list.py @@ -7,10 +7,11 @@ from helpers import bytes_to_human from helpers.dataview import BaseObservableDataViewListModel, ColumnField +from helpers.logger import logger -from structures.engines.database import SQLTable, SQLDatabase +from structures.engines.database import SQLTable, SQLDatabase, SQLView, SQLProcedure, SQLFunction, SQLTrigger, SQLEvent -from windows.main import CURRENT_DATABASE, CURRENT_TABLE, CURRENT_SESSION +from windows.main import CURRENT_DATABASE, CURRENT_TABLE, CURRENT_SESSION, CURRENT_VIEW, CURRENT_PROCEDURE, CURRENT_FUNCTION, CURRENT_TRIGGER, CURRENT_EVENT class ModelDatabaseTable(BaseObservableDataViewListModel): @@ -36,14 +37,43 @@ def GetValueByRow(self, row, col): return self.MAP_COLUMN_FIELDS[col].get_value(table) + def Compare(self, item1, item2, col, ascending): + table1: SQLTable = self.get_data_by_item(item1) + table2: SQLTable = self.get_data_by_item(item2) + + if col == 1: + rows1 = int(table1.total_rows) + rows2 = int(table2.total_rows) + if rows1 == rows2: + return 0 + if ascending: + return -1 if rows1 < rows2 else 1 + return -1 if rows1 > rows2 else 1 + + if col == 2: + size1 = int(table1.total_bytes) + size2 = int(table2.total_bytes) + if size1 == size2: + return 0 + if ascending: + return -1 if size1 < size2 else 1 + return -1 if size1 > size2 else 1 + + value1 = self.MAP_COLUMN_FIELDS[col].get_value(table1) + value2 = self.MAP_COLUMN_FIELDS[col].get_value(table2) + if value1 == value2: + return 0 + if ascending: + return -1 if value1 < value2 else 1 + return -1 if value1 > value2 else 1 -class ListDatabaseTable: - _app = wx.GetApp() +class ListDatabaseTable: def __init__(self, list_ctrl_database_tables: wx.dataview.DataViewCtrl): + self._app = wx.GetApp() + self.list_ctrl_database_tables = list_ctrl_database_tables self.list_ctrl_database_tables.Bind(wx.dataview.EVT_DATAVIEW_ITEM_ACTIVATED, self._on_item_activated) - self.list_ctrl_database_tables.Bind(wx.dataview.EVT_DATAVIEW_SELECTION_CHANGED, self._on_selection_changed) self.model = ModelDatabaseTable(7) self.list_ctrl_database_tables.AssociateModel(self.model) @@ -52,6 +82,11 @@ def __init__(self, list_ctrl_database_tables: wx.dataview.DataViewCtrl): CURRENT_TABLE.subscribe(self._select_table) def _load_database(self, database: SQLDatabase): + if not wx.IsMainThread(): + logger.debug("ui trace: list._load_database rescheduled to main thread") + wx.CallAfter(self._load_database, database) + return + if not database: return @@ -87,6 +122,11 @@ def _load_database(self, database: SQLDatabase): ) def _select_table(self, table: SQLTable): + if not wx.IsMainThread(): + logger.debug("ui trace: list._select_table rescheduled to main thread") + wx.CallAfter(self._select_table, table) + return + if table: database = CURRENT_DATABASE.get_value() if index := database.tables.index(table): @@ -98,6 +138,7 @@ def _on_selection_changed(self, event: wx.dataview.DataViewEvent): return if table := self.model.get_data_by_item(item): + CURRENT_VIEW.set_value(None) CURRENT_TABLE.set_value(table.copy()) def _on_item_activated(self, event: wx.dataview.DataViewEvent): @@ -106,4 +147,410 @@ def _on_item_activated(self, event: wx.dataview.DataViewEvent): return if table := self.model.get_data_by_item(item): + CURRENT_VIEW.set_value(None) CURRENT_TABLE.set_value(table.copy()) + + +def _truncate_statement(value: str) -> str: + if not value: + return "" + value = " ".join(value.split()) + return value[:120] + "..." if len(value) > 120 else value + + +class ModelDatabaseView(BaseObservableDataViewListModel): + MAP_COLUMN_FIELDS = { + 0: ColumnField("name", str), + 1: ColumnField("statement", _truncate_statement), + } + + def __init__(self): + super().__init__(2) + + def GetValueByRow(self, row, col): + if not len(self.data): + return None + view: SQLView = self.get_data_by_row(row) + return self.MAP_COLUMN_FIELDS[col].get_value(view) + + +class ListDatabaseView: + def __init__(self, list_ctrl: wx.dataview.DataViewCtrl): + self._app = wx.GetApp() + + self.list_ctrl = list_ctrl + self.list_ctrl.Bind(wx.dataview.EVT_DATAVIEW_ITEM_ACTIVATED, self._on_item_activated) + self.list_ctrl.Bind(wx.dataview.EVT_DATAVIEW_SELECTION_CHANGED, self._on_selection_changed) + + self.model = ModelDatabaseView() + self.list_ctrl.AssociateModel(self.model) + + CURRENT_DATABASE.subscribe(self._load_database) + CURRENT_VIEW.subscribe(self._select_view) + + def _load_database(self, database: SQLDatabase): + if not wx.IsMainThread(): + wx.CallAfter(self._load_database, database) + return + + if not database: + return + + try: + self.model.set_observable(database.views) + except Exception as ex: + logger.error(str(ex), exc_info=True) + + def _select_view(self, view: SQLView): + if not wx.IsMainThread(): + wx.CallAfter(self._select_view, view) + return + + if not view or view.is_new: + return + + database = CURRENT_DATABASE.get_value() + if not database: + return + + views = database.views.get_value() + index = next((i for i, v in enumerate(views) if v.id == view.id), None) + if index is not None: + self.list_ctrl.Select(self.model.GetItem(index)) + + def _on_selection_changed(self, event: wx.dataview.DataViewEvent): + item = event.GetItem() + if not item.IsOk(): + return + + if view := self.model.get_data_by_item(item): + CURRENT_TABLE.set_value(None) + CURRENT_VIEW.set_value(view.copy()) + + def _on_item_activated(self, event: wx.dataview.DataViewEvent): + item = event.GetItem() + if not item.IsOk(): + return + + if view := self.model.get_data_by_item(item): + CURRENT_TABLE.set_value(None) + CURRENT_VIEW.set_value(view.copy()) + + +class ModelDatabaseProcedure(BaseObservableDataViewListModel): + MAP_COLUMN_FIELDS = { + 0: ColumnField("name", str), + 1: ColumnField("parameters", str), + } + + def __init__(self): + super().__init__(2) + + def GetValueByRow(self, row, col): + if not len(self.data): + return None + procedure: SQLProcedure = self.get_data_by_row(row) + return self.MAP_COLUMN_FIELDS[col].get_value(procedure) + + +class ListDatabaseProcedure: + def __init__(self, list_ctrl: wx.dataview.DataViewCtrl): + self.list_ctrl = list_ctrl + self.list_ctrl.Bind(wx.dataview.EVT_DATAVIEW_ITEM_ACTIVATED, self._on_item_activated) + self.list_ctrl.Bind(wx.dataview.EVT_DATAVIEW_SELECTION_CHANGED, self._on_selection_changed) + + self.model = ModelDatabaseProcedure() + self.list_ctrl.AssociateModel(self.model) + + CURRENT_DATABASE.subscribe(self._load_database) + CURRENT_PROCEDURE.subscribe(self._select_procedure) + + def _load_database(self, database: SQLDatabase): + if not wx.IsMainThread(): + wx.CallAfter(self._load_database, database) + return + + if not database: + return + + if not hasattr(database, "procedures"): + return + + try: + self.model.set_observable(database.procedures) + except Exception as ex: + logger.error(str(ex), exc_info=True) + + def _select_procedure(self, procedure: SQLProcedure): + if not wx.IsMainThread(): + wx.CallAfter(self._select_procedure, procedure) + return + + if not procedure or procedure.is_new: + return + + database = CURRENT_DATABASE.get_value() + if not database or not hasattr(database, "procedures"): + return + + procedures = database.procedures.get_value() + index = next((i for i, p in enumerate(procedures) if p.id == procedure.id), None) + if index is not None: + self.list_ctrl.Select(self.model.GetItem(index)) + + def _on_selection_changed(self, event: wx.dataview.DataViewEvent): + item = event.GetItem() + if not item.IsOk(): + return + + if procedure := self.model.get_data_by_item(item): + CURRENT_TABLE.set_value(None) + CURRENT_VIEW.set_value(None) + CURRENT_PROCEDURE.set_value(procedure.copy()) + + def _on_item_activated(self, event: wx.dataview.DataViewEvent): + item = event.GetItem() + if not item.IsOk(): + return + + if procedure := self.model.get_data_by_item(item): + CURRENT_TABLE.set_value(None) + CURRENT_VIEW.set_value(None) + CURRENT_PROCEDURE.set_value(procedure.copy()) + + +class ModelDatabaseFunction(BaseObservableDataViewListModel): + MAP_COLUMN_FIELDS = { + 0: ColumnField("name", str), + 1: ColumnField("statement", _truncate_statement), + } + + def __init__(self): + super().__init__(2) + + def GetValueByRow(self, row, col): + if not len(self.data): + return None + function: SQLFunction = self.get_data_by_row(row) + return self.MAP_COLUMN_FIELDS[col].get_value(function) + + +class ListDatabaseFunction: + def __init__(self, list_ctrl: wx.dataview.DataViewCtrl): + self.list_ctrl = list_ctrl + self.list_ctrl.Bind(wx.dataview.EVT_DATAVIEW_ITEM_ACTIVATED, self._on_item_activated) + self.list_ctrl.Bind(wx.dataview.EVT_DATAVIEW_SELECTION_CHANGED, self._on_selection_changed) + + self.model = ModelDatabaseFunction() + self.list_ctrl.AssociateModel(self.model) + + CURRENT_DATABASE.subscribe(self._load_database) + CURRENT_FUNCTION.subscribe(self._select_function) + + def _load_database(self, database: SQLDatabase): + if not wx.IsMainThread(): + wx.CallAfter(self._load_database, database) + return + + if not database or not hasattr(database, "functions"): + return + + try: + self.model.set_observable(database.functions) + except Exception as ex: + logger.error(str(ex), exc_info=True) + + def _select_function(self, function: SQLFunction): + if not wx.IsMainThread(): + wx.CallAfter(self._select_function, function) + return + + if not function or function.is_new: + return + + database = CURRENT_DATABASE.get_value() + if not database or not hasattr(database, "functions"): + return + + functions = database.functions.get_value() + index = next((i for i, f in enumerate(functions) if f.id == function.id), None) + if index is not None: + self.list_ctrl.Select(self.model.GetItem(index)) + + def _on_selection_changed(self, event: wx.dataview.DataViewEvent): + item = event.GetItem() + if not item.IsOk(): + return + + if function := self.model.get_data_by_item(item): + CURRENT_TABLE.set_value(None) + CURRENT_VIEW.set_value(None) + CURRENT_FUNCTION.set_value(function.copy()) + + def _on_item_activated(self, event: wx.dataview.DataViewEvent): + item = event.GetItem() + if not item.IsOk(): + return + + if function := self.model.get_data_by_item(item): + CURRENT_TABLE.set_value(None) + CURRENT_VIEW.set_value(None) + CURRENT_FUNCTION.set_value(function.copy()) + + +class ModelDatabaseTrigger(BaseObservableDataViewListModel): + MAP_COLUMN_FIELDS = { + 0: ColumnField("name", str), + 1: ColumnField("statement", _truncate_statement), + } + + def __init__(self): + super().__init__(2) + + def GetValueByRow(self, row, col): + if not len(self.data): + return None + trigger: SQLTrigger = self.get_data_by_row(row) + return self.MAP_COLUMN_FIELDS[col].get_value(trigger) + + +class ListDatabaseTrigger: + def __init__(self, list_ctrl: wx.dataview.DataViewCtrl): + self.list_ctrl = list_ctrl + self.list_ctrl.Bind(wx.dataview.EVT_DATAVIEW_ITEM_ACTIVATED, self._on_item_activated) + self.list_ctrl.Bind(wx.dataview.EVT_DATAVIEW_SELECTION_CHANGED, self._on_selection_changed) + + self.model = ModelDatabaseTrigger() + self.list_ctrl.AssociateModel(self.model) + + CURRENT_DATABASE.subscribe(self._load_database) + CURRENT_TRIGGER.subscribe(self._select_trigger) + + def _load_database(self, database: SQLDatabase): + if not wx.IsMainThread(): + wx.CallAfter(self._load_database, database) + return + + if not database or not hasattr(database, "triggers"): + return + + try: + self.model.set_observable(database.triggers) + except Exception as ex: + logger.error(str(ex), exc_info=True) + + def _select_trigger(self, trigger: SQLTrigger): + if not wx.IsMainThread(): + wx.CallAfter(self._select_trigger, trigger) + return + + if not trigger or trigger.is_new: + return + + database = CURRENT_DATABASE.get_value() + if not database or not hasattr(database, "triggers"): + return + + triggers = database.triggers.get_value() + index = next((i for i, t in enumerate(triggers) if t.id == trigger.id), None) + if index is not None: + self.list_ctrl.Select(self.model.GetItem(index)) + + def _on_selection_changed(self, event: wx.dataview.DataViewEvent): + item = event.GetItem() + if not item.IsOk(): + return + + if trigger := self.model.get_data_by_item(item): + CURRENT_TABLE.set_value(None) + CURRENT_VIEW.set_value(None) + CURRENT_TRIGGER.set_value(trigger.copy()) + + def _on_item_activated(self, event: wx.dataview.DataViewEvent): + item = event.GetItem() + if not item.IsOk(): + return + + if trigger := self.model.get_data_by_item(item): + CURRENT_TABLE.set_value(None) + CURRENT_VIEW.set_value(None) + CURRENT_TRIGGER.set_value(trigger.copy()) + + +class ModelDatabaseEvent(BaseObservableDataViewListModel): + MAP_COLUMN_FIELDS = { + 0: ColumnField("name", str), + 1: ColumnField("statement", _truncate_statement), + } + + def __init__(self): + super().__init__(2) + + def GetValueByRow(self, row, col): + if not len(self.data): + return None + db_event: SQLEvent = self.get_data_by_row(row) + return self.MAP_COLUMN_FIELDS[col].get_value(db_event) + + +class ListDatabaseEvent: + def __init__(self, list_ctrl: wx.dataview.DataViewCtrl): + self.list_ctrl = list_ctrl + self.list_ctrl.Bind(wx.dataview.EVT_DATAVIEW_ITEM_ACTIVATED, self._on_item_activated) + self.list_ctrl.Bind(wx.dataview.EVT_DATAVIEW_SELECTION_CHANGED, self._on_selection_changed) + + self.model = ModelDatabaseEvent() + self.list_ctrl.AssociateModel(self.model) + + CURRENT_DATABASE.subscribe(self._load_database) + CURRENT_EVENT.subscribe(self._select_event) + + def _load_database(self, database: SQLDatabase): + if not wx.IsMainThread(): + wx.CallAfter(self._load_database, database) + return + + if not database or not hasattr(database, "events"): + return + + try: + self.model.set_observable(database.events) + except Exception as ex: + logger.error(str(ex), exc_info=True) + + def _select_event(self, event: SQLEvent): + if not wx.IsMainThread(): + wx.CallAfter(self._select_event, event) + return + + if not event or event.is_new: + return + + database = CURRENT_DATABASE.get_value() + if not database or not hasattr(database, "events"): + return + + events = database.events.get_value() + index = next((i for i, e in enumerate(events) if e.id == event.id), None) + if index is not None: + self.list_ctrl.Select(self.model.GetItem(index)) + + def _on_selection_changed(self, event: wx.dataview.DataViewEvent): + item = event.GetItem() + if not item.IsOk(): + return + + if db_event := self.model.get_data_by_item(item): + CURRENT_TABLE.set_value(None) + CURRENT_VIEW.set_value(None) + CURRENT_EVENT.set_value(db_event.copy()) + + def _on_item_activated(self, event: wx.dataview.DataViewEvent): + item = event.GetItem() + if not item.IsOk(): + return + + if db_event := self.model.get_data_by_item(item): + CURRENT_TABLE.set_value(None) + CURRENT_VIEW.set_value(None) + CURRENT_EVENT.set_value(db_event.copy()) diff --git a/windows/main/database/options.py b/windows/main/database/options.py index e3197c5..3b6c369 100644 --- a/windows/main/database/options.py +++ b/windows/main/database/options.py @@ -2,146 +2,134 @@ import wx -from helpers.bindings import AbstractModel -from helpers.observables import CallbackEvent, Observable, debounce +from helpers.bindings import AbstractModel, wx_call_after_debounce +from helpers.logger import logger +from helpers.observables import Observable, debounce from structures.connection import ConnectionEngine from windows.main import CURRENT_DATABASE, CURRENT_SESSION +from windows.state import NEW_DATABASE -class EditDatabaseOptionsModel(AbstractModel): +class EditDatabaseModel(AbstractModel): def __init__(self): - self.database_name = Observable() - self.database_collation = Observable() - self.database_encryption = Observable(False) - self.database_read_only = Observable(False) - self.database_tablespace = Observable() - self.database_connection_limit = Observable(0) - self.database_password = Observable() - self.database_profile = Observable() - self.database_default_tablespace = Observable() - self.database_temporary_tablespace = Observable() - self.database_quota = Observable() - self.database_unlimited_quota = Observable(False) - self.database_account_status = Observable() - self.database_password_expire = Observable(False) - - debounce( - self.database_name, - self.database_collation, - self.database_encryption, - self.database_read_only, - self.database_tablespace, - self.database_connection_limit, - self.database_password, - self.database_profile, - self.database_default_tablespace, - self.database_temporary_tablespace, - self.database_quota, - self.database_unlimited_quota, - self.database_account_status, - self.database_password_expire, - callback=self._update_database, + super().__init__() + + self.name = Observable() + self.collation = Observable() + self.encryption = Observable(False) + self.tablespace = Observable() + self.connection_limit = Observable(0) + + wx_call_after_debounce( + self.name, + self.collation, + self.encryption, + self.tablespace, + self.connection_limit, + callback=self.update_database, ) CURRENT_DATABASE.subscribe(self._load_database) - @staticmethod - def _first_attr(source, names: list[str], default=None): - if source is None: - return default - - for name in names: - if hasattr(source, name): - value = getattr(source, name) - if value is not None: - return value - - return default - @staticmethod def _encryption_to_bool(value) -> bool: if isinstance(value, bool): return value - if value is None: return False - - return str(value).strip().upper() in ["Y", "YES", "TRUE", "1", "ON"] + return str(value).strip().upper() in ("Y", "YES", "TRUE", "1", "ON") def _load_database(self, database) -> None: - self.database_name.set_initial(self._first_attr(database, ["name"], "")) - self.database_collation.set_initial( - self._first_attr(database, ["default_collation", "collation", "collation_name"], "") - ) + NEW_DATABASE.set_value(None) - self.database_encryption.set_initial( - self._encryption_to_bool(self._first_attr(database, ["encryption"], None)) - ) - self.database_read_only.set_initial(bool(self._first_attr(database, ["read_only", "is_read_only"], False))) - self.database_tablespace.set_initial(self._first_attr(database, ["tablespace", "default_tablespace"], "")) - self.database_connection_limit.set_initial(int(self._first_attr(database, ["connection_limit"], 0) or 0)) - self.database_password.set_initial(self._first_attr(database, ["password"], "")) - self.database_profile.set_initial(self._first_attr(database, ["profile"], "")) - self.database_default_tablespace.set_initial(self._first_attr(database, ["default_tablespace"], "")) - self.database_temporary_tablespace.set_initial(self._first_attr(database, ["temporary_tablespace"], "")) - self.database_quota.set_initial(self._first_attr(database, ["quota"], "")) - self.database_unlimited_quota.set_initial(bool(self._first_attr(database, ["unlimited_quota"], False))) - self.database_account_status.set_initial(self._first_attr(database, ["account_status"], "")) - self.database_password_expire.set_initial(bool(self._first_attr(database, ["password_expire"], False))) - - def _update_database(self, *args) -> None: - if not any(arg is not None for arg in args): + if database is None: return - database = CURRENT_DATABASE.get_value() + self.name.set_initial(database.name or "") + self.collation.set_initial(getattr(database, "default_collation", None) or "") + self.encryption.set_initial(self._encryption_to_bool(getattr(database, "encryption", None))) + self.tablespace.set_initial(getattr(database, "tablespace", None) or "") + self.connection_limit.set_initial(int(getattr(database, "connection_limit", None) or 0)) + + def update_database(self, *args) -> None: + if not any(args): + return + + database = NEW_DATABASE.get_value() or CURRENT_DATABASE.get_value() if database is None: return session = CURRENT_SESSION.get_value() engine = session.engine if session else None - encryption_value = self.database_encryption.get_value() - if engine in [ConnectionEngine.MYSQL, ConnectionEngine.MARIADB]: - encryption_value = "Y" if bool(encryption_value) else "N" - - if hasattr(database, "name"): - database.name = self.database_name.get_value() - - mapping = { - "default_collation": self.database_collation.get_value(), - "collation": self.database_collation.get_value(), - "collation_name": self.database_collation.get_value(), - "encryption": encryption_value, - "read_only": self.database_read_only.get_value(), - "is_read_only": self.database_read_only.get_value(), - "tablespace": self.database_tablespace.get_value(), - "default_tablespace": self.database_default_tablespace.get_value(), - "temporary_tablespace": self.database_temporary_tablespace.get_value(), - "connection_limit": self.database_connection_limit.get_value(), - "password": self.database_password.get_value(), - "profile": self.database_profile.get_value(), - "quota": self.database_quota.get_value(), - "unlimited_quota": self.database_unlimited_quota.get_value(), - "account_status": self.database_account_status.get_value(), - "password_expire": self.database_password_expire.get_value(), - } + new_name = self.name.get_value() or "" + new_collation = self.collation.get_value() or None + new_tablespace = self.tablespace.get_value() or None + new_connection_limit = int(self.connection_limit.get_value() or 0) + + is_mysql = engine == ConnectionEngine.MYSQL + new_encryption = ("Y" if self.encryption.get_value() else "N") if is_mysql else None + + name_changed = new_name != (database.name or "") + collation_changed = new_collation != getattr(database, "default_collation", None) + encryption_changed = new_encryption != getattr(database, "encryption", None) + tablespace_changed = new_tablespace != getattr(database, "tablespace", None) + connection_limit_changed = new_connection_limit != int(getattr(database, "connection_limit", None) or 0) + + if not any([name_changed, collation_changed, encryption_changed, tablespace_changed, connection_limit_changed]): + return - for attr, value in mapping.items(): - if hasattr(database, attr): - setattr(database, attr, value) + logger.debug( + "ui trace: update_database db_id=%s old_name=%r new_name=%r obs_name=%r name_changed=%s", + getattr(database, "id", None), + getattr(database, "name", None), + new_name, + self.name.get_value(), + name_changed, + ) + + if name_changed: + if not hasattr(database, "_original_name"): + database._original_name = database.name + database.name = new_name + logger.debug( + "ui trace: update_database applied name db_id=%s name=%r", + getattr(database, "id", None), + database.name, + ) - CURRENT_DATABASE.execute_callback(CallbackEvent.AFTER_CHANGE) + changed_fields: set[str] = getattr(database, "_changed_fields", set()) + + if collation_changed and hasattr(database, "default_collation"): + database.default_collation = new_collation + changed_fields.add("default_collation") + if hasattr(database, "character_set") and new_collation: + collations = getattr(database.context, "COLLATIONS", {}) or {} + if charset := collations.get(new_collation): + database.character_set = charset + changed_fields.add("character_set") + + if encryption_changed and hasattr(database, "encryption"): + database.encryption = new_encryption + changed_fields.add("encryption") + + if tablespace_changed and hasattr(database, "tablespace"): + database.tablespace = new_tablespace + changed_fields.add("tablespace") + + if connection_limit_changed and hasattr(database, "connection_limit"): + database.connection_limit = new_connection_limit or None + changed_fields.add("connection_limit") + + NEW_DATABASE.set_value(database) 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.model = EditDatabaseModel() self._panel_by_name = self._build_panel_by_name() self._panels_all = list(self._panel_by_name.values()) self._controls_all = self._build_controls_all() @@ -151,123 +139,74 @@ def __init__(self, parent): CURRENT_SESSION.subscribe(self._on_current_session) CURRENT_DATABASE.subscribe(self._on_current_database) - 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): - if source is None: - return default - - for name in names: - if hasattr(source, name): - value = getattr(source, name) - if value is not None: - return value - - return default - - @staticmethod - def _safe_text(value) -> str: - if value is None: - return "" + def _on_current_database(self, _=None) -> None: + wx.CallAfter(self._apply_current_state) - if isinstance(value, bytes): - return value.decode("utf-8", errors="replace") + def _on_current_session(self, _=None) -> None: + wx.CallAfter(self._apply_current_state) - text = str(value) - return text.encode("utf-8", errors="replace").decode("utf-8", errors="replace") + def _apply_current_state(self) -> None: + session = CURRENT_SESSION.get_value() + database = CURRENT_DATABASE.get_value() + engine = session.engine if session else None + self._populate_choices(database, engine) + self._apply_engine(engine) - 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) + def _apply_choice(self, choice: wx.Choice, items: list, selected: Optional[str]) -> None: + normalized = [str(item) for item in items if item is not None and str(item)] + selected_str = str(selected) if selected else "" - if selected_text and selected_text not in normalized: - normalized.append(selected_text) + if selected_str and selected_str not in normalized: + normalized.append(selected_str) if not normalized: - if choice.GetCount() > 0: - choice.Clear() - + choice.Clear() return choice.SetItems(normalized) - - if selected_text and choice.SetStringSelection(selected_text): + if selected_str and choice.SetStringSelection(selected_str): return choice.SetSelection(0) def _apply_engine(self, engine: Optional[ConnectionEngine]) -> None: panel_names = self._get_panel_names_for_engine(engine) - visible_panels = [self._panel_by_name[name] for name in panel_names] - - self._batch_show_hide(show=visible_panels, hide=self._panels_all) + visible = {self._panel_by_name[name] for name in panel_names} + for panel in self._panels_all: + panel.Show(panel in visible) self._apply_readonly_rules(engine) self._layout_database_options() def _apply_readonly_rules(self, engine: Optional[ConnectionEngine]) -> None: is_sqlite = engine == ConnectionEngine.SQLITE - + database = CURRENT_DATABASE.get_value() + is_new = database is not None and database.is_new + name_readonly = not is_new and engine in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB, ConnectionEngine.SQLITE) self.parent.database_name.Enable(True) - self.parent.database_name.SetEditable(not is_sqlite) - self._set_controls_enabled(enabled=not is_sqlite) - - def _batch_show_hide(self, show: list[wx.Window], hide: list[wx.Window]) -> None: - show_set = set(show) - for panel in hide: - panel.Show(panel in show_set) + self.parent.database_name.SetEditable(not name_readonly) + for control in self._controls_all: + control.Enable(not is_sqlite) def _bind_controls(self) -> None: self.model.bind_controls( - database_name=self.parent.database_name, - database_collation=self.parent.database_collation, - database_encryption=self.parent.database_encryption, - database_read_only=self.parent.database_read_only, - database_tablespace=self.parent.database_tablespace, - database_connection_limit=self.parent.database_connection_limit, - database_password=self.parent.m_textCtrl36, - database_profile=self.parent.database_profile, - database_default_tablespace=self.parent.database_default_tablespace, - database_temporary_tablespace=self.parent.database_temporary_tablespace, - database_quota=self.parent.database_quota, - database_unlimited_quota=self.parent.database_unlimited_quota, - database_account_status=self.parent.database_account_status, - database_password_expire=self.parent.database_password_expire, + name=self.parent.database_name, + collation=self.parent.database_collation, + encryption=self.parent.database_encryption, + tablespace=self.parent.database_tablespace, + connection_limit=self.parent.database_connection_limit, ) def _build_controls_all(self) -> list[wx.Window]: return [ self.parent.database_collation, self.parent.database_encryption, - self.parent.database_read_only, self.parent.database_tablespace, self.parent.database_connection_limit, - self.parent.m_textCtrl36, - self.parent.database_profile, - self.parent.database_default_tablespace, - self.parent.database_temporary_tablespace, - self.parent.database_quota, - self.parent.database_unlimited_quota, - self.parent.database_account_status, - self.parent.database_password_expire, ] def _build_panel_by_name(self) -> dict[str, wx.Window]: return { "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, "database_tablespace_panel": self.parent.database_tablespace_panel, "database_connection_limit_panel": self.parent.database_connection_limit_panel, "database_password_panel": self.parent.database_password_panel, @@ -280,13 +219,19 @@ def _build_panel_by_name(self) -> dict[str, wx.Window]: "database_password_expire_panel": self.parent.database_password_expire_panel, } - def _get_panel_names_for_engine(self, engine: Optional[ConnectionEngine]) -> list[str]: - if engine in [ConnectionEngine.MYSQL, ConnectionEngine.MARIADB]: + @staticmethod + def _get_panel_names_for_engine(engine: Optional[ConnectionEngine]) -> list[str]: + if engine == ConnectionEngine.MYSQL: return [ "database_collation_panel", "database_encryption_panel", ] + if engine == ConnectionEngine.MARIADB: + return [ + "database_collation_panel", + ] + if engine == ConnectionEngine.POSTGRESQL: return [ "database_collation_panel", @@ -313,93 +258,27 @@ def _layout_database_options(self) -> None: if parent := self.parent.m_panel54.GetParent(): parent.Layout() - def _on_current_database(self, database) -> None: - self._schedule_apply_for_current_state() - - def _on_current_session(self, session) -> None: - self._schedule_apply_for_current_state() - def _populate_choices(self, database, engine: Optional[ConnectionEngine]) -> None: if database is None: return context = database.context if database else None - - collations = [] collations_source = getattr(context, "COLLATIONS", None) if context else None - + collations = [] if isinstance(collations_source, dict): - collations = sorted( - str(key) - for key in collations_source.keys() - if key is not None and str(key) - ) + collations = sorted(str(k) for k in collations_source if k is not None and str(k)) - if engine in [ConnectionEngine.MYSQL, ConnectionEngine.MARIADB, ConnectionEngine.POSTGRESQL]: + if engine in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB, ConnectionEngine.POSTGRESQL): self._apply_choice( self.parent.database_collation, collations, - self.model.database_collation.get_value(), + self.model.collation.get_value(), ) if engine == ConnectionEngine.POSTGRESQL: + tablespace = getattr(database, "tablespace", None) self._apply_choice( self.parent.database_tablespace, - [self._first_attr(database, ["tablespace", "default_tablespace"])], - self.model.database_tablespace.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(), + [tablespace] if tablespace else [], + self.model.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 - - 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/database/routine.py b/windows/main/database/routine.py new file mode 100644 index 0000000..685c548 --- /dev/null +++ b/windows/main/database/routine.py @@ -0,0 +1,651 @@ +import dataclasses +from typing import Optional + +import wx +import wx.dataview + +from gettext import gettext as _ + +from helpers.bindings import AbstractModel, wx_call_after_debounce +from helpers.dataview import BaseDataViewListModel, ColumnField +from helpers.logger import logger +from helpers.observables import Observable + +from structures.connection import ConnectionEngine +from structures.engines.database import SQLFunction, SQLProcedure + +from windows.main import ( + CURRENT_DATABASE, + CURRENT_FUNCTION, + CURRENT_PROCEDURE, + CURRENT_SESSION, +) + + +@dataclasses.dataclass +class RoutineParameter: + index: int = 0 + name: str = "" + datatype: str = "" + context: str = "IN" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, RoutineParameter): + return False + return ( + self.index == other.index + and self.name == other.name + and self.datatype == other.datatype + and self.context == other.context + ) + + def copy(self) -> "RoutineParameter": + return RoutineParameter( + index=self.index, + name=self.name, + datatype=self.datatype, + context=self.context, + ) + + +class RoutineParametersModel(BaseDataViewListModel): + MAP_COLUMN_FIELDS = { + 0: ColumnField("index"), + 1: ColumnField("name"), + 2: ColumnField("datatype"), + 3: ColumnField("context"), + } + + def __init__(self) -> None: + super().__init__(column_count=4) + self._parameters: list[RoutineParameter] = [] + + def load_parameters(self, parameters: list[RoutineParameter]) -> None: + self._parameters = [p.copy() for p in parameters] + self.Reset(len(self._parameters)) + + def get_data_by_row(self, row: int) -> RoutineParameter: + return self._parameters[row] + + def set_data_by_row(self, row: int, data: RoutineParameter) -> None: + self._parameters[row] = data + + @property + def parameters(self) -> list[RoutineParameter]: + return self._parameters + + def insert_parameter(self, index: int, parameter: RoutineParameter) -> None: + self._parameters.insert(index, parameter) + self._reindex() + self.Reset(len(self._parameters)) + + def remove_parameter(self, parameter: RoutineParameter) -> None: + self._parameters.remove(parameter) + self._reindex() + self.Reset(len(self._parameters)) + + def clear_parameters(self) -> None: + self._parameters = [] + self.Reset(0) + + def _reindex(self) -> None: + for i, param in enumerate(self._parameters): + param.index = i + + +class RoutineModel(AbstractModel): + def __init__(self) -> None: + super().__init__() + + self.routine_name = Observable() + self.routine_schema = Observable() + self.routine_type = Observable() + self.routine_return_type = Observable() + self.routine_comment = Observable() + self.routine_language = Observable() + self.routine_body = Observable() + + self.behavior_data_access = Observable() + self.behavior_deterministic = Observable() + self.behavior_volatility = Observable() + self.behavior_parallel = Observable() + self.behavior_cost = Observable() + self.behavior_rows = Observable() + + self.security_definer = Observable() + self.security_sql_security = Observable() + + self.parameters_model = RoutineParametersModel() + + wx_call_after_debounce( + self.routine_name, + self.routine_schema, + self.routine_type, + self.routine_return_type, + self.routine_comment, + self.routine_language, + self.routine_body, + self.behavior_data_access, + self.behavior_deterministic, + self.behavior_volatility, + self.behavior_parallel, + self.behavior_cost, + self.behavior_rows, + self.security_definer, + self.security_sql_security, + callback=self._on_model_changed, + ) + + CURRENT_PROCEDURE.subscribe(self._on_current_procedure_changed) + CURRENT_FUNCTION.subscribe(self._on_current_function_changed) + + def _on_model_changed(self, *args) -> None: + pass + + def _on_current_procedure_changed( + self, procedure: Optional[SQLProcedure] + ) -> None: + if procedure is None: + return + self._load_routine(procedure, is_function=False) + + def _on_current_function_changed( + self, function: Optional[SQLFunction] + ) -> None: + if function is None: + return + self._load_routine(function, is_function=True) + + def _load_routine( + self, routine: SQLFunction | SQLProcedure, is_function: bool + ) -> None: + self.routine_name.set_initial(routine.name) + self.routine_type.set_initial( + "FUNCTION" if is_function else "PROCEDURE" + ) + + if is_function: + self.routine_return_type.set_initial( + getattr(routine, "returns", "") + ) + else: + self.routine_return_type.set_initial("") + + self.routine_body.set_initial(getattr(routine, "statement", "")) + self.routine_comment.set_initial(getattr(routine, "comment", "")) + + parameters_text = getattr(routine, "parameters", "") + parameters = self._parse_parameters(parameters_text) + self.parameters_model.load_parameters(parameters) + + session = CURRENT_SESSION.get_value() + if not session: + return + + engine = session.engine + if engine in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): + self._load_mysql_fields(routine) + elif engine == ConnectionEngine.POSTGRESQL: + self._load_postgresql_fields(routine) + + def _load_mysql_fields(self, routine: SQLFunction | SQLProcedure) -> None: + if hasattr(routine, "definer"): + self.security_definer.set_initial(getattr(routine, "definer", "")) + if hasattr(routine, "deterministic"): + self.behavior_deterministic.set_initial( + getattr(routine, "deterministic", False) + ) + if hasattr(routine, "data_access"): + self.behavior_data_access.set_initial( + getattr(routine, "data_access", "") + ) + + def _load_postgresql_fields( + self, routine: SQLFunction | SQLProcedure + ) -> None: + if hasattr(routine, "language"): + self.routine_language.set_initial( + getattr(routine, "language", "plpgsql") + ) + if hasattr(routine, "volatility"): + self.behavior_volatility.set_initial( + getattr(routine, "volatility", "VOLATILE") + ) + + def _parse_parameters(self, parameters_text: str) -> list[RoutineParameter]: + if not parameters_text or not parameters_text.strip(): + return [] + + parameters: list[RoutineParameter] = [] + for idx, param_str in enumerate(parameters_text.split(",")): + param_str = param_str.strip() + if not param_str: + continue + + parts = param_str.split() + name = "" + datatype = "" + context = "IN" + + if len(parts) >= 3: + context = parts[0].upper() + name = parts[1] + datatype = " ".join(parts[2:]) + elif len(parts) == 2: + name = parts[0] + datatype = parts[1] + elif len(parts) == 1: + datatype = parts[0] + + parameters.append( + RoutineParameter( + index=idx, + name=name, + datatype=datatype, + context=context if context in ("IN", "OUT", "INOUT") else "IN", + ) + ) + + return parameters + + def format_parameters(self) -> str: + parts: list[str] = [] + for param in self.parameters_model.parameters: + if param.name and param.datatype: + parts.append(f"{param.context} {param.name} {param.datatype}") + elif param.datatype: + parts.append(param.datatype) + return ", ".join(parts) + + def sync_to_routine( + self, routine: SQLFunction | SQLProcedure, is_function: bool + ) -> None: + routine.name = self.routine_name.get_value() or routine.name + if hasattr(routine, "parameters"): + routine.parameters = self.format_parameters() + if hasattr(routine, "statement"): + routine.statement = self.routine_body.get_value() or "" + if hasattr(routine, "comment"): + routine.comment = self.routine_comment.get_value() or "" + + if is_function and hasattr(routine, "returns"): + routine.returns = self.routine_return_type.get_value() or "" + + session = CURRENT_SESSION.get_value() + if not session: + return + + engine = session.engine + if engine in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): + self._sync_mysql_fields(routine) + elif engine == ConnectionEngine.POSTGRESQL: + self._sync_postgresql_fields(routine) + + def _sync_mysql_fields(self, routine: SQLFunction | SQLProcedure) -> None: + if hasattr(routine, "definer"): + routine.definer = self.security_definer.get_value() or "" + if hasattr(routine, "deterministic"): + routine.deterministic = self.behavior_deterministic.get_value() or False + + def _sync_postgresql_fields( + self, routine: SQLFunction | SQLProcedure + ) -> None: + if hasattr(routine, "language"): + routine.language = self.routine_language.get_value() or "plpgsql" + if hasattr(routine, "volatility"): + routine.volatility = ( + self.behavior_volatility.get_value() or "VOLATILE" + ) + + +class RoutineController: + def __init__(self, parent) -> None: + self.parent = parent + + try: + from windows.components.stc.styles import apply_stc_theme + from windows.components.stc.profiles import SQL + + apply_stc_theme(self.parent.routine_stc) + SQL.apply(self.parent.routine_stc) + except Exception: + pass + + self.model = RoutineModel() + self._bind_controls() + self._bind_parameter_tools() + self._bind_type_change() + + wx_call_after_debounce( + self.model.routine_name, + self.model.routine_type, + self.model.routine_return_type, + self.model.routine_comment, + self.model.routine_language, + self.model.routine_body, + self.model.behavior_data_access, + self.model.behavior_deterministic, + self.model.behavior_volatility, + self.model.behavior_parallel, + self.model.behavior_cost, + self.model.behavior_rows, + self.model.security_definer, + self.model.security_sql_security, + callback=self._update_button_states, + ) + + CURRENT_PROCEDURE.subscribe(self._on_routine_changed) + CURRENT_FUNCTION.subscribe(self._on_routine_changed) + + def _bind_controls(self) -> None: + self.model.bind_controls( + routine_name=self.parent.routine_name, + routine_body=self.parent.routine_stc, + security_definer=self.parent.routine_definer, + ) + + def _bind_parameter_tools(self) -> None: + self.parent.routine_parameters.AssociateModel(self.model.parameters_model) + + def _bind_type_change(self) -> None: + self.parent.routine_type.Bind(wx.EVT_CHOICE, self.on_type_changed) + + def _get_current_routine( + self, + ) -> Optional[SQLFunction | SQLProcedure]: + proc = CURRENT_PROCEDURE.get_value() + if proc is not None: + return proc + func = CURRENT_FUNCTION.get_value() + if func is not None: + return func + return None + + def _is_function(self) -> bool: + routine = self._get_current_routine() + if routine is None: + return False + return isinstance(routine, SQLFunction) + + def _is_procedure(self) -> bool: + routine = self._get_current_routine() + if routine is None: + return False + return isinstance(routine, SQLProcedure) + + def _has_changes(self) -> bool: + routine = self._get_current_routine() + if routine is None: + return False + if routine.is_new: + return True + + name = self.model.routine_name.get_value() + body = self.model.routine_body.get_value() + params = self.model.format_parameters() + + if name and name != routine.name: + return True + if body and body != getattr(routine, "statement", ""): + return True + if params != getattr(routine, "parameters", ""): + return True + + return False + + def _update_button_states(self, *args, **kwargs) -> None: + routine = self._get_current_routine() + if routine is None: + self.parent.btn_routine_save.Enable(False) + self.parent.btn_routine_cancel.Enable(False) + self.parent.btn_routine_delete.Enable(False) + return + + has_changes = self._has_changes() + self.parent.btn_routine_save.Enable(has_changes) + self.parent.btn_routine_cancel.Enable(has_changes) + self.parent.btn_routine_delete.Enable(not routine.is_new) + + def _on_routine_changed( + self, routine: Optional[SQLFunction | SQLProcedure] + ) -> None: + self._update_button_states() + + if routine is None: + return + + session = CURRENT_SESSION.get_value() + if session: + engine = session.engine + self._apply_engine_visibility(engine) + self._populate_definers(engine, session) + + def _populate_definers( + self, engine: ConnectionEngine, session + ) -> None: + if engine not in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): + return + cmb = self.parent.routine_definer + try: + definers = session.context.get_definers() + cmb.Clear() + for definer in definers: + cmb.Append(definer) + + current_definer = self.model.security_definer.get_value() or "" + if current_definer: + idx = cmb.FindString(current_definer) + if idx == wx.NOT_FOUND: + cmb.Append(current_definer) + idx = cmb.FindString(current_definer) + cmb.SetSelection(idx) + elif cmb.GetCount() > 0: + cmb.SetSelection(0) + except Exception: + pass + + def _apply_engine_visibility(self, engine: ConnectionEngine) -> None: + mysql_panel = self.parent.panel_behavior_mysql_mariadb + pg_panel = self.parent.panel_behavior_postgresql + definer_panel = self.parent.routine_definer_panel + sql_security_panel = self.parent.routine_security_panel + + if engine in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): + self._batch_show_hide( + show=[mysql_panel, definer_panel, sql_security_panel], + hide=[pg_panel], + ) + elif engine == ConnectionEngine.POSTGRESQL: + self._batch_show_hide( + show=[pg_panel], + hide=[mysql_panel, definer_panel, sql_security_panel], + ) + else: + self._batch_show_hide( + show=[], + hide=[mysql_panel, pg_panel, definer_panel, sql_security_panel], + ) + + self.parent.m_panel73.GetSizer().Layout() + self.parent.panel_routine.Layout() + + def _batch_show_hide( + self, show: list[wx.Window], hide: list[wx.Window] + ) -> None: + for widget in show: + widget.Show(True) + sizer = widget.GetContainingSizer() + if sizer: + sizer.Show(widget, True) + for widget in hide: + widget.Show(False) + sizer = widget.GetContainingSizer() + if sizer: + sizer.Show(widget, False) + + def on_type_changed(self, event: wx.CommandEvent) -> None: + type_selection = self.parent.routine_type.GetStringSelection() + is_function = "Function" in type_selection or "function" in type_selection.lower() + self.parent.routine_return_type.Enable(is_function) + if not is_function: + self.parent.routine_return_type.SetSelection(wx.NOT_FOUND) + + def on_parameter_insert(self, event: wx.Event) -> None: + selected = self.parent.routine_parameters.GetSelection() + idx = len(self.model.parameters_model.parameters) + if selected.IsOk(): + row = self.model.parameters_model.GetRow(selected) + idx = row + 1 + + new_param = RoutineParameter( + index=idx, + name="", + datatype="", + context="IN", + ) + self.model.parameters_model.insert_parameter(idx, new_param) + + def on_parameter_remove(self, event: wx.Event) -> None: + selected = self.parent.routine_parameters.GetSelection() + if not selected.IsOk(): + return + row = self.model.parameters_model.GetRow(selected) + param = self.model.parameters_model.get_data_by_row(row) + self.model.parameters_model.remove_parameter(param) + + def on_parameter_clear(self, event: wx.Event) -> None: + self.model.parameters_model.clear_parameters() + + def on_save(self, event: wx.Event) -> None: + self.do_save() + + def on_delete(self, event: wx.Event) -> None: + self.do_delete() + + def on_cancel(self, event: wx.Event) -> None: + self.do_cancel() + + def do_save(self) -> None: + routine = self._get_current_routine() + if not routine: + return + session = CURRENT_SESSION.get_value() + if not session: + return + + is_function = isinstance(routine, SQLFunction) + is_new = routine.is_new + + try: + self.model.sync_to_routine(routine, is_function) + routine.save() + + if is_function: + message = ( + _("Function created successfully") + if is_new + else _("Function updated successfully") + ) + else: + message = ( + _("Procedure created successfully") + if is_new + else _("Procedure updated successfully") + ) + + wx.MessageBox(message, _("Success"), wx.OK | wx.ICON_INFORMATION) + self.parent.controller_tree_connections.refresh_current_database() + + if is_new: + database = CURRENT_DATABASE.get_value() + if is_function: + saved = next( + ( + f + for f in database.functions + if f.name == routine.name + ), + None, + ) + if saved: + CURRENT_FUNCTION.set_value(None) + CURRENT_FUNCTION.set_value(saved) + else: + saved = next( + ( + p + for p in database.procedures + if p.name == routine.name + ), + None, + ) + if saved: + CURRENT_PROCEDURE.set_value(None) + CURRENT_PROCEDURE.set_value(saved) + return + + self._update_button_states() + except Exception as e: + wx.MessageBox( + _("Error saving routine: {}").format(str(e)), + _("Error"), + wx.OK | wx.ICON_ERROR, + ) + + def do_delete(self) -> None: + routine = self._get_current_routine() + if not routine: + return + session = CURRENT_SESSION.get_value() + if not session: + return + + is_function = isinstance(routine, SQLFunction) + type_label = _("Function") if is_function else _("Procedure") + + result = wx.MessageBox( + _("Are you sure you want to delete {} '{}'?").format( + type_label, routine.name + ), + _("Confirm Delete"), + wx.YES_NO | wx.ICON_QUESTION, + ) + if result != wx.YES: + return + + try: + routine.drop() + wx.MessageBox( + _("{} deleted successfully").format(type_label), + _("Success"), + wx.OK | wx.ICON_INFORMATION, + ) + + database = CURRENT_DATABASE.get_value() + if is_function: + CURRENT_FUNCTION.set_value(None) + database.functions.refresh() + else: + CURRENT_PROCEDURE.set_value(None) + database.procedures.refresh() + self.parent.controller_tree_connections.refresh_current_database() + except Exception as e: + wx.MessageBox( + _("Error deleting routine: {}").format(str(e)), + _("Error"), + wx.OK | wx.ICON_ERROR, + ) + + def do_cancel(self) -> None: + routine = self._get_current_routine() + if not routine: + return + + is_function = isinstance(routine, SQLFunction) + if is_function: + CURRENT_FUNCTION.set_value(None) + CURRENT_FUNCTION.set_value(routine) + else: + CURRENT_PROCEDURE.set_value(None) + CURRENT_PROCEDURE.set_value(routine) + self._update_button_states() diff --git a/windows/main/database/view.py b/windows/main/database/view.py index 5c1cb44..c450a44 100644 --- a/windows/main/database/view.py +++ b/windows/main/database/view.py @@ -7,6 +7,7 @@ from helpers.sql import format_sql from helpers.bindings import AbstractModel, wx_call_after_debounce +from helpers.logger import logger from helpers.observables import Observable from structures.connection import ConnectionEngine @@ -17,6 +18,7 @@ class EditViewModel(AbstractModel): def __init__(self): + super().__init__() self.name = Observable() self.schema = Observable() self.definer = Observable() @@ -216,9 +218,9 @@ def _has_changes(self, view: SQLView) -> bool: self.model.update_view(view) return view != original - def update_button_states(self): + def update_button_states(self, *args, **kwargs): view = CURRENT_VIEW.get_value() - + if view is None: self.parent.btn_save_view.Enable(False) self.parent.btn_cancel_view.Enable(False) @@ -247,10 +249,20 @@ def do_save_view(self): if not session: return + is_new = view.is_new try: view.save() - message = _("View created successfully") if view.is_new else _("View updated successfully") + message = _("View created successfully") if is_new else _("View updated successfully") wx.MessageBox(message, _("Success"), wx.OK | wx.ICON_INFORMATION) + self.parent.controller_tree_connections.refresh_current_database() + + if is_new: + database = CURRENT_DATABASE.get_value() + saved = next((v for v in database.views if v.name == view.name), None) + if saved: + CURRENT_VIEW.set_value(None) + CURRENT_VIEW.set_value(saved) + return self.update_button_states() except Exception as e: wx.MessageBox(_("Error saving view: {}").format(str(e)), _("Error"), wx.OK | wx.ICON_ERROR) @@ -278,6 +290,9 @@ def do_delete_view(self): view.drop() wx.MessageBox(_("View deleted successfully"), _("Success"), wx.OK | wx.ICON_INFORMATION) CURRENT_VIEW.set_value(None) + database = CURRENT_DATABASE.get_value() + database.views.refresh() + self.parent.controller_tree_connections.refresh_current_database() except Exception as e: wx.MessageBox(_("Error deleting view: {}").format(str(e)), _("Error"), wx.OK | wx.ICON_ERROR) diff --git a/windows/main/explorer.py b/windows/main/explorer.py index db28fd1..74ef4e5 100755 --- a/windows/main/explorer.py +++ b/windows/main/explorer.py @@ -1,4 +1,5 @@ import dataclasses +import os from typing import Callable import wx @@ -8,6 +9,7 @@ from helpers import bytes_to_human from helpers.loader import Loader +from helpers.logger import logger from helpers.observables import CallbackEvent from structures.session import Session @@ -44,8 +46,8 @@ def __init__(self, parent, max_range=100, size=(100, 20)): self.label.SetFont(font) def SetValue(self, val): - self.gauge.SetValue(val) - self.label.SetLabel(f"{val}%") + self.gauge.SetValue(int(val)) + self.label.SetLabel(f"{val:0.1f}%") self.label.Refresh() @@ -57,6 +59,7 @@ def __init__(self, tree_ctrl_explorer: wx.lib.agw.hypertreelist.HyperTreeList): self.app = wx.GetApp() self.tree_ctrl_explorer = tree_ctrl_explorer + self._database_items: dict = {} self.tree_ctrl_explorer.AddColumn("Name", width=200) self.tree_ctrl_explorer.AddColumn("Usage", width=100, flag=wx.ALIGN_RIGHT) @@ -103,16 +106,21 @@ def _load_items(self, event: wx.lib.agw.hypertreelist.TreeEvent): if isinstance(obj, Session): self.select_session(obj, event) elif isinstance(obj, SQLDatabase): + parent_item = self.tree_ctrl_explorer.GetItemParent(item) + parent_session = self.tree_ctrl_explorer.GetItemPyData(parent_item) + if isinstance(parent_session, Session): + self.select_session(parent_session, event) self.select_database(obj, item, event) elif isinstance( obj, (SQLTable, SQLView, SQLTrigger, SQLProcedure, SQLFunction, SQLEvent), ): - self.select_sql_object(obj) + wx.CallAfter(self.select_sql_object, obj) event.Skip() def populate_tree(self): + self._database_items = {} self.tree_ctrl_explorer.DeleteAllItems() self.root_item = self.tree_ctrl_explorer.AddRoot("") @@ -125,6 +133,7 @@ def append_session(self, session: Session): session_item = self.tree_ctrl_explorer.AppendItem(self.root_item, session.name, image=wx.GetApp().icon_registry_16.get_index(getattr(IconList, session.engine.name, IconList.NOT_FOUND)), data=session) for database in session.context.databases.get_value(): db_item = self.tree_ctrl_explorer.AppendItem(session_item, database.name, image=wx.GetApp().icon_registry_16.get_index(IconList.DATABASE), data=database) + self._database_items[id(database)] = db_item self.tree_ctrl_explorer.SetItemText(db_item, bytes_to_human(database.total_bytes), column=1) self.tree_ctrl_explorer.AppendItem(db_item, "Loading...", image=wx.GetApp().icon_registry_16.get_index(IconList.CLOCK), data=None) @@ -165,7 +174,7 @@ def load_observables(self, db_item, database: SQLDatabase): ) if isinstance(obj, SQLTable): - percentage = int((obj.total_bytes / database.total_bytes) * 100) if database.total_bytes else 0 + percentage = float((obj.total_bytes / database.total_bytes) * 100) if database.total_bytes else 0 gauge_panel = GaugeWithLabel(self.tree_ctrl_explorer, max_range=100, size=(self.tree_ctrl_explorer.GetColumnWidth(1) - 20, self.tree_ctrl_explorer.CharHeight)) gauge_panel.SetValue(percentage) @@ -178,20 +187,28 @@ def load_observables(self, db_item, database: SQLDatabase): self.tree_ctrl_explorer.Delete(loading_item) def reset_current_objects(self): + logger.debug( + "ui trace: explorer.reset_current_objects before table=%s view=%s trigger=%s", + getattr(CURRENT_TABLE.get_value(), "name", None) if CURRENT_TABLE.get_value() is not None else None, + getattr(CURRENT_VIEW.get_value(), "name", None) if CURRENT_VIEW.get_value() is not None else None, + getattr(CURRENT_TRIGGER.get_value(), "name", None) if CURRENT_TRIGGER.get_value() is not None else None, + ) CURRENT_TABLE.set_value(None) CURRENT_VIEW.set_value(None) CURRENT_TRIGGER.set_value(None) CURRENT_PROCEDURE.set_value(None) CURRENT_EVENT.set_value(None) CURRENT_FUNCTION.set_value(None) + logger.debug("ui trace: explorer.reset_current_objects after clear") def select_session(self, session: Session, event): if session == CURRENT_SESSION.get_value() and CURRENT_DATABASE.get_value(): + CURRENT_SESSION.execute_callback(CallbackEvent.AFTER_CHANGE) event.Skip() return CURRENT_SESSION.set_value(session) CURRENT_CONNECTION.set_value(session.connection) - # CURRENT_DATABASE.set_value(None) + CURRENT_DATABASE.set_value(None) def select_database(self, database: SQLDatabase, item, event): if database != CURRENT_DATABASE.get_value(): @@ -204,6 +221,7 @@ def select_database(self, database: SQLDatabase, item, event): message="not connected").ShowModal() == wx.ID_OK: session.connect() + self.reset_current_objects() CURRENT_DATABASE.set_value(database) CURRENT_SESSION.get_value().context.set_database(database) @@ -221,7 +239,6 @@ def select_sql_object(self, sql_obj): if database != CURRENT_DATABASE.get_value(): CURRENT_DATABASE.set_value(database) - database.context CURRENT_SESSION.get_value().context.set_database(database) if isinstance(sql_obj, SQLTable): @@ -247,3 +264,64 @@ def select_sql_object(self, sql_obj): elif isinstance(sql_obj, SQLEvent): if not CURRENT_EVENT.get_value() or sql_obj != CURRENT_EVENT.get_value(): CURRENT_EVENT.set_value(sql_obj.copy()) + + def refresh_current_database(self): + database = CURRENT_DATABASE.get_value() + if not database: + logger.debug("explorer refresh: no current database") + return + + db_item = self._database_items.get(id(database)) + if db_item is None or not db_item.IsOk(): + logger.debug("explorer refresh: db_item not found for database=%s id=%s keys=%s", database.name, id(database), list(self._database_items.keys())) + return + + logger.debug("explorer refresh: refreshing database=%s", database.name) + for observable_name in ["tables", "views", "procedures", "functions", "triggers", "events"]: + observable = getattr(database, observable_name, None) + if observable is not None and observable.is_loaded: + logger.debug("explorer refresh: refreshing observable=%s", observable_name) + observable.refresh() + else: + logger.debug("explorer refresh: skipping observable=%s is_loaded=%s", observable_name, getattr(observable, "is_loaded", None)) + + self.tree_ctrl_explorer.DeleteChildren(db_item) + self._load_observables_for_refresh(db_item, database) + self.tree_ctrl_explorer.Expand(db_item) + self.tree_ctrl_explorer.Layout() + + def _load_observables_for_refresh(self, db_item, database: SQLDatabase): + for observable_name in ["tables", "views", "procedures", "functions", "triggers", "events"]: + observable = getattr(database, observable_name, None) + + category_item = self.tree_ctrl_explorer.AppendItem( + db_item, + observable_name.capitalize(), + image=wx.GetApp().icon_registry_16.get_index( + getattr(IconList, observable_name[:-1].upper(), IconList.NOT_FOUND) + ), + data=None + ) + + if observable is None or not observable.is_loaded: + continue + + objs = observable.get_value() + if not objs: + continue + + for obj in objs: + obj_item = self.tree_ctrl_explorer.AppendItem( + category_item, + obj.name, + image=wx.GetApp().icon_registry_16.get_index( + getattr(IconList, observable_name[:-1].upper(), IconList.NOT_FOUND) + ), + data=obj + ) + + if isinstance(obj, SQLTable): + percentage = int((obj.total_bytes / database.total_bytes) * 100) if database.total_bytes else 0 + self.tree_ctrl_explorer.SetItemText(obj_item, f"{percentage}%", column=1) + else: + self.tree_ctrl_explorer.SetItemText(obj_item, "", column=1) diff --git a/windows/main/query/controller.py b/windows/main/query/controller.py index 81376dc..1151251 100644 --- a/windows/main/query/controller.py +++ b/windows/main/query/controller.py @@ -27,6 +27,7 @@ def __init__( 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, + on_connection_lost: Optional[Callable[['Session', str], None]] = None, ): self.editor = stc_editor self.notebook = results_notebook @@ -39,9 +40,11 @@ def __init__( 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.on_connection_lost = on_connection_lost self.parser: Optional[SQLStatementParser] = None self.selector = StatementSelector(stc_editor) + # The executor is created on demand to ensure it always uses the current session. self.executor: Optional[QueryExecutor] = None self.renderer: Optional[QueryResultsRenderer] = None self._cancel_feedback_pending = False @@ -179,9 +182,16 @@ def _execute(self, mode: ExecutionMode) -> None: ) return + # Ensure the parser matches the current engine. if not self.parser or self.parser.engine != session.engine: self.parser = SQLStatementParser(session.engine) + + # Recreate the executor if the session has changed. + if self.executor is None or getattr(self.executor, "session", None) is not session: self.executor = QueryExecutor(session) + + # Create the renderer once; it does not depend on the session. + if self.renderer is None: self.renderer = QueryResultsRenderer(self.notebook, session) sql_text = self.editor.GetText() @@ -200,6 +210,9 @@ def _execute(self, mode: ExecutionMode) -> None: if not statements_to_execute: return + if self.executor and self.executor.is_running(): + return + self.renderer.clear_all_tabs() self._cancel_feedback_pending = False self._set_cancel_button_enabled(True) @@ -216,6 +229,11 @@ def _on_statement_complete(self, result: ExecutionResult) -> None: if result.cancelled: return + if result.connection_lost and self.on_connection_lost is not None: + session = self.get_session() + if session is not None: + self.on_connection_lost(session, result.error or _("Database connection lost")) + if self.renderer: self.renderer.create_result_tab(result) @@ -256,6 +274,7 @@ def __init__( 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, + on_connection_lost: Optional[Callable[['Session', str], None]] = None, ): from windows.main import CURRENT_DATABASE, CURRENT_SESSION # Lazy import: unavoidable circular dependency. @@ -271,4 +290,5 @@ def __init__( on_save_as_query=on_save_as_query, on_stop_state_changed=on_stop_state_changed, on_before_execute=on_before_execute, + on_connection_lost=on_connection_lost, ) diff --git a/windows/main/query/executor.py b/windows/main/query/executor.py index dea2698..9af752e 100644 --- a/windows/main/query/executor.py +++ b/windows/main/query/executor.py @@ -29,6 +29,7 @@ class ExecutionResult: error: Optional[str] = None cancelled: bool = False warnings: list[str] = dataclasses.field(default_factory=list) + connection_lost: bool = False @dataclasses.dataclass @@ -59,6 +60,10 @@ def execute_statements( current_database: Optional[Any] = None, stop_on_error: bool = True ) -> None: + if self._current_thread and self._current_thread.is_alive(): + logger.warning("Attempted to start a new execution while one is already running.") + return + self._cancel_requested = False self._loader_context = Loader.cursor_wait() self._loader_context.__enter__() @@ -176,12 +181,16 @@ def _execute_single(self, context: Any, statement: ParsedStatement) -> Execution elapsed_ms = (time.time() - start_time) * 1000 is_cancelled = self._cancel_requested + from structures.engines.context import ConnectionLostError + connection_lost = isinstance(ex, ConnectionLostError) + return ExecutionResult( statement=statement, success=False, error=str(ex), cancelled=is_cancelled, - elapsed_ms=elapsed_ms + elapsed_ms=elapsed_ms, + connection_lost=connection_lost, ) def _build_worker_connection(self) -> Connection: @@ -256,4 +265,4 @@ def cancel(self) -> None: 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 + return self._current_thread is not None and self._current_thread.is_alive() diff --git a/windows/main/query/history.py b/windows/main/query/history.py new file mode 100644 index 0000000..7eee898 --- /dev/null +++ b/windows/main/query/history.py @@ -0,0 +1,103 @@ +import os +import time + +from collections import defaultdict +from gettext import gettext as _ +from typing import Callable + +import wx +import wx.dataview + + +class QueryHistoryController: + def __init__(self, tree_ctrl: wx.dataview.DataViewTreeCtrl, on_open_query: Callable[[str], None]): + self.tree_ctrl = tree_ctrl + self.on_open_query = on_open_query + + self.tree_ctrl.Bind(wx.dataview.EVT_DATAVIEW_ITEM_ACTIVATED, self._on_item_activated) + self.tree_ctrl.Bind(wx.dataview.EVT_DATAVIEW_ITEM_START_EDITING, self._on_item_start_editing) + + def _open_history_item(self, item: wx.dataview.DataViewItem) -> None: + if not item.IsOk(): + return + + file_path = self.tree_ctrl.GetItemData(item) + if not isinstance(file_path, str): + return + + if not os.path.isfile(file_path): + return + + self.on_open_query(file_path) + + def _on_item_activated(self, event: wx.dataview.DataViewEvent) -> None: + item = event.GetItem() + if item.IsOk(): + self._open_history_item(item) + + @staticmethod + def _on_item_start_editing(event: wx.dataview.DataViewEvent) -> None: + event.Veto() + + @staticmethod + def get_query_history_directory() -> str: + query_dir = os.path.join(os.getcwd(), ".queries") + os.makedirs(query_dir, exist_ok=True) + return query_dir + + @staticmethod + def _build_query_preview(content: str) -> str: + for line in content.splitlines(): + query_line = line.strip() + if query_line: + return query_line[:120] + + return _("(empty query)") + + @staticmethod + def _group_query_paths_by_date(query_paths: list[str]) -> dict[str, list[str]]: + grouped_paths: dict[str, list[str]] = defaultdict(list) + for file_path in query_paths: + modified_date = time.strftime("%Y-%m-%d", time.localtime(os.path.getmtime(file_path))) + grouped_paths[modified_date].append(file_path) + + return grouped_paths + + def _list_query_paths(self) -> list[str]: + query_dir = self.get_query_history_directory() + query_paths = [] + + for filename in os.listdir(query_dir): + if not filename.endswith(".sql"): + continue + + file_path = os.path.join(query_dir, filename) + if os.path.isfile(file_path): + query_paths.append(file_path) + + return query_paths + + def refresh(self) -> None: + self.tree_ctrl.DeleteAllItems() + + grouped_paths = self._group_query_paths_by_date(self._list_query_paths()) + root_item = wx.dataview.NullDataViewItem + sorted_dates = sorted(grouped_paths.keys(), reverse=True) + + for date_index, modified_date in enumerate(sorted_dates): + date_item = self.tree_ctrl.AppendContainer(root_item, modified_date) + sorted_paths = sorted(grouped_paths[modified_date], key=os.path.getmtime, reverse=True) + + for file_path in sorted_paths: + try: + with open(file_path, "r", encoding="utf-8") as file_obj: + preview = self._build_query_preview(file_obj.read()) + except Exception: + preview = os.path.basename(file_path) + + self.tree_ctrl.AppendItem(date_item, preview, data=file_path) + + if date_index == 0: + self.tree_ctrl.Expand(date_item) + else: + self.tree_ctrl.Collapse(date_item) \ No newline at end of file diff --git a/windows/main/table/column.py b/windows/main/table/column.py index 20671c0..5fbe808 100644 --- a/windows/main/table/column.py +++ b/windows/main/table/column.py @@ -159,8 +159,44 @@ def _load_session(self, session: Session): def _load_table(self, table: SQLTable): with Loader.cursor_wait(): self.model.clear() + logger.debug( + "ui trace: columns._load_table clear table_arg=%s", + getattr(table, "name", None) if table is not None else None, + ) if table := NEW_TABLE.get_value() or CURRENT_TABLE.get_value(): + logger.debug( + "ui trace: columns._load_table set_observable table=%s columns=%s", + table.name, + len(table.columns), + ) self.model.set_observable(table.columns) + logger.debug( + "ui trace: columns._load_table bound rows=%s", + len(self.model.data), + ) + + def do_refresh_columns(self): + table = CURRENT_TABLE.get_value() + if table is None: + logger.debug("ui trace: columns.do_refresh_columns skipped table=None") + return + + logger.debug( + "ui trace: columns.do_refresh_columns start table=%s before=%s", + table.name, + len(table.columns), + ) + table.columns.refresh() + logger.debug( + "ui trace: columns.do_refresh_columns after refresh table=%s count=%s", + table.name, + len(table.columns), + ) + self.model.set_observable(table.columns) + logger.debug( + "ui trace: columns.do_refresh_columns bound rows=%s", + len(self.model.data), + ) def _on_selection_change(self, event): item = event.GetItem() @@ -332,7 +368,9 @@ def insert_column_index(self, event: wx.Event, index_type: SQLIndexType): while (name := f"{index_type.prefix}{table.name}_{col.name}_{str(counter).zfill(3)}") in indexes: counter += 1 - new_index = session.context.build_empty_index(name=name, table=table, type=index_type, columns=[col.name]) + new_index = session.context.build_empty_index( + table, index_type, [col.name], name=name + ) table.indexes.append(new_index) diff --git a/windows/main/table/executor.py b/windows/main/table/executor.py index 1fdd2be..36f1f80 100644 --- a/windows/main/table/executor.py +++ b/windows/main/table/executor.py @@ -15,6 +15,7 @@ from structures.engines.database import SQLTable, SQLRecord from windows.main.query.executor import QueryExecutor +from windows.state import CURRENT_DATABASE @dataclasses.dataclass @@ -154,7 +155,7 @@ def _load_records_operation(self, context: Any, operation_kwargs: dict) -> Recor orders = operation_kwargs.get("orders") records = context.get_records( - table=table, + table, filters=filters, limit=limit, offset=offset, @@ -209,7 +210,7 @@ def _create_worker_context(self) -> Any: # context.connect(**connect_kwargs) # return context - context.connect(skip_before_connect=True, skip_after_connect=True, database=self.session.database) + context.connect(skip_before_connect=True, skip_after_connect=True, database=CURRENT_DATABASE.get_value().name) return context def _set_worker_context(self, context: Any) -> None: diff --git a/windows/main/table/foreign_key.py b/windows/main/table/foreign_key.py index ee53470..b8d51be 100755 --- a/windows/main/table/foreign_key.py +++ b/windows/main/table/foreign_key.py @@ -140,9 +140,9 @@ def on_foreign_key_insert(self, event : wx.Event): index = len(table.foreign_keys) new_empty_foreign_key = session.context.build_empty_foreign_key( - name="", - table=table, - columns=[] + table, + [], + name = "", ) table.foreign_keys.append(new_empty_foreign_key) diff --git a/windows/main/table/options.py b/windows/main/table/options.py index a948aa9..e565a6e 100644 --- a/windows/main/table/options.py +++ b/windows/main/table/options.py @@ -1,4 +1,4 @@ -from helpers.bindings import AbstractModel +from helpers.bindings import AbstractModel, wx_call_after_debounce from helpers.observables import Observable, debounce, ObservableList from structures.engines.database import SQLTable @@ -8,6 +8,8 @@ class EditTableModel(AbstractModel): def __init__(self): + super().__init__() + self.name = Observable() self.comment = Observable() self.columns = ObservableList() @@ -18,7 +20,7 @@ def __init__(self): self.engine = Observable() self.row_format = Observable() - debounce( + wx_call_after_debounce( self.name, self.comment, self.auto_increment, self.collation, self.convert_data, self.engine, self.row_format, callback=self.update_table diff --git a/windows/main/table/records.py b/windows/main/table/records.py index fe1d747..57ac467 100644 --- a/windows/main/table/records.py +++ b/windows/main/table/records.py @@ -34,7 +34,7 @@ def __init__(self, table: SQLTable, column_count: Optional[int] = None): self.table: SQLTable = table def _load(self, data): - super()._load([record.copy() for record in data]) + super()._load(data) def _is_null(self, row, col): column = self.table.columns[col] @@ -52,8 +52,8 @@ def GetValueByRow(self, row, col): value = record.values.get(column.name) if value is None: - if column.datatype.name == "BOOLEAN": - return False + # if column.datatype.name == "BOOLEAN": + # return False return NULL_DISPLAY if not str(value).strip(): @@ -101,7 +101,7 @@ def GetAttr(self, item, col, attr): attr.SetColour(wx.Colour(180, 180, 120)) else: color = column.datatype.category.value.color - attr.SetColour(wx.Colour(color)) + attr.SetColour(wx.Colour(*color)) if column.is_primary_key: attr.SetBold(True) @@ -120,7 +120,7 @@ def add_row(self, data: SQLRecord) -> wx.dataview.DataViewItem: class TableRecordsController: app = wx.GetApp() - + executor: Optional[RecordsExecutor] = None def __init__(self, list_ctrl_records: TableRecordsDataViewCtrl): self.list_ctrl_records = list_ctrl_records self.list_ctrl_records.make_advanced_dialog = self.make_advanced_dialog @@ -132,8 +132,6 @@ def __init__(self, list_ctrl_records: TableRecordsDataViewCtrl): 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 @@ -142,55 +140,54 @@ def _load_database(self, database: SQLDatabase): self.database = database def _load_table(self, table: SQLTable): - if table is not None: - self.table = table - self.load_records_async() + self.table = table 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): + def load_records_async(self, obj=None, 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: + target = obj or self.table + if not self.executor or not target: return - + self.executor.load_records( - table=self.table, - on_complete=self._on_records_loaded, + table=target, + on_complete=lambda result: self._on_records_loaded(result, target), filters=filters, limit=limit, offset=offset, orders=orders ) - - def _on_records_loaded(self, result: RecordsOperationResult): + + def _on_records_loaded(self, result: RecordsOperationResult, obj=None): """Handle completion of records loading.""" + target = obj or self.table if result.success and result.records is not None: - self.table.records.set_value(result.records) - self.load_model() + target.records.set_value(result.records) else: logger.error(f"Failed to load records: {result.error}") - # Fallback to synchronous loading try: - self.table.load_records() - self.load_model() + target.load_records() 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)) - self.model.set_observable(self.table.records) + def load_model_for(self, obj): + self.model = RecordsModel(obj, len(obj.columns)) + self.model.set_observable(obj.records) self.list_ctrl_records.AssociateModel(self.model) + def load_model(self): + self.load_model_for(self.table) + def _do_edit(self, item, model_column: int = 1): column = self.list_ctrl_records.GetColumn(model_column) self.list_ctrl_records.edit_item(item, column) def _on_item_value_changed(self, event: wx.dataview.DataViewEvent): logger.debug(f"{'#' * 10} ON RECORD EDITING DONE {'#' * 10}") - table: SQLTable = CURRENT_TABLE.get_value() item = event.GetItem() @@ -199,19 +196,16 @@ def _on_item_value_changed(self, event: wx.dataview.DataViewEvent): return current_record = self.model.data[self.model.GetRow(item)] - original_record = next((r for r in list(table.records) if r.id == current_record.id), None) - - if current_record.id == -1 or current_record != original_record: - if AUTO_APPLY.get_value() and current_record.is_valid(): - try: - current_record.save() - except Exception as ex: - logger.error(f"Error saving record: {ex}", exc_info=True) - else: - # Refresh records after successful save - self.load_records_async() + + if AUTO_APPLY.get_value() and current_record.is_valid(): + try: + current_record.save() + except Exception as ex: + logger.error(f"Error saving record: {ex}", exc_info=True) else: - NEW_RECORDS.append(current_record, replace_existing=True) + self.load_records_async() + else: + NEW_RECORDS.append(current_record, replace_existing=True) event.Skip() @@ -219,24 +213,23 @@ def _on_selection_changed(self, event: wx.dataview.DataViewEvent): logger.debug(f"{'#' * 10} ON SELECTION CHANGED {'#' * 10}") 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() + 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, @@ -346,9 +339,12 @@ def do_apply_records(self): def do_cancel_records(self): """Discard all pending changes in NEW_RECORDS.""" + # BUG FIX: was load_model() which recreates the model from in-memory table.records + # (already modified by SetValueByRow), so edits were never actually discarded. + # load_records_async() fetches fresh data from the DB, properly reversing edits. NEW_RECORDS.clear() if self.table: - self.load_model() + self.load_records_async() def do_refresh_records(self): """Refresh records from database.""" diff --git a/windows/splash.py b/windows/splash.py new file mode 100644 index 0000000..09a2867 --- /dev/null +++ b/windows/splash.py @@ -0,0 +1,29 @@ +import wx + +from windows.views import SplashScreen + +_TICK_MS = 50 + + +class SplashController(SplashScreen): + _MIN_DURATION = 1.5 + + def __init__(self): + super().__init__(None) + self._timer = wx.Timer(self) + self._tick_count = 0 + self._total_ticks = int(self._MIN_DURATION * 1000 / _TICK_MS) + self._on_done = None + self.Bind(wx.EVT_TIMER, self._on_tick) + + def start_close(self, on_done) -> None: + self._on_done = on_done + self._timer.Start(_TICK_MS) + + def _on_tick(self, event: wx.TimerEvent) -> None: + self._tick_count += 1 + self.m_gauge1.SetValue(round(100 * self._tick_count / self._total_ticks)) + if self._tick_count >= self._total_ticks: + self._timer.Stop() + self.Destroy() + self._on_done() diff --git a/windows/state.py b/windows/state.py index 19a861e..b061bb6 100644 --- a/windows/state.py +++ b/windows/state.py @@ -2,7 +2,7 @@ from structures.session import Session from structures.connection import Connection -from structures.engines.database import SQLColumn, SQLDatabase, SQLForeignKey, SQLIndex, SQLRecord, SQLTable, SQLTrigger, SQLView +from structures.engines.database import SQLColumn, SQLDatabase, SQLForeignKey, SQLIndex, SQLRecord, SQLTable, SQLTrigger, SQLView, SQLProcedure SESSIONS_LIST: ObservableList[Session] = ObservableList() @@ -13,13 +13,16 @@ CURRENT_VIEW: Observable[SQLView] = Observable() CURRENT_TRIGGER: Observable[SQLTrigger] = Observable() CURRENT_FUNCTION: Observable[SQLTrigger] = Observable() -CURRENT_PROCEDURE: Observable[SQLTrigger] = Observable() +CURRENT_PROCEDURE: Observable[SQLProcedure] = Observable() CURRENT_EVENT: Observable[SQLTrigger] = Observable() CURRENT_COLUMN: Observable[SQLColumn] = Observable() CURRENT_INDEX: Observable[SQLIndex] = Observable() CURRENT_FOREIGN_KEY: Observable[SQLForeignKey] = Observable() CURRENT_RECORDS: ObservableList[SQLRecord] = ObservableList() +NEW_DATABASE: Observable[SQLDatabase] = Observable() NEW_TABLE: Observable[SQLTable] = Observable() AUTO_APPLY: Observable[bool] = Observable(True) + +WRITE_OVERRIDE: Observable[bool] = Observable(False) diff --git a/windows/views.py b/windows/views.py index acf2939..df059a2 100755 --- a/windows/views.py +++ b/windows/views.py @@ -18,11 +18,40 @@ import wx.dataview import wx.stc import wx.lib.agw.hypertreelist -import wx.adv import gettext _ = gettext.gettext +########################################################################### +## Class SplashScreen +########################################################################### + +class SplashScreen ( wx.Frame ): + + def __init__( self, parent ): + wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.Size( 640,480 ), style = wx.FRAME_NO_TASKBAR|wx.STAY_ON_TOP|wx.TAB_TRAVERSAL ) + + self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) + + bSizer161 = wx.BoxSizer( wx.VERTICAL ) + + self.m_bitmap3 = wx.StaticBitmap( self, wx.ID_ANY, wx.Bitmap( u"petersql_large.png", wx.BITMAP_TYPE_ANY ), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer161.Add( self.m_bitmap3, 1, wx.ALL|wx.EXPAND, 5 ) + + self.m_gauge1 = wx.Gauge( self, wx.ID_ANY, 100, wx.DefaultPosition, wx.DefaultSize, wx.GA_HORIZONTAL ) + self.m_gauge1.SetValue( 0 ) + bSizer161.Add( self.m_gauge1, 0, wx.ALL|wx.EXPAND, 5 ) + + + self.SetSizer( bSizer161 ) + self.Layout() + + self.Centre( wx.BOTH ) + + def __del__( self ): + pass + + ########################################################################### ## Class ConnectionsDialog ########################################################################### @@ -195,6 +224,17 @@ def __init__( self, parent ): bSizer103.Add( bSizer116, 0, wx.EXPAND, 5 ) + bSizer1631 = wx.BoxSizer( wx.HORIZONTAL ) + + + bSizer1631.Add( ( 156, 0), 0, wx.EXPAND, 5 ) + + self.read_only = wx.CheckBox( self.panel_credentials, wx.ID_ANY, _(u"Mark read only"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer1631.Add( self.read_only, 0, wx.ALL, 5 ) + + + bSizer103.Add( bSizer1631, 0, wx.EXPAND, 5 ) + bSizer163 = wx.BoxSizer( wx.HORIZONTAL ) @@ -269,7 +309,6 @@ def __init__( self, parent ): self.m_notebook4.AddPage( self.panel_connection, _(u"Settings"), True ) self.panel_ssh_tunnel = wx.Panel( self.m_notebook4, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) self.panel_ssh_tunnel.Enable( False ) - self.panel_ssh_tunnel.Hide() bSizer102 = wx.BoxSizer( wx.VERTICAL ) @@ -901,14 +940,22 @@ def __init__( self, parent ): 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.tool_refresh_database = 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() - self.database_add = self.m_toolBar1.AddTool( wx.ID_ANY, _(u"Add"), wx.Bitmap( u"icons/16x16/database_add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + self.tool_add_database = self.m_toolBar1.AddTool( wx.ID_ANY, _(u"Add"), wx.Bitmap( u"icons/16x16/database_add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) self.database_delete = self.m_toolBar1.AddTool( wx.ID_ANY, _(u"Add"), wx.Bitmap( u"icons/16x16/database_delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + self.m_toolBar1.AddSeparator() + + + self.m_toggleBtn1 = wx.ToggleButton( self.m_toolBar1, wx.ID_ANY, _(u"{mode}"), wx.DefaultPosition, wx.DefaultSize, 0 ) + + self.m_toggleBtn1.SetBitmap( wx.Bitmap( u"icons/16x16/lock.png", wx.BITMAP_TYPE_ANY ) ) + self.m_toggleBtn1.SetBitmapPressed( wx.Bitmap( u"icons/16x16/bullet_green.png", wx.BITMAP_TYPE_ANY ) ) + self.m_toolBar1.AddControl( self.m_toggleBtn1 ) self.m_toolBar1.Realize() bSizer19 = wx.BoxSizer( wx.VERTICAL ) @@ -927,7 +974,7 @@ def __init__( self, parent ): self.m_splitter4.Bind( wx.EVT_IDLE, self.m_splitter4OnIdle ) self.m_splitter4.SetMinimumPaneSize( 100 ) - self.m_panel14 = wx.Panel( self.m_splitter4, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + self.m_panel14 = wx.Panel( self.m_splitter4, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.FULL_REPAINT_ON_RESIZE|wx.TAB_TRAVERSAL ) bSizer24 = wx.BoxSizer( wx.HORIZONTAL ) self.tree_ctrl_explorer = wx.lib.agw.hypertreelist.HyperTreeList( @@ -1294,65 +1341,168 @@ def __init__( self, parent ): self.m_panel54.SetSizer( bSizer158 ) self.m_panel54.Layout() bSizer158.Fit( self.m_panel54 ) - self.m_panel55 = wx.Panel( self.m_splitter7, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + self.m_panel651 = wx.Panel( self.m_splitter7, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer149 = wx.BoxSizer( wx.VERTICAL ) + + self.m_notebook10 = wx.Notebook( self.m_panel651, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_panel55 = wx.Panel( self.m_notebook10, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer154 = wx.BoxSizer( wx.VERTICAL ) - bSizer531 = wx.BoxSizer( wx.HORIZONTAL ) + self.m_toolBar51 = wx.ToolBar( self.m_panel55, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL|wx.TB_HORZ_TEXT ) + self.tool_insert_table = self.m_toolBar51.AddTool( wx.ID_ANY, _(u"Add new table"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Add new table"), _(u"Add new table"), None ) + + self.tool_clone_table = self.m_toolBar51.AddTool( wx.ID_ANY, _(u"Clone table"), wx.Bitmap( u"icons/16x16/page_copy.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Clone table"), _(u"Clone table"), None ) - self.m_staticText391 = wx.StaticText( self.m_panel55, wx.ID_ANY, _(u"Table:"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText391.Wrap( -1 ) + self.tool_delete_table = self.m_toolBar51.AddTool( wx.ID_ANY, _(u"Delete table"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Delete table"), _(u"Delete table"), None ) - bSizer531.Add( self.m_staticText391, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) + self.m_toolBar51.Realize() + bSizer154.Add( self.m_toolBar51, 0, wx.EXPAND, 5 ) - bSizer531.Add( ( 100, 0), 0, wx.EXPAND, 5 ) + self.list_ctrl_database_tables = wx.dataview.DataViewCtrl( self.m_panel55, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_dataViewColumn12 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn13 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Rows"), 1, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_RIGHT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn14 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Size"), 2, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_RIGHT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn15 = self.list_ctrl_database_tables.AppendDateColumn( _(u"Created at"), 3, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn16 = self.list_ctrl_database_tables.AppendDateColumn( _(u"Updated at"), 4, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn17 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Engine"), 5, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn19 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Collation"), 6, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn18 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Comments"), 7, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + bSizer154.Add( self.list_ctrl_database_tables, 1, wx.ALL|wx.EXPAND, 5 ) - self.btn_insert_table = wx.Button( self.m_panel55, wx.ID_ANY, _(u"Insert"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - self.btn_insert_table.SetBitmap( wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ) ) - bSizer531.Add( self.btn_insert_table, 0, wx.ALL|wx.EXPAND, 2 ) + self.m_panel55.SetSizer( bSizer154 ) + self.m_panel55.Layout() + bSizer154.Fit( self.m_panel55 ) + self.m_notebook10.AddPage( self.m_panel55, _(u"Tables"), True ) + self.m_panel65 = wx.Panel( self.m_notebook10, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer1482 = wx.BoxSizer( wx.VERTICAL ) - self.btn_clone_table = wx.Button( self.m_panel55, wx.ID_ANY, _(u"Clone"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.m_toolBar5 = wx.ToolBar( self.m_panel65, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL|wx.TB_HORZ_TEXT ) + self.tool_insert_view = self.m_toolBar5.AddTool( wx.ID_ANY, _(u"Add new view"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Add new view"), _(u"Add new view"), None ) - self.btn_clone_table.SetBitmap( wx.Bitmap( u"icons/16x16/table_multiple.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_clone_table.Enable( False ) + self.tool_clone_view = self.m_toolBar5.AddTool( wx.ID_ANY, _(u"Clone view"), wx.Bitmap( u"icons/16x16/page_copy.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Clone view"), _(u"Clone view"), None ) - bSizer531.Add( self.btn_clone_table, 0, wx.ALL|wx.EXPAND, 5 ) + self.tool_delete_view = self.m_toolBar5.AddTool( wx.ID_ANY, _(u"Delete view"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Delete view"), _(u"Delete view"), None ) - self.btn_delete_table1 = wx.Button( self.m_panel55, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.m_toolBar5.Realize() - self.btn_delete_table1.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_table1.Enable( False ) + bSizer1482.Add( self.m_toolBar5, 0, wx.EXPAND, 5 ) - bSizer531.Add( self.btn_delete_table1, 0, wx.ALL|wx.EXPAND, 2 ) + self.list_ctrl_database_views = wx.dataview.DataViewCtrl( self.m_panel65, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_dataViewColumn121 = self.list_ctrl_database_views.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn131 = self.list_ctrl_database_views.AppendTextColumn( _(u"Definition"), 1, wx.dataview.DATAVIEW_CELL_INERT, -1, 0, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + bSizer1482.Add( self.list_ctrl_database_views, 1, wx.ALL|wx.EXPAND, 5 ) - bSizer531.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + self.m_panel65.SetSizer( bSizer1482 ) + self.m_panel65.Layout() + bSizer1482.Fit( self.m_panel65 ) + self.m_notebook10.AddPage( self.m_panel65, _(u"Views"), False ) + self.m_panel652 = wx.Panel( self.m_notebook10, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer14821 = wx.BoxSizer( wx.VERTICAL ) + self.m_toolBar52 = wx.ToolBar( self.m_panel652, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL|wx.TB_HORZ_TEXT ) + self.tool_insert_procedure = self.m_toolBar52.AddTool( wx.ID_ANY, _(u"Add new procedure"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Add new procedure"), _(u"Add new procedure"), None ) - bSizer154.Add( bSizer531, 0, wx.EXPAND, 5 ) + self.tool_clone_procedure = self.m_toolBar52.AddTool( wx.ID_ANY, _(u"Clone procedure"), wx.Bitmap( u"icons/16x16/page_copy.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Clone procedure"), _(u"Clone procedure"), None ) - bSizer152 = wx.BoxSizer( wx.VERTICAL ) + self.tool_delete_procedure = self.m_toolBar52.AddTool( wx.ID_ANY, _(u"Delete procedure"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Delete procedure"), _(u"Delete procedure"), None ) - self.list_ctrl_database_tables = wx.dataview.DataViewCtrl( self.m_panel55, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_dataViewColumn12 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) - self.m_dataViewColumn13 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Rows"), 1, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_RIGHT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) - self.m_dataViewColumn14 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Size"), 2, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_RIGHT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) - self.m_dataViewColumn15 = self.list_ctrl_database_tables.AppendDateColumn( _(u"Created at"), 3, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) - self.m_dataViewColumn16 = self.list_ctrl_database_tables.AppendDateColumn( _(u"Updated at"), 4, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) - self.m_dataViewColumn17 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Engine"), 5, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) - self.m_dataViewColumn19 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Collation"), 6, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) - self.m_dataViewColumn18 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Comments"), 7, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) - bSizer152.Add( self.list_ctrl_database_tables, 1, wx.ALL|wx.EXPAND, 5 ) + self.m_toolBar52.Realize() + bSizer14821.Add( self.m_toolBar52, 0, wx.EXPAND, 5 ) - bSizer154.Add( bSizer152, 1, wx.EXPAND, 5 ) + self.list_ctrl_database_procedure = wx.dataview.DataViewCtrl( self.m_panel652, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_dataViewColumn1211 = self.list_ctrl_database_procedure.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn1311 = self.list_ctrl_database_procedure.AppendTextColumn( _(u"Definition"), 1, wx.dataview.DATAVIEW_CELL_INERT, -1, 0, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + bSizer14821.Add( self.list_ctrl_database_procedure, 1, wx.ALL|wx.EXPAND, 5 ) - self.m_panel55.SetSizer( bSizer154 ) - self.m_panel55.Layout() - bSizer154.Fit( self.m_panel55 ) - self.m_splitter7.SplitHorizontally( self.m_panel54, self.m_panel55, 200 ) + self.m_panel652.SetSizer( bSizer14821 ) + self.m_panel652.Layout() + bSizer14821.Fit( self.m_panel652 ) + self.m_notebook10.AddPage( self.m_panel652, _(u"Procedures"), False ) + self.m_panel6521 = wx.Panel( self.m_notebook10, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer148211 = wx.BoxSizer( wx.VERTICAL ) + + self.m_toolBar521 = wx.ToolBar( self.m_panel6521, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL|wx.TB_HORZ_TEXT ) + self.tool_insert_function = self.m_toolBar521.AddTool( wx.ID_ANY, _(u"Add new function"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Add new function"), _(u"Add new function"), None ) + + self.tool_clone_function = self.m_toolBar521.AddTool( wx.ID_ANY, _(u"Clone function"), wx.Bitmap( u"icons/16x16/page_copy.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Clone function"), _(u"Clone function"), None ) + + self.tool_delete_function = self.m_toolBar521.AddTool( wx.ID_ANY, _(u"Delete function"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Delete function"), _(u"Delete function"), None ) + + self.m_toolBar521.Realize() + + bSizer148211.Add( self.m_toolBar521, 0, wx.EXPAND, 5 ) + + self.list_ctrl_database_function = wx.dataview.DataViewCtrl( self.m_panel6521, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_dataViewColumn12111 = self.list_ctrl_database_function.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn13111 = self.list_ctrl_database_function.AppendTextColumn( _(u"Definition"), 1, wx.dataview.DATAVIEW_CELL_INERT, -1, 0, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + bSizer148211.Add( self.list_ctrl_database_function, 1, wx.ALL|wx.EXPAND, 5 ) + + + self.m_panel6521.SetSizer( bSizer148211 ) + self.m_panel6521.Layout() + bSizer148211.Fit( self.m_panel6521 ) + self.m_notebook10.AddPage( self.m_panel6521, _(u"Functions"), False ) + self.m_panel65211 = wx.Panel( self.m_notebook10, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer1482111 = wx.BoxSizer( wx.VERTICAL ) + + self.m_toolBar5211 = wx.ToolBar( self.m_panel65211, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL|wx.TB_HORZ_TEXT ) + self.tool_insert_trigger = self.m_toolBar5211.AddTool( wx.ID_ANY, _(u"Add new trigger"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Add new trigger"), _(u"Add new trigger"), None ) + + self.tool_clone_trigger = self.m_toolBar5211.AddTool( wx.ID_ANY, _(u"Clone trigger"), wx.Bitmap( u"icons/16x16/page_copy.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Clone trigger"), _(u"Clone trigger"), None ) + + self.tool_delete_trigger = self.m_toolBar5211.AddTool( wx.ID_ANY, _(u"Delete trigger"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Delete trigger"), _(u"Delete trigger"), None ) + + self.m_toolBar5211.Realize() + + bSizer1482111.Add( self.m_toolBar5211, 0, wx.EXPAND, 5 ) + + self.list_ctrl_database_trigger = wx.dataview.DataViewCtrl( self.m_panel65211, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_dataViewColumn121111 = self.list_ctrl_database_trigger.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn131111 = self.list_ctrl_database_trigger.AppendTextColumn( _(u"Definition"), 1, wx.dataview.DATAVIEW_CELL_INERT, -1, 0, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + bSizer1482111.Add( self.list_ctrl_database_trigger, 1, wx.ALL|wx.EXPAND, 5 ) + + + self.m_panel65211.SetSizer( bSizer1482111 ) + self.m_panel65211.Layout() + bSizer1482111.Fit( self.m_panel65211 ) + self.m_notebook10.AddPage( self.m_panel65211, _(u"Triggers"), False ) + self.m_panel652111 = wx.Panel( self.m_notebook10, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer14821111 = wx.BoxSizer( wx.VERTICAL ) + + self.m_toolBar52111 = wx.ToolBar( self.m_panel652111, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL|wx.TB_HORZ_TEXT ) + self.tool_insert_event = self.m_toolBar52111.AddTool( wx.ID_ANY, _(u"Add new event"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Add new event"), _(u"Add new event"), None ) + + self.tool_clone_event = self.m_toolBar52111.AddTool( wx.ID_ANY, _(u"Clone event"), wx.Bitmap( u"icons/16x16/page_copy.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Clone event"), _(u"Clone event"), None ) + + self.tool_delete_event = self.m_toolBar52111.AddTool( wx.ID_ANY, _(u"Delete event"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Delete event"), _(u"Delete event"), None ) + + self.m_toolBar52111.Realize() + + bSizer14821111.Add( self.m_toolBar52111, 0, wx.EXPAND, 5 ) + + self.list_ctrl_database_event = wx.dataview.DataViewCtrl( self.m_panel652111, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_dataViewColumn1211111 = self.list_ctrl_database_event.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn1311111 = self.list_ctrl_database_event.AppendTextColumn( _(u"Definition"), 1, wx.dataview.DATAVIEW_CELL_INERT, -1, 0, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + bSizer14821111.Add( self.list_ctrl_database_event, 1, wx.ALL|wx.EXPAND, 5 ) + + + self.m_panel652111.SetSizer( bSizer14821111 ) + self.m_panel652111.Layout() + bSizer14821111.Fit( self.m_panel652111 ) + self.m_notebook10.AddPage( self.m_panel652111, _(u"Events"), False ) + + bSizer149.Add( self.m_notebook10, 1, wx.EXPAND | wx.ALL, 5 ) + + + self.m_panel651.SetSizer( bSizer149 ) + self.m_panel651.Layout() + bSizer149.Fit( self.m_panel651 ) + self.m_splitter7.SplitHorizontally( self.m_panel54, self.m_panel651, 200 ) bSizer80.Add( self.m_splitter7, 1, wx.EXPAND, 5 ) bSizer138 = wx.BoxSizer( wx.HORIZONTAL ) @@ -1421,7 +1571,7 @@ def __init__( self, parent ): self.m_menu15 = wx.Menu() self.panel_database.Bind( wx.EVT_RIGHT_DOWN, self.panel_databaseOnContextMenu ) - self.MainFrameNotebook.AddPage( self.panel_database, _(u"Database"), False ) + self.MainFrameNotebook.AddPage( self.panel_database, _(u"Database"), True ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/database.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -1432,6 +1582,7 @@ def __init__( self, parent ): bSizer251 = wx.BoxSizer( wx.VERTICAL ) self.m_splitter41 = wx.SplitterWindow( self.panel_table, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_LIVE_UPDATE ) + self.m_splitter41.SetSashGravity( 0.5 ) self.m_splitter41.Bind( wx.EVT_IDLE, self.m_splitter41OnIdle ) self.m_splitter41.SetMinimumPaneSize( 200 ) @@ -1568,22 +1719,14 @@ def __init__( self, parent ): self.PanelTableIndex = wx.Panel( self.m_notebook3, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer28 = wx.BoxSizer( wx.HORIZONTAL ) - bSizer791 = wx.BoxSizer( wx.VERTICAL ) - - self.btn_delete_index = wx.Button( self.PanelTableIndex, wx.ID_ANY, _(u"Remove"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_delete_index.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_index.Enable( False ) - - bSizer791.Add( self.btn_delete_index, 0, wx.ALL|wx.EXPAND, 5 ) - - self.btn_clear_index = wx.Button( self.PanelTableIndex, wx.ID_ANY, _(u"Clear"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.m_toolBar12 = wx.ToolBar( self.PanelTableIndex, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORZ_TEXT|wx.TB_TEXT|wx.TB_VERTICAL ) + self.m_tool43 = self.m_toolBar12.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_clear_index.SetBitmap( wx.Bitmap( u"icons/16x16/cross.png", wx.BITMAP_TYPE_ANY ) ) - bSizer791.Add( self.btn_clear_index, 0, wx.ALL|wx.EXPAND, 5 ) + self.m_tool44 = self.m_toolBar12.AddTool( wx.ID_ANY, _(u"Clear"), wx.Bitmap( u"icons/16x16/cross.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + self.m_toolBar12.Realize() - bSizer28.Add( bSizer791, 0, wx.ALIGN_CENTER, 5 ) + bSizer28.Add( self.m_toolBar12, 0, wx.EXPAND, 5 ) self.dv_table_indexes = TableIndexesDataViewCtrl( self.PanelTableIndex, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) bSizer28.Add( self.dv_table_indexes, 1, wx.ALL|wx.EXPAND, 0 ) @@ -1600,37 +1743,21 @@ def __init__( self, parent ): m_notebook3Index += 1 self.PanelTableFK = wx.Panel( self.m_notebook3, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer77 = wx.BoxSizer( wx.VERTICAL ) + bSizer77 = wx.BoxSizer( wx.HORIZONTAL ) - bSizer78 = wx.BoxSizer( wx.HORIZONTAL ) + self.m_toolBar121 = wx.ToolBar( self.PanelTableFK, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORZ_TEXT|wx.TB_TEXT|wx.TB_VERTICAL ) + self.m_tool49 = self.m_toolBar121.AddTool( wx.ID_ANY, _(u"Insert"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - bSizer79 = wx.BoxSizer( wx.VERTICAL ) + self.m_tool431 = self.m_toolBar121.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_foreign_key = wx.Button( self.PanelTableFK, wx.ID_ANY, _(u"Insert"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.m_tool441 = self.m_toolBar121.AddTool( wx.ID_ANY, _(u"Clear"), wx.Bitmap( u"icons/16x16/cross.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - self.btn_insert_foreign_key.SetBitmap( wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ) ) - bSizer79.Add( self.btn_insert_foreign_key, 0, wx.ALL|wx.EXPAND, 5 ) + self.m_toolBar121.Realize() - self.btn_delete_foreign_key = wx.Button( self.PanelTableFK, wx.ID_ANY, _(u"Remove"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_delete_foreign_key.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_foreign_key.Enable( False ) - - bSizer79.Add( self.btn_delete_foreign_key, 0, wx.ALL|wx.EXPAND, 5 ) - - self.btn_clear_foreign_key = wx.Button( self.PanelTableFK, wx.ID_ANY, _(u"Clear"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_clear_foreign_key.SetBitmap( wx.Bitmap( u"icons/16x16/cross.png", wx.BITMAP_TYPE_ANY ) ) - bSizer79.Add( self.btn_clear_foreign_key, 0, wx.ALL|wx.EXPAND, 5 ) - - - bSizer78.Add( bSizer79, 0, wx.ALIGN_CENTER, 5 ) + bSizer77.Add( self.m_toolBar121, 0, wx.EXPAND, 5 ) self.dv_table_foreign_keys = TableForeignKeysDataViewCtrl( self.PanelTableFK, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer78.Add( self.dv_table_foreign_keys, 1, wx.ALL|wx.EXPAND, 0 ) - - - bSizer77.Add( bSizer78, 1, wx.EXPAND, 5 ) + bSizer77.Add( self.dv_table_foreign_keys, 1, wx.ALL|wx.EXPAND, 0 ) self.PanelTableFK.SetSizer( bSizer77 ) @@ -1644,37 +1771,21 @@ def __init__( self, parent ): m_notebook3Index += 1 self.PanelTableCheck = wx.Panel( self.m_notebook3, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer771 = wx.BoxSizer( wx.VERTICAL ) + bSizer771 = wx.BoxSizer( wx.HORIZONTAL ) - bSizer781 = wx.BoxSizer( wx.HORIZONTAL ) + self.m_toolBar1211 = wx.ToolBar( self.PanelTableCheck, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORZ_TEXT|wx.TB_TEXT|wx.TB_VERTICAL ) + self.m_tool491 = self.m_toolBar1211.AddTool( wx.ID_ANY, _(u"Insert"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - bSizer792 = wx.BoxSizer( wx.VERTICAL ) + self.m_tool4311 = self.m_toolBar1211.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_check = wx.Button( self.PanelTableCheck, wx.ID_ANY, _(u"Insert"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.m_tool4411 = self.m_toolBar1211.AddTool( wx.ID_ANY, _(u"Clear"), wx.Bitmap( u"icons/16x16/cross.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - self.btn_insert_check.SetBitmap( wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ) ) - bSizer792.Add( self.btn_insert_check, 0, wx.ALL|wx.EXPAND, 5 ) + self.m_toolBar1211.Realize() - self.btn_delete_check = wx.Button( self.PanelTableCheck, wx.ID_ANY, _(u"Remove"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_delete_check.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_check.Enable( False ) - - bSizer792.Add( self.btn_delete_check, 0, wx.ALL|wx.EXPAND, 5 ) - - self.btn_clear_check = wx.Button( self.PanelTableCheck, wx.ID_ANY, _(u"Clear"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_clear_check.SetBitmap( wx.Bitmap( u"icons/16x16/cross.png", wx.BITMAP_TYPE_ANY ) ) - bSizer792.Add( self.btn_clear_check, 0, wx.ALL|wx.EXPAND, 5 ) - - - bSizer781.Add( bSizer792, 0, wx.ALIGN_CENTER, 5 ) + bSizer771.Add( self.m_toolBar1211, 0, wx.EXPAND, 5 ) self.dv_table_checks = TableCheckDataViewCtrl( self.PanelTableCheck, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer781.Add( self.dv_table_checks, 1, wx.ALL|wx.EXPAND, 0 ) - - - bSizer771.Add( bSizer781, 1, wx.EXPAND, 5 ) + bSizer771.Add( self.dv_table_checks, 1, wx.ALL|wx.EXPAND, 0 ) self.PanelTableCheck.SetSizer( bSizer771 ) @@ -1822,7 +1933,13 @@ def __init__( self, parent ): self.panel_views = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer84 = wx.BoxSizer( wx.VERTICAL ) - self.m_notebook7 = wx.Notebook( self.panel_views, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_splitter11 = wx.SplitterWindow( self.panel_views, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_3D ) + self.m_splitter11.Bind( wx.EVT_IDLE, self.m_splitter11OnIdle ) + + self.m_panel79 = wx.Panel( self.m_splitter11, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer170 = wx.BoxSizer( wx.VERTICAL ) + + self.m_notebook7 = wx.Notebook( self.m_panel79, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) self.pnl_view_editor_root = wx.Panel( self.m_notebook7, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer85 = wx.BoxSizer( wx.VERTICAL ) @@ -1845,26 +1962,6 @@ def __init__( self, parent ): bSizer116 = wx.BoxSizer( wx.VERTICAL ) - self.pnl_row_definer = wx.Panel( self.pnl_view_editor_root, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - szr_view_definer = wx.BoxSizer( wx.HORIZONTAL ) - - self.lbl_view_definer = wx.StaticText( self.pnl_row_definer, wx.ID_ANY, _(u"Definer"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.lbl_view_definer.Wrap( -1 ) - - self.lbl_view_definer.SetMinSize( wx.Size( 150,-1 ) ) - - szr_view_definer.Add( self.lbl_view_definer, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - cmb_view_definerChoices = [] - self.cmb_view_definer = wx.ComboBox( self.pnl_row_definer, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, cmb_view_definerChoices, 0 ) - szr_view_definer.Add( self.cmb_view_definer, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) - - - self.pnl_row_definer.SetSizer( szr_view_definer ) - self.pnl_row_definer.Layout() - szr_view_definer.Fit( self.pnl_row_definer ) - bSizer116.Add( self.pnl_row_definer, 0, wx.EXPAND | wx.ALL, 5 ) - self.pnl_row_schema = wx.Panel( self.pnl_view_editor_root, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) szr_view_schema = wx.BoxSizer( wx.HORIZONTAL ) @@ -1889,31 +1986,19 @@ def __init__( self, parent ): bSizer89.Add( bSizer116, 1, wx.EXPAND, 5 ) - bSizer8711 = wx.BoxSizer( wx.VERTICAL ) - - self.pnl_row_sql_security = wx.Panel( self.pnl_view_editor_root, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - szr_view_sql_security = wx.BoxSizer( wx.HORIZONTAL ) - - self.lbl_view_sql_security = wx.StaticText( self.pnl_row_sql_security, wx.ID_ANY, _(u"SQL security"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.lbl_view_sql_security.Wrap( -1 ) - - self.lbl_view_sql_security.SetMinSize( wx.Size( 150,-1 ) ) - - szr_view_sql_security.Add( self.lbl_view_sql_security, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - cho_view_sql_securityChoices = [ _(u"DEFINER"), _(u"INVOKER") ] - self.cho_view_sql_security = wx.Choice( self.pnl_row_sql_security, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, cho_view_sql_securityChoices, 0 ) - self.cho_view_sql_security.SetSelection( 0 ) - szr_view_sql_security.Add( self.cho_view_sql_security, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) + bSizer85.Add( bSizer89, 0, wx.EXPAND, 5 ) - self.pnl_row_sql_security.SetSizer( szr_view_sql_security ) - self.pnl_row_sql_security.Layout() - szr_view_sql_security.Fit( self.pnl_row_sql_security ) - bSizer8711.Add( self.pnl_row_sql_security, 0, wx.EXPAND | wx.ALL, 5 ) + self.pnl_view_editor_root.SetSizer( bSizer85 ) + self.pnl_view_editor_root.Layout() + bSizer85.Fit( self.pnl_view_editor_root ) + self.m_notebook7.AddPage( self.pnl_view_editor_root, _(u"General"), False ) + self.m_panel76 = wx.Panel( self.m_notebook7, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer1661 = wx.BoxSizer( wx.VERTICAL ) - self.pnl_row_algorithm = wx.Panel( self.pnl_view_editor_root, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - szr_view_algorithm = wx.StaticBoxSizer( wx.VERTICAL, self.pnl_row_algorithm, _(u"Algorithm") ) + self.pnl_row_algorithm = wx.Panel( self.m_panel76, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + szr_view_algorithm = wx.StaticBoxSizer( wx.HORIZONTAL, self.pnl_row_algorithm, _(u"Algorithm") ) self.rad_view_algorithm_undefined = wx.RadioButton( szr_view_algorithm.GetStaticBox(), wx.ID_ANY, _(u"UNDEFINED"), wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP ) szr_view_algorithm.Add( self.rad_view_algorithm_undefined, 0, wx.ALL, 5 ) @@ -1928,9 +2013,9 @@ def __init__( self, parent ): self.pnl_row_algorithm.SetSizer( szr_view_algorithm ) self.pnl_row_algorithm.Layout() szr_view_algorithm.Fit( self.pnl_row_algorithm ) - bSizer8711.Add( self.pnl_row_algorithm, 0, wx.ALL|wx.EXPAND, 5 ) + bSizer1661.Add( self.pnl_row_algorithm, 0, wx.ALL|wx.EXPAND, 5 ) - self.pnl_row_constraint = wx.Panel( self.pnl_view_editor_root, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + self.pnl_row_constraint = wx.Panel( self.m_panel76, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) szr_view_constraint = wx.StaticBoxSizer( wx.VERTICAL, self.pnl_row_constraint, _(u"View constraint") ) self.rad_view_constraint_none = wx.RadioButton( szr_view_constraint.GetStaticBox(), wx.ID_ANY, _(u"None"), wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP ) @@ -1952,9 +2037,58 @@ def __init__( self, parent ): self.pnl_row_constraint.SetSizer( szr_view_constraint ) self.pnl_row_constraint.Layout() szr_view_constraint.Fit( self.pnl_row_constraint ) - bSizer8711.Add( self.pnl_row_constraint, 0, wx.ALL|wx.EXPAND, 5 ) + bSizer1661.Add( self.pnl_row_constraint, 0, wx.ALL|wx.EXPAND, 5 ) + + + self.m_panel76.SetSizer( bSizer1661 ) + self.m_panel76.Layout() + bSizer1661.Fit( self.m_panel76 ) + self.m_notebook7.AddPage( self.m_panel76, _(u"Behavior"), False ) + self.m_panel75 = wx.Panel( self.m_notebook7, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer165 = wx.BoxSizer( wx.VERTICAL ) + + self.pnl_row_definer = wx.Panel( self.m_panel75, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + szr_view_definer = wx.BoxSizer( wx.HORIZONTAL ) + + self.lbl_view_definer = wx.StaticText( self.pnl_row_definer, wx.ID_ANY, _(u"Definer"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.lbl_view_definer.Wrap( -1 ) + + self.lbl_view_definer.SetMinSize( wx.Size( 150,-1 ) ) + + szr_view_definer.Add( self.lbl_view_definer, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + cmb_view_definerChoices = [] + self.cmb_view_definer = wx.ComboBox( self.pnl_row_definer, wx.ID_ANY, _(u"*"), wx.DefaultPosition, wx.DefaultSize, cmb_view_definerChoices, 0 ) + szr_view_definer.Add( self.cmb_view_definer, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) + + + self.pnl_row_definer.SetSizer( szr_view_definer ) + self.pnl_row_definer.Layout() + szr_view_definer.Fit( self.pnl_row_definer ) + bSizer165.Add( self.pnl_row_definer, 0, wx.EXPAND | wx.ALL, 5 ) + + self.pnl_row_sql_security = wx.Panel( self.m_panel75, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + szr_view_sql_security = wx.BoxSizer( wx.HORIZONTAL ) + + self.lbl_view_sql_security = wx.StaticText( self.pnl_row_sql_security, wx.ID_ANY, _(u"SQL security"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.lbl_view_sql_security.Wrap( -1 ) + + self.lbl_view_sql_security.SetMinSize( wx.Size( 150,-1 ) ) + + szr_view_sql_security.Add( self.lbl_view_sql_security, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + cho_view_sql_securityChoices = [ _(u"DEFINER"), _(u"INVOKER") ] + self.cho_view_sql_security = wx.Choice( self.pnl_row_sql_security, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, cho_view_sql_securityChoices, 0 ) + self.cho_view_sql_security.SetSelection( 0 ) + szr_view_sql_security.Add( self.cho_view_sql_security, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) - self.pnl_row_security_barrier = wx.Panel( self.pnl_view_editor_root, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + + self.pnl_row_sql_security.SetSizer( szr_view_sql_security ) + self.pnl_row_sql_security.Layout() + szr_view_sql_security.Fit( self.pnl_row_sql_security ) + bSizer165.Add( self.pnl_row_sql_security, 0, wx.EXPAND | wx.ALL, 5 ) + + self.pnl_row_security_barrier = wx.Panel( self.m_panel75, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer126 = wx.BoxSizer( wx.VERTICAL ) self.chk_view_force = wx.CheckBox( self.pnl_row_security_barrier, wx.ID_ANY, _(u"Force"), wx.DefaultPosition, wx.DefaultSize, 0 ) @@ -1964,9 +2098,9 @@ def __init__( self, parent ): self.pnl_row_security_barrier.SetSizer( bSizer126 ) self.pnl_row_security_barrier.Layout() bSizer126.Fit( self.pnl_row_security_barrier ) - bSizer8711.Add( self.pnl_row_security_barrier, 0, wx.EXPAND, 5 ) + bSizer165.Add( self.pnl_row_security_barrier, 0, wx.EXPAND, 5 ) - self.pnl_row_force = wx.Panel( self.pnl_view_editor_root, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + self.pnl_row_force = wx.Panel( self.m_panel75, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer127 = wx.BoxSizer( wx.VERTICAL ) self.chk_view_security_barrier = wx.CheckBox( self.pnl_row_force, wx.ID_ANY, _(u"Security barrier"), wx.DefaultPosition, wx.DefaultSize, 0 ) @@ -1976,23 +2110,24 @@ def __init__( self, parent ): self.pnl_row_force.SetSizer( bSizer127 ) self.pnl_row_force.Layout() bSizer127.Fit( self.pnl_row_force ) - bSizer8711.Add( self.pnl_row_force, 0, wx.EXPAND, 5 ) - + bSizer165.Add( self.pnl_row_force, 0, wx.EXPAND, 5 ) - bSizer89.Add( bSizer8711, 1, wx.EXPAND, 5 ) + self.m_panel75.SetSizer( bSizer165 ) + self.m_panel75.Layout() + bSizer165.Fit( self.m_panel75 ) + self.m_notebook7.AddPage( self.m_panel75, _(u"Security"), True ) - bSizer85.Add( bSizer89, 0, wx.EXPAND, 5 ) - + bSizer170.Add( self.m_notebook7, 0, wx.EXPAND | wx.ALL, 5 ) - self.pnl_view_editor_root.SetSizer( bSizer85 ) - self.pnl_view_editor_root.Layout() - bSizer85.Fit( self.pnl_view_editor_root ) - self.m_notebook7.AddPage( self.pnl_view_editor_root, _(u"Options"), False ) - bSizer84.Add( self.m_notebook7, 0, wx.EXPAND | wx.ALL, 5 ) + self.m_panel79.SetSizer( bSizer170 ) + self.m_panel79.Layout() + bSizer170.Fit( self.m_panel79 ) + self.m_panel80 = wx.Panel( self.m_splitter11, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer168 = wx.BoxSizer( wx.VERTICAL ) - self.stc_view_select = wx.stc.StyledTextCtrl( self.panel_views, wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,-1 ), 0) + self.stc_view_select = wx.stc.StyledTextCtrl( self.m_panel80, wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,-1 ), 0) self.stc_view_select.SetUseTabs ( True ) self.stc_view_select.SetTabWidth ( 4 ) self.stc_view_select.SetIndent ( 4 ) @@ -2025,7 +2160,14 @@ def __init__( self, parent ): self.stc_view_select.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) ) self.stc_view_select.SetMinSize( wx.Size( -1,200 ) ) - bSizer84.Add( self.stc_view_select, 1, wx.EXPAND | wx.ALL, 5 ) + bSizer168.Add( self.stc_view_select, 1, wx.EXPAND | wx.ALL, 5 ) + + + self.m_panel80.SetSizer( bSizer168 ) + self.m_panel80.Layout() + bSizer168.Fit( self.m_panel80 ) + self.m_splitter11.SplitHorizontally( self.m_panel79, self.m_panel80, 0 ) + bSizer84.Add( self.m_splitter11, 1, wx.EXPAND, 5 ) bSizer91 = wx.BoxSizer( wx.HORIZONTAL ) @@ -2051,113 +2193,517 @@ def __init__( self, parent ): self.panel_views.SetSizer( bSizer84 ) self.panel_views.Layout() bSizer84.Fit( self.panel_views ) - self.MainFrameNotebook.AddPage( self.panel_views, _(u"Views"), False ) + self.MainFrameNotebook.AddPage( self.panel_views, _(u"View"), False ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/view.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex ) MainFrameNotebookIndex += 1 - self.panel_triggers = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - self.MainFrameNotebook.AddPage( self.panel_triggers, _(u"Triggers"), False ) - MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/cog.png", wx.BITMAP_TYPE_ANY ) - if ( MainFrameNotebookBitmap.IsOk() ): - MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) - self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex ) - MainFrameNotebookIndex += 1 + self.panel_routine = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer160 = wx.BoxSizer( wx.VERTICAL ) - self.panel_records = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer61 = wx.BoxSizer( wx.VERTICAL ) + self.m_splitter9 = wx.SplitterWindow( self.panel_routine, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_3D ) + self.m_splitter9.SetSashGravity( 0 ) + self.m_splitter9.Bind( wx.EVT_IDLE, self.m_splitter9OnIdle ) - 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_panel73 = wx.Panel( self.m_splitter9, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer166 = wx.BoxSizer( wx.VERTICAL ) - self.m_toolBar3.AddSeparator() + self.m_notebook11 = wx.Notebook( self.m_panel73, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_panel81 = wx.Panel( self.m_notebook11, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer1701 = wx.BoxSizer( wx.VERTICAL ) - 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 ) + bSizer871 = 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.m_staticText401 = wx.StaticText( self.m_panel81, wx.ID_ANY, _(u"Name"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText401.Wrap( -1 ) - 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.m_staticText401.SetMinSize( wx.Size( 150,-1 ) ) - self.m_toolBar3.AddSeparator() + bSizer871.Add( self.m_staticText401, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - 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.routine_name = wx.TextCtrl( self.m_panel81, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer871.Add( self.routine_name, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) - 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 ) - 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 ) + bSizer1701.Add( bSizer871, 0, wx.EXPAND, 5 ) - self.m_toolBar3.Realize() + szr_view_schema1 = wx.BoxSizer( wx.HORIZONTAL ) - bSizer61.Add( self.m_toolBar3, 0, wx.EXPAND, 5 ) + self.lbl_view_schema1 = wx.StaticText( self.m_panel81, wx.ID_ANY, _(u"Schema"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.lbl_view_schema1.Wrap( -1 ) - bSizer94 = wx.BoxSizer( wx.HORIZONTAL ) + self.lbl_view_schema1.SetMinSize( wx.Size( 150,-1 ) ) - 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 ) + szr_view_schema1.Add( self.lbl_view_schema1, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - bSizer94.Add( self.name_database_table, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + routine_schemaChoices = [] + self.routine_schema = wx.Choice( self.m_panel81, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, routine_schemaChoices, 0 ) + self.routine_schema.SetSelection( 0 ) + szr_view_schema1.Add( self.routine_schema, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) - bSizer94.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + bSizer1701.Add( szr_view_schema1, 0, wx.EXPAND, 5 ) - self.btn_first_records = wx.Button( self.panel_records, wx.ID_ANY, _(u"First"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + bSizer181 = wx.BoxSizer( wx.HORIZONTAL ) - 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 ) + bSizer891 = wx.BoxSizer( wx.HORIZONTAL ) - 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.m_staticText77 = wx.StaticText( self.m_panel81, wx.ID_ANY, _(u"Type"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText77.Wrap( -1 ) - 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 ) + self.m_staticText77.SetMinSize( wx.Size( 150,-1 ) ) - 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 ) + bSizer891.Add( self.m_staticText77, 0, wx.ALIGN_CENTER|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 ) + routine_typeChoices = [ _(u"Procedure (doesn't return a result)"), _(u"Function (return a result)") ] + self.routine_type = wx.Choice( self.m_panel81, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, routine_typeChoices, 0 ) + self.routine_type.SetSelection( 0 ) + bSizer891.Add( self.routine_type, 1, wx.ALL, 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.btn_last_records = wx.Button( self.panel_records, wx.ID_ANY, _(u"Last"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + bSizer181.Add( bSizer891, 1, wx.EXPAND, 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 ) + bSizer1161 = wx.BoxSizer( wx.HORIZONTAL ) + self.m_staticText78 = wx.StaticText( self.m_panel81, wx.ID_ANY, _(u"Return type"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) + self.m_staticText78.Wrap( -1 ) - bSizer61.Add( bSizer94, 0, wx.EXPAND, 5 ) + bSizer1161.Add( self.m_staticText78, 0, wx.ALIGN_CENTER|wx.ALL, 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( True ) + routine_return_typeChoices = [] + self.routine_return_type = wx.Choice( self.m_panel81, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, routine_return_typeChoices, 0 ) + self.routine_return_type.SetSelection( 0 ) + self.routine_return_type.Enable( False ) - bSizer831 = wx.BoxSizer( wx.VERTICAL ) + bSizer1161.Add( self.routine_return_type, 1, wx.ALL, 5 ) - self.sql_query_filters = wx.stc.StyledTextCtrl( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,100 ), 0) - self.sql_query_filters.SetUseTabs ( True ) - self.sql_query_filters.SetTabWidth ( 4 ) - self.sql_query_filters.SetIndent ( 4 ) - self.sql_query_filters.SetTabIndents( True ) - self.sql_query_filters.SetBackSpaceUnIndents( True ) - self.sql_query_filters.SetViewEOL( False ) - self.sql_query_filters.SetViewWhiteSpace( False ) - self.sql_query_filters.SetMarginWidth( 2, 0 ) - self.sql_query_filters.SetIndentationGuides( True ) - self.sql_query_filters.SetReadOnly( False ) - self.sql_query_filters.SetMarginWidth( 1, 0 ) - self.sql_query_filters.SetMarginWidth ( 0, 0 ) - self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS ) - self.sql_query_filters.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK) - self.sql_query_filters.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE) - self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS ) - self.sql_query_filters.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK ) - self.sql_query_filters.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE ) - self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY ) + + bSizer181.Add( bSizer1161, 1, wx.EXPAND, 5 ) + + + bSizer1701.Add( bSizer181, 0, wx.EXPAND, 5 ) + + bSizer182 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText79 = wx.StaticText( self.m_panel81, wx.ID_ANY, _(u"Comment"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText79.Wrap( -1 ) + + bSizer182.Add( self.m_staticText79, 0, wx.ALL, 5 ) + + self.routine_comment = wx.TextCtrl( self.m_panel81, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE ) + bSizer182.Add( self.routine_comment, 1, wx.ALL|wx.EXPAND, 5 ) + + + bSizer1701.Add( bSizer182, 1, wx.EXPAND, 5 ) + + + self.m_panel81.SetSizer( bSizer1701 ) + self.m_panel81.Layout() + bSizer1701.Fit( self.m_panel81 ) + self.m_notebook11.AddPage( self.m_panel81, _(u"General"), True ) + self.m_panel82 = wx.Panel( self.m_notebook11, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer178 = wx.BoxSizer( wx.HORIZONTAL ) + + bSizer185 = wx.BoxSizer( wx.VERTICAL ) + + + bSizer178.Add( bSizer185, 1, wx.EXPAND, 5 ) + + self.m_toolBar11 = wx.ToolBar( self.m_panel82, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORZ_TEXT|wx.TB_RIGHT|wx.TB_TEXT|wx.TB_VERTICAL ) + self.m_tool40 = self.m_toolBar11.AddTool( wx.ID_ANY, _(u"Insert"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Insert"), wx.EmptyString, None ) + + self.m_tool41 = self.m_toolBar11.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.m_tool42 = self.m_toolBar11.AddTool( wx.ID_ANY, _(u"Clear"), wx.Bitmap( u"icons/16x16/cross.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + + self.m_toolBar11.Realize() + + bSizer178.Add( self.m_toolBar11, 0, wx.EXPAND, 5 ) + + self.routine_parameters = wx.dataview.DataViewCtrl( self.m_panel82, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_dataViewColumn27 = self.routine_parameters.AppendTextColumn( _(u"#"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) + self.m_dataViewColumn28 = self.routine_parameters.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, 600, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) + self.m_dataViewColumn29 = self.routine_parameters.AppendTextColumn( _(u"Datatype"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) + self.m_dataViewColumn30 = self.routine_parameters.AppendTextColumn( _(u"Context"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) + bSizer178.Add( self.routine_parameters, 1, wx.ALL|wx.EXPAND, 5 ) + + + self.m_panel82.SetSizer( bSizer178 ) + self.m_panel82.Layout() + bSizer178.Fit( self.m_panel82 ) + self.m_notebook11.AddPage( self.m_panel82, _(u"Parameters"), False ) + self.m_panel86 = wx.Panel( self.m_notebook11, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer183 = wx.BoxSizer( wx.VERTICAL ) + + self.panel_behavior_mysql_mariadb = wx.Panel( self.m_panel86, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer184 = wx.BoxSizer( wx.HORIZONTAL ) + + bSizer186 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText80 = wx.StaticText( self.panel_behavior_mysql_mariadb, wx.ID_ANY, _(u"Data access"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText80.Wrap( -1 ) + + self.m_staticText80.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer186.Add( self.m_staticText80, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + behavior_data_accessChoices = [ _(u"CONTAINS SQL"), _(u"NO SQL"), _(u"READS SQL DATA"), _(u"MODIFIES SQL DATA"), wx.EmptyString, wx.EmptyString ] + self.behavior_data_access = wx.Choice( self.panel_behavior_mysql_mariadb, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, behavior_data_accessChoices, 0 ) + self.behavior_data_access.SetSelection( 0 ) + bSizer186.Add( self.behavior_data_access, 0, wx.ALL, 5 ) + + + bSizer184.Add( bSizer186, 1, wx.EXPAND, 5 ) + + bSizer1851 = wx.BoxSizer( wx.VERTICAL ) + + self.behavior_deterministic = wx.CheckBox( self.panel_behavior_mysql_mariadb, wx.ID_ANY, _(u"Deterministic"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer1851.Add( self.behavior_deterministic, 1, wx.ALL, 5 ) + + + bSizer184.Add( bSizer1851, 1, wx.EXPAND, 5 ) + + + self.panel_behavior_mysql_mariadb.SetSizer( bSizer184 ) + self.panel_behavior_mysql_mariadb.Layout() + bSizer184.Fit( self.panel_behavior_mysql_mariadb ) + bSizer183.Add( self.panel_behavior_mysql_mariadb, 0, wx.EXPAND | wx.ALL, 5 ) + + self.panel_behavior_postgresql = wx.Panel( self.m_panel86, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer187 = wx.BoxSizer( wx.VERTICAL ) + + bSizer188 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText81 = wx.StaticText( self.panel_behavior_postgresql, wx.ID_ANY, _(u"Language"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText81.Wrap( -1 ) + + self.m_staticText81.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer188.Add( self.m_staticText81, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + behavior_postgresql_languageChoices = [ _(u"SQL"), _(u"PLPGSQL") ] + self.behavior_postgresql_language = wx.Choice( self.panel_behavior_postgresql, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, behavior_postgresql_languageChoices, 0 ) + self.behavior_postgresql_language.SetSelection( 0 ) + bSizer188.Add( self.behavior_postgresql_language, 1, wx.ALL, 5 ) + + + bSizer187.Add( bSizer188, 1, wx.EXPAND, 5 ) + + bSizer1881 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText811 = wx.StaticText( self.panel_behavior_postgresql, wx.ID_ANY, _(u"Volatility"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText811.Wrap( -1 ) + + self.m_staticText811.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer1881.Add( self.m_staticText811, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + behavior_postgresql_volatilityChoices = [ _(u"VOLATILE"), _(u"STABLE"), _(u"IMMUTABLE"), wx.EmptyString ] + self.behavior_postgresql_volatility = wx.Choice( self.panel_behavior_postgresql, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, behavior_postgresql_volatilityChoices, 0 ) + self.behavior_postgresql_volatility.SetSelection( 0 ) + bSizer1881.Add( self.behavior_postgresql_volatility, 1, wx.ALL, 5 ) + + + bSizer187.Add( bSizer1881, 1, wx.EXPAND, 5 ) + + bSizer18811 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText8112 = wx.StaticText( self.panel_behavior_postgresql, wx.ID_ANY, _(u"Parallel"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText8112.Wrap( -1 ) + + self.m_staticText8112.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer18811.Add( self.m_staticText8112, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + behavior_postgresql_parallelChoices = [ _(u"UNSAFE"), _(u"RESTRICTED"), _(u"SAFE") ] + self.behavior_postgresql_parallel = wx.Choice( self.panel_behavior_postgresql, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, behavior_postgresql_parallelChoices, 0 ) + self.behavior_postgresql_parallel.SetSelection( 0 ) + bSizer18811.Add( self.behavior_postgresql_parallel, 1, wx.ALL, 5 ) + + + bSizer187.Add( bSizer18811, 1, wx.EXPAND, 5 ) + + bSizer192 = wx.BoxSizer( wx.HORIZONTAL ) + + bSizer196 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText87 = wx.StaticText( self.panel_behavior_postgresql, wx.ID_ANY, _(u"Cost"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText87.Wrap( -1 ) + + self.m_staticText87.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer196.Add( self.m_staticText87, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + self.behavior_postgresql_cost = wx.SpinCtrlDouble( self.panel_behavior_postgresql, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_ARROW_KEYS, 0, 100, 0, 1 ) + self.behavior_postgresql_cost.SetDigits( 0 ) + bSizer196.Add( self.behavior_postgresql_cost, 1, wx.ALL, 5 ) + + + bSizer192.Add( bSizer196, 1, wx.EXPAND, 5 ) + + bSizer193 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText85 = wx.StaticText( self.panel_behavior_postgresql, wx.ID_ANY, _(u"Rows"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) + self.m_staticText85.Wrap( -1 ) + + bSizer193.Add( self.m_staticText85, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + self.behavior_postgresql_rows = wx.SpinCtrl( self.panel_behavior_postgresql, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_ARROW_KEYS, 0, 10, 0 ) + self.behavior_postgresql_rows.Enable( False ) + + bSizer193.Add( self.behavior_postgresql_rows, 0, wx.ALL, 5 ) + + + bSizer192.Add( bSizer193, 1, wx.EXPAND, 5 ) + + + bSizer187.Add( bSizer192, 1, wx.EXPAND, 5 ) + + + self.panel_behavior_postgresql.SetSizer( bSizer187 ) + self.panel_behavior_postgresql.Layout() + bSizer187.Fit( self.panel_behavior_postgresql ) + bSizer183.Add( self.panel_behavior_postgresql, 1, wx.EXPAND | wx.ALL, 5 ) + + + self.m_panel86.SetSizer( bSizer183 ) + self.m_panel86.Layout() + bSizer183.Fit( self.m_panel86 ) + self.m_notebook11.AddPage( self.m_panel86, _(u"Behavior"), False ) + self.m_panel83 = wx.Panel( self.m_notebook11, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + Security = wx.BoxSizer( wx.VERTICAL ) + + self.routine_definer_panel = wx.Panel( self.m_panel83, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + szr_view_definer1 = wx.BoxSizer( wx.HORIZONTAL ) + + self.lbl_view_definer1 = wx.StaticText( self.routine_definer_panel, wx.ID_ANY, _(u"Definer"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.lbl_view_definer1.Wrap( -1 ) + + self.lbl_view_definer1.SetMinSize( wx.Size( 150,-1 ) ) + + szr_view_definer1.Add( self.lbl_view_definer1, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + routine_definerChoices = [] + self.routine_definer = wx.ComboBox( self.routine_definer_panel, wx.ID_ANY, _(u"*"), wx.DefaultPosition, wx.DefaultSize, routine_definerChoices, 0 ) + szr_view_definer1.Add( self.routine_definer, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) + + + self.routine_definer_panel.SetSizer( szr_view_definer1 ) + self.routine_definer_panel.Layout() + szr_view_definer1.Fit( self.routine_definer_panel ) + Security.Add( self.routine_definer_panel, 0, wx.EXPAND | wx.ALL, 5 ) + + self.routine_security_panel = wx.Panel( self.m_panel83, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + szr_view_sql_security1 = wx.BoxSizer( wx.HORIZONTAL ) + + self.lbl_view_sql_security1 = wx.StaticText( self.routine_security_panel, wx.ID_ANY, _(u"SQL security"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.lbl_view_sql_security1.Wrap( -1 ) + + self.lbl_view_sql_security1.SetMinSize( wx.Size( 150,-1 ) ) + + szr_view_sql_security1.Add( self.lbl_view_sql_security1, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + routine_security_sqlChoices = [ _(u"DEFINER"), _(u"INVOKER") ] + self.routine_security_sql = wx.Choice( self.routine_security_panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, routine_security_sqlChoices, 0 ) + self.routine_security_sql.SetSelection( 0 ) + szr_view_sql_security1.Add( self.routine_security_sql, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) + + + self.routine_security_panel.SetSizer( szr_view_sql_security1 ) + self.routine_security_panel.Layout() + szr_view_sql_security1.Fit( self.routine_security_panel ) + Security.Add( self.routine_security_panel, 0, wx.EXPAND | wx.ALL, 5 ) + + + self.m_panel83.SetSizer( Security ) + self.m_panel83.Layout() + Security.Fit( self.m_panel83 ) + self.m_notebook11.AddPage( self.m_panel83, _(u"Security"), False ) + + bSizer166.Add( self.m_notebook11, 1, wx.EXPAND | wx.ALL, 5 ) + + + self.m_panel73.SetSizer( bSizer166 ) + self.m_panel73.Layout() + bSizer166.Fit( self.m_panel73 ) + self.m_panel74 = wx.Panel( self.m_splitter9, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer161 = wx.BoxSizer( wx.VERTICAL ) + + self.routine_stc = wx.stc.StyledTextCtrl( self.m_panel74, wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,-1 ), 0) + self.routine_stc.SetUseTabs ( True ) + self.routine_stc.SetTabWidth ( 4 ) + self.routine_stc.SetIndent ( 4 ) + self.routine_stc.SetTabIndents( True ) + self.routine_stc.SetBackSpaceUnIndents( True ) + self.routine_stc.SetViewEOL( False ) + self.routine_stc.SetViewWhiteSpace( False ) + self.routine_stc.SetMarginWidth( 2, 0 ) + self.routine_stc.SetIndentationGuides( True ) + self.routine_stc.SetReadOnly( False ) + self.routine_stc.SetMarginWidth( 1, 0 ) + self.routine_stc.SetMarginType( 0, wx.stc.STC_MARGIN_NUMBER ) + self.routine_stc.SetMarginWidth( 0, self.routine_stc.TextWidth( wx.stc.STC_STYLE_LINENUMBER, "_99999" ) ) + self.routine_stc.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS ) + self.routine_stc.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK) + self.routine_stc.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE) + self.routine_stc.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS ) + self.routine_stc.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK ) + self.routine_stc.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE ) + self.routine_stc.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY ) + self.routine_stc.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS ) + self.routine_stc.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK ) + self.routine_stc.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE ) + self.routine_stc.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUS ) + self.routine_stc.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.BLACK) + self.routine_stc.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.WHITE) + self.routine_stc.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY ) + self.routine_stc.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY ) + self.routine_stc.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) ) + self.routine_stc.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) ) + self.routine_stc.SetMinSize( wx.Size( -1,200 ) ) + + bSizer161.Add( self.routine_stc, 1, wx.EXPAND | wx.ALL, 5 ) + + + self.m_panel74.SetSizer( bSizer161 ) + self.m_panel74.Layout() + bSizer161.Fit( self.m_panel74 ) + self.m_splitter9.SplitHorizontally( self.m_panel73, self.m_panel74, 0 ) + bSizer160.Add( self.m_splitter9, 1, wx.EXPAND, 5 ) + + bSizer911 = wx.BoxSizer( wx.HORIZONTAL ) + + self.btn_routine_delete = wx.Button( self.panel_routine, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btn_routine_delete.Enable( False ) + + bSizer911.Add( self.btn_routine_delete, 0, wx.ALL, 5 ) + + self.btn_routine_cancel = wx.Button( self.panel_routine, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btn_routine_cancel.Enable( False ) + + bSizer911.Add( self.btn_routine_cancel, 0, wx.ALL, 5 ) + + self.btn_routine_save = wx.Button( self.panel_routine, wx.ID_ANY, _(u"Save"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btn_routine_save.Enable( False ) + + bSizer911.Add( self.btn_routine_save, 0, wx.ALL, 5 ) + + + bSizer160.Add( bSizer911, 0, wx.EXPAND, 5 ) + + + self.panel_routine.SetSizer( bSizer160 ) + self.panel_routine.Layout() + bSizer160.Fit( self.panel_routine ) + self.MainFrameNotebook.AddPage( self.panel_routine, _(u"Routine"), False ) + MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/code-folding.png", wx.BITMAP_TYPE_ANY ) + if ( MainFrameNotebookBitmap.IsOk() ): + MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) + self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex ) + MainFrameNotebookIndex += 1 + + self.panel_triggers = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + self.MainFrameNotebook.AddPage( self.panel_triggers, _(u"Trigger"), False ) + MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/cog.png", wx.BITMAP_TYPE_ANY ) + if ( MainFrameNotebookBitmap.IsOk() ): + MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) + self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex ) + MainFrameNotebookIndex += 1 + + self.panel_records = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer61 = wx.BoxSizer( wx.VERTICAL ) + + 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"Refresh"), 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() + + 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 ) + + 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.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.m_toolBar3.AddSeparator() + + 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.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 ) + + 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.m_toolBar3.Realize() + + bSizer61.Add( self.m_toolBar3, 0, wx.EXPAND, 5 ) + + bSizer94 = wx.BoxSizer( wx.HORIZONTAL ) + + 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 ) + + bSizer94.Add( self.name_database_table, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + + bSizer94.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + + self.btn_first_records = wx.Button( self.panel_records, wx.ID_ANY, _(u"First"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + + 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_prev_records = wx.Button( self.panel_records, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE|wx.BU_EXACTFIT ) + + 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 ) + + 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 ) + + 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.btn_last_records = wx.Button( self.panel_records, wx.ID_ANY, _(u"Last"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + + 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( 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( True ) + + bSizer831 = wx.BoxSizer( wx.VERTICAL ) + + self.sql_query_filters = wx.stc.StyledTextCtrl( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,100 ), 0) + self.sql_query_filters.SetUseTabs ( True ) + self.sql_query_filters.SetTabWidth ( 4 ) + self.sql_query_filters.SetIndent ( 4 ) + self.sql_query_filters.SetTabIndents( True ) + self.sql_query_filters.SetBackSpaceUnIndents( True ) + self.sql_query_filters.SetViewEOL( False ) + self.sql_query_filters.SetViewWhiteSpace( False ) + self.sql_query_filters.SetMarginWidth( 2, 0 ) + self.sql_query_filters.SetIndentationGuides( True ) + self.sql_query_filters.SetReadOnly( False ) + self.sql_query_filters.SetMarginWidth( 1, 0 ) + self.sql_query_filters.SetMarginWidth ( 0, 0 ) + self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS ) + self.sql_query_filters.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK) + self.sql_query_filters.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE) + self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS ) + self.sql_query_filters.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK ) + self.sql_query_filters.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE ) + self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY ) self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS ) self.sql_query_filters.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK ) self.sql_query_filters.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE ) @@ -2170,12 +2716,23 @@ def __init__( self, parent ): self.sql_query_filters.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) ) bSizer831.Add( self.sql_query_filters, 1, wx.EXPAND | wx.ALL, 5 ) + bSizer1591 = wx.BoxSizer( wx.HORIZONTAL ) + self.m_button41 = wx.Button( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, _(u"Apply"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) self.m_button41.SetBitmap( wx.Bitmap( u"icons/16x16/tick.png", wx.BITMAP_TYPE_ANY ) ) + self.m_button41.SetToolTip( _(u"Apply filters in data\nCTRL+ENTER") ) self.m_button41.SetHelpText( _(u"CTRL+ENTER") ) - bSizer831.Add( self.m_button41, 0, wx.ALL, 5 ) + bSizer1591.Add( self.m_button41, 0, wx.ALL, 5 ) + + self.m_button56 = wx.Button( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, _(u"Clear"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + + self.m_button56.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) + bSizer1591.Add( self.m_button56, 0, wx.ALL, 5 ) + + + bSizer831.Add( bSizer1591, 0, wx.EXPAND, 5 ) self.m_collapsiblePane1.GetPane().SetSizer( bSizer831 ) @@ -2192,16 +2749,27 @@ def __init__( self, parent ): self.panel_records.SetSizer( bSizer61 ) self.panel_records.Layout() bSizer61.Fit( self.panel_records ) - self.m_menu10 = wx.Menu() - self.m_menuItem13 = wx.MenuItem( self.m_menu10, wx.ID_ANY, _(u"Insert row")+ u"\t" + u"Ins", wx.EmptyString, wx.ITEM_NORMAL ) - self.m_menu10.Append( self.m_menuItem13 ) + self.menu_table_records = wx.Menu() + self.m_menuItem13 = wx.MenuItem( self.menu_table_records, wx.ID_ANY, _(u"Insert row")+ u"\t" + u"Ins", wx.EmptyString, wx.ITEM_NORMAL ) + self.menu_table_records.Append( self.m_menuItem13 ) + + self.m_menuItem14 = wx.MenuItem( self.menu_table_records, wx.ID_ANY, _(u"MyMenuItem"), wx.EmptyString, wx.ITEM_NORMAL ) + self.menu_table_records.Append( self.m_menuItem14 ) + + self.menu_table_records.AppendSeparator() + + self.m_menuItem20 = wx.MenuItem( self.menu_table_records, wx.ID_ANY, _(u"MyMenuItem"), wx.EmptyString, wx.ITEM_NORMAL ) + self.menu_table_records.Append( self.m_menuItem20 ) - self.m_menuItem14 = wx.MenuItem( self.m_menu10, wx.ID_ANY, _(u"MyMenuItem"), wx.EmptyString, wx.ITEM_NORMAL ) - self.m_menu10.Append( self.m_menuItem14 ) + self.m_menu41 = wx.Menu() + self.m_menuItem21 = wx.MenuItem( self.m_menu41, wx.ID_ANY, _(u"NULL"), wx.EmptyString, wx.ITEM_NORMAL ) + self.m_menu41.Append( self.m_menuItem21 ) + + self.menu_table_records.AppendSubMenu( self.m_menu41, _(u"MyMenu") ) self.panel_records.Bind( wx.EVT_RIGHT_DOWN, self.panel_recordsOnContextMenu ) - self.MainFrameNotebook.AddPage( self.panel_records, _(u"Data"), True ) + self.MainFrameNotebook.AddPage( self.panel_records, _(u"Data"), False ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/text_columns.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2238,7 +2806,16 @@ def __init__( self, parent ): 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 ) + bSizer150 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_splitter8 = wx.SplitterWindow( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_3D ) + self.m_splitter8.SetSashGravity( 1 ) + self.m_splitter8.Bind( wx.EVT_IDLE, self.m_splitter8OnIdle ) + + self.m_panel70 = wx.Panel( self.m_splitter8, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer157 = wx.BoxSizer( wx.VERTICAL ) + + self.notebook_query_editor = wx.Notebook( self.m_panel70, 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 ) @@ -2286,7 +2863,29 @@ def __init__( self, parent ): 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 ) + bSizer157.Add( self.notebook_query_editor, 1, wx.EXPAND | wx.ALL, 5 ) + + + self.m_panel70.SetSizer( bSizer157 ) + self.m_panel70.Layout() + bSizer157.Fit( self.m_panel70 ) + self.m_panel71 = wx.Panel( self.m_splitter8, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer1581 = wx.BoxSizer( wx.VERTICAL ) + + self.tree_ctrl_query_history = wx.dataview.DataViewTreeCtrl( self.m_panel71, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.DV_NO_HEADER|wx.dataview.DV_ROW_LINES ) + self.tree_ctrl_query_history.SetMinSize( wx.Size( 200,-1 ) ) + + bSizer1581.Add( self.tree_ctrl_query_history, 1, wx.ALL|wx.EXPAND, 5 ) + + + self.m_panel71.SetSizer( bSizer1581 ) + self.m_panel71.Layout() + bSizer1581.Fit( self.m_panel71 ) + self.m_splitter8.SplitVertically( self.m_panel70, self.m_panel71, -480 ) + bSizer150.Add( self.m_splitter8, 1, wx.EXPAND, 5 ) + + + bSizer125.Add( bSizer150, 1, wx.EXPAND, 5 ) self.m_panel52.SetSizer( bSizer125 ) @@ -2373,7 +2972,7 @@ def __init__( self, parent ): self.panel_sql_log.SetSizer( sizer_log_sql ) self.panel_sql_log.Layout() sizer_log_sql.Fit( self.panel_sql_log ) - self.m_splitter51.SplitHorizontally( self.m_panel22, self.panel_sql_log, -150 ) + self.m_splitter51.SplitHorizontally( self.m_panel22, self.panel_sql_log, -200 ) bSizer21.Add( self.m_splitter51, 1, wx.EXPAND, 5 ) @@ -2395,22 +2994,39 @@ def __init__( self, parent ): 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.on_database_disconnect, id = self.m_tool4.GetId() ) - self.Bind( wx.EVT_TOOL, self.on_database_refresh, id = self.database_refresh.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_refresh_database, id = self.tool_refresh_database.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_add_database, id = self.tool_add_database.GetId() ) + self.m_toggleBtn1.Bind( wx.EVT_TOGGLEBUTTON, self.on_toggle_read_only ) 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 ) - self.btn_delete_table1.Bind( wx.EVT_BUTTON, self.on_delete_table ) + self.Bind( wx.EVT_TOOL, self.on_insert_table, id = self.tool_insert_table.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_clone_table, id = self.tool_clone_table.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_table, id = self.tool_delete_table.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_insert_view, id = self.tool_insert_view.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_clone_view, id = self.tool_clone_view.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_view, id = self.tool_delete_view.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_insert_procedure, id = self.tool_insert_procedure.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_clone_procedure, id = self.tool_clone_procedure.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_procedure, id = self.tool_delete_procedure.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_insert_function, id = self.tool_insert_function.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_clone_function, id = self.tool_clone_function.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_function, id = self.tool_delete_function.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_insert_view, id = self.tool_insert_trigger.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_clone_view, id = self.tool_clone_trigger.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_view, id = self.tool_delete_trigger.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_insert_view, id = self.tool_insert_event.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_clone_view, id = self.tool_clone_event.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_view, id = self.tool_delete_event.GetId() ) self.btn_cancel_database.Bind( wx.EVT_BUTTON, self.on_cancel_database ) self.btn_delete_database.Bind( wx.EVT_BUTTON, self.on_delete_database ) self.btn_apply_database.Bind( wx.EVT_BUTTON, self.on_apply_database ) - self.btn_delete_index.Bind( wx.EVT_BUTTON, self.on_delete_index ) - self.btn_clear_index.Bind( wx.EVT_BUTTON, self.on_clear_index ) - self.btn_insert_foreign_key.Bind( wx.EVT_BUTTON, self.on_insert_foreign_key ) - self.btn_delete_foreign_key.Bind( wx.EVT_BUTTON, self.on_delete_foreign_key ) - self.btn_clear_foreign_key.Bind( wx.EVT_BUTTON, self.on_clear_foreign_key ) - 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.Bind( wx.EVT_TOOL, self.on_delete_index, id = self.m_tool43.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_clear_index, id = self.m_tool44.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_insert_foreign_key, id = self.m_tool49.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_foreign_key, id = self.m_tool431.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_clear_foreign_key, id = self.m_tool441.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_insert_check, id = self.m_tool491.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_check, id = self.m_tool4311.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_clear_check, id = self.m_tool4411.GetId() ) 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() ) @@ -2418,6 +3034,12 @@ def __init__( self, parent ): 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.Bind( wx.EVT_TOOL, self.on_routine_parameters_insert, id = self.m_tool40.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_routine_parameters_delete, id = self.m_tool41.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_routine_parameters_clear, id = self.m_tool42.GetId() ) + self.btn_routine_delete.Bind( wx.EVT_BUTTON, self.on_routine_delete ) + self.btn_routine_cancel.Bind( wx.EVT_BUTTON, self.on_routine_cancel ) + self.btn_routine_save.Bind( wx.EVT_BUTTON, self.on_routine_save ) 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() ) @@ -2431,6 +3053,7 @@ def __init__( self, parent ): 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.m_button56.Bind( wx.EVT_BUTTON, self.on_clear_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() ) @@ -2458,7 +3081,13 @@ def do_open_connection_manager( self, event ): def on_database_disconnect( self, event ): event.Skip() - def on_database_refresh( self, event ): + def on_refresh_database( self, event ): + event.Skip() + + def on_add_database( self, event ): + event.Skip() + + def on_toggle_read_only( self, event ): event.Skip() def on_page_chaged( self, event ): @@ -2473,43 +3102,82 @@ def on_clone_table( self, event ): def on_delete_table( self, event ): event.Skip() - def on_cancel_database( self, event ): + def on_insert_view( self, event ): event.Skip() - def on_delete_database( self, event ): + def on_clone_view( self, event ): event.Skip() - def on_apply_database( self, event ): + def on_delete_view( self, event ): event.Skip() - def on_delete_index( self, event ): + def on_insert_procedure( self, event ): event.Skip() - def on_clear_index( self, event ): + def on_clone_procedure( self, event ): event.Skip() - def on_insert_foreign_key( self, event ): + def on_delete_procedure( self, event ): event.Skip() - def on_delete_foreign_key( self, event ): + def on_insert_function( self, event ): event.Skip() - def on_clear_foreign_key( self, event ): + def on_clone_function( self, event ): event.Skip() + def on_delete_function( self, event ): + event.Skip() - def on_insert_column( self, event ): - event.Skip() - def on_delete_column( self, event ): - event.Skip() - def on_move_up_column( self, event ): - event.Skip() - def on_move_down_column( self, event ): + + def on_cancel_database( self, event ): + event.Skip() + + def on_delete_database( self, event ): + event.Skip() + + def on_apply_database( self, event ): + event.Skip() + + def on_delete_index( self, event ): + event.Skip() + + def on_clear_index( self, event ): + event.Skip() + + def on_insert_foreign_key( self, event ): + event.Skip() + + def on_delete_foreign_key( self, event ): + event.Skip() + + def on_clear_foreign_key( self, event ): + event.Skip() + + def on_insert_check( self, event ): + event.Skip() + + def on_delete_check( self, event ): + event.Skip() + + def on_clear_check( self, event ): + event.Skip() + + def on_insert_column( self, event ): + event.Skip() + + def on_delete_column( self, event ): + event.Skip() + + def on_move_up_column( self, event ): + event.Skip() + + def on_move_down_column( self, event ): event.Skip() @@ -2519,6 +3187,24 @@ def on_cancel_table( self, event ): def do_apply_table( self, event ): event.Skip() + def on_routine_parameters_insert( self, event ): + event.Skip() + + def on_routine_parameters_delete( self, event ): + event.Skip() + + def on_routine_parameters_clear( self, event ): + event.Skip() + + def on_routine_delete( self, event ): + event.Skip() + + def on_routine_cancel( self, event ): + event.Skip() + + def on_routine_save( self, event ): + event.Skip() + def on_refresh_records( self, event ): event.Skip() @@ -2558,6 +3244,9 @@ def on_collapsible_pane_changed( self, event ): def on_apply_filters( self, event ): event.Skip() + def on_clear_filters( self, event ): + event.Skip() + def on_new_query( self, event ): event.Skip() @@ -2577,7 +3266,7 @@ def on_save( self, event ): event.Skip() def m_splitter51OnIdle( self, event ): - self.m_splitter51.SetSashPosition( -150 ) + self.m_splitter51.SetSashPosition( -200 ) self.m_splitter51.Unbind( wx.EVT_IDLE ) def m_splitter4OnIdle( self, event ): @@ -2601,547 +3290,47 @@ def m_splitter41OnIdle( self, event ): def panel_table_columnsOnContextMenu( self, event ): self.panel_table_columns.PopupMenu( self.menu_table_columns, event.GetPosition() ) + def m_splitter11OnIdle( self, event ): + self.m_splitter11.SetSashPosition( 0 ) + self.m_splitter11.Unbind( wx.EVT_IDLE ) + + def m_splitter9OnIdle( self, event ): + self.m_splitter9.SetSashPosition( 0 ) + self.m_splitter9.Unbind( wx.EVT_IDLE ) + def panel_recordsOnContextMenu( self, event ): - self.panel_records.PopupMenu( self.m_menu10, event.GetPosition() ) + self.panel_records.PopupMenu( self.menu_table_records, event.GetPosition() ) def m_splitter6OnIdle( self, event ): self.m_splitter6.SetSashPosition( -300 ) self.m_splitter6.Unbind( wx.EVT_IDLE ) - -########################################################################### -## Class Trash -########################################################################### - -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 ) - - bSizer144 = wx.BoxSizer( wx.VERTICAL ) - - self.database_character_set_panel = wx.Panel( self, 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 ) - bSizer144.Add( self.database_character_set_panel, 1, wx.ALIGN_CENTER, 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() - - 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 ) - bSizer144.Add( self.QueryPanelTpl, 1, wx.EXPAND | wx.ALL, 5 ) - - bSizer83 = wx.BoxSizer( wx.HORIZONTAL ) - - 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 ) - - - bSizer144.Add( bSizer83, 0, wx.EXPAND, 5 ) - - self.btn_insert_record = wx.Button( self, wx.ID_ANY, _(u"Insert record"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - 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.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 ) - - bSizer144.Add( self.btn_duplicate_record, 0, wx.ALL, 5 ) - - self.btn_delete_record = wx.Button( self, wx.ID_ANY, _(u"Delete record"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_delete_record.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_record.Enable( False ) - - bSizer144.Add( self.btn_delete_record, 0, wx.ALL, 5 ) - - self.btn_cancel_record = wx.Button( self, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_cancel_record.SetBitmap( wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_cancel_record.Enable( False ) - - bSizer144.Add( self.btn_cancel_record, 0, wx.ALL, 5 ) - - self.btn_apply_record = wx.Button( self, wx.ID_ANY, _(u"Apply"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_apply_record.SetBitmap( wx.Bitmap( u"icons/16x16/disk.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_apply_record.Enable( False ) - - bSizer144.Add( self.btn_apply_record, 0, wx.ALL, 5 ) - - bSizer53 = wx.BoxSizer( wx.HORIZONTAL ) + def m_splitter8OnIdle( self, event ): + self.m_splitter8.SetSashPosition( -480 ) + self.m_splitter8.Unbind( wx.EVT_IDLE ) - bSizer53.Add( ( 100, 0), 0, wx.EXPAND, 5 ) - - 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 ) - - 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 ) - - bSizer53.Add( self.btn_delete_column, 0, wx.LEFT|wx.RIGHT, 2 ) - - self.btn_move_up_column = wx.Button( self, 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, 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 ) - - - bSizer53.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - - bSizer144.Add( bSizer53, 0, wx.ALL|wx.EXPAND, 5 ) - - - self.SetSizer( bSizer144 ) - self.Layout() - - # Connect Events - 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_insert_record( self, event ): - event.Skip() - - def on_duplicate_record( self, event ): - event.Skip() - - def on_delete_record( self, event ): - event.Skip() - - def on_insert_column( self, event ): - event.Skip() - - def on_delete_column( self, event ): - event.Skip() - - def on_move_up_column( self, event ): - event.Skip() - - def on_move_down_column( self, event ): - event.Skip() - - -########################################################################### -## Class TablePanel ########################################################################### - -class TablePanel ( wx.Panel ): - - def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.Size( 640,480 ), style = wx.TAB_TRAVERSAL, name = wx.EmptyString ): - wx.Panel.__init__ ( self, parent, id = id, pos = pos, size = size, style = style, name = name ) - - bSizer251 = wx.BoxSizer( wx.VERTICAL ) - - self.m_splitter41 = wx.SplitterWindow( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_LIVE_UPDATE ) - self.m_splitter41.Bind( wx.EVT_IDLE, self.m_splitter41OnIdle ) - self.m_splitter41.SetMinimumPaneSize( 200 ) - - self.m_panel19 = wx.Panel( self.m_splitter41, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer55 = wx.BoxSizer( wx.VERTICAL ) - - self.m_notebook3 = wx.Notebook( self.m_panel19, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.NB_FIXEDWIDTH ) - m_notebook3ImageSize = wx.Size( 16,16 ) - m_notebook3Index = 0 - m_notebook3Images = wx.ImageList( m_notebook3ImageSize.GetWidth(), m_notebook3ImageSize.GetHeight() ) - self.m_notebook3.AssignImageList( m_notebook3Images ) - self.PanelTableBase = wx.Panel( self.m_notebook3, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer262 = wx.BoxSizer( wx.VERTICAL ) - - bSizer271 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText8 = wx.StaticText( self.PanelTableBase, wx.ID_ANY, _(u"Name"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) - self.m_staticText8.Wrap( -1 ) - - bSizer271.Add( self.m_staticText8, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.table_name = wx.TextCtrl( self.PanelTableBase, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer271.Add( self.table_name, 1, wx.ALL|wx.EXPAND, 5 ) - - - bSizer262.Add( bSizer271, 0, wx.EXPAND, 5 ) - - bSizer273 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText83 = wx.StaticText( self.PanelTableBase, wx.ID_ANY, _(u"Comments"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) - self.m_staticText83.Wrap( -1 ) - - bSizer273.Add( self.m_staticText83, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.table_comment = wx.TextCtrl( self.PanelTableBase, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE ) - bSizer273.Add( self.table_comment, 1, wx.ALL|wx.EXPAND, 5 ) - - - bSizer262.Add( bSizer273, 1, wx.EXPAND, 5 ) - - - self.PanelTableBase.SetSizer( bSizer262 ) - self.PanelTableBase.Layout() - bSizer262.Fit( self.PanelTableBase ) - self.m_notebook3.AddPage( self.PanelTableBase, _(u"Base"), True ) - m_notebook3Bitmap = wx.Bitmap( u"icons/16x16/table.png", wx.BITMAP_TYPE_ANY ) - if ( m_notebook3Bitmap.IsOk() ): - m_notebook3Images.Add( m_notebook3Bitmap ) - self.m_notebook3.SetPageImage( m_notebook3Index, m_notebook3Index ) - m_notebook3Index += 1 - - self.PanelTableOptions = wx.Panel( self.m_notebook3, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer261 = wx.BoxSizer( wx.VERTICAL ) - - gSizer11 = wx.GridSizer( 0, 2, 0, 0 ) - - bSizer27111 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText8111 = wx.StaticText( self.PanelTableOptions, wx.ID_ANY, _(u"Auto Increment"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) - self.m_staticText8111.Wrap( -1 ) - - bSizer27111.Add( self.m_staticText8111, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.table_auto_increment = wx.TextCtrl( self.PanelTableOptions, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer27111.Add( self.table_auto_increment, 1, wx.ALL|wx.EXPAND, 5 ) - - - gSizer11.Add( bSizer27111, 1, wx.EXPAND, 5 ) - - bSizer2712 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText812 = wx.StaticText( self.PanelTableOptions, wx.ID_ANY, _(u"Engine"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) - self.m_staticText812.Wrap( -1 ) - - bSizer2712.Add( self.m_staticText812, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - table_engineChoices = [ wx.EmptyString ] - self.table_engine = wx.Choice( self.PanelTableOptions, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, table_engineChoices, 0 ) - self.table_engine.SetSelection( 1 ) - bSizer2712.Add( self.table_engine, 1, wx.ALL|wx.EXPAND, 5 ) - - - gSizer11.Add( bSizer2712, 0, wx.EXPAND, 5 ) - - bSizer2721 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText821 = wx.StaticText( self.PanelTableOptions, wx.ID_ANY, _(u"Default Collation"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) - self.m_staticText821.Wrap( -1 ) - - bSizer2721.Add( self.m_staticText821, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.table_collation = wx.TextCtrl( self.PanelTableOptions, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer2721.Add( self.table_collation, 1, wx.ALL|wx.EXPAND, 5 ) - - - gSizer11.Add( bSizer2721, 0, wx.EXPAND, 5 ) - - - bSizer261.Add( gSizer11, 0, wx.EXPAND, 5 ) - - - self.PanelTableOptions.SetSizer( bSizer261 ) - self.PanelTableOptions.Layout() - bSizer261.Fit( self.PanelTableOptions ) - self.m_notebook3.AddPage( self.PanelTableOptions, _(u"Options"), False ) - m_notebook3Bitmap = wx.Bitmap( u"icons/16x16/wrench.png", wx.BITMAP_TYPE_ANY ) - if ( m_notebook3Bitmap.IsOk() ): - m_notebook3Images.Add( m_notebook3Bitmap ) - self.m_notebook3.SetPageImage( m_notebook3Index, m_notebook3Index ) - m_notebook3Index += 1 - - self.PanelTableIndex = wx.Panel( self.m_notebook3, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer28 = wx.BoxSizer( wx.HORIZONTAL ) - - - self.PanelTableIndex.SetSizer( bSizer28 ) - self.PanelTableIndex.Layout() - bSizer28.Fit( self.PanelTableIndex ) - self.m_notebook3.AddPage( self.PanelTableIndex, _(u"Indexes"), False ) - m_notebook3Bitmap = wx.Bitmap( u"icons/16x16/lightning.png", wx.BITMAP_TYPE_ANY ) - if ( m_notebook3Bitmap.IsOk() ): - m_notebook3Images.Add( m_notebook3Bitmap ) - self.m_notebook3.SetPageImage( m_notebook3Index, m_notebook3Index ) - m_notebook3Index += 1 - - - bSizer55.Add( self.m_notebook3, 1, wx.EXPAND | wx.ALL, 5 ) - - - self.m_panel19.SetSizer( bSizer55 ) - self.m_panel19.Layout() - bSizer55.Fit( self.m_panel19 ) - self.panel_table_columns = wx.Panel( self.m_splitter41, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - self.panel_table_columns.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) ) - - 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.m_staticText39.Wrap( -1 ) - - bSizer53.Add( self.m_staticText39, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - - 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.btn_column_delete = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_column_delete.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_column_delete.Enable( False ) - - bSizer53.Add( self.btn_column_delete, 0, wx.LEFT|wx.RIGHT, 2 ) - - self.btn_column_move_up = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Up"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_column_move_up.SetBitmap( wx.Bitmap( u"icons/16x16/arrow_up.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_column_move_up.Enable( False ) - - bSizer53.Add( self.btn_column_move_up, 0, wx.LEFT|wx.RIGHT, 2 ) - - self.btn_column_move_down = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Down"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_column_move_down.SetBitmap( wx.Bitmap( u"icons/16x16/arrow_down.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_column_move_down.Enable( False ) - - bSizer53.Add( self.btn_column_move_down, 0, wx.LEFT|wx.RIGHT, 2 ) - - - bSizer53.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - - bSizer54.Add( bSizer53, 0, wx.ALL|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 ) - - bSizer52 = wx.BoxSizer( wx.HORIZONTAL ) - - self.btn_table_delete = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer52.Add( self.btn_table_delete, 0, wx.ALL, 5 ) - - self.btn_table_cancel = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.btn_table_cancel.Enable( False ) - - bSizer52.Add( self.btn_table_cancel, 0, wx.ALL, 5 ) - - self.btn_table_save = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Save"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.btn_table_save.Enable( False ) - - bSizer52.Add( self.btn_table_save, 0, wx.ALL, 5 ) - - - bSizer54.Add( bSizer52, 0, wx.EXPAND, 5 ) - - - self.panel_table_columns.SetSizer( bSizer54 ) - self.panel_table_columns.Layout() - bSizer54.Fit( self.panel_table_columns ) - self.menu_table_columns = wx.Menu() - self.add_index = wx.MenuItem( self.menu_table_columns, wx.ID_ANY, _(u"Add Index"), wx.EmptyString, wx.ITEM_NORMAL ) - self.menu_table_columns.Append( self.add_index ) - - self.m_menu21 = wx.Menu() - self.m_menuItem8 = wx.MenuItem( self.m_menu21, wx.ID_ANY, _(u"Add PrimaryKey"), wx.EmptyString, wx.ITEM_NORMAL ) - self.m_menu21.Append( self.m_menuItem8 ) - - self.m_menuItem9 = wx.MenuItem( self.m_menu21, wx.ID_ANY, _(u"Add Index"), wx.EmptyString, wx.ITEM_NORMAL ) - self.m_menu21.Append( self.m_menuItem9 ) - - self.menu_table_columns.AppendSubMenu( self.m_menu21, _(u"MyMenu") ) - - self.panel_table_columns.Bind( wx.EVT_RIGHT_DOWN, self.panel_table_columnsOnContextMenu ) - - self.m_splitter41.SplitHorizontally( self.m_panel19, self.panel_table_columns, 200 ) - bSizer251.Add( self.m_splitter41, 1, wx.EXPAND, 0 ) - - - self.SetSizer( bSizer251 ) - self.Layout() - - # Connect Events - self.btn_insert_column.Bind( wx.EVT_BUTTON, self.on_column_insert ) - self.btn_column_delete.Bind( wx.EVT_BUTTON, self.on_column_delete ) - self.btn_column_move_up.Bind( wx.EVT_BUTTON, self.on_column_move_up ) - self.btn_column_move_down.Bind( wx.EVT_BUTTON, self.on_column_move_down ) - self.btn_table_delete.Bind( wx.EVT_BUTTON, self.on_delete_table ) - self.btn_table_cancel.Bind( wx.EVT_BUTTON, self.do_cancel_table ) - self.btn_table_save.Bind( wx.EVT_BUTTON, self.do_save_table ) - - def __del__( self ): - pass - - - # Virtual event handlers, override them in your derived class - def on_column_insert( self, event ): - event.Skip() - - def on_column_delete( self, event ): - event.Skip() - - def on_column_move_up( self, event ): - event.Skip() - - def on_column_move_down( self, event ): - event.Skip() - - def on_delete_table( self, event ): - event.Skip() - - def do_cancel_table( self, event ): - event.Skip() - - def do_save_table( self, event ): - event.Skip() - - def m_splitter41OnIdle( self, event ): - self.m_splitter41.SetSashPosition( 200 ) - self.m_splitter41.Unbind( wx.EVT_IDLE ) - - def panel_table_columnsOnContextMenu( self, event ): - self.panel_table_columns.PopupMenu( self.menu_table_columns, event.GetPosition() ) - - -########################################################################### -## Class MyWizard1 +## Class Trash ########################################################################### -class MyWizard1 ( wx.adv.Wizard ): +class Trash ( wx.Frame ): 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 ) + wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.Size( 500,300 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL ) self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) - self.m_pages = [] - - self.Centre( wx.BOTH ) + bSizer147 = wx.BoxSizer( wx.VERTICAL ) - 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 ) + bSizer152 = wx.BoxSizer( wx.VERTICAL ) - bSizer163.Add( bSizer165, 0, wx.EXPAND, 5 ) + bSizer147.Add( bSizer152, 1, wx.EXPAND, 5 ) - self.SetSizer( bSizer163 ) + self.SetSizer( bSizer147 ) self.Layout() - bSizer163.Fit( self ) self.Centre( wx.BOTH )