From ab6756e1cca2425c9ddb3ff83d39e4502493bc5a Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 12 Mar 2026 17:19:23 +0100 Subject: [PATCH 01/93] chore: migrate gettext strings to python brace format AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- PeterSQL.fbp | 1042 ++++++++++++++--- main.py | 5 +- settings.yml | 19 +- structures/configurations.py | 8 +- structures/engines/mariadb/context.py | 40 +- structures/engines/mysql/context.py | 38 +- structures/engines/postgresql/context.py | 22 +- structures/engines/sqlite/context.py | 12 +- structures/ssh_tunnel.py | 17 +- tests/engines/mariadb/test_context.py | 6 +- tests/engines/mysql/test_context.py | 6 +- windows/components/dataview.py | 40 +- .../stc/autocomplete/auto_complete.py | 6 +- .../stc/autocomplete/autocomplete_popup.py | 4 +- .../advanced_cell_editor/controller.py | 31 +- windows/dialogs/connections/controller.py | 2 - windows/dialogs/connections/model.py | 44 +- windows/dialogs/connections/repository.py | 41 +- windows/dialogs/connections/view.py | 142 +-- windows/main/controller.py | 394 ++++++- windows/main/tabs/query.py | 88 +- windows/main/tabs/records.py | 18 +- windows/views.py | 127 +- 23 files changed, 1776 insertions(+), 376 deletions(-) diff --git a/PeterSQL.fbp b/PeterSQL.fbp index ec7b972..2b5d944 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -50,7 +50,7 @@ -1,-1 ConnectionsDialog - 800,600 + 900,768 wxDEFAULT_DIALOG_STYLE|wxDIALOG_NO_PARENT|wxRESIZE_BORDER ; ; forward_declare Connection @@ -1436,6 +1436,142 @@ + + 5 + wxEXPAND + 1 + + + bSizer159 + 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 + Connection timeout + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText84 + 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 + 10 + 60 + + 0 + + 0 + + 0 + + 1 + connection_timeout + 1 + + + protected + 1 + + Resizable + 1 + + wxSP_ARROW_KEYS + ; ; forward_declare + 0 + + + + + + + + + 5 wxEXPAND @@ -1497,7 +1633,7 @@ 0 1 - use_tls_enabled + use_tls 1 @@ -1520,6 +1656,27 @@ + + + + 5 + wxEXPAND + 0 + + + bSizer163 + wxHORIZONTAL + none + + 5 + wxEXPAND + 0 + + 0 + protected + 156 + + 5 wxALL @@ -1589,61 +1746,88 @@ 5 - wxEXPAND | 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_staticline5 - 1 - - - protected - 1 - - Resizable - 1 - - wxLI_HORIZONTAL - ; ; forward_declare - 0 - - - - + bSizer164 + wxHORIZONTAL + none + + 5 + wxEXPAND + 0 + + 0 + protected + 156 + + + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Compressed client/server protocol + + 0 + + + 0 + + 1 + compressed_protocol + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + @@ -1852,6 +2036,65 @@ + + 5 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_staticline5 + 1 + + + protected + 1 + + Resizable + 1 + + wxLI_HORIZONTAL + ; ; forward_declare + 0 + + + + + + 5 wxEXPAND @@ -3263,9 +3506,141 @@ 5 - wxALL|wxEXPAND - 0 - + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + Load From File; icons/16x16/information.png + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_bitmap1 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + Remote host/port is the real DB target (defaults to DB Host/Port). + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer121322 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + SSH extra args + 0 + + 0 + + + 0 + -1,-1 + 1 + m_staticText21322 + 1 + + + protected + 1 + + Resizable + 1 + 150,-1 + + + 0 + + + + + -1 + + + + 5 + wxALIGN_CENTER|wxALL + 1 + 1 1 1 @@ -3276,7 +3651,6 @@ 0 - Load From File; icons/16x16/information.png 1 0 @@ -3298,11 +3672,12 @@ 0 + 0 0 1 - m_bitmap1 + ssh_tunnel_extra_args 1 @@ -3312,9 +3687,15 @@ Resizable 1 - ; ; forward_declare + + 0 - Remote host/port is the real DB target (defaults to DB Host/Port). + + + wxFILTER_NONE + wxDefaultValidator + + @@ -10917,7 +11298,7 @@ Load From File; icons/16x16/table.png Table - 1 + 0 1 1 @@ -16030,11 +16411,11 @@ wxTAB_TRAVERSAL - + Load From File; icons/16x16/text_columns.png Data - 0 - + 1 + 1 1 1 @@ -16086,25 +16467,310 @@ wxTAB_TRAVERSAL - + bSizer61 wxVERTICAL none - + 5 wxEXPAND 0 - + bSizer94 wxHORIZONTAL none 5 - wxALL + 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 @@ -16113,15 +16779,20 @@ 0 0 + 0 + Load From File; icons/16x16/resultset_next.png 1 0 1 1 + + 0 0 + Dock 0 Left @@ -16129,11 +16800,13 @@ 1 1 + 0 0 wxID_ANY - Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total + + 0 0 @@ -16142,42 +16815,37 @@ 0 1 - name_database_table + btn_next_records 1 protected 1 + + Resizable 1 - + wxBORDER_NONE|wxBU_EXACTFIT|wxBU_NOTEXT ; ; forward_declare 0 + + wxFILTER_NONE + wxDefaultValidator + - -1 + on_next_records - - - - 5 - wxEXPAND - 0 - - - bSizer83 - wxHORIZONTAL - none - + 5 - wxALIGN_CENTER|wxALL + wxALL|wxEXPAND 0 - + 1 1 1 @@ -16189,7 +16857,7 @@ 0 - Load From File; icons/16x16/add.png + Load From File; icons/16x16/resultset_last.png 1 0 @@ -16212,7 +16880,7 @@ 0 0 wxID_ANY - Insert record + Last 0 @@ -16222,14 +16890,14 @@ 0 1 - btn_insert_record + btn_last_records 1 protected 1 - + wxRIGHT Resizable 1 @@ -16245,9 +16913,20 @@ - on_insert_record + on_last_records + + + + 5 + wxEXPAND + 0 + + + bSizer83 + wxHORIZONTAL + none 5 wxALIGN_CENTER|wxALL @@ -16279,7 +16958,7 @@ 0 Left 0 - 0 + 1 1 @@ -16287,7 +16966,7 @@ 0 0 wxID_ANY - Duplicate record + Insert record 0 @@ -16297,7 +16976,7 @@ 0 1 - btn_duplicate_record + btn_insert_record 1 @@ -16320,7 +16999,7 @@ - on_duplicate_record + on_insert_record @@ -16339,7 +17018,7 @@ 0 - Load From File; icons/16x16/delete.png + Load From File; icons/16x16/add.png 1 0 @@ -16362,7 +17041,7 @@ 0 0 wxID_ANY - Delete record + Duplicate record 0 @@ -16372,7 +17051,7 @@ 0 1 - btn_delete_record + btn_duplicate_record 1 @@ -16395,14 +17074,14 @@ - on_delete_record + on_duplicate_record 5 - wxEXPAND | wxALL + wxALIGN_CENTER|wxALL 0 - + 1 1 1 @@ -16411,26 +17090,35 @@ 0 0 + 0 + Load From File; icons/16x16/delete.png 1 0 1 1 + + 0 0 + Dock 0 Left 0 - 1 + 0 1 + 0 0 wxID_ANY + Delete record + + 0 0 @@ -16438,30 +17126,37 @@ 0 1 - m_staticline3 + btn_delete_record 1 protected 1 + + Resizable 1 - wxLI_VERTICAL + wxBORDER_NONE ; ; forward_declare 0 + + wxFILTER_NONE + wxDefaultValidator + + on_delete_record 5 - wxALIGN_CENTER|wxALL + wxEXPAND | wxALL 0 - + 1 1 1 @@ -16475,9 +17170,8 @@ 1 0 - 1 1 - If enabled, table edits are applied immediately without pressing Apply or Cancel + 1 0 Dock @@ -16491,7 +17185,6 @@ 0 0 wxID_ANY - Apply changes automatically 0 @@ -16499,7 +17192,7 @@ 0 1 - chb_auto_apply + m_staticline3 1 @@ -16509,25 +17202,20 @@ Resizable 1 - + wxLI_VERTICAL ; ; forward_declare 0 - If enabled, table edits are applied immediately without pressing Apply or Cancel - - wxFILTER_NONE - wxDefaultValidator - + - on_auto_apply 5 wxALIGN_CENTER|wxALL 0 - + 1 1 1 @@ -16536,35 +17224,28 @@ 0 0 - 0 - Load From File; icons/16x16/cancel.png 1 0 + 1 1 - + If enabled, table edits are applied immediately without pressing Apply or Cancel 1 - - 0 0 - Dock 0 Left 0 - 0 + 1 1 - 0 0 wxID_ANY - Cancel - - 0 + Apply changes automatically 0 @@ -16572,22 +17253,20 @@ 0 1 - btn_cancel_record + chb_auto_apply 1 protected 1 - - Resizable 1 - wxBORDER_NONE + ; ; forward_declare 0 - + If enabled, table edits are applied immediately without pressing Apply or Cancel wxFILTER_NONE wxDefaultValidator @@ -16595,6 +17274,7 @@ + on_auto_apply @@ -16613,7 +17293,7 @@ 0 - Load From File; icons/16x16/disk.png + Load From File; icons/16x16/cancel.png 1 0 @@ -16636,7 +17316,7 @@ 0 0 wxID_ANY - Apply + Cancel 0 @@ -16646,7 +17326,7 @@ 0 1 - btn_apply_record + btn_cancel_record 1 @@ -16673,17 +17353,7 @@ 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL + wxALIGN_CENTER|wxALL 0 1 @@ -16697,7 +17367,7 @@ 0 - Load From File; icons/16x16/resultset_next.png + Load From File; icons/16x16/disk.png 1 0 @@ -16712,7 +17382,7 @@ 0 Left 0 - 1 + 0 1 @@ -16720,7 +17390,7 @@ 0 0 wxID_ANY - Next + Apply 0 @@ -16730,7 +17400,7 @@ 0 1 - m_button40 + btn_apply_record 1 @@ -16753,7 +17423,6 @@ - on_next_records @@ -17002,7 +17671,7 @@ - + MyMenu m_menu10 protected @@ -18049,6 +18718,65 @@ + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + Load From File; icons/16x16/hourglass.png + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + total_rows_loading + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + + + 5 wxEXPAND diff --git a/main.py b/main.py index 0fee084..9fb84e8 100755 --- a/main.py +++ b/main.py @@ -67,7 +67,10 @@ def _init_locale(self): _locale = self.settings.get_value("locale") if _locale is None: - _locale, encoding = locale.getdefaultlocale() + _locale = locale.getlocale()[0] + + if not _locale: + _locale = "en_US" translation = gettext.translation( 'petersql', diff --git a/settings.yml b/settings.yml index 9f18920..ae75cb9 100755 --- a/settings.yml +++ b/settings.yml @@ -1,5 +1,5 @@ window: - size: 1920,1048 + size: 1920,1080 position: 0,0 appearance: theme: petersql @@ -12,13 +12,12 @@ shortcuts: query_editor: execute: F5 execute_selection: Ctrl+Enter -settings: - autocomplete: - debounce_ms: 80 - min_prefix_length: 1 - add_space_after_completion: true - popup_width: 300 - popup_max_height: 10 +autocomplete: + debounce_ms: 80 + min_prefix_length: 1 + add_space_after_completion: true + popup_width: 300 + popup_max_height: 10 language: en_US query_editor: statement_separator: ; @@ -30,3 +29,7 @@ advanced: connection_timeout: 10 query_timeout: 10 logging_level: INFO +connections_dialog: + expanded_directories: + - - 3 +records: {} diff --git a/structures/configurations.py b/structures/configurations.py index 9bcca82..f071b3b 100644 --- a/structures/configurations.py +++ b/structures/configurations.py @@ -1,4 +1,4 @@ -from typing import NamedTuple, Optional +from typing import NamedTuple, Optional, Union class CredentialsConfiguration(NamedTuple): @@ -6,7 +6,9 @@ class CredentialsConfiguration(NamedTuple): username: str password: Optional[str] port: int - use_tls_enabled: bool = False + use_tls: bool = False + connect_timeout: int = 10 + compressed_protocol: bool = False class SourceConfiguration(NamedTuple): @@ -24,7 +26,7 @@ class SSHTunnelConfiguration(NamedTuple): remote_host: Optional[str] = None remote_port: Optional[int] = None identity_file: Optional[str] = None - extra_args: Optional[list[str]] = None + extra_args: Optional[Union[str, list[str]]] = None @property def is_enabled(self) -> bool: diff --git a/structures/engines/mariadb/context.py b/structures/engines/mariadb/context.py index af4284b..0a93d7f 100755 --- a/structures/engines/mariadb/context.py +++ b/structures/engines/mariadb/context.py @@ -120,9 +120,19 @@ def connect(self, **connect_kwargs) -> None: if self._connection is None: self.before_connect() - use_tls_enabled = bool( - getattr(self.connection.configuration, "use_tls_enabled", False) + connect_timeout_override = connect_kwargs.pop("connect_timeout", None) + compressed_protocol_override = connect_kwargs.pop("compress", None) + compressed_protocol = bool( + compressed_protocol_override + if compressed_protocol_override is not None + else getattr(self.connection.configuration, "compressed_protocol", False) ) + connect_timeout = ( + int(connect_timeout_override) + if connect_timeout_override is not None + else int(getattr(self.connection.configuration, "connect_timeout", 10)) + ) + use_tls = bool(getattr(self.connection.configuration, "use_tls", False)) try: base_kwargs = dict( @@ -131,19 +141,23 @@ def connect(self, **connect_kwargs) -> None: password=self.password, cursorclass=pymysql.cursors.DictCursor, port=self.port, + connect_timeout=connect_timeout, + compress=compressed_protocol, **connect_kwargs, ) - if use_tls_enabled: + if use_tls: base_kwargs["ssl"] = { "cert_reqs": ssl.CERT_NONE, "check_hostname": False, } logger.debug( - "MariaDB connect target host=%s port=%s user=%s use_tls_enabled=%s", + "MariaDB connect target host=%s port=%s user=%s compressed_protocol=%s connect_timeout=%s use_tls=%s", base_kwargs.get("host"), base_kwargs.get("port"), base_kwargs.get("user"), - use_tls_enabled, + compressed_protocol, + connect_timeout, + use_tls, ) # # # SSH tunnel support via connection configuration @@ -190,7 +204,7 @@ def connect(self, **connect_kwargs) -> None: if hasattr(self.connection, "configuration"): self.connection.configuration = ( - self.connection.configuration._replace(use_tls_enabled=True) + self.connection.configuration._replace(use_tls=True) ) logger.info( "MariaDB connection succeeded after enabling TLS automatically." @@ -575,7 +589,7 @@ def build_empty_table( id = MariaDBContext.get_temporary_id(database.tables) if name is None: - name = _(f"Table{str(id * -1):03}") + name = _("Table{table_index:03}").format(table_index=id * -1) default_values.setdefault("engine", "InnoDB") default_values.setdefault("collation_name", "utf8mb4_general_ci") @@ -603,7 +617,7 @@ def build_empty_column( id = MariaDBContext.get_temporary_id(table.columns) if name is None: - name = _(f"Column{str(id * -1):03}") + name = _("Column{column_index:03}").format(column_index=id * -1) return MariaDBColumn( id=id, name=name, table=table, datatype=datatype, **default_values @@ -621,7 +635,7 @@ def build_empty_index( id = MariaDBContext.get_temporary_id(table.indexes) if name is None: - name = _(f"Index{str(id * -1):03}") + name = _("Index{index_number:03}").format(index_number=id * -1) return MariaDBIndex( id=id, @@ -661,7 +675,9 @@ def build_empty_foreign_key( id = MariaDBContext.get_temporary_id(table.foreign_keys) if name is None: - name = _(f"ForeignKey{str(id * -1):03}") + name = _("ForeignKey{foreign_key_number:03}").format( + foreign_key_number=id * -1 + ) reference_table = default_values.get("reference_table", "") reference_columns = default_values.get("reference_columns", []) @@ -692,7 +708,7 @@ def build_empty_view( id = MariaDBContext.get_temporary_id(database.views) if name is None: - name = _(f"View{str(id * -1):03}") + name = _("View{view_index:03}").format(view_index=id * -1) return MariaDBView( id=id, @@ -743,7 +759,7 @@ def build_empty_trigger( id = MariaDBContext.get_temporary_id(database.triggers) if name is None: - name = _(f"Trigger{str(id * -1):03}") + name = _("Trigger{trigger_index:03}").format(trigger_index=id * -1) return MariaDBTrigger( id=id, diff --git a/structures/engines/mysql/context.py b/structures/engines/mysql/context.py index ffd1012..b3061aa 100644 --- a/structures/engines/mysql/context.py +++ b/structures/engines/mysql/context.py @@ -122,9 +122,19 @@ def connect(self, **connect_kwargs) -> None: if self._connection is None: self.before_connect() - use_tls_enabled = bool( - getattr(self.connection.configuration, "use_tls_enabled", False) + connect_timeout_override = connect_kwargs.pop("connect_timeout", None) + compressed_protocol_override = connect_kwargs.pop("compress", None) + compressed_protocol = bool( + compressed_protocol_override + if compressed_protocol_override is not None + else getattr(self.connection.configuration, "compressed_protocol", False) ) + connect_timeout = ( + int(connect_timeout_override) + if connect_timeout_override is not None + else int(getattr(self.connection.configuration, "connect_timeout", 10)) + ) + use_tls = bool(getattr(self.connection.configuration, "use_tls", False)) base_kwargs = dict( host=self.host, @@ -132,19 +142,23 @@ def connect(self, **connect_kwargs) -> None: password=self.password, port=self.port, cursorclass=pymysql.cursors.DictCursor, + connect_timeout=connect_timeout, + compress=compressed_protocol, **connect_kwargs, ) - if use_tls_enabled: + if use_tls: base_kwargs["ssl"] = { "cert_reqs": ssl.CERT_NONE, "check_hostname": False, } logger.debug( - "MySQL connect target host=%s port=%s user=%s use_tls_enabled=%s", + "MySQL connect target host=%s port=%s user=%s compressed_protocol=%s connect_timeout=%s use_tls=%s", base_kwargs.get("host"), base_kwargs.get("port"), base_kwargs.get("user"), - use_tls_enabled, + compressed_protocol, + connect_timeout, + use_tls, ) try: @@ -175,7 +189,7 @@ def connect(self, **connect_kwargs) -> None: if hasattr(self.connection, "configuration"): self.connection.configuration = ( - self.connection.configuration._replace(use_tls_enabled=True) + self.connection.configuration._replace(use_tls=True) ) logger.info( "MySQL connection succeeded after enabling TLS automatically." @@ -546,7 +560,7 @@ def build_empty_table( id = MySQLContext.get_temporary_id(database.tables) if name is None: - name = _(f"Table{str(id * -1):03}") + name = _("Table{table_index:03}").format(table_index=id * -1) default_values.setdefault("engine", "InnoDB") default_values.setdefault("collation_name", "utf8mb4_general_ci") @@ -574,7 +588,7 @@ def build_empty_column( id = MySQLContext.get_temporary_id(table.columns) if name is None: - name = _(f"Column{str(id * -1):03}") + name = _("Column{column_index:03}").format(column_index=id * -1) return MySQLColumn( id=id, name=name, table=table, datatype=datatype, **default_values @@ -592,7 +606,7 @@ def build_empty_index( id = MySQLContext.get_temporary_id(table.indexes) if name is None: - name = _(f"Index{str(id * -1):03}") + name = _("Index{index_number:03}").format(index_number=id * -1) return MySQLIndex( id=id, @@ -632,7 +646,9 @@ def build_empty_foreign_key( id = MySQLContext.get_temporary_id(table.foreign_keys) if name is None: - name = _(f"ForeignKey{str(id * -1):03}") + name = _("ForeignKey{foreign_key_number:03}").format( + foreign_key_number=id * -1 + ) reference_table = default_values.get("reference_table", "") reference_columns = default_values.get("reference_columns", []) @@ -661,7 +677,7 @@ def build_empty_view( id = MySQLContext.get_temporary_id(database.views) if name is None: - name = _(f"View{str(id * -1):03}") + name = _("View{view_index:03}").format(view_index=id * -1) return MySQLView( id=id, diff --git a/structures/engines/postgresql/context.py b/structures/engines/postgresql/context.py index d058073..8750c81 100644 --- a/structures/engines/postgresql/context.py +++ b/structures/engines/postgresql/context.py @@ -107,6 +107,12 @@ def connect(self, **connect_kwargs) -> None: try: self.before_connect() database = connect_kwargs.pop("database", "postgres") + connect_timeout_override = connect_kwargs.pop("connect_timeout", None) + connect_timeout = ( + int(connect_timeout_override) + if connect_timeout_override is not None + else int(getattr(self.connection.configuration, "connect_timeout", 10)) + ) base_kwargs = dict( host=self.host, @@ -114,14 +120,16 @@ def connect(self, **connect_kwargs) -> None: password=self.password, database=database, port=self.port, + connect_timeout=connect_timeout, **connect_kwargs, ) logger.debug( - "PostgreSQL connect target host=%s port=%s user=%s database=%s", + "PostgreSQL connect target host=%s port=%s user=%s database=%s connect_timeout=%s", base_kwargs.get("host"), base_kwargs.get("port"), base_kwargs.get("user"), base_kwargs.get("database"), + base_kwargs.get("connect_timeout"), ) self._connection = psycopg2.connect(**base_kwargs) @@ -568,7 +576,7 @@ def build_empty_table( id = PostgreSQLContext.get_temporary_id(database.tables) if name is None: - name = _(f"Table{str(id * -1):03}") + name = _("Table{table_index:03}").format(table_index=id * -1) return PostgreSQLTable( id=id, @@ -593,7 +601,7 @@ def build_empty_column( id = PostgreSQLContext.get_temporary_id(table.columns) if name is None: - name = _(f"Column{str(id * -1):03}") + name = _("Column{column_index:03}").format(column_index=id * -1) return PostgreSQLColumn( id=id, name=name, table=table, datatype=datatype, **default_values @@ -611,7 +619,7 @@ def build_empty_index( id = PostgreSQLContext.get_temporary_id(table.indexes) if name is None: - name = _(f"Index{str(id * -1):03}") + name = _("Index{index_number:03}").format(index_number=id * -1) return PostgreSQLIndex( id=id, @@ -651,7 +659,9 @@ def build_empty_foreign_key( id = PostgreSQLContext.get_temporary_id(table.foreign_keys) if name is None: - name = _(f"ForeignKey{str(id * -1):03}") + name = _("ForeignKey{foreign_key_number:03}").format( + foreign_key_number=id * -1 + ) return PostgreSQLForeignKey( id=id, @@ -679,7 +689,7 @@ def build_empty_view( id = PostgreSQLContext.get_temporary_id(database.views) if name is None: - name = _(f"View{str(id * -1):03}") + name = _("View{view_index:03}").format(view_index=id * -1) return PostgreSQLView( id=id, diff --git a/structures/engines/sqlite/context.py b/structures/engines/sqlite/context.py index 23b1ebc..7a7fb81 100755 --- a/structures/engines/sqlite/context.py +++ b/structures/engines/sqlite/context.py @@ -515,7 +515,7 @@ def build_empty_table( id = SQLiteContext.get_temporary_id(database.tables) if name is None: - name = _(f"Table{str(id * -1):03}") + name = _("Table{table_index:03}").format(table_index=id * -1) return SQLiteTable( id=id, @@ -539,7 +539,7 @@ def build_empty_column( id = SQLiteContext.get_temporary_id(table.columns) if name is None: - name = _(f"Column{str(id * -1):03}") + name = _("Column{column_index:03}").format(column_index=id * -1) return SQLiteColumn( id=id, name=name, table=table, datatype=datatype, **default_values @@ -557,7 +557,7 @@ def build_empty_index( id = SQLiteContext.get_temporary_id(table.indexes) if name is None: - name = _(f"Index{str(id * -1):03}") + name = _("Index{index_number:03}").format(index_number=id * -1) return SQLiteIndex( id=id, @@ -599,7 +599,9 @@ def build_empty_foreign_key( id = SQLiteContext.get_temporary_id(table.foreign_keys) if name is None: - name = _(f"ForeignKey{str(id * -1):03}") + name = _("ForeignKey{foreign_key_number:03}").format( + foreign_key_number=id * -1 + ) return SQLiteForeignKey( id=id, @@ -625,7 +627,7 @@ def build_empty_view( id = SQLiteContext.get_temporary_id(database.views) if name is None: - name = _(f"View{str(id * -1):03}") + name = _("View{view_index:03}").format(view_index=id * -1) return SQLiteView( id=id, diff --git a/structures/ssh_tunnel.py b/structures/ssh_tunnel.py index 24b960b..73fc88e 100644 --- a/structures/ssh_tunnel.py +++ b/structures/ssh_tunnel.py @@ -1,12 +1,13 @@ import atexit import os +import shlex import shutil import signal import socket import subprocess import time -from typing import Optional +from typing import Optional, Union from gettext import gettext as _ @@ -27,7 +28,7 @@ def __init__( local_bind_address: tuple[str, int] = ("localhost", 0), ssh_executable: str = "ssh", identity_file: Optional[str] = None, - extra_args: Optional[list[str]] = None, + extra_args: Optional[Union[str, list[str]]] = None, ): self.ssh_hostname = ssh_hostname self.ssh_port = ssh_port @@ -40,9 +41,19 @@ def __init__( self.ssh_executable = ssh_executable self.identity_file = identity_file - self.extra_args = extra_args or [] + self.extra_args = self._normalize_extra_args(extra_args) self._process: Optional[subprocess.Popen] = None + @staticmethod + def _normalize_extra_args(extra_args: Optional[Union[str, list[str]]]) -> list[str]: + if not extra_args: + return [] + + if isinstance(extra_args, list): + return [str(value) for value in extra_args if str(value).strip()] + + return [value for value in shlex.split(extra_args) if value.strip()] + def __enter__(self): self.start() return self.local_port diff --git a/tests/engines/mariadb/test_context.py b/tests/engines/mariadb/test_context.py index fcdd160..5f8ade7 100644 --- a/tests/engines/mariadb/test_context.py +++ b/tests/engines/mariadb/test_context.py @@ -13,6 +13,8 @@ def test_connect_retries_with_tls_on_auth_error(self, monkeypatch): username="root", password="secret", port=3306, + connect_timeout=4, + compressed_protocol=True, ) connection = Connection( id=1, @@ -41,9 +43,11 @@ def fake_connect(**kwargs): context.connect(connect_timeout=1) assert len(calls) == 2 + assert calls[0]["connect_timeout"] == 1 + assert calls[0]["compress"] is True assert "ssl" not in calls[0] assert "ssl" in calls[1] - assert connection.configuration.use_tls_enabled is True + assert connection.configuration.use_tls is True @pytest.mark.integration diff --git a/tests/engines/mysql/test_context.py b/tests/engines/mysql/test_context.py index 523e7e2..e5d157b 100644 --- a/tests/engines/mysql/test_context.py +++ b/tests/engines/mysql/test_context.py @@ -13,6 +13,8 @@ def test_connect_retries_with_tls_on_auth_error(self, monkeypatch): username="root", password="secret", port=3306, + connect_timeout=3, + compressed_protocol=True, ) connection = Connection( id=1, @@ -41,9 +43,11 @@ def fake_connect(**kwargs): context.connect(connect_timeout=1) assert len(calls) == 2 + assert calls[0]["connect_timeout"] == 1 + assert calls[0]["compress"] is True assert "ssl" not in calls[0] assert "ssl" in calls[1] - assert connection.configuration.use_tls_enabled is True + assert connection.configuration.use_tls is True @pytest.mark.integration diff --git a/windows/components/dataview.py b/windows/components/dataview.py index 618b748..21e3eab 100644 --- a/windows/components/dataview.py +++ b/windows/components/dataview.py @@ -16,6 +16,7 @@ from windows.components import BaseDataViewCtrl 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.dialogs.advanced_cell_editor import AdvancedCellEditorController from windows.state import CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE, NEW_TABLE @@ -449,7 +450,44 @@ def autosize_columns_from_content(self, sample_rows: int = 30): class QueryEditorResultsDataViewCtrl(TableRecordsDataViewCtrl): - pass + def __init__(self, *args, **kwargs): + BaseDataViewCtrl.__init__(self, *args, **kwargs) + self.Bind(wx.dataview.EVT_DATAVIEW_ITEM_ACTIVATED, self._on_item_activated) + + def _get_activated_model_column(self, event: wx.dataview.DataViewEvent) -> Optional[int]: + current_column = self.CurrentColumn + if current_column: + return current_column.GetModelColumn() + + model_column = event.GetColumn() + return model_column if model_column >= 0 else None + + def _on_item_activated(self, event: wx.dataview.DataViewEvent) -> None: + item = event.GetItem() + if not item.IsOk(): + event.Skip() + return + + model = self.GetModel() + if not model: + event.Skip() + return + + model_column = self._get_activated_model_column(event) + if model_column is None: + event.Skip() + return + + row = model.GetRow(item) + value = model.GetValueByRow(row, model_column) + + dialog = AdvancedCellEditorController(self, str(value or ""), read_only=True) + try: + dialog.ShowModal() + finally: + dialog.Destroy() + + event.Skip() class DatabaseTablesDataViewCtrl(BaseDataViewCtrl): diff --git a/windows/components/stc/autocomplete/auto_complete.py b/windows/components/stc/autocomplete/auto_complete.py index 65195f4..067292d 100644 --- a/windows/components/stc/autocomplete/auto_complete.py +++ b/windows/components/stc/autocomplete/auto_complete.py @@ -126,15 +126,15 @@ def __init__( if settings: self._debounce_ms = ( - settings.get_value("settings", "autocomplete", "debounce_ms") + settings.get_value("autocomplete", "debounce_ms") or debounce_ms ) self._min_prefix_length = ( - settings.get_value("settings", "autocomplete", "min_prefix_length") + settings.get_value("autocomplete", "min_prefix_length") or min_prefix_length ) self._add_space_after_completion = settings.get_value( - "settings", "autocomplete", "add_space_after_completion" + "autocomplete", "add_space_after_completion" ) if self._add_space_after_completion is None: self._add_space_after_completion = True diff --git a/windows/components/stc/autocomplete/autocomplete_popup.py b/windows/components/stc/autocomplete/autocomplete_popup.py index e7ada99..86687e5 100644 --- a/windows/components/stc/autocomplete/autocomplete_popup.py +++ b/windows/components/stc/autocomplete/autocomplete_popup.py @@ -16,8 +16,8 @@ def __init__(self, parent: wx.Window, settings: object = None, theme_loader: The self._theme_loader = theme_loader if settings: - self._popup_width = settings.get_value("settings", "autocomplete", "popup_width") or 300 - self._popup_max_height = settings.get_value("settings", "autocomplete", "popup_max_height") or 10 + self._popup_width = settings.get_value("autocomplete", "popup_width") or 300 + self._popup_max_height = settings.get_value("autocomplete", "popup_max_height") or 10 else: self._popup_width = 300 self._popup_max_height = 10 diff --git a/windows/dialogs/advanced_cell_editor/controller.py b/windows/dialogs/advanced_cell_editor/controller.py index 15ceb12..db2a036 100644 --- a/windows/dialogs/advanced_cell_editor/controller.py +++ b/windows/dialogs/advanced_cell_editor/controller.py @@ -1,3 +1,5 @@ +from typing import Optional + import wx from windows.components.stc.detectors import detect_syntax_id @@ -9,9 +11,11 @@ class AdvancedCellEditorController(AdvancedCellEditorDialog): app = wx.GetApp() - def __init__(self, parent, value: str): + def __init__(self, parent, value: str, read_only: bool = False): super().__init__(parent) + self._read_only = read_only + self.syntax_choice.AppendItems(self.app.syntax_registry.labels()) self.advanced_stc_editor.SetText(value or "") self.advanced_stc_editor.EmptyUndoBuffer() @@ -21,6 +25,10 @@ def __init__(self, parent, value: str): self.syntax_choice.SetStringSelection(self._auto_syntax_profile().label) self.do_apply_syntax(do_format=True) + self._apply_read_only_state() + + self.m_button48.Bind(wx.EVT_BUTTON, self._on_ok) + self.m_button49.Bind(wx.EVT_BUTTON, self._on_cancel) def _auto_syntax_profile(self) -> SyntaxProfile: text = self.advanced_stc_editor.GetText() @@ -33,8 +41,22 @@ def _get_current_syntax_profile(self) -> SyntaxProfile: return self.app.syntax_registry.get(label) def on_syntax_changed(self, _evt): - label = self.syntax_choice.GetStringSelection() - self.do_apply_syntax(label) + self.do_apply_syntax(do_format=True) + + def _apply_read_only_state(self) -> None: + if not self._read_only: + return + + self.advanced_stc_editor.SetReadOnly(True) + self.m_button49.Hide() + self.m_button48.SetLabel("Close") + self.Layout() + + def _on_ok(self, _evt: wx.Event) -> None: + self.EndModal(wx.ID_OK) + + def _on_cancel(self, _evt: wx.Event) -> None: + self.EndModal(wx.ID_CANCEL) def do_apply_syntax(self, do_format: bool = True): label = self.syntax_choice.GetStringSelection() @@ -58,3 +80,6 @@ def _replace_text_undo_friendly(self, new_text: str): self.advanced_stc_editor.SetText(new_text) finally: self.advanced_stc_editor.EndUndoAction() + + def get_value(self) -> Optional[str]: + return self.advanced_stc_editor.GetText() diff --git a/windows/dialogs/connections/controller.py b/windows/dialogs/connections/controller.py index 7acfbb8..c2f4ebf 100644 --- a/windows/dialogs/connections/controller.py +++ b/windows/dialogs/connections/controller.py @@ -133,8 +133,6 @@ def _on_item_start_editing(self, event): self._allow_next_edit = False def do_filter_connections(self, search_text): - # self.search_text = search_text - # self._update_displayed_connections() self.repository.connections.filter( lambda x: search_text.lower() in x.name.lower() ) diff --git a/windows/dialogs/connections/model.py b/windows/dialogs/connections/model.py index bd9624e..0df3922 100644 --- a/windows/dialogs/connections/model.py +++ b/windows/dialogs/connections/model.py @@ -20,7 +20,9 @@ def __init__(self): self.hostname = Observable[str]() self.username = Observable[str]() self.password = Observable[str]() - self.use_tls_enabled = Observable[bool](initial=False) + self.use_tls = Observable[bool](initial=False) + self.connection_timeout = Observable[int](initial=10) + self.compressed_protocol = Observable[bool](initial=False) self.port = Observable[int](initial=3306) self.filename = Observable[str]() self.comments = Observable[str]("") @@ -45,6 +47,7 @@ def __init__(self): self.ssh_tunnel_identity_file = Observable[str]() self.ssh_tunnel_remote_hostname = Observable[str]() self.ssh_tunnel_remote_port = Observable[int](initial=3306) + self.ssh_tunnel_extra_args = Observable[str]() self.engine.subscribe(self._set_default_port) @@ -54,7 +57,9 @@ def __init__(self): self.hostname, self.username, self.password, - self.use_tls_enabled, + self.use_tls, + self.connection_timeout, + self.compressed_protocol, self.port, self.filename, self.comments, @@ -77,6 +82,7 @@ def __init__(self): self.ssh_tunnel_identity_file, self.ssh_tunnel_remote_hostname, self.ssh_tunnel_remote_port, + self.ssh_tunnel_extra_args, callback=self._build, ) @@ -97,7 +103,9 @@ def clear(self, *args): self.hostname: None, self.username: None, self.password: None, - self.use_tls_enabled: False, + self.use_tls: False, + self.connection_timeout: 10, + self.compressed_protocol: False, self.port: 3306, self.filename: None, self.comments: None, @@ -120,6 +128,7 @@ def clear(self, *args): self.ssh_tunnel_identity_file: None, self.ssh_tunnel_remote_hostname: None, self.ssh_tunnel_remote_port: 3306, + self.ssh_tunnel_extra_args: "", } for observable, value in defaults.items(): @@ -157,8 +166,12 @@ def apply(self, connection: Connection): self.hostname(connection.configuration.hostname) self.username(connection.configuration.username) self.password(connection.configuration.password) - self.use_tls_enabled( - getattr(connection.configuration, "use_tls_enabled", False) + self.use_tls(getattr(connection.configuration, "use_tls", False)) + self.connection_timeout( + getattr(connection.configuration, "connect_timeout", 10) + ) + self.compressed_protocol( + getattr(connection.configuration, "compressed_protocol", False) ) self.port(connection.configuration.port) @@ -178,8 +191,14 @@ def apply(self, connection: Connection): self.ssh_tunnel_remote_port( getattr(ssh_tunnel, "remote_port", None) or self.port() ) + extra_args = getattr(ssh_tunnel, "extra_args", "") + if isinstance(extra_args, list): + self.ssh_tunnel_extra_args(" ".join(extra_args)) + else: + self.ssh_tunnel_extra_args(extra_args or "") else: self.ssh_tunnel_enabled(False) + self.ssh_tunnel_extra_args("") def _build_empty_connection(self): return Connection( @@ -187,7 +206,12 @@ def _build_empty_connection(self): name=self.name() or _("New connection"), engine=ConnectionEngine.MYSQL, configuration=CredentialsConfiguration( - hostname="localhost", username="root", password="", port=3306 + hostname="localhost", + username="root", + password="", + port=3306, + connect_timeout=10, + compressed_protocol=False, ), ) @@ -224,7 +248,9 @@ def _build(self, *args): username=db_username, password=db_password, port=self.port.get_value() or 3306, - use_tls_enabled=bool(self.use_tls_enabled.get_value()), + use_tls=bool(self.use_tls.get_value()), + connect_timeout=self.connection_timeout.get_value() or 10, + compressed_protocol=bool(self.compressed_protocol.get_value()), ) if ssh_tunnel_enabled := bool(self.ssh_tunnel_enabled()): @@ -253,6 +279,10 @@ def _build(self, *args): identity_file=self.ssh_tunnel_identity_file.get_value() or None, remote_host=remote_host, remote_port=remote_port, + extra_args=( + self.ssh_tunnel_extra_args.get_value() or "" + ).strip() + or None, ) elif connection_engine == ConnectionEngine.SQLITE: diff --git a/windows/dialogs/connections/repository.py b/windows/dialogs/connections/repository.py index 5902c22..22f6bcc 100644 --- a/windows/dialogs/connections/repository.py +++ b/windows/dialogs/connections/repository.py @@ -96,7 +96,7 @@ def _connection_from_dict( ConnectionEngine.MARIADB, ConnectionEngine.POSTGRESQL, ]: - configuration = CredentialsConfiguration(**config_data) + configuration = self._build_credentials_configuration(config_data) elif engine == ConnectionEngine.SQLITE: configuration = SourceConfiguration(**config_data) @@ -326,7 +326,44 @@ def _build_ssh_configuration( if data.get("remote_port") else None, identity_file=data.get("identity_file"), - extra_args=data.get("extra_args"), + extra_args=self._normalize_ssh_extra_args(data.get("extra_args")), ) except (TypeError, ValueError): return None + + def _build_credentials_configuration( + self, data: dict[str, Any] + ) -> Optional[CredentialsConfiguration]: + if not data: + return None + + try: + return CredentialsConfiguration( + hostname=str(data.get("hostname", "")), + username=str(data.get("username", "")), + password=data.get("password"), + port=int(data.get("port", 3306)), + use_tls=bool( + data.get("use_tls", data.get("use_tls_enabled", False)) + ), + connect_timeout=int(data.get("connect_timeout", 10)), + compressed_protocol=bool(data.get("compressed_protocol", False)), + ) + except (TypeError, ValueError): + return None + + @staticmethod + def _normalize_ssh_extra_args(extra_args: Any) -> Optional[Union[str, list[str]]]: + if isinstance(extra_args, str): + value = extra_args.strip() + return value if value else None + + if isinstance(extra_args, list): + values = [ + str(value).strip() + for value in extra_args + if isinstance(value, str) and value.strip() + ] + return values if values else None + + return None diff --git a/windows/dialogs/connections/view.py b/windows/dialogs/connections/view.py index 94b4417..16272bf 100644 --- a/windows/dialogs/connections/view.py +++ b/windows/dialogs/connections/view.py @@ -55,7 +55,9 @@ def __init__(self, parent): port=self.port, username=self.username, password=self.password, - use_tls_enabled=self.use_tls_enabled, + use_tls=self.use_tls, + connection_timeout=self.connection_timeout, + compressed_protocol=self.compressed_protocol, filename=self.filename, comments=self.comments, created_at=self.created_at, @@ -77,6 +79,7 @@ def __init__(self, parent): ssh_tunnel_identity_file=self.identity_file, ssh_tunnel_remote_hostname=self.remote_hostname, ssh_tunnel_remote_port=self.remote_port, + ssh_tunnel_extra_args=self.ssh_tunnel_extra_args, ) self.connections_model.engine.subscribe(self._on_change_engine) @@ -195,6 +198,10 @@ def _on_connection_activated(self, connection: Connection): def _on_change_engine(self, value: str): connection_engine = ConnectionEngine.from_name(value) + supports_compressed_protocol = connection_engine in [ + ConnectionEngine.MYSQL, + ConnectionEngine.MARIADB, + ] self.panel_credentials.Show( connection_engine @@ -214,6 +221,9 @@ def _on_change_engine(self, value: str): ) self.panel_source.Show(connection_engine == ConnectionEngine.SQLITE) + self.compressed_protocol.Enable(supports_compressed_protocol) + if not supports_compressed_protocol: + self.connections_model.compressed_protocol(False) self.panel_source.GetParent().Layout() @@ -368,13 +378,10 @@ def _walk(nodes, parent_path=()): _walk(self._repository.connections.get_value()) def do_open_session(self, session: Session): - # CONNECTIONS_LIST.append(connection) - SESSIONS_LIST.append(session) CURRENT_SESSION(session) if not self.GetParent(): - # CURRENT_CONNECTION(connection) self._app.open_main_frame() self.Hide() @@ -403,7 +410,9 @@ def on_save(self, *args): dialog = wx.MessageDialog( None, - message=_(f"Do you want save the connection {connection.name}?"), + message=_("Do you want save the connection {connection_name}?").format( + connection_name=connection.name + ), caption=_("Confirm save"), style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, ) @@ -704,65 +713,57 @@ def on_rename(self, event): def verify_session(self, session: Session): started_at = time.perf_counter() - with Loader.cursor_wait(): - try: - tls_was_enabled = bool( - getattr(session.connection.configuration, "use_tls_enabled", False) - ) + try: + tls_enabled = bool( + getattr(session.connection.configuration, "use_tls", False) + ) - logger.debug( - "Verifying session connection=%s engine=%s host=%s port=%s user=%s use_tls_enabled=%s", - session.connection.name, - session.connection.engine, - getattr(session.connection.configuration, "hostname", None), - getattr(session.connection.configuration, "port", None), - getattr(session.connection.configuration, "username", None), - tls_was_enabled, - ) - session.connect(connect_timeout=10) + connect_timeout = int( + getattr(session.connection.configuration, "connect_timeout", 10) + ) + session.connect(connect_timeout=connect_timeout) - tls_is_enabled = bool( - getattr(session.connection.configuration, "use_tls_enabled", False) - ) - if not tls_was_enabled and tls_is_enabled: - self.connections_model.use_tls_enabled(True) - - if not session.connection.is_new: - self._repository.save_connection(session.connection) - - wx.MessageDialog( - None, - message=_( - "This connection cannot work without TLS. TLS has been enabled automatically." - ), - caption=_("Connection"), - style=wx.OK | wx.ICON_INFORMATION, - ).ShowModal() - - duration_ms = int((time.perf_counter() - started_at) * 1000) - self._record_connection_attempt( - session.connection, - success=True, - duration_ms=duration_ms, - ) - self._sync_statistics_to_model(session.connection) - except Exception as ex: - duration_ms = int((time.perf_counter() - started_at) * 1000) - self._record_connection_attempt( - session.connection, - success=False, - duration_ms=duration_ms, - failure_reason=str(ex), - ) - self._sync_statistics_to_model(session.connection) + if not tls_enabled and bool( + getattr(session.connection.configuration, "use_tls", False) + ): + self.connections_model.use_tls(True) + + if not session.connection.is_new: + self._repository.save_connection(session.connection) wx.MessageDialog( None, - message=_(f"Connection error:\n{str(ex)}"), - caption=_("Connection error"), - style=wx.OK | wx.OK_DEFAULT | wx.ICON_ERROR, + message=_( + "This connection cannot work without TLS. TLS has been enabled automatically." + ), + caption=_("Connection"), + style=wx.OK | wx.ICON_INFORMATION, ).ShowModal() - raise ConnectionError(ex) + + duration_ms = int((time.perf_counter() - started_at) * 1000) + self._record_connection_attempt( + session.connection, + success=True, + duration_ms=duration_ms, + ) + self._sync_statistics_to_model(session.connection) + except Exception as ex: + duration_ms = int((time.perf_counter() - started_at) * 1000) + self._record_connection_attempt( + session.connection, + success=False, + duration_ms=duration_ms, + failure_reason=str(ex), + ) + self._sync_statistics_to_model(session.connection) + + wx.MessageDialog( + None, + message=_("Connection error:\n{error}").format(error=str(ex)), + caption=_("Connection error"), + style=wx.OK | wx.OK_DEFAULT | wx.ICON_ERROR, + ).ShowModal() + raise ConnectionError(ex) def on_connect(self, event): if PENDING_CONNECTION() and not self.on_save(event): @@ -772,19 +773,22 @@ def on_connect(self, event): session = Session(connection) - try: - self.verify_session(session) - except ConnectionError as ex: - logger.info(ex) - except Exception as ex: - logger.error(ex, exc_info=True) - else: - self.do_open_session(session) + with Loader.cursor_wait(): + try: + self.verify_session(session) + except ConnectionError as ex: + logger.info(ex) + except Exception as ex: + logger.error(ex, exc_info=True) + else: + self.do_open_session(session) def on_delete_connection(self, connection: Connection): dialog = wx.MessageDialog( None, - message=_(f"Do you want to delete the connection '{connection.name}'?"), + message=_("Do you want to delete the connection '{connection_name}'?").format( + connection_name=connection.name + ), caption=_("Confirm delete"), style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, ) @@ -799,7 +803,9 @@ def on_delete_connection(self, connection: Connection): def on_delete_directory(self, directory: ConnectionDirectory): dialog = wx.MessageDialog( None, - message=_(f"Do you want to delete the directory '{directory.name}'?"), + message=_("Do you want to delete the directory '{directory_name}'?").format( + directory_name=directory.name + ), caption=_("Confirm delete"), style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, ) diff --git a/windows/main/controller.py b/windows/main/controller.py index 214169e..b4fb83a 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -1,11 +1,14 @@ import math import os +import threading import time +import contextlib from collections import defaultdict from gettext import gettext as _ -from typing import Optional, Union +from typing import Any, Optional, Union +import babel.numbers import psutil import sqlglot import wx.adv @@ -13,6 +16,7 @@ import wx.stc from helpers import bytes_to_human +from helpers.loader import Loader from helpers.logger import logger from helpers.observables import CallbackEvent @@ -76,6 +80,19 @@ def __init__(self): self.controller_view_editor = ViewEditorController(self) + records_limit = self._load_records_limit_from_settings() + self.limit_records.SetValue(records_limit) + + self._records_offset = 0 + self._records_limit = records_limit + self._records_total_rows = 0 + self._records_total_key = None + self._records_total_request_id = 0 + self._records_total_is_loading = False + self._records_label_template = self.name_database_table.GetLabel() + + self.limit_records.Bind(wx.EVT_SPINCTRL, self.on_limit_records_changed) + self._setup_query_editors() self._setup_subscribers() @@ -280,12 +297,305 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S if self.MainFrameNotebook.GetSelection() < 4: self.MainFrameNotebook.SetSelection(3) + def _get_records_filters(self) -> str: + return (self.sql_query_filters.GetSelectedText() or self.sql_query_filters.GetText()).strip() + + def _build_records_total_key(self, table: SQLTable, filters: str) -> tuple[str, str, str, str]: + schema = str(getattr(table, "schema", "") or "") + return table.database.name, schema, table.name, filters + + def _count_table_records( + self, + table: SQLTable, + filters: str, + context: Optional[Any] = None, + ) -> int: + if context is None: + context = table.database.context + + where = f" WHERE {filters}" if filters else "" + schema = str(getattr(table, "schema", "") or "") + + if schema: + from_clause = context.qualify(schema, table.name) + else: + from_clause = context.qualify(table.database.name, table.name) + + query = f"SELECT COUNT(*) AS total_rows FROM {from_clause}{where}" + context.execute(query) + + row = context.fetchone() or {} + total_rows = row.get("total_rows") + if total_rows is None and row: + total_rows = next(iter(row.values()), 0) + + return int(total_rows or 0) + + def _count_table_records_worker( + self, + session: Session, + table: SQLTable, + filters: str, + total_key: tuple[str, str, str, str], + request_id: int, + ) -> None: + total_rows = 0 + error = None + context = None + + try: + context = session._get_context_class()(self._build_records_count_connection(session)) + context.connect() + context.set_database(table.database) + total_rows = self._count_table_records(table, filters, context) + except Exception as ex: + error = str(ex) + logger.warning("Failed async records count: %s", ex, exc_info=True) + finally: + if context is not None: + try: + context.disconnect() + except Exception: + pass + + wx.CallAfter( + self._on_records_count_complete, + total_key, + request_id, + total_rows, + error, + ) + + def _build_records_count_connection(self, session: Session) -> Connection: + connection = session.connection.copy() + + if not connection.has_enabled_tunnel(): + return connection + + context = getattr(session, "context", None) + configuration = getattr(connection, "configuration", None) + + if context is not None and configuration is not None and hasattr(configuration, "_replace"): + replace_kwargs = {} + + if hasattr(configuration, "hostname") and getattr(context, "host", None): + replace_kwargs["hostname"] = context.host + + if hasattr(configuration, "port") and getattr(context, "port", None) is not None: + replace_kwargs["port"] = int(context.port) + + if replace_kwargs: + connection.configuration = configuration._replace(**replace_kwargs) + + connection.ssh_tunnel = None + return connection + + def _format_records_number(self, value: int) -> str: + locale = wx.GetApp().settings.get_value("locale") or wx.GetApp().settings.get_value("language") or "en_US" + try: + return babel.numbers.format_decimal(value, locale=locale) + except Exception: + return str(value) + + def _load_records_limit_from_settings(self) -> int: + settings = wx.GetApp().settings + if settings.get_value("records") is None: + settings.set_value("records", value={}) + + max_limit = 1000 + if hasattr(self, "limit_records"): + with contextlib.suppress(Exception): + max_limit = max(1, int(self.limit_records.GetMax())) + + saved_limit = settings.get_value("records", "limit") + if saved_limit is None: + return min(100, max_limit) + + try: + return min(max(1, int(saved_limit)), max_limit) + except Exception: + return min(100, max_limit) + + def _can_skip_count_query(self, table: SQLTable, filters: str) -> bool: + if filters: + return False + + if table.total_rows is None: + return False + + return int(table.total_rows) <= self._records_limit + + def _get_loaded_records_count(self, table: SQLTable) -> int: + records = getattr(table, "records", None) + if records is None: + return 0 + + if getattr(records, "is_loaded", False): + return len(records) + + return 0 + + def _get_loading_total_text(self, table: SQLTable, filters: str) -> str: + if not filters and table.total_rows is not None: + estimated = self._format_records_number(int(table.total_rows)) + return _("~{estimated} (Loading...)").format(estimated=estimated) + + return _("~ (Loading...)") + + def _refresh_records_total_rows(self, table: SQLTable, filters: str) -> None: + total_key = self._build_records_total_key(table, filters) + if self._records_total_key == total_key: + return + + self._records_total_key = total_key + self._records_total_request_id += 1 + + if self._can_skip_count_query(table, filters): + self._records_total_is_loading = False + self._records_total_rows = max(int(table.total_rows or 0), 0) + self._update_records_label(table) + self._set_records_paging_buttons(table) + return + + self._records_total_is_loading = True + + self._update_records_label(table) + self._set_records_paging_buttons(table) + + session = CURRENT_SESSION.get_value() + if session is None: + self._records_total_is_loading = False + self._update_records_label(table) + self._set_records_paging_buttons(table) + return + + worker = threading.Thread( + target=self._count_table_records_worker, + args=( + session, + table, + filters, + total_key, + self._records_total_request_id, + ), + daemon=True, + ) + worker.start() + + def _on_records_count_complete( + self, + total_key: tuple[str, str, str, str], + request_id: int, + total_rows: int, + error: Optional[str], + ) -> None: + if request_id != self._records_total_request_id: + return + + self._records_total_is_loading = False + + if error: + table = CURRENT_TABLE.get_value() + if table is not None: + self._update_records_label(table) + self._set_records_paging_buttons(table) + return + + table = CURRENT_TABLE.get_value() + if table is None: + return + + filters = self._get_records_filters() + if self._build_records_total_key(table, filters) != total_key: + return + + self._records_total_rows = max(int(total_rows), 0) + last_offset = self._get_records_last_offset(self._records_limit) + + if self._records_offset > last_offset: + self._records_offset = last_offset + self._load_records_page() + return + + self._update_records_label(table) + self._set_records_paging_buttons(table) + + def _get_records_last_offset(self, limit: int) -> int: + total_rows = int(self._records_total_rows or 0) + if total_rows <= 0: + return 0 + + return ((total_rows - 1) // limit) * limit + + def _load_records_page(self): + table = CURRENT_TABLE.get_value() + if table is None: + return + + limit = max(1, self.limit_records.GetValue()) + self._records_limit = limit + + filters = self._get_records_filters() + self._refresh_records_total_rows(table, filters) + + last_offset = self._get_records_last_offset(limit) + + self._records_offset = min(max(self._records_offset, 0), last_offset) + + with Loader.cursor_wait(): + table.load_records(filters=filters, limit=limit, offset=self._records_offset) + self.controller_list_table_records.load_model() + + self._update_records_label(table) + self._set_records_paging_buttons(table) + + def _update_records_label(self, table: SQLTable): + rows_count = self._get_loaded_records_count(table) + from_row = 0 if rows_count == 0 else self._records_offset + 1 + to_row = 0 if rows_count == 0 else self._records_offset + rows_count + + if self._records_total_is_loading: + total_rows_text = self._get_loading_total_text(table, self._get_records_filters()) + else: + total_rows_text = self._format_records_number(int(self._records_total_rows or 0)) + + self.name_database_table.SetLabel( + self._records_label_template.format( + database_name=table.database.name, + table_name=table.name, + total_rows=total_rows_text, + from_row=self._format_records_number(from_row), + to_row=self._format_records_number(to_row), + ) + ) + + def _set_records_paging_buttons(self, table: SQLTable): + if self._records_total_is_loading: + rows_count = self._get_loaded_records_count(table) + at_first_page = self._records_offset <= 0 + has_next_page = rows_count >= self._records_limit + + self.btn_first_records.Enable(not at_first_page) + self.btn_prev_records.Enable(not at_first_page) + self.btn_next_records.Enable(has_next_page) + self.btn_last_records.Enable(False) + return + + total_rows = int(self._records_total_rows or 0) + at_first_page = self._records_offset <= 0 + at_last_page = self._records_offset >= self._get_records_last_offset(self._records_limit) + has_rows = total_rows > 0 + + self.btn_first_records.Enable(has_rows and not at_first_page) + self.btn_prev_records.Enable(has_rows and not at_first_page) + self.btn_next_records.Enable(has_rows and not at_last_page) + self.btn_last_records.Enable(has_rows and not at_last_page) + def on_page_chaged(self, event): if int(event.Selection) == 5: if table := CURRENT_TABLE.get_value(): - table.load_records() - - # self.controller_list_table_records.load_model() + self._records_offset = 0 + self._load_records_page() def _on_current_session(self, session: Session): from structures.session import Session @@ -365,7 +675,9 @@ def on_cancel_database(self, event: wx.Event): if wx.MessageDialog( None, - message=_(f"Do you want discard the change to {database.name}?"), + message=_("Do you want discard the change to {database_name}?").format( + database_name=database.name + ), style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, ).ShowModal() != wx.ID_YES: return @@ -461,16 +773,15 @@ def _on_current_table(self, table: SQLTable): return if table: - # `%(database_name)s`.`%(table_name)s` - self.name_database_table.SetLabel( - self.name_database_table.GetLabel() % { - "database_name": table.database.name, - "table_name": table.name, - "total_rows": table.total_rows - } - ) + self._records_offset = 0 + self._records_limit = max(1, self.limit_records.GetValue()) + self._records_total_rows = 0 + self._records_total_key = None + self._records_total_is_loading = False + self._update_records_label(table) self.toggle_panel(table) + self._set_records_paging_buttons(table) CURRENT_COLUMN.set_value(None) CURRENT_RECORDS.set_value([]) @@ -486,6 +797,9 @@ def _on_current_table(self, table: SQLTable): table.raw_create() ) + if self.MainFrameNotebook.GetSelection() == 5: + self._load_records_page() + self.btn_clone_table.Enable(table is not None) self.btn_delete_table.Enable(table is not None) @@ -554,7 +868,9 @@ def do_apply_table(self, event: wx.Event): def on_cancel_table(self, event: wx.Event): if new_table := NEW_TABLE.get_value(): if wx.MessageDialog(None, - message=_(f'Do you want discard the change to {new_table.name}?'), + message=_("Do you want discard the change to {table_name}?").format( + table_name=new_table.name + ), style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION ).ShowModal() == wx.ID_YES: return self.do_cancel_table(event) @@ -578,7 +894,9 @@ def on_delete_table(self, event): table = CURRENT_TABLE.get_value() dialog = wx.MessageDialog(None, - message=_(f'Do you want delete the table {table.name}?'), + message=_("Do you want delete the table {table_name}?").format( + table_name=table.name + ), caption=_("Delete table"), style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION ) @@ -598,7 +916,7 @@ def on_clone_table(self, event): if table: new_table = table.copy() new_table.id = -1 - new_table.name = _(f"{new_table.name} (COPY)") + new_table.name = _("{table_name} (COPY)").format(table_name=new_table.name) for column in new_table.columns: column.id = -1 @@ -696,21 +1014,51 @@ def on_duplicate_record(self, event): def on_delete_record(self, event): dialog = wx.MessageDialog(None, - message=_(f'Do you want delete the records?'), + message=_("Do you want delete the records?"), style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION ) if dialog.ShowModal() == wx.ID_YES: self.controller_list_table_records.do_delete_record() - def on_apply_filters(self, event): - # self.controller_list_table_records.do_apply_filters() + def on_first_records(self, event): + self._records_offset = 0 + self._load_records_page() + + def on_prev_records(self, event): + self._records_offset = max(self._records_offset - self._records_limit, 0) + self._load_records_page() + + def on_next_records(self, event): table = CURRENT_TABLE.get_value() - if table: - filters = (self.sql_query_filters.GetSelectedText() or self.sql_query_filters.GetText()).strip() - table.load_records(filters) + if table is None: + return + + self._records_offset = min( + self._records_offset + self._records_limit, + self._get_records_last_offset(self._records_limit), + ) + self._load_records_page() - # self.controller_list_table_records.load_model() + def on_last_records(self, event): + table = CURRENT_TABLE.get_value() + if table is None: + return + + self._records_offset = self._get_records_last_offset(self._records_limit) + self._load_records_page() + + def on_limit_records_changed(self, event): + min_limit = max(1, int(self.limit_records.GetMin())) + max_limit = max(min_limit, int(self.limit_records.GetMax())) + self._records_limit = min(max(int(self.limit_records.GetValue()), min_limit), max_limit) + wx.GetApp().settings.set_value("records", "limit", value=self._records_limit) + self._records_offset = 0 + self._load_records_page() + + def on_apply_filters(self, event): + self._records_offset = 0 + self._load_records_page() # def on_clear_record(self, event): # self.controller_list_table_records.on_row_clear() diff --git a/windows/main/tabs/query.py b/windows/main/tabs/query.py index bc35b70..dc44e8a 100644 --- a/windows/main/tabs/query.py +++ b/windows/main/tabs/query.py @@ -10,6 +10,7 @@ import wx.dataview from helpers.logger import logger +from helpers.dataview import BaseDataViewListModel from structures.session import Session from structures.connection import ConnectionEngine @@ -296,22 +297,29 @@ def create_result_tab(self, result: ExecutionResult) -> wx.Panel: sizer = wx.BoxSizer(wx.VERTICAL) if result.success and result.columns: - grid = QueryEditorResultsDataViewCtrl(panel) - self._populate_grid(grid, result) - sizer.Add(grid, 1, wx.EXPAND | wx.ALL, 5) + results_dataview = QueryEditorResultsDataViewCtrl(panel) + self._populate_grid(results_dataview, result) + sizer.Add(results_dataview, 1, wx.EXPAND | wx.ALL, 5) tab_name = self._generate_tab_name(result) elif result.success: - msg = wx.StaticText(panel, label=_("{} rows affected").format(result.affected_rows or 0)) + msg = wx.StaticText( + panel, + label=_("{affected_rows} rows affected").format( + affected_rows=result.affected_rows or 0 + ), + ) msg.SetFont(msg.GetFont().MakeBold()) sizer.Add(msg, 1, wx.ALIGN_CENTER | wx.ALL, 20) - tab_name = _("Query {}").format(self._tab_counter) + tab_name = _("Query {query_number}").format(query_number=self._tab_counter) else: error_panel = self._create_error_panel(panel, result) sizer.Add(error_panel, 1, wx.EXPAND | wx.ALL, 5) - tab_name = _("Query {} (Error)").format(self._tab_counter) + tab_name = _("Query {query_number} (Error)").format( + query_number=self._tab_counter + ) footer = self._create_footer(panel, result) sizer.Add(footer, 0, wx.EXPAND | wx.ALL, 5) @@ -323,39 +331,43 @@ def create_result_tab(self, result: ExecutionResult) -> wx.Panel: def _generate_tab_name(self, result: ExecutionResult) -> str: if result.columns and result.rows is not None: - return _("Query {} ({} rows × {} cols)").format( - self._tab_counter, - len(result.rows), - len(result.columns) + return _("Query {query_number} ({rows_count} rows × {columns_count} cols)").format( + query_number=self._tab_counter, + rows_count=len(result.rows), + columns_count=len(result.columns), ) - return _("Query {}").format(self._tab_counter) + return _("Query {query_number}").format(query_number=self._tab_counter) def _populate_grid( self, - grid: QueryEditorResultsDataViewCtrl, + results_dataview: QueryEditorResultsDataViewCtrl, result: ExecutionResult ) -> None: - if not result.columns or not result.rows: + if not result.columns: return - for col_name in result.columns: - grid.AppendTextColumn(col_name, wx.dataview.DATAVIEW_CELL_INERT) + for i, col_name in enumerate(result.columns): + results_dataview.AppendTextColumn(col_name, i, wx.dataview.DATAVIEW_CELL_INERT) - model = grid.GetModel() - if hasattr(model, 'data'): - model.data = list(result.rows) - model.Reset(len(result.rows)) + model = QueryResultsGridModel(column_count=len(result.columns)) + model.load_rows(result.columns, result.rows or []) + results_dataview.AssociateModel(model) + results_dataview._query_results_model = model def _create_footer(self, parent: wx.Panel, result: ExecutionResult) -> wx.StaticText: parts = [] if result.affected_rows is not None: - parts.append(_("{} rows").format(result.affected_rows)) + parts.append(_("{rows_count} rows").format(rows_count=result.affected_rows)) - parts.append(_("{:.1f} ms").format(result.elapsed_ms)) + parts.append(_("{elapsed_ms:.1f} ms").format(elapsed_ms=result.elapsed_ms)) if result.warnings: - parts.append(_("{} warnings").format(len(result.warnings))) + parts.append( + _("{warnings_count} warnings").format( + warnings_count=len(result.warnings) + ) + ) footer_text = " | ".join(parts) footer = wx.StaticText(parent, label=footer_text) @@ -388,6 +400,38 @@ def clear_all_tabs(self) -> None: self._tab_counter = 0 +class QueryResultsGridModel(BaseDataViewListModel): + def __init__(self, column_count: int): + super().__init__(column_count) + self._columns: list[str] = [] + + def load_rows(self, columns: list[str], rows: list[Any]) -> None: + self._columns = list(columns) + + normalized_rows: list[tuple[Any, ...]] = [] + for row in rows: + if isinstance(row, dict): + normalized_rows.append(tuple(row.get(column) for column in self._columns)) + elif isinstance(row, (list, tuple)): + normalized_rows.append(tuple(row)) + else: + normalized_rows.append((row,)) + + self._data = normalized_rows + self.Reset(len(self._data)) + + def GetValueByRow(self, row, col): + if row < 0 or row >= len(self.data): + return "" + + row_values = self.data[row] + if col < 0 or col >= len(row_values): + return "" + + value = row_values[col] + return "" if value is None else str(value) + + class QueryEditorController: def __init__( self, diff --git a/windows/main/tabs/records.py b/windows/main/tabs/records.py index 4f15fd9..535b4dc 100644 --- a/windows/main/tabs/records.py +++ b/windows/main/tabs/records.py @@ -76,7 +76,10 @@ def SetValueByRow(self, value, row, col): return True def GetAttr(self, item, col, attr): - column: SQLColumn = self.table.columns[col] + try : + column: SQLColumn = self.table.columns[col] + except Exception as ex : + logger.error(exc_info=True) color = column.datatype.category.value.color @@ -92,17 +95,6 @@ def add_row(self, data: SQLRecord) -> wx.dataview.DataViewItem: self.RowAppended() return self.GetItem(len(self.data) - 1) - # - # def del_row(self, item: wx.dataview.DataViewItem): - # row = self.GetRow(item) - # del self.data[row] - # self.RowDeleted(row) - # - # def clear(self): - # self.data = [] - # self.Reset(0) - # self.Cleared() - class TableRecordsController: app = wx.GetApp() @@ -127,6 +119,8 @@ def _load_database(self, database: SQLDatabase): def _load_table(self, table: SQLTable): if table is not None: self.table = table + self.table.load_records() + self.load_model() def load_model(self): self.model = RecordsModel(self.table, len(self.table.columns)) diff --git a/windows/views.py b/windows/views.py index eb525bd..59f6b85 100755 --- a/windows/views.py +++ b/windows/views.py @@ -30,7 +30,7 @@ class ConnectionsDialog ( wx.Dialog ): def __init__( self, parent ): - wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Connection"), pos = wx.DefaultPosition, size = wx.Size( 800,600 ), style = wx.DEFAULT_DIALOG_STYLE|wx.DIALOG_NO_PARENT|wx.RESIZE_BORDER ) + wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Connection"), pos = wx.DefaultPosition, size = wx.Size( 900,768 ), style = wx.DEFAULT_DIALOG_STYLE|wx.DIALOG_NO_PARENT|wx.RESIZE_BORDER ) self.SetSizeHints( wx.Size( -1,-1 ), wx.DefaultSize ) @@ -169,22 +169,53 @@ def __init__( self, parent ): bSizer103.Add( bSizer1221, 0, wx.EXPAND, 5 ) + bSizer159 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText84 = wx.StaticText( self.panel_credentials, wx.ID_ANY, _(u"Connection timeout"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText84.Wrap( -1 ) + + self.m_staticText84.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer159.Add( self.m_staticText84, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) + + self.connection_timeout = wx.SpinCtrl( self.panel_credentials, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_ARROW_KEYS, 0, 60, 10 ) + bSizer159.Add( self.connection_timeout, 1, wx.ALL, 5 ) + + + bSizer103.Add( bSizer159, 1, wx.EXPAND, 5 ) + bSizer116 = wx.BoxSizer( wx.HORIZONTAL ) bSizer116.Add( ( 156, 0), 0, wx.EXPAND, 5 ) - self.use_tls_enabled = wx.CheckBox( self.panel_credentials, wx.ID_ANY, _(u"Use TLS"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer116.Add( self.use_tls_enabled, 0, wx.ALL, 5 ) + self.use_tls = wx.CheckBox( self.panel_credentials, wx.ID_ANY, _(u"Use TLS"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer116.Add( self.use_tls, 0, wx.ALL, 5 ) + + + bSizer103.Add( bSizer116, 0, wx.EXPAND, 5 ) + + bSizer163 = wx.BoxSizer( wx.HORIZONTAL ) + + + bSizer163.Add( ( 156, 0), 0, wx.EXPAND, 5 ) self.ssh_tunnel_enabled = wx.CheckBox( self.panel_credentials, wx.ID_ANY, _(u"Use SSH tunnel"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer116.Add( self.ssh_tunnel_enabled, 0, wx.ALL, 5 ) + bSizer163.Add( self.ssh_tunnel_enabled, 0, wx.ALL, 5 ) - bSizer103.Add( bSizer116, 0, wx.EXPAND, 5 ) + bSizer103.Add( bSizer163, 0, wx.EXPAND, 5 ) + + bSizer164 = wx.BoxSizer( wx.HORIZONTAL ) + + + bSizer164.Add( ( 156, 0), 0, wx.EXPAND, 5 ) - self.m_staticline5 = wx.StaticLine( self.panel_credentials, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) - bSizer103.Add( self.m_staticline5, 0, wx.EXPAND | wx.ALL, 5 ) + self.compressed_protocol = wx.CheckBox( self.panel_credentials, wx.ID_ANY, _(u"Compressed client/server protocol"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer164.Add( self.compressed_protocol, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + + bSizer103.Add( bSizer164, 0, wx.EXPAND, 5 ) self.panel_credentials.SetSizer( bSizer103 ) @@ -216,6 +247,9 @@ def __init__( self, parent ): bSizer105.Fit( self.panel_source ) bSizer12.Add( self.panel_source, 0, wx.EXPAND | wx.ALL, 0 ) + self.m_staticline5 = wx.StaticLine( self.panel_connection, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) + bSizer12.Add( self.m_staticline5, 0, wx.EXPAND | wx.ALL, 5 ) + bSizer122111 = wx.BoxSizer( wx.HORIZONTAL ) self.m_staticText22111 = wx.StaticText( self.panel_connection, wx.ID_ANY, _(u"Comments"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) @@ -351,6 +385,19 @@ def __init__( self, parent ): bSizer102.Add( bSizer121311, 0, wx.EXPAND, 5 ) + bSizer121322 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText21322 = wx.StaticText( self.panel_ssh_tunnel, wx.ID_ANY, _(u"SSH extra args"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) + self.m_staticText21322.Wrap( -1 ) + + bSizer121322.Add( self.m_staticText21322, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + self.ssh_tunnel_extra_args = wx.TextCtrl( self.panel_ssh_tunnel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer121322.Add( self.ssh_tunnel_extra_args, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) + + + bSizer102.Add( bSizer121322, 0, wx.EXPAND, 5 ) + self.panel_ssh_tunnel.SetSizer( bSizer102 ) self.panel_ssh_tunnel.Layout() @@ -1777,7 +1824,7 @@ def __init__( self, parent ): self.panel_table.SetSizer( bSizer251 ) self.panel_table.Layout() bSizer251.Fit( self.panel_table ) - self.MainFrameNotebook.AddPage( self.panel_table, _(u"Table"), True ) + self.MainFrameNotebook.AddPage( self.panel_table, _(u"Table"), False ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/table.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2036,10 +2083,37 @@ def __init__( self, parent ): bSizer94 = wx.BoxSizer( wx.HORIZONTAL ) - self.name_database_table = wx.StaticText( self.panel_records, wx.ID_ANY, _(u"Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.name_database_table = 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.ALL, 5 ) + 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 ) @@ -2090,14 +2164,6 @@ def __init__( self, parent ): bSizer83.Add( self.btn_apply_record, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - bSizer83.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.m_button40 = wx.Button( self.panel_records, wx.ID_ANY, _(u"Next"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.m_button40.SetBitmap( wx.Bitmap( u"icons/16x16/resultset_next.png", wx.BITMAP_TYPE_ANY ) ) - bSizer83.Add( self.m_button40, 0, wx.ALL, 5 ) - - bSizer61.Add( bSizer83, 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 ) @@ -2168,7 +2234,7 @@ def __init__( self, parent ): self.panel_records.Bind( wx.EVT_RIGHT_DOWN, self.panel_recordsOnContextMenu ) - self.MainFrameNotebook.AddPage( self.panel_records, _(u"Data"), False ) + self.MainFrameNotebook.AddPage( self.panel_records, _(u"Data"), True ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/text_columns.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2381,11 +2447,14 @@ 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.btn_first_records.Bind( wx.EVT_BUTTON, self.on_first_records ) + self.btn_prev_records.Bind( wx.EVT_BUTTON, self.on_prev_records ) + self.btn_next_records.Bind( wx.EVT_BUTTON, self.on_next_records ) + self.btn_last_records.Bind( wx.EVT_BUTTON, self.on_last_records ) self.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.chb_auto_apply.Bind( wx.EVT_CHECKBOX, self.on_auto_apply ) - self.m_button40.Bind( wx.EVT_BUTTON, self.on_next_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 ) @@ -2467,6 +2536,18 @@ def on_cancel_table( self, event ): def do_apply_table( self, event ): event.Skip() + def on_first_records( self, event ): + event.Skip() + + def on_prev_records( self, event ): + event.Skip() + + def on_next_records( self, event ): + event.Skip() + + def on_last_records( self, event ): + event.Skip() + def on_insert_record( self, event ): event.Skip() @@ -2479,9 +2560,6 @@ def on_delete_record( self, event ): def on_auto_apply( self, event ): event.Skip() - def on_next_records( self, event ): - event.Skip() - def on_collapsible_pane_changed( self, event ): event.Skip() @@ -2531,6 +2609,9 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. self.m_textCtrl221 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) bSizer90.Add( self.m_textCtrl221, 1, wx.ALL|wx.EXPAND, 5 ) + self.total_rows_loading = wx.StaticBitmap( self, wx.ID_ANY, wx.Bitmap( u"icons/16x16/hourglass.png", wx.BITMAP_TYPE_ANY ), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer90.Add( self.total_rows_loading, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + bSizer93 = wx.BoxSizer( wx.VERTICAL ) self.tree_ctrl_explorer____ = wx.dataview.TreeListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.TL_DEFAULT_STYLE ) From 4625a08237b7602c03e600fd085d637a4b282e4b Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 14 Mar 2026 18:16:15 +0100 Subject: [PATCH 02/93] Clean up engine contexts and document AbstractContext AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- structures/engines/context.py | 152 +++++++++++++++-------- structures/engines/mariadb/context.py | 76 ++++++------ structures/engines/mysql/context.py | 37 +++++- structures/engines/postgresql/context.py | 67 +++++++++- 4 files changed, 233 insertions(+), 99 deletions(-) diff --git a/structures/engines/context.py b/structures/engines/context.py index 1728107..15ce547 100755 --- a/structures/engines/context.py +++ b/structures/engines/context.py @@ -33,6 +33,8 @@ class AbstractContext(abc.ABC): + """Base context API for SQL engines.""" + _connection: Any = None _cursor: Any = None _ssh_tunnel: Optional[SSHTunnel] = None @@ -50,32 +52,29 @@ class AbstractContext(abc.ABC): databases: ObservableLazyList[SQLDatabase] def __init__(self, connection: Connection): + """Initialize the context with the selected connection.""" self.connection = connection self.databases = ObservableLazyList(self.get_databases) def __del__(self): + """Ensure resources are released during object destruction.""" with contextlib.suppress(Exception): self.disconnect() def before_connect(self, *args, **kwargs): + """Prepare transport details before opening the DB connection.""" # SSH tunnel support via connection configuration if hasattr(self.connection, "ssh_tunnel") and self.connection.ssh_tunnel: ssh_config = self.connection.ssh_tunnel if not ssh_config.is_enabled: return - base_host = getattr(self, "_base_host", getattr(self, "host", "127.0.0.1")) - base_port = getattr(self, "_base_port", getattr(self, "port", 0)) - self._base_host = base_host - self._base_port = base_port + self._base_host = getattr(self, "_base_host", getattr(self, "host", "127.0.0.1")) + self._base_port = getattr(self, "_base_port", getattr(self, "port", 0)) - remote_host = getattr(ssh_config, "remote_host", None) or getattr( - self, "_base_host", "127.0.0.1" - ) - remote_port = int( - getattr(ssh_config, "remote_port", 0) or getattr(self, "_base_port", 0) - ) + remote_host = getattr(ssh_config, "remote_host", None) or self._base_host + remote_port = int(getattr(ssh_config, "remote_port", 0) or self._base_port) local_port = int(getattr(ssh_config, "local_port", 0) or 0) logger.debug( "Preparing DB SSH tunnel: connection=%s engine=%s base=%s:%s remote=%s:%s requested_local_port=%s", @@ -110,9 +109,11 @@ def before_connect(self, *args, **kwargs): ) def after_connect(self, *args, **kwargs): + """Run engine-specific setup right after a successful connection.""" pass def before_disconnect(self, *args, **kwargs): + """Release pre-disconnect resources and restore base host settings.""" if self._ssh_tunnel is not None: logger.debug( "Stopping DB SSH tunnel for connection=%s", @@ -128,10 +129,12 @@ def before_disconnect(self, *args, **kwargs): self.port = self._base_port def after_disconnect(self): + """Run engine-specific cleanup after disconnection.""" pass @staticmethod def _extract_spec_names(values: Any) -> list[str]: + """Extract normalized names from spec values.""" if not isinstance(values, list): return [] @@ -149,6 +152,7 @@ def _extract_spec_names(values: Any) -> list[str]: @staticmethod def _load_yaml_file(path: str) -> dict[str, Any]: + """Load a YAML file from the project workspace.""" file_path = WORKDIR / path if not file_path.exists(): return {} @@ -163,8 +167,9 @@ def _load_yaml_file(path: str) -> dict[str, Any]: @staticmethod def _merge_spec_values( - base_values: list[str], add_values: list[str], remove_values: list[str] + base_values: list[str], add_values: list[str], remove_values: list[str] ) -> list[str]: + """Merge additions and removals into a base vocabulary list.""" removed = {value.upper() for value in remove_values} merged = [value for value in base_values if value.upper() not in removed] @@ -179,6 +184,7 @@ def _merge_spec_values( @staticmethod def _extract_major(version: Optional[str]) -> str: + """Extract the first numeric major version from a version string.""" if not version: return "" @@ -189,13 +195,14 @@ def _extract_major(version: Optional[str]) -> str: @staticmethod def _select_version_spec( - versions_map: dict[str, Any], major_version: str + versions_map: dict[str, Any], major_version: str ) -> dict[str, Any]: + """Select the most suitable version spec for a target major version.""" if not versions_map: return {} if major_version in versions_map and isinstance( - versions_map[major_version], dict + versions_map[major_version], dict ): return versions_map[major_version] @@ -223,8 +230,9 @@ def _select_version_spec( return selected_spec def get_engine_vocabulary( - self, engine: str, server_version: Optional[str] + self, engine: str, server_version: Optional[str] ) -> tuple[tuple[str, ...], tuple[str, ...]]: + """Build engine keywords and functions from shared and engine specs.""" global_spec = self._load_yaml_file("structures/engines/specification.yaml") engine_spec = self._load_yaml_file( f"structures/engines/{engine}/specification.yaml" @@ -280,16 +288,19 @@ def get_engine_vocabulary( @property def is_connected(self): + """Return True when both connection and cursor are available.""" return self._connection is not None and self._cursor is not None @property def cursor(self) -> Any: + """Return the active cursor or raise when not connected.""" if self._cursor is None: raise RuntimeError("Not connected to the database. Call connect() first.") return self._cursor @staticmethod def get_temporary_id(container: list[SQLTypeAlias]) -> int: + """Generate a temporary negative identifier for new objects.""" return min([0] + [t.id for t in container]) - 1 @abc.abstractmethod @@ -299,126 +310,154 @@ def connect(self, **connect_kwargs) -> None: @abc.abstractmethod def set_database(self, database: SQLDatabase) -> None: + """Select the active database for subsequent operations.""" raise NotImplementedError @abc.abstractmethod def get_server_version(self) -> str: + """Return the database server version string.""" raise NotImplementedError @abc.abstractmethod def get_server_uptime(self) -> Optional[int]: + """Return the server uptime in seconds when available.""" raise NotImplementedError @abc.abstractmethod def get_databases(self) -> list[SQLDatabase]: + """Return all databases visible to the current connection.""" raise NotImplementedError @abc.abstractmethod def get_views(self, database: SQLDatabase) -> list[SQLView]: + """Return views for the given database.""" raise NotImplementedError @abc.abstractmethod def get_triggers(self, database: SQLDatabase) -> list[SQLTrigger]: + """Return triggers for the given database.""" raise NotImplementedError @abc.abstractmethod def get_tables(self, database: SQLDatabase) -> list[SQLTable]: + """Return tables for the given database.""" raise NotImplementedError @abc.abstractmethod def get_columns(self, table: SQLTable) -> list[SQLColumn]: + """Return columns for the given table.""" raise NotImplementedError @abc.abstractmethod def get_indexes(self, table: SQLTable) -> list[SQLIndex]: + """Return indexes for the given table.""" raise NotImplementedError @abc.abstractmethod def get_foreign_keys(self, table: SQLTable) -> list[SQLForeignKey]: + """Return foreign keys for the given table.""" raise NotImplementedError @abc.abstractmethod def build_empty_table( - self, database: SQLDatabase, /, name: Optional[str] = None, **default_values + self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> SQLTable: + """Build a new in-memory table model with default values.""" raise NotImplementedError @abc.abstractmethod def build_empty_column( - self, - table: SQLTable, - datatype: SQLDataType, - /, - name: Optional[str] = None, - **default_values, + self, + table: SQLTable, + datatype: SQLDataType, + /, + name: Optional[str] = None, + **default_values, ) -> SQLColumn: + """Build a new in-memory column model with default values.""" raise NotImplementedError @abc.abstractmethod def build_empty_index( - self, - table: SQLTable, - indextype: SQLIndexType, - columns: list[str], - /, - name: Optional[str] = None, - **default_values, + self, + table: SQLTable, + indextype: SQLIndexType, + columns: list[str], + /, + name: Optional[str] = None, + **default_values, ) -> SQLIndex: + """Build a new in-memory index model with default values.""" raise NotImplementedError @abc.abstractmethod def build_empty_check( - self, - table: SQLTable, - /, - name: Optional[str] = None, - expression: Optional[str] = None, - **default_values, + self, + table: SQLTable, + /, + name: Optional[str] = None, + expression: Optional[str] = None, + **default_values, ) -> SQLCheck: + """Build a new in-memory check constraint model.""" raise NotImplementedError @abc.abstractmethod def build_empty_foreign_key( - self, - table: SQLTable, - columns: list[str], - /, - name: Optional[str] = None, - **default_values, + self, + table: SQLTable, + columns: list[str], + /, + name: Optional[str] = None, + **default_values, ) -> SQLForeignKey: + """Build a new in-memory foreign key model.""" raise NotImplementedError @abc.abstractmethod def build_empty_record( - self, table: SQLTable, /, *, values: dict[str, Any] + self, table: SQLTable, /, *, values: dict[str, Any] ) -> SQLRecord: + """Build a new in-memory record model.""" raise NotImplementedError @abc.abstractmethod def build_empty_view( - self, database: SQLDatabase, /, name: Optional[str] = None, **default_values + self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> SQLView: + """Build a new in-memory view model.""" raise NotImplementedError @abc.abstractmethod def build_empty_function( - self, database: SQLDatabase, /, name: Optional[str] = None, **default_values + self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> "SQLFunction": + """Build a new in-memory function model.""" raise NotImplementedError @abc.abstractmethod def build_empty_procedure( - self, database: SQLDatabase, /, name: Optional[str] = None, **default_values + self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> "SQLProcedure": + """Build a new in-memory procedure model.""" raise NotImplementedError @abc.abstractmethod def build_empty_trigger( - self, database: SQLDatabase, /, name: Optional[str] = None, **default_values + self, database: SQLDatabase, /, name: Optional[str] = None, **default_values ) -> SQLTrigger: + """Build a new in-memory trigger model.""" + raise NotImplementedError + + @abc.abstractmethod + def get_result_column_datatypes( + self, cursor: Any + ) -> list[Optional[SQLDataType]]: + """Infer SQL data types for result columns from a driver cursor.""" raise NotImplementedError def quote_identifier(self, name: str) -> str: + """Quote an SQL identifier only when needed.""" value = name.strip() if not value: assert False, "Invalid identifier name: %s" % name @@ -432,18 +471,20 @@ def quote_identifier(self, name: str) -> str: return f"{self.IDENTIFIER_QUOTE_CHAR}{escaped_name}{self.IDENTIFIER_QUOTE_CHAR}" def qualify(self, *parts): + """Build a qualified SQL identifier from multiple parts.""" return ".".join(self.quote_identifier(part) for part in parts) def get_records( - self, - table: SQLTable, - /, - *, - filters: Optional[str] = None, - limit: int = 1000, - offset: int = 0, - orders: Optional[str] = None, + self, + table: SQLTable, + /, + *, + filters: Optional[str] = None, + limit: int = 1000, + offset: int = 0, + orders: Optional[str] = None, ) -> list[dict[str, Any]]: + """Fetch records from a table using optional filtering and pagination.""" logger.debug(f"get records for table={table.name}") QUERY_LOGS.append(f"/* get_records for table={table.name} */") if table is None or table.is_new: @@ -479,6 +520,7 @@ def get_records( # EXECUTION def execute(self, query: str) -> bool: + """Execute a SQL query and append it to query logs.""" query_clean = re.sub(r"\s+", " ", str(query)).strip() logger.debug("execute query: %s", query_clean) QUERY_LOGS.append(query_clean) @@ -493,6 +535,7 @@ def execute(self, query: str) -> bool: return True def fetchone(self) -> Any: + """Fetch a single row from the active cursor.""" try: return self.cursor.fetchone() except Exception as ex: @@ -500,6 +543,7 @@ def fetchone(self) -> Any: raise def fetchall(self) -> list[Any]: + """Fetch all rows from the active cursor.""" try: return self.cursor.fetchall() except Exception as ex: @@ -507,6 +551,7 @@ def fetchall(self) -> list[Any]: raise def disconnect(self) -> None: + """Close cursor and connection resources safely.""" self.before_disconnect() if self._cursor is not None: @@ -523,6 +568,7 @@ def disconnect(self) -> None: @contextlib.contextmanager def transaction(self): + """Provide a simple BEGIN/COMMIT/ROLLBACK transaction scope.""" try: self.execute("BEGIN") yield self diff --git a/structures/engines/mariadb/context.py b/structures/engines/mariadb/context.py index 0a93d7f..e5db6bf 100755 --- a/structures/engines/mariadb/context.py +++ b/structures/engines/mariadb/context.py @@ -6,6 +6,8 @@ import pymysql +from pymysql.constants import FIELD_TYPE + from helpers.logger import logger from structures.connection import Connection @@ -116,6 +118,39 @@ def _parse_type(self, column_type: str): return dict() + def _get_field_type_name(self, type_code: Optional[int]) -> Optional[str]: + if type_code is None: + return None + + for name, value in vars(FIELD_TYPE).items(): + if not name.isupper() or not isinstance(value, int): + continue + + if value == type_code: + return name + + return None + + def get_result_column_datatypes( + self, cursor: pymysql.cursors.Cursor + ) -> list[Optional[SQLDataType]]: + datatypes: list[Optional[SQLDataType]] = [] + + for description in cursor.description or []: + type_code = description[1] if len(description) > 1 else None + datatype_name = self._get_field_type_name(type_code) + + if datatype_name is None: + datatypes.append(None) + continue + + try: + datatypes.append(self.DATATYPE.get_by_name(datatype_name)) + except Exception: + datatypes.append(None) + + return datatypes + def connect(self, **connect_kwargs) -> None: if self._connection is None: self.before_connect() @@ -159,23 +194,6 @@ def connect(self, **connect_kwargs) -> None: connect_timeout, use_tls, ) - # - # # SSH tunnel support via connection configuration - # if hasattr(self.connection, 'ssh_tunnel') and self.connection.ssh_tunnel: - # ssh_config = self.connection.ssh_tunnel - # self._ssh_tunnel = SSHTunnel( - # ssh_config.hostname, int(ssh_config.port), - # ssh_username=ssh_config.username, - # ssh_password=ssh_config.password, - # remote_port=self.port, - # local_bind_address=(self.host, int(getattr(ssh_config, 'local_port', 0))), - # extra_args=ssh_config.extra_args - # ) - # self._ssh_tunnel.start() - # base_kwargs.update( - # host=self.host, - # port=self._ssh_tunnel.local_port, - # ) self._connection = pymysql.connect(**base_kwargs) self._cursor = self._connection.cursor() @@ -216,30 +234,6 @@ def connect(self, **connect_kwargs) -> None: if self._cursor is not None: self.after_connect() - def disconnect(self) -> None: - """Disconnect from database and stop SSH tunnel if active.""" - try: - if self._cursor: - self._cursor.close() - except Exception: - pass - - try: - if self._connection: - self._connection.close() - except Exception: - pass - - try: - if self._ssh_tunnel: - self._ssh_tunnel.stop() - self._ssh_tunnel = None - except Exception: - pass - - self._cursor = None - self._connection = None - def set_database(self, database: SQLDatabase) -> None: self.execute(f"USE {database.quoted_name}") diff --git a/structures/engines/mysql/context.py b/structures/engines/mysql/context.py index b3061aa..68b01ce 100644 --- a/structures/engines/mysql/context.py +++ b/structures/engines/mysql/context.py @@ -4,6 +4,8 @@ import pymysql +from pymysql.constants import FIELD_TYPE + from gettext import gettext as _ from helpers.logger import logger @@ -34,8 +36,6 @@ from structures.engines.mysql.datatype import MySQLDataType from structures.engines.mysql.indextype import MySQLIndexType -from structures.ssh_tunnel import SSHTunnel - class MySQLContext(AbstractContext): MAP_COLUMN_FIELDS = MAP_COLUMN_FIELDS @@ -118,6 +118,39 @@ def _parse_type(self, column_type: str): return dict() + def _get_field_type_name(self, type_code: Optional[int]) -> Optional[str]: + if type_code is None: + return None + + for name, value in vars(FIELD_TYPE).items(): + if not name.isupper() or not isinstance(value, int): + continue + + if value == type_code: + return name + + return None + + def get_result_column_datatypes( + self, cursor: pymysql.cursors.Cursor + ) -> list[Optional[SQLDataType]]: + datatypes: list[Optional[SQLDataType]] = [] + + for description in cursor.description or []: + type_code = description[1] if len(description) > 1 else None + datatype_name = self._get_field_type_name(type_code) + + if datatype_name is None: + datatypes.append(None) + continue + + try: + datatypes.append(self.DATATYPE.get_by_name(datatype_name)) + except Exception: + datatypes.append(None) + + return datatypes + def connect(self, **connect_kwargs) -> None: if self._connection is None: self.before_connect() diff --git a/structures/engines/postgresql/context.py b/structures/engines/postgresql/context.py index 8750c81..d6f9b2f 100644 --- a/structures/engines/postgresql/context.py +++ b/structures/engines/postgresql/context.py @@ -1,12 +1,13 @@ import psycopg2 import psycopg2.extras +from psycopg2.extensions import cursor as PostgreSQLCursor + from typing import Any, Optional from gettext import gettext as _ from helpers.logger import logger from structures.connection import Connection -from structures.ssh_tunnel import SSHTunnel from structures.engines.context import QUERY_LOGS, AbstractContext from structures.engines.database import ( @@ -102,6 +103,66 @@ def _load_custom_types(self) -> None: ) setattr(PostgreSQLDataType, row["typname"].upper(), datatype) + def _extract_description_type_code(self, description: Any) -> Optional[int]: + if hasattr(description, "type_code"): + value = getattr(description, "type_code") + return int(value) if value is not None else None + + if len(description) > 1 and description[1] is not None: + return int(description[1]) + + return None + + def _load_result_type_names_by_oid( + self, cursor: PostgreSQLCursor, oid_values: list[int] + ) -> dict[int, str]: + type_name_by_oid: dict[int, str] = {} + + lookup_cursor = cursor.connection.cursor() + try: + lookup_cursor.execute( + "SELECT oid, format_type(oid, NULL) AS type_name FROM pg_type WHERE oid = ANY(%s)", + (oid_values,), + ) + for oid, type_name in lookup_cursor.fetchall(): + type_name_by_oid[int(oid)] = str(type_name) + finally: + lookup_cursor.close() + + return type_name_by_oid + + def _normalize_result_type_name(self, datatype_name: str) -> str: + normalized = datatype_name.strip().upper() + normalized = normalized.split("(")[0].strip() + normalized = " ".join(normalized.split()) + return normalized + + def get_result_column_datatypes( + self, cursor: PostgreSQLCursor + ) -> list[Optional[SQLDataType]]: + descriptions = list(cursor.description or []) + oids = [self._extract_description_type_code(desc) for desc in descriptions] + oid_values = [oid for oid in oids if oid is not None] + + if not oid_values: + return [None for _ in descriptions] + + type_name_by_oid = self._load_result_type_names_by_oid(cursor, oid_values) + datatypes: list[Optional[SQLDataType]] = [] + + for oid in oids: + if oid is None or oid not in type_name_by_oid: + datatypes.append(None) + continue + + try: + type_name = self._normalize_result_type_name(type_name_by_oid[oid]) + datatypes.append(self.DATATYPE.get_by_name(type_name)) + except Exception: + datatypes.append(None) + + return datatypes + def connect(self, **connect_kwargs) -> None: if self._connection is None: try: @@ -427,7 +488,7 @@ def get_indexes(self, table: SQLTable) -> list[SQLIndex]: return results - def get_checks(self, table: PostgreSQLTable) -> list[PostgreSQLCheck]: + def get_checks(self, table: PostgreSQLTable) -> list["PostgreSQLCheck"]: from structures.engines.postgresql.database import PostgreSQLCheck if table is None or table.is_new: @@ -636,7 +697,7 @@ def build_empty_check( name: Optional[str] = None, expression: Optional[str] = None, **default_values, - ) -> PostgreSQLCheck: + ) -> "PostgreSQLCheck": from structures.engines.postgresql.database import PostgreSQLCheck id = PostgreSQLContext.get_temporary_id(table.checks) From 90709bf37ab954fa5cf514fd70fdabb616b5c417 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 14 Mar 2026 18:21:07 +0100 Subject: [PATCH 03/93] refactor(dataview): add observable models and update tab models AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- helpers/dataview.py | 266 +++++++++++++--------- windows/dialogs/connections/controller.py | 4 +- windows/main/tabs/check.py | 4 +- windows/main/tabs/column.py | 4 +- windows/main/tabs/database.py | 4 +- windows/main/tabs/foreign_key.py | 4 +- windows/main/tabs/index.py | 4 +- windows/main/tabs/records.py | 4 +- 8 files changed, 172 insertions(+), 122 deletions(-) diff --git a/helpers/dataview.py b/helpers/dataview.py index a59467c..f5d0a19 100644 --- a/helpers/dataview.py +++ b/helpers/dataview.py @@ -26,16 +26,13 @@ def has_value(self, *args): return self.get_value(args[0]) is not None -class AbstractBaseDataModel(): +class BaseDataModel(): def __init__(self, column_count: Optional[int] = None): self._data: list[Any] = [] - self._observable: Union[ObservableList, ObservableLazyList] = None - self._column_count = column_count def load(self, data: list[Any]): - if data: - self._data = data.copy() + self._data = data def filter(self, data: list[Any]): if data: @@ -59,7 +56,7 @@ def replace(self, data: Any, index: int) -> int: return index - def move(self, data: Any, current: int, future: int) -> (int, int): + def move(self, data: Any, current: int, future: int) -> tuple[int, int]: self._data[current], self._data[future] = self._data[future], self._data[current] return current, future @@ -93,100 +90,186 @@ def get_item_by_name(self, name: str): def get_item_by_filters(self, **filters): return next((d for d in self._data if all(hasattr(d, k) and getattr(d, k) == v for k, v in filters.items())), None) - @abc.abstractmethod - def set_observable(self, observable: Union[ObservableList, ObservableLazyList]): - raise NotImplementedError - data = property(lambda self: self._data, lambda self, *args: None, lambda self: None) -class BaseDataViewTreeModel(AbstractBaseDataModel, wx.dataview.PyDataViewModel): +class _DataViewListValueMixin: + def _get_column_fields(self): + return getattr(self, "MAP_COLUMN_FIELDS", None) + + def get_data_by_item(self, item: wx.dataview.DataViewItem): + return self.get_data_by_row(self.GetRow(item)) + + def clear(self): + super().clear() + self.Reset(0) + self.Cleared() + + def GetColumnCount(self) -> int: + fields = self._get_column_fields() + if fields: + return len(fields) + + return self._column_count + + def GetValueByRow(self, row, col): + if not self.data or row >= len(self.data): + return "" + + if fields := self._get_column_fields(): + return fields[col].get_value(self.get_data_by_row(row)) + + return self.get_data_by_row(row)[col] + + def HasValue(self, item, col): + if not self.data: + return False + + if fields := self._get_column_fields(): + return fields[col].has_value(self.get_data_by_item(item)) + + return self.get_data_by_item(item)[col] is not None + + +class BaseDataViewListModel(_DataViewListValueMixin, BaseDataModel, wx.dataview.DataViewIndexListModel): def __init__(self, column_count: Optional[int] = None): - AbstractBaseDataModel.__init__(self, column_count) - wx.dataview.PyDataViewModel.__init__(self) + BaseDataModel.__init__(self, column_count) + wx.dataview.DataViewIndexListModel.__init__(self) - def _load(self, data: list[Any]): + def load(self, data: list[Any]): self.clear() - AbstractBaseDataModel.load(self, data) - self.Cleared() - def _append(self, data: Any) -> wx.dataview.DataViewItem: - AbstractBaseDataModel.append(self, data) + BaseDataModel.load(self, data) - item = self.ObjectToItem(data) + self.Reset(len(self._data)) - if item.IsOk(): - self.ItemAdded(wx.dataview.NullDataViewItem, item) + def append(self, data: Any) -> wx.dataview.DataViewItem: + index = BaseDataModel.append(self, data) - self.Cleared() + self.RowAppended() - return item + return self.GetItem(index) - def _replace(self, data: Any, index: int) -> wx.dataview.DataViewItem: - AbstractBaseDataModel.replace(self, data, index) + def insert(self, data: Any, index: int) -> wx.dataview.DataViewItem: + index = BaseDataModel.insert(self, data, index) - item = self.ObjectToItem(data) + self.RowInserted(index) - if item.IsOk(): - self.ItemAdded(wx.dataview.NullDataViewItem, item) + return self.GetItem(index) - self.Cleared() + def remove(self, data: Any) -> bool: + index = BaseDataModel.remove(self, data) - return item + self.RowDeleted(index) - def _insert(self, data: Any, index: int) -> wx.dataview.DataViewItem: - AbstractBaseDataModel.insert(self, data, index) + self.Reset(len(self._data)) - item = self.ObjectToItem(data) + return True - if item.IsOk(): - self.ItemAdded(wx.dataview.NullDataViewItem, item) + def replace(self, data: Any, index: int) -> bool: + index = BaseDataModel.replace(self, data, index) - self.Cleared() + self.RowChanged(index) - return item + return True - def _remove(self, data: Any) -> wx.dataview.DataViewItem: - AbstractBaseDataModel.remove(self, data) + def move(self, data: Any, current: int, future: int) -> bool: + BaseDataModel.move(self, data, current, future) - item = self.ObjectToItem(data) + self.RowChanged(current) + self.RowChanged(future) - if item.IsOk(): - self.ItemDeleted(wx.dataview.NullDataViewItem, item) + return True - self.Cleared() - return item +class BaseObservableDataModel(BaseDataModel): + def __init__(self, column_count: Optional[int] = None): + super().__init__(column_count) + self._observable: Union[ObservableList, ObservableLazyList] = None - def _pop(self, data: Any): - AbstractBaseDataModel.pop(self, data) + def load(self, data: list[Any]): + super().load(data.copy()) - item = self.ObjectToItem(data) + @abc.abstractmethod + def set_observable(self, observable: Union[ObservableList, ObservableLazyList]): + raise NotImplementedError + + def _set_observable_handlers( + self, + observable: Union[ObservableList, ObservableLazyList], + on_load: Callable, + handlers: dict[CallbackEvent, Callable], + ): + self._observable = observable + self._observable.subscribe(on_load) + for event, handler in handlers.items(): + self._observable.subscribe(handler, callback_event=event) + + +class BaseObservableDataViewTreeModel(BaseObservableDataModel, wx.dataview.PyDataViewModel): + def __init__(self, column_count: Optional[int] = None): + BaseObservableDataModel.__init__(self, column_count) + wx.dataview.PyDataViewModel.__init__(self) + + def _load(self, data: list[Any]): + self.clear() + BaseObservableDataModel.load(self, data) + self.Cleared() + + def _apply_tree_update(self, data: Any, deleted: bool = False) -> wx.dataview.DataViewItem: + item = self.ObjectToItem(data) if item.IsOk(): - self.ItemDeleted(wx.dataview.NullDataViewItem, item) + if deleted: + self.ItemDeleted(wx.dataview.NullDataViewItem, item) + else: + self.ItemAdded(wx.dataview.NullDataViewItem, item) self.Cleared() return item + def _append(self, data: Any) -> wx.dataview.DataViewItem: + BaseObservableDataModel.append(self, data) + return self._apply_tree_update(data) + + def _replace(self, data: Any, index: int) -> wx.dataview.DataViewItem: + BaseObservableDataModel.replace(self, data, index) + return self._apply_tree_update(data) + + def _insert(self, data: Any, index: int) -> wx.dataview.DataViewItem: + BaseObservableDataModel.insert(self, data, index) + return self._apply_tree_update(data) + + def _remove(self, data: Any) -> wx.dataview.DataViewItem: + BaseObservableDataModel.remove(self, data) + return self._apply_tree_update(data, deleted=True) + + def _pop(self, data: Any): + BaseObservableDataModel.pop(self, data) + return self._apply_tree_update(data, deleted=True) + def _filter(self, data: Any): self.clear() - AbstractBaseDataModel.filter(self, data) + BaseObservableDataModel.filter(self, data) self.Cleared() def find(self, resolution: Callable[[Any], bool]) -> Optional[Any]: return next((v for v in self._data if resolution(v)), None) def set_observable(self, observable: Union[ObservableList, ObservableLazyList]): - self._observable = observable - self._observable.subscribe(self._load) - self._observable.subscribe(self._append, callback_event=CallbackEvent.ON_APPEND) - self._observable.subscribe(self._replace, callback_event=CallbackEvent.ON_REPLACE) - self._observable.subscribe(self._insert, callback_event=CallbackEvent.ON_INSERT) - self._observable.subscribe(self._remove, callback_event=CallbackEvent.ON_REMOVE) - self._observable.subscribe(self._pop, callback_event=CallbackEvent.ON_POP) - self._observable.subscribe(self._filter, callback_event=CallbackEvent.ON_FILTER) + self._set_observable_handlers( + observable, + self._load, + { + CallbackEvent.ON_APPEND: self._append, + CallbackEvent.ON_REPLACE: self._replace, + CallbackEvent.ON_INSERT: self._insert, + CallbackEvent.ON_REMOVE: self._remove, + CallbackEvent.ON_POP: self._pop, + CallbackEvent.ON_FILTER: self._filter, + }, + ) def clear(self): super().clear() @@ -202,33 +285,33 @@ def GetColumnType(self, col): return "string" -class BaseDataViewListModel(AbstractBaseDataModel, wx.dataview.DataViewIndexListModel): +class BaseObservableDataViewListModel(_DataViewListValueMixin, BaseObservableDataModel, wx.dataview.DataViewIndexListModel): def __init__(self, column_count: Optional[int] = None): - AbstractBaseDataModel.__init__(self, column_count) + BaseObservableDataModel.__init__(self, column_count) wx.dataview.DataViewIndexListModel.__init__(self) def _load(self, data: list[Any]): self.clear() - AbstractBaseDataModel.load(self, data) + BaseObservableDataModel.load(self, data) self.Reset(len(self._data)) def _append(self, data: Any) -> wx.dataview.DataViewItem: - index = AbstractBaseDataModel.append(self, data) + index = BaseObservableDataModel.append(self, data) self.RowAppended() return self.GetItem(index) def _insert(self, data: Any, index: int) -> wx.dataview.DataViewItem: - index = AbstractBaseDataModel.insert(self, data, index) + index = BaseObservableDataModel.insert(self, data, index) self.RowInserted(index) return self.GetItem(index) def _remove(self, data: Any) -> bool: - index = AbstractBaseDataModel.remove(self, data) + index = BaseObservableDataModel.remove(self, data) self.RowDeleted(index) @@ -237,14 +320,14 @@ def _remove(self, data: Any) -> bool: return True def _replace(self, data: Any, index: int) -> bool: - index = AbstractBaseDataModel.replace(self, data, index) + index = BaseObservableDataModel.replace(self, data, index) self.RowChanged(index) return True def _move(self, data: Any, current: int, future: int) -> bool: - AbstractBaseDataModel.move(self, data, current, future) + BaseObservableDataModel.move(self, data, current, future) self.RowChanged(current) self.RowChanged(future) @@ -252,46 +335,13 @@ def _move(self, data: Any, current: int, future: int) -> bool: return True def set_observable(self, observable: Union[ObservableList, ObservableLazyList]): - self._observable = observable - self._observable.subscribe(self._load) - self._observable.subscribe(self._append, callback_event=CallbackEvent.ON_APPEND) - self._observable.subscribe(self._insert, callback_event=CallbackEvent.ON_INSERT) - self._observable.subscribe(self._remove, callback_event=CallbackEvent.ON_REMOVE) - self._observable.subscribe(self._move, callback_event=CallbackEvent.ON_MOVE) - - def get_data_by_item(self, item: wx.dataview.DataViewItem): - row = self.GetRow(item) - - return self.get_data_by_row(row) - - def clear(self): - AbstractBaseDataModel.clear(self) - self.Reset(0) - self.Cleared() - - def GetColumnCount(self) -> int: - if hasattr(self, "MAP_COLUMN_FIELDS"): - return len(self.MAP_COLUMN_FIELDS.keys()) - - return self._column_count - - def GetValueByRow(self, row, col): - if not self.data: - return "" - - if row >= len(self.data): - return "" - - if not hasattr(self, "MAP_COLUMN_FIELDS"): - return "" - - return self.MAP_COLUMN_FIELDS[col].get_value(self.get_data_by_row(row)) - - def HasValue(self, item, col): - if not self.data: - return False - - if not hasattr(self, "MAP_COLUMN_FIELDS"): - return True - - return self.MAP_COLUMN_FIELDS[col].has_value(self.get_data_by_item(item)) + self._set_observable_handlers( + observable, + self._load, + { + CallbackEvent.ON_APPEND: self._append, + CallbackEvent.ON_INSERT: self._insert, + CallbackEvent.ON_REMOVE: self._remove, + CallbackEvent.ON_MOVE: self._move, + }, + ) diff --git a/windows/dialogs/connections/controller.py b/windows/dialogs/connections/controller.py index c2f4ebf..f955086 100644 --- a/windows/dialogs/connections/controller.py +++ b/windows/dialogs/connections/controller.py @@ -3,7 +3,7 @@ import wx import wx.dataview -from helpers.dataview import BaseDataViewTreeModel +from helpers.dataview import BaseObservableDataViewTreeModel from structures.connection import Connection @@ -11,7 +11,7 @@ from windows.dialogs.connections.repository import ConnectionsRepository -class ConnectionsTreeModel(BaseDataViewTreeModel): +class ConnectionsTreeModel(BaseObservableDataViewTreeModel): def __init__(self): super().__init__(column_count=2) self._parent_map = {} diff --git a/windows/main/tabs/check.py b/windows/main/tabs/check.py index 2928d75..99c3c94 100755 --- a/windows/main/tabs/check.py +++ b/windows/main/tabs/check.py @@ -3,7 +3,7 @@ import wx import wx.dataview -from helpers.dataview import BaseDataViewListModel, ColumnField +from helpers.dataview import BaseObservableDataViewListModel, ColumnField from structures.helpers import merge_original_current from structures.engines.database import SQLCheck, SQLTable @@ -12,7 +12,7 @@ from windows.main.tabs.column import NEW_TABLE -class TableCheckModel(BaseDataViewListModel): +class TableCheckModel(BaseObservableDataViewListModel): MAP_COLUMN_FIELDS = { 0: ColumnField("name", lambda s, x: wx.dataview.DataViewIconText(s.name or "", wx.NullBitmap)), 1: ColumnField("expression"), diff --git a/windows/main/tabs/column.py b/windows/main/tabs/column.py index c4aca9b..dec0d29 100644 --- a/windows/main/tabs/column.py +++ b/windows/main/tabs/column.py @@ -5,7 +5,7 @@ from helpers.loader import Loader from helpers.logger import logger -from helpers.dataview import BaseDataViewListModel, ColumnField +from helpers.dataview import BaseObservableDataViewListModel, ColumnField from structures.helpers import merge_original_current from structures.session import Session @@ -16,7 +16,7 @@ from windows.state import CURRENT_COLUMN, CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE, NEW_TABLE -class ColumnModel(BaseDataViewListModel): +class ColumnModel(BaseObservableDataViewListModel): MAP_COLUMN_FIELDS: dict[int, ColumnField] def GetColumnCount(self): diff --git a/windows/main/tabs/database.py b/windows/main/tabs/database.py index d9333fc..e80bb46 100644 --- a/windows/main/tabs/database.py +++ b/windows/main/tabs/database.py @@ -6,7 +6,7 @@ from gettext import gettext as _ from helpers import bytes_to_human -from helpers.dataview import BaseDataViewListModel, ColumnField +from helpers.dataview import BaseObservableDataViewListModel, ColumnField from structures.engines.database import SQLTable, SQLDatabase @@ -15,7 +15,7 @@ # SELECTED_TABLE: Observable[SQLTable] = Observable() -class ModelDatabaseTable(BaseDataViewListModel): +class ModelDatabaseTable(BaseObservableDataViewListModel): MAP_COLUMN_FIELDS = { 0: ColumnField("name", str), 1: ColumnField("total_rows", str), diff --git a/windows/main/tabs/foreign_key.py b/windows/main/tabs/foreign_key.py index 20c2c54..ee53470 100755 --- a/windows/main/tabs/foreign_key.py +++ b/windows/main/tabs/foreign_key.py @@ -5,7 +5,7 @@ from helpers.loader import Loader from helpers.logger import logger -from helpers.dataview import BaseDataViewListModel +from helpers.dataview import BaseObservableDataViewListModel from structures.helpers import merge_original_current @@ -16,7 +16,7 @@ from structures.engines.database import SQLForeignKey, SQLTable -class TableForeignKeyModel(BaseDataViewListModel): +class TableForeignKeyModel(BaseObservableDataViewListModel): def GetValueByRow(self, row, col): if row >= len(self.data): diff --git a/windows/main/tabs/index.py b/windows/main/tabs/index.py index 00bce6e..8a469f3 100755 --- a/windows/main/tabs/index.py +++ b/windows/main/tabs/index.py @@ -1,7 +1,7 @@ import wx import wx.dataview -from helpers.dataview import BaseDataViewListModel, ColumnField +from helpers.dataview import BaseObservableDataViewListModel, ColumnField from structures.helpers import merge_original_current @@ -11,7 +11,7 @@ from structures.engines.database import SQLTable, SQLIndex -class TableIndexModel(BaseDataViewListModel): +class TableIndexModel(BaseObservableDataViewListModel): MAP_COLUMN_FIELDS = { 0: ColumnField("name", lambda i, x: wx.dataview.DataViewIconText(i.name, wx.GetApp().icon_registry_16.get_bitmap(i.type.bitmap))), 1: ColumnField("expression", lambda i, x: ", ".join(i.columns)), diff --git a/windows/main/tabs/records.py b/windows/main/tabs/records.py index 535b4dc..669e476 100644 --- a/windows/main/tabs/records.py +++ b/windows/main/tabs/records.py @@ -5,7 +5,7 @@ import wx.dataview import wx.stc -from helpers.dataview import BaseDataViewListModel +from helpers.dataview import BaseObservableDataViewListModel from helpers.logger import logger from helpers.observables import ObservableList @@ -22,7 +22,7 @@ NEW_RECORDS: ObservableList[SQLRecord] = ObservableList() -class RecordsModel(BaseDataViewListModel): +class RecordsModel(BaseObservableDataViewListModel): def __init__(self, table: SQLTable, column_count: Optional[int] = None): super().__init__(column_count) From df43621df4488814c7bcf8f4030174764e9a29f1 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 14 Mar 2026 18:21:37 +0100 Subject: [PATCH 04/93] ui(dataview): reuse app icon registry and editor dialog factory AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- windows/components/__init__.py | 1 + windows/components/dataview.py | 17 +++++------------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/windows/components/__init__.py b/windows/components/__init__.py index cc56dd6..12c6c5f 100644 --- a/windows/components/__init__.py +++ b/windows/components/__init__.py @@ -134,6 +134,7 @@ class BaseDataViewCtrl(wx.dataview.DataViewCtrl): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.app = wx.GetApp() self.Bind(wx.EVT_CHAR_HOOK, self._on_char_hook) def finish_editing(self, current_column): diff --git a/windows/components/dataview.py b/windows/components/dataview.py index 21e3eab..37ea226 100644 --- a/windows/components/dataview.py +++ b/windows/components/dataview.py @@ -16,7 +16,6 @@ from windows.components import BaseDataViewCtrl 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.dialogs.advanced_cell_editor import AdvancedCellEditorController from windows.state import CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE, NEW_TABLE @@ -288,7 +287,6 @@ def __init__(self, *args, **kwargs): self.Bind(wx.EVT_CONTEXT_MENU, self._on_context_menu) def _on_context_menu(self, event): - from icons import BitmapList selected = self.GetSelection() model = self.GetModel() @@ -298,23 +296,18 @@ def _on_context_menu(self, event): menu = wx.Menu() add_item = wx.MenuItem(menu, wx.ID_ANY, _("Add foreign key"), wx.EmptyString, wx.ITEM_NORMAL) - add_item.SetBitmap(BitmapList.ADD) + add_item.SetBitmap(self.app.icon_registry_16.get_bitmap(IconList.ADD)) menu.Append(add_item) self.Bind(wx.EVT_MENU, self.on_foreign_key_insert, add_item) delete_item = wx.MenuItem(menu, wx.ID_ANY, _("Remove foreign key"), wx.EmptyString, wx.ITEM_NORMAL) - delete_item.SetBitmap(BitmapList.DELETE) + delete_item.SetBitmap(self.app.icon_registry_16.get_bitmap(IconList.DELETE)) menu.Append(delete_item) menu.Enable(delete_item.GetId(), selected.IsOk()) self.Bind(wx.EVT_MENU, self.on_foreign_key_delete, delete_item) - # Forse non necessario, dato che editing è già disponibile - # update_item = wx.MenuItem(menu, wx.ID_ANY, _("Update foreign key"), wx.EmptyString, wx.ITEM_NORMAL) - # menu.Append(update_item) - # self.Bind(wx.EVT_MENU, self.on_foreign_key_update, update_item) - self.PopupMenu(menu) def _load_table_columns(self, popup, column2_render: PopupRenderer) -> list[str]: @@ -336,9 +329,9 @@ def __init__(self, *args, **kwargs): CURRENT_TABLE.subscribe(self._load_table) - def make_advanced_dialog(self, parent, value: str): + def make_advanced_dialog(self, parent, value: str, read_only : bool = False): from windows.dialogs.advanced_cell_editor import AdvancedCellEditorController - return AdvancedCellEditorController(parent, value) + return AdvancedCellEditorController(parent, value, read_only) def _get_column_renderer(self, column: SQLColumn) -> wx.dataview.DataViewRenderer: for foreign_key in column.table.foreign_keys: @@ -481,7 +474,7 @@ def _on_item_activated(self, event: wx.dataview.DataViewEvent) -> None: row = model.GetRow(item) value = model.GetValueByRow(row, model_column) - dialog = AdvancedCellEditorController(self, str(value or ""), read_only=True) + dialog = self.make_advanced_dialog(self, str(value or ""), read_only=True) try: dialog.ShowModal() finally: From 9dc6ed0f2050ae574d97ff99df21eddb61313043 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 14 Mar 2026 18:21:47 +0100 Subject: [PATCH 05/93] feat(query): add cancelable execution and richer result metadata AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- windows/main/controller.py | 9 +- windows/main/tabs/query.py | 441 +++++++++++++++++++++++++++++++------ windows/views.py | 19 +- 3 files changed, 400 insertions(+), 69 deletions(-) diff --git a/windows/main/controller.py b/windows/main/controller.py index b4fb83a..82bfd3a 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -76,7 +76,11 @@ def __init__(self): self.controller_list_table_check = TableCheckController(self.dv_table_checks) self.controller_list_table_foreign_key = TableForeignKeyController(self.dv_table_foreign_keys) - self.controller_query_records = QueryResultsController(self.sql_query_editor, self.notebook_sql_results) + self.controller_query_records = QueryResultsController( + self.sql_query_editor, + self.notebook_sql_results, + cancel_button=self.cancel_query_execution, + ) self.controller_view_editor = ViewEditorController(self) @@ -1060,5 +1064,8 @@ def on_apply_filters(self, event): self._records_offset = 0 self._load_records_page() + def on_cancel_query_execution(self, event): + self.controller_query_records.cancel_execution(event) + # def on_clear_record(self, event): # self.controller_list_table_records.on_row_clear() diff --git a/windows/main/tabs/query.py b/windows/main/tabs/query.py index dc44e8a..4a696b4 100644 --- a/windows/main/tabs/query.py +++ b/windows/main/tabs/query.py @@ -1,4 +1,6 @@ +import contextlib import dataclasses +import datetime import enum import threading import time @@ -13,11 +15,24 @@ from helpers.dataview import BaseDataViewListModel from structures.session import Session -from structures.connection import ConnectionEngine +from structures.connection import Connection, ConnectionEngine +from structures.engines.datatype import DataTypeCategory, SQLDataType +from windows.components.popup import PopupCalendar, PopupCalendarTime +from windows.components.renders import AdvancedTextRenderer, FloatRenderer, IntegerRenderer, PopupRenderer, TextRenderer, TimeRenderer from windows.components.dataview import QueryEditorResultsDataViewCtrl +class _ReadOnlyPopupRenderer(PopupRenderer): + def ActivateCell(self, rect, model, item, col, mouseEvent): + return False + + +class _ReadOnlyTimeRenderer(TimeRenderer): + def HasEditorCtrl(self): + return False + + @dataclasses.dataclass class ParsedStatement: text: str @@ -32,12 +47,25 @@ class ExecutionResult: success: bool columns: Optional[list[str]] = None rows: Optional[list[tuple]] = None + column_datatypes: Optional[list[Optional[SQLDataType]]] = None affected_rows: Optional[int] = None elapsed_ms: float = 0.0 error: Optional[str] = None + cancelled: bool = False warnings: list[str] = dataclasses.field(default_factory=list) +@dataclasses.dataclass +class ExecutionSummary: + total_statements: int = 0 + completed_statements: int = 0 + successful_statements: int = 0 + failed_statements: int = 0 + elapsed_ms: float = 0.0 + cancelled: bool = False + last_statement: Optional[ParsedStatement] = None + + class ExecutionMode(enum.Enum): ALL = "all" SELECTION = "selection" @@ -73,7 +101,7 @@ def parse(self, sql_text: str) -> list[ParsedStatement]: continue if in_block_comment: - if i + 1 < length and sql_text[i:i+2] == '*/': + if i + 1 < length and sql_text[i:i + 2] == '*/': in_block_comment = False i += 2 continue @@ -92,13 +120,13 @@ def parse(self, sql_text: str) -> list[ParsedStatement]: continue if char == "'" and not in_double_quote: - if i + 1 < length and sql_text[i+1] == "'": + if i + 1 < length and sql_text[i + 1] == "'": i += 2 continue in_single_quote = not in_single_quote elif char == '"' and not in_single_quote: - if i + 1 < length and sql_text[i+1] == '"': + if i + 1 < length and sql_text[i + 1] == '"': i += 2 continue in_double_quote = not in_double_quote @@ -131,12 +159,12 @@ def parse(self, sql_text: str) -> list[ParsedStatement]: def _is_line_comment_start(self, text: str, pos: int) -> bool: if pos + 1 >= len(text): return False - return text[pos:pos+2] in ('--', '# ') + return text[pos:pos + 2] in ('--', '# ') def _is_block_comment_start(self, text: str, pos: int) -> bool: if pos + 1 >= len(text): return False - return text[pos:pos+2] == '/*' + return text[pos:pos + 2] == '/*' class StatementSelector: @@ -144,8 +172,8 @@ def __init__(self, stc_editor: wx.stc.StyledTextCtrl): self.editor = stc_editor def get_execution_scope( - self, - statements: list[ParsedStatement] + self, + statements: list[ParsedStatement] ) -> tuple[ExecutionMode, list[ParsedStatement]]: selection_start = self.editor.GetSelectionStart() selection_end = self.editor.GetSelectionEnd() @@ -169,9 +197,9 @@ def get_execution_scope( return (ExecutionMode.ALL, statements) def _find_statement_at_caret( - self, - caret_pos: int, - statements: list[ParsedStatement] + self, + caret_pos: int, + statements: list[ParsedStatement] ) -> Optional[ParsedStatement]: for stmt in statements: if stmt.start_pos <= caret_pos <= stmt.end_pos: @@ -194,39 +222,63 @@ def __init__(self, session: Session): self.session = session self._cancel_requested = False self._current_thread: Optional[threading.Thread] = None + self._worker_context: Optional[Any] = None self._lock = threading.Lock() def execute_statements( - self, - statements: list[ParsedStatement], - on_statement_complete: Callable[[ExecutionResult], None], - on_all_complete: Callable[[], None], - stop_on_error: bool = True + self, + statements: list[ParsedStatement], + on_statement_complete: Callable[[ExecutionResult], None], + on_all_complete: Callable[[ExecutionSummary], None], + current_database: Optional[Any] = None, + stop_on_error: bool = True ) -> None: self._cancel_requested = False self._current_thread = threading.Thread( target=self._execute_worker, - args=(statements, on_statement_complete, on_all_complete, stop_on_error), + args=( + statements, + on_statement_complete, + on_all_complete, + current_database, + stop_on_error, + ), daemon=True ) self._current_thread.start() def _execute_worker( - self, - statements: list[ParsedStatement], - on_statement_complete: Callable[[ExecutionResult], None], - on_all_complete: Callable[[], None], - stop_on_error: bool + self, + statements: list[ParsedStatement], + on_statement_complete: Callable[[ExecutionResult], None], + on_all_complete: Callable[[ExecutionSummary], None], + current_database: Optional[Any], + stop_on_error: bool ) -> None: + time_start = time.perf_counter() + summary = ExecutionSummary(total_statements=len(statements)) + try: + context = self._create_worker_context(current_database) + self._set_worker_context(context) + for stmt in statements: if self._cancel_requested: + summary.cancelled = True break - result = self._execute_single(stmt) - # Thread-safe UI update - wx.CallAfter(on_statement_complete, result) + summary.last_statement = stmt + result = self._execute_single(context, stmt) + + if result.success: + summary.completed_statements += 1 + summary.successful_statements += 1 + elif not result.cancelled: + summary.completed_statements += 1 + summary.failed_statements += 1 + + self._dispatch_statement_result(on_statement_complete, result) if not result.success and stop_on_error: break @@ -234,26 +286,48 @@ def _execute_worker( except Exception as ex: logger.error(f"Execution worker error: {ex}", exc_info=True) finally: - wx.CallAfter(on_all_complete) + summary.cancelled = summary.cancelled or self._cancel_requested + summary.elapsed_ms = (time.perf_counter() - time_start) * 1000 + + self._clear_worker_context() + + wx.CallAfter(on_all_complete, summary) + + def _dispatch_statement_result( + self, + on_statement_complete: Callable[[ExecutionResult], None], + result: ExecutionResult, + ) -> None: + ui_done_event = threading.Event() + + def _on_ui_thread() -> None: + on_statement_complete(result) + ui_done_event.set() - def _execute_single(self, statement: ParsedStatement) -> ExecutionResult: + wx.CallAfter(_on_ui_thread) + while not ui_done_event.wait(0.05): + continue + + def _execute_single(self, context: Any, statement: ParsedStatement) -> ExecutionResult: start_time = time.time() try: - self.session.context.execute(statement.text) + context.execute(statement.text) elapsed_ms = (time.time() - start_time) * 1000 - cursor = self.session.context.cursor + cursor = context.cursor if cursor.description: columns = [desc[0] for desc in cursor.description] - rows = self.session.context.fetchall() + column_datatypes = context.get_result_column_datatypes(cursor) + rows = context.fetchall() return ExecutionResult( statement=statement, success=True, columns=columns, rows=rows, + column_datatypes=column_datatypes, affected_rows=len(rows), elapsed_ms=elapsed_ms ) @@ -269,16 +343,68 @@ def _execute_single(self, statement: ParsedStatement) -> ExecutionResult: except Exception as ex: elapsed_ms = (time.time() - start_time) * 1000 + is_cancelled = self._cancel_requested return ExecutionResult( statement=statement, success=False, error=str(ex), + cancelled=is_cancelled, elapsed_ms=elapsed_ms ) + def _build_worker_connection(self) -> Connection: + connection = self.session.connection.copy() + + if not connection.has_enabled_tunnel(): + return connection + + context = getattr(self.session, "context", None) + configuration = getattr(connection, "configuration", None) + + if context is not None and configuration is not None and hasattr(configuration, "_replace"): + replace_kwargs = {} + + if hasattr(configuration, "hostname") and getattr(context, "host", None): + replace_kwargs["hostname"] = context.host + + if hasattr(configuration, "port") and getattr(context, "port", None) is not None: + replace_kwargs["port"] = int(context.port) + + if replace_kwargs: + connection.configuration = configuration._replace(**replace_kwargs) + + connection.ssh_tunnel = None + return connection + + def _create_worker_context(self, current_database: Optional[Any]) -> Any: + context = self.session._get_context_class()(self._build_worker_connection()) + context.connect() + + if current_database is not None: + with contextlib.suppress(Exception): + context.set_database(current_database) + + return context + + def _set_worker_context(self, context: Any) -> None: + with self._lock: + self._worker_context = context + + def _clear_worker_context(self) -> None: + context = None + + with self._lock: + context = self._worker_context + self._worker_context = None + + if context is not None: + with contextlib.suppress(Exception): + context.disconnect() + def cancel(self) -> None: self._cancel_requested = True + self._clear_worker_context() def is_running(self) -> bool: return self._current_thread is not None and self._current_thread.is_alive() @@ -288,6 +414,7 @@ class QueryResultsRenderer: def __init__(self, notebook: wx.Notebook, session: Session): self.notebook = notebook self.session = session + self._models: list[Any] = [] self._tab_counter = 0 def create_result_tab(self, result: ExecutionResult) -> wx.Panel: @@ -339,20 +466,79 @@ def _generate_tab_name(self, result: ExecutionResult) -> str: return _("Query {query_number}").format(query_number=self._tab_counter) def _populate_grid( - self, - results_dataview: QueryEditorResultsDataViewCtrl, - result: ExecutionResult + self, + results_dataview: QueryEditorResultsDataViewCtrl, + result: ExecutionResult ) -> None: if not result.columns: return for i, col_name in enumerate(result.columns): - results_dataview.AppendTextColumn(col_name, i, wx.dataview.DATAVIEW_CELL_INERT) + datatype = self._get_column_datatype(result, i) + renderer = self._get_column_renderer(results_dataview, datatype) + align = wx.ALIGN_CENTER if datatype and datatype.name == "BOOLEAN" else wx.ALIGN_LEFT + + column = wx.dataview.DataViewColumn( + col_name, + renderer, + i, + width=results_dataview.measure_text(col_name), + align=align, + flags=wx.dataview.DATAVIEW_COL_RESIZABLE, + ) + results_dataview.AppendColumn(column) - model = QueryResultsGridModel(column_count=len(result.columns)) - model.load_rows(result.columns, result.rows or []) + model = QueryResultsModel(column_count=len(result.columns)) + model.load(result.rows, result.columns, result.column_datatypes) + self._models.append(model) results_dataview.AssociateModel(model) - results_dataview._query_results_model = model + wx.CallAfter(results_dataview.autosize_columns_from_content) + + def _get_column_datatype(self, result: ExecutionResult, column_index: int) -> Optional[SQLDataType]: + if not result.column_datatypes: + return None + + if column_index >= len(result.column_datatypes): + return None + + return result.column_datatypes[column_index] + + def _get_column_renderer( + self, + results_dataview: QueryEditorResultsDataViewCtrl, + datatype: Optional[SQLDataType] + ) -> wx.dataview.DataViewRenderer: + if datatype is None: + return TextRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) + + if datatype.name == "BOOLEAN": + return wx.dataview.DataViewToggleRenderer( + mode=wx.dataview.DATAVIEW_CELL_INERT, + align=wx.ALIGN_CENTER, + ) + + if datatype.name == "DATE": + return _ReadOnlyPopupRenderer(PopupCalendar) + + if datatype.name == "TIME": + return _ReadOnlyTimeRenderer() + + if datatype.name in ["DATETIME", "TIMESTAMP"]: + return _ReadOnlyPopupRenderer(PopupCalendarTime) + + if datatype.category == DataTypeCategory.INTEGER: + return IntegerRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) + + if datatype.category == DataTypeCategory.REAL: + return FloatRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) + + if datatype.category == DataTypeCategory.TEXT: + return AdvancedTextRenderer( + mode=wx.dataview.DATAVIEW_CELL_INERT, + dialog_factory=results_dataview.make_advanced_dialog, + ) + + return TextRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) def _create_footer(self, parent: wx.Panel, result: ExecutionResult) -> wx.StaticText: parts = [] @@ -397,56 +583,129 @@ def _create_error_panel(self, parent: wx.Panel, result: ExecutionResult) -> wx.P def clear_all_tabs(self) -> None: while self.notebook.GetPageCount() > 0: self.notebook.DeletePage(0) + self._models = [] self._tab_counter = 0 -class QueryResultsGridModel(BaseDataViewListModel): +class QueryResultsModel(BaseDataViewListModel): def __init__(self, column_count: int): super().__init__(column_count) self._columns: list[str] = [] + self._column_datatypes: list[Optional[SQLDataType]] = [] - def load_rows(self, columns: list[str], rows: list[Any]) -> None: - self._columns = list(columns) - - normalized_rows: list[tuple[Any, ...]] = [] - for row in rows: - if isinstance(row, dict): - normalized_rows.append(tuple(row.get(column) for column in self._columns)) - elif isinstance(row, (list, tuple)): - normalized_rows.append(tuple(row)) - else: - normalized_rows.append((row,)) - - self._data = normalized_rows - self.Reset(len(self._data)) + def load( + self, + data: list[Any], + columns: list[str], + column_datatypes: Optional[list[Optional[SQLDataType]]] = None, + ): + self._columns = columns + self._column_datatypes = column_datatypes or [None for _ in columns] + BaseDataViewListModel.load(self, data) def GetValueByRow(self, row, col): if row < 0 or row >= len(self.data): return "" - row_values = self.data[row] - if col < 0 or col >= len(row_values): + if col < 0 or col >= len(self._columns): return "" - value = row_values[col] - return "" if value is None else str(value) + value = self._get_cell_value(self.data[row], col) + if value is None: + return "" + + datatype = self._get_column_datatype(col) + if datatype is None: + return str(value) + + if datatype.name == "BOOLEAN": + return bool(value) + + if datatype.category == DataTypeCategory.TEMPORAL: + return self._format_temporal_value(value, datatype.name) + + return str(value) + + def SetValueByRow(self, value, row, col): + return False + + def HasValue(self, item, col): + if col < 0 or col >= len(self._columns): + return False + + row = self.GetRow(item) + if row < 0 or row >= len(self.data): + return False + + return self._get_cell_value(self.data[row], col) is not None + + def GetAttr(self, item, col, attr): + datatype = self._get_column_datatype(col) + if datatype is None: + return super().GetAttr(item, col, attr) + + color = datatype.category.value.color + attr.SetColour(wx.Colour(color)) + return super().GetAttr(item, col, attr) + + def _format_temporal_value(self, value: Any, datatype_name: str) -> str: + if isinstance(value, datetime.datetime): + if datatype_name == "DATE": + return value.strftime("%Y-%m-%d") + + if datatype_name == "TIME": + return value.strftime("%H:%M:%S") + + if datatype_name in ["DATETIME", "TIMESTAMP"]: + return value.strftime("%Y-%m-%d %H:%M:%S") + + if datatype_name == "YEAR": + return value.strftime("%Y") + + if isinstance(value, datetime.date) and datatype_name == "DATE": + return value.strftime("%Y-%m-%d") + + if isinstance(value, datetime.time) and datatype_name == "TIME": + return value.strftime("%H:%M:%S") + + return str(value) + + def _get_cell_value(self, row_data: Any, col: int) -> Any: + if isinstance(row_data, dict): + return row_data.get(self._columns[col]) + + if col < len(row_data): + return row_data[col] + + return None + + def _get_column_datatype(self, col: int) -> Optional[SQLDataType]: + if col < 0 or col >= len(self._column_datatypes): + return None + + return self._column_datatypes[col] class QueryEditorController: def __init__( - self, - stc_editor: wx.stc.StyledTextCtrl, - results_notebook: wx.Notebook, - session_provider: Callable[[], Optional[Session]] + self, + stc_editor: wx.stc.StyledTextCtrl, + results_notebook: wx.Notebook, + session_provider: Callable[[], Optional[Session]], + database_provider: Optional[Callable[[], Optional[Any]]] = None, + cancel_button: Optional[wx.Button] = None, ): self.editor = stc_editor self.notebook = results_notebook self.get_session = session_provider + self.get_database = database_provider or (lambda: None) + self.cancel_button = cancel_button self.parser: Optional[SQLStatementParser] = None self.selector = StatementSelector(stc_editor) self.executor: Optional[QueryExecutor] = None self.renderer: Optional[QueryResultsRenderer] = None + self._cancel_feedback_pending = False self._bind_shortcuts() @@ -483,6 +742,7 @@ def execute_current(self, event: wx.Event) -> None: def cancel_execution(self, event: wx.Event) -> None: if self.executor and self.executor.is_running(): + self._cancel_feedback_pending = True self.executor.cancel() logger.info("Query execution cancelled") @@ -518,28 +778,81 @@ def _execute(self, mode: ExecutionMode) -> None: return self.renderer.clear_all_tabs() + self._cancel_feedback_pending = False + self._set_cancel_button_enabled(True) self.executor.execute_statements( statements=statements_to_execute, on_statement_complete=self._on_statement_complete, on_all_complete=self._on_all_complete, + current_database=self.get_database(), stop_on_error=True ) def _on_statement_complete(self, result: ExecutionResult) -> None: + if result.cancelled: + return + if self.renderer: self.renderer.create_result_tab(result) - def _on_all_complete(self) -> None: + def _set_cancel_button_enabled(self, enabled: bool) -> None: + if self.cancel_button is not None: + self.cancel_button.Enable(enabled) + + def _format_elapsed(self, elapsed_ms: float) -> str: + if elapsed_ms < 1000: + return _("{elapsed_ms:.0f} ms").format(elapsed_ms=elapsed_ms) + + return _("{elapsed_s:.2f} s").format(elapsed_s=elapsed_ms / 1000) + + def _show_cancel_message(self, summary: ExecutionSummary) -> None: + last_statement_label = _("none") + if summary.last_statement is not None: + last_statement_label = str(summary.last_statement.statement_index + 1) + + wx.MessageBox( + _( + "Query execution stopped after {elapsed}.\n" + "Completed statements: {completed}/{total}.\n" + "Successful: {success}.\n" + "Failed: {failed}.\n" + "Last statement: #{last}." + ).format( + elapsed=self._format_elapsed(summary.elapsed_ms), + completed=summary.completed_statements, + total=summary.total_statements, + success=summary.successful_statements, + failed=summary.failed_statements, + last=last_statement_label, + ), + _("Query execution cancelled"), + wx.OK | wx.ICON_INFORMATION, + ) + + def _on_all_complete(self, summary: ExecutionSummary) -> None: + self._set_cancel_button_enabled(False) + + if summary.cancelled and self._cancel_feedback_pending: + self._show_cancel_message(summary) + + self._cancel_feedback_pending = False logger.info("Query execution completed") class QueryResultsController(QueryEditorController): - def __init__(self, stc_sql_query: wx.stc.StyledTextCtrl, notebook_sql_results: wx.Notebook): - from windows.main import CURRENT_SESSION + def __init__( + self, + stc_sql_query: wx.stc.StyledTextCtrl, + notebook_sql_results: wx.Notebook, + cancel_button: Optional[wx.Button] = None, + ): + from windows.main import CURRENT_DATABASE, CURRENT_SESSION # Lazy import: unavoidable circular dependency. super().__init__( stc_editor=stc_sql_query, results_notebook=notebook_sql_results, - session_provider=lambda: CURRENT_SESSION.get_value() + session_provider=lambda: CURRENT_SESSION.get_value(), + database_provider=lambda: CURRENT_DATABASE.get_value(), + cancel_button=cancel_button, ) diff --git a/windows/views.py b/windows/views.py index 59f6b85..7771316 100755 --- a/windows/views.py +++ b/windows/views.py @@ -527,7 +527,7 @@ def __init__( self, parent ): bSizer37111211 = wx.BoxSizer( wx.HORIZONTAL ) - self.m_staticText15111211 = wx.StaticText( self.panel_statistics, wx.ID_ANY, _(u" Average connection time (ms)"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText15111211 = wx.StaticText( self.panel_statistics, wx.ID_ANY, _(u"Average connection time (ms)"), wx.DefaultPosition, wx.DefaultSize, 0 ) self.m_staticText15111211.Wrap( -1 ) self.m_staticText15111211.SetMinSize( wx.Size( 200,-1 ) ) @@ -544,7 +544,7 @@ def __init__( self, parent ): bSizer371112111 = wx.BoxSizer( wx.HORIZONTAL ) - self.m_staticText151112111 = wx.StaticText( self.panel_statistics, wx.ID_ANY, _(u" Most recent connection duration"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText151112111 = wx.StaticText( self.panel_statistics, wx.ID_ANY, _(u"Most recent connection duration"), wx.DefaultPosition, wx.DefaultSize, 0 ) self.m_staticText151112111.Wrap( -1 ) self.m_staticText151112111.SetMinSize( wx.Size( 200,-1 ) ) @@ -2234,7 +2234,7 @@ def __init__( self, parent ): 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 ) @@ -2250,6 +2250,13 @@ def __init__( self, parent ): self.m_panel52 = wx.Panel( self.m_splitter6, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer125 = wx.BoxSizer( wx.VERTICAL ) + self.cancel_query_execution = wx.Button( self.m_panel52, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + + self.cancel_query_execution.SetBitmap( wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ) ) + self.cancel_query_execution.Enable( False ) + + bSizer125.Add( self.cancel_query_execution, 0, wx.ALIGN_RIGHT|wx.ALL, 5 ) + self.sql_query_editor = wx.stc.StyledTextCtrl( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0) self.sql_query_editor.SetUseTabs ( True ) self.sql_query_editor.SetTabWidth ( 4 ) @@ -2315,7 +2322,7 @@ def __init__( self, parent ): self.panel_query.SetSizer( bSizer26 ) self.panel_query.Layout() bSizer26.Fit( self.panel_query ) - self.MainFrameNotebook.AddPage( self.panel_query, _(u"Query"), False ) + self.MainFrameNotebook.AddPage( self.panel_query, _(u"Query"), True ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/arrow_right.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2457,6 +2464,7 @@ def __init__( self, parent ): self.chb_auto_apply.Bind( wx.EVT_CHECKBOX, self.on_auto_apply ) 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.cancel_query_execution.Bind( wx.EVT_BUTTON, self.on_cancel_query_execution ) def __del__( self ): pass @@ -2566,6 +2574,9 @@ def on_collapsible_pane_changed( self, event ): def on_apply_filters( self, event ): event.Skip() + def on_cancel_query_execution( self, event ): + event.Skip() + def m_splitter51OnIdle( self, event ): self.m_splitter51.SetSashPosition( -150 ) self.m_splitter51.Unbind( wx.EVT_IDLE ) From 16be462a76db9db196269e924428c8140f9e03df Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 14 Mar 2026 18:21:56 +0100 Subject: [PATCH 06/93] feat(engines): improve datatype alias mapping and result typing AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- structures/engines/mariadb/datatype.py | 22 +++++++++++----------- structures/engines/mysql/datatype.py | 22 +++++++++++----------- structures/engines/sqlite/context.py | 5 +++++ 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/structures/engines/mariadb/datatype.py b/structures/engines/mariadb/datatype.py index bb399f7..4060773 100755 --- a/structures/engines/mariadb/datatype.py +++ b/structures/engines/mariadb/datatype.py @@ -3,20 +3,20 @@ class MariaDBDataType(StandardDataType): # Integer types - TINYINT = SQLDataType(name="TINYINT", category=DataTypeCategory.INTEGER, has_unsigned=True) - SMALLINT = SQLDataType(name="SMALLINT", category=DataTypeCategory.INTEGER, has_unsigned=True) - MEDIUMINT = SQLDataType(name="MEDIUMINT", category=DataTypeCategory.INTEGER, has_unsigned=True) - INT = SQLDataType(name="INT", category=DataTypeCategory.INTEGER, alias=["INTEGER"], has_unsigned=True) - BIGINT = SQLDataType(name="BIGINT", category=DataTypeCategory.INTEGER, has_unsigned=True) + TINYINT = SQLDataType(name="TINYINT", category=DataTypeCategory.INTEGER, alias=["TINY"], has_unsigned=True) + SMALLINT = SQLDataType(name="SMALLINT", category=DataTypeCategory.INTEGER, alias=["SHORT"], has_unsigned=True) + MEDIUMINT = SQLDataType(name="MEDIUMINT", category=DataTypeCategory.INTEGER, alias=["INT24"], has_unsigned=True) + INT = SQLDataType(name="INT", category=DataTypeCategory.INTEGER, alias=["INTEGER", "LONG"], has_unsigned=True) + BIGINT = SQLDataType(name="BIGINT", category=DataTypeCategory.INTEGER, alias=["LONGLONG"], has_unsigned=True) # Floating point FLOAT = SQLDataType(name="FLOAT", category=DataTypeCategory.REAL, has_precision=True) DOUBLE = SQLDataType(name="DOUBLE", category=DataTypeCategory.REAL, alias=["REAL"], has_precision=True) - DECIMAL = SQLDataType(name="DECIMAL", category=DataTypeCategory.REAL, alias=["DEC"], has_precision=True, has_scale=True) + DECIMAL = SQLDataType(name="DECIMAL", category=DataTypeCategory.REAL, alias=["DEC", "NEWDECIMAL"], has_precision=True, has_scale=True) # Text types - CHAR = SQLDataType(name="CHAR", category=DataTypeCategory.TEXT, has_length=True, max_size=255) - VARCHAR = SQLDataType(name="VARCHAR", category=DataTypeCategory.TEXT, has_length=True, max_size=65535, format=DataTypeFormat.STRING) + CHAR = SQLDataType(name="CHAR", category=DataTypeCategory.TEXT, alias=["STRING"], has_length=True, max_size=255) + VARCHAR = SQLDataType(name="VARCHAR", category=DataTypeCategory.TEXT, alias=["VAR_STRING"], has_length=True, max_size=65535, format=DataTypeFormat.STRING) TINYTEXT = SQLDataType(name="TINYTEXT", category=DataTypeCategory.TEXT, format=DataTypeFormat.STRING) TEXT = SQLDataType(name="TEXT", category=DataTypeCategory.TEXT, format=DataTypeFormat.STRING) MEDIUMTEXT = SQLDataType(name="MEDIUMTEXT", category=DataTypeCategory.TEXT, format=DataTypeFormat.STRING) @@ -25,10 +25,10 @@ class MariaDBDataType(StandardDataType): # Binary types BINARY = SQLDataType(name="BINARY", category=DataTypeCategory.BINARY, has_length=True, max_size=255) VARBINARY = SQLDataType(name="VARBINARY", category=DataTypeCategory.BINARY, has_length=True, max_size=65535) - TINYBLOB = SQLDataType(name="TINYBLOB", category=DataTypeCategory.BINARY) + TINYBLOB = SQLDataType(name="TINYBLOB", category=DataTypeCategory.BINARY, alias=["TINY_BLOB"]) BLOB = SQLDataType(name="BLOB", category=DataTypeCategory.BINARY) - MEDIUMBLOB = SQLDataType(name="MEDIUMBLOB", category=DataTypeCategory.BINARY) - LONGBLOB = SQLDataType(name="LONGBLOB", category=DataTypeCategory.BINARY) + MEDIUMBLOB = SQLDataType(name="MEDIUMBLOB", category=DataTypeCategory.BINARY, alias=["MEDIUM_BLOB"]) + LONGBLOB = SQLDataType(name="LONGBLOB", category=DataTypeCategory.BINARY, alias=["LONG_BLOB"]) # Date and time DATE = SQLDataType(name="DATE", category=DataTypeCategory.TEMPORAL) diff --git a/structures/engines/mysql/datatype.py b/structures/engines/mysql/datatype.py index 8a18716..0846df2 100644 --- a/structures/engines/mysql/datatype.py +++ b/structures/engines/mysql/datatype.py @@ -8,20 +8,20 @@ class MySQLDataType(StandardDataType): # Integer types - TINYINT = SQLDataType(name="TINYINT", category=DataTypeCategory.INTEGER, has_unsigned=True) - SMALLINT = SQLDataType(name="SMALLINT", category=DataTypeCategory.INTEGER, has_unsigned=True) - MEDIUMINT = SQLDataType(name="MEDIUMINT", category=DataTypeCategory.INTEGER, has_unsigned=True) - INT = SQLDataType(name="INT", category=DataTypeCategory.INTEGER, alias=["INTEGER"], has_unsigned=True) - BIGINT = SQLDataType(name="BIGINT", category=DataTypeCategory.INTEGER, has_unsigned=True) + TINYINT = SQLDataType(name="TINYINT", category=DataTypeCategory.INTEGER, alias=["TINY"], has_unsigned=True) + SMALLINT = SQLDataType(name="SMALLINT", category=DataTypeCategory.INTEGER, alias=["SHORT"], has_unsigned=True) + MEDIUMINT = SQLDataType(name="MEDIUMINT", category=DataTypeCategory.INTEGER, alias=["INT24"], has_unsigned=True) + INT = SQLDataType(name="INT", category=DataTypeCategory.INTEGER, alias=["INTEGER", "LONG"], has_unsigned=True) + BIGINT = SQLDataType(name="BIGINT", category=DataTypeCategory.INTEGER, alias=["LONGLONG"], has_unsigned=True) # Floating point FLOAT = SQLDataType(name="FLOAT", category=DataTypeCategory.REAL, has_precision=True) DOUBLE = SQLDataType(name="DOUBLE", category=DataTypeCategory.REAL, alias=["REAL"], has_precision=True) - DECIMAL = SQLDataType(name="DECIMAL", category=DataTypeCategory.REAL, alias=["DEC"], has_precision=True, has_scale=True) + DECIMAL = SQLDataType(name="DECIMAL", category=DataTypeCategory.REAL, alias=["DEC", "NEWDECIMAL"], has_precision=True, has_scale=True) # Text types - CHAR = SQLDataType(name="CHAR", category=DataTypeCategory.TEXT, has_length=True, max_size=255) - VARCHAR = SQLDataType(name="VARCHAR", category=DataTypeCategory.TEXT, has_length=True, max_size=65535, format=DataTypeFormat.STRING) + CHAR = SQLDataType(name="CHAR", category=DataTypeCategory.TEXT, alias=["STRING"], has_length=True, max_size=255) + VARCHAR = SQLDataType(name="VARCHAR", category=DataTypeCategory.TEXT, alias=["VAR_STRING"], has_length=True, max_size=65535, format=DataTypeFormat.STRING) TINYTEXT = SQLDataType(name="TINYTEXT", category=DataTypeCategory.TEXT, format=DataTypeFormat.STRING) TEXT = SQLDataType(name="TEXT", category=DataTypeCategory.TEXT, format=DataTypeFormat.STRING) MEDIUMTEXT = SQLDataType(name="MEDIUMTEXT", category=DataTypeCategory.TEXT, format=DataTypeFormat.STRING) @@ -30,10 +30,10 @@ class MySQLDataType(StandardDataType): # Binary types BINARY = SQLDataType(name="BINARY", category=DataTypeCategory.BINARY, has_length=True, max_size=255) VARBINARY = SQLDataType(name="VARBINARY", category=DataTypeCategory.BINARY, has_length=True, max_size=65535) - TINYBLOB = SQLDataType(name="TINYBLOB", category=DataTypeCategory.BINARY) + TINYBLOB = SQLDataType(name="TINYBLOB", category=DataTypeCategory.BINARY, alias=["TINY_BLOB"]) BLOB = SQLDataType(name="BLOB", category=DataTypeCategory.BINARY) - MEDIUMBLOB = SQLDataType(name="MEDIUMBLOB", category=DataTypeCategory.BINARY) - LONGBLOB = SQLDataType(name="LONGBLOB", category=DataTypeCategory.BINARY) + MEDIUMBLOB = SQLDataType(name="MEDIUMBLOB", category=DataTypeCategory.BINARY, alias=["MEDIUM_BLOB"]) + LONGBLOB = SQLDataType(name="LONGBLOB", category=DataTypeCategory.BINARY, alias=["LONG_BLOB"]) # Date and time DATE = SQLDataType(name="DATE", category=DataTypeCategory.TEMPORAL) diff --git a/structures/engines/sqlite/context.py b/structures/engines/sqlite/context.py index 7a7fb81..35291cf 100755 --- a/structures/engines/sqlite/context.py +++ b/structures/engines/sqlite/context.py @@ -660,3 +660,8 @@ def build_empty_trigger( database=database, statement=default_values.get("statement", ""), ) + + def get_result_column_datatypes( + self, cursor: sqlite3.Cursor + ) -> list[Optional[SQLDataType]]: + return [None for _ in (cursor.description or [])] From b991e273e6d8fc61cf0f372f0da5ac3b9598315d Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 14 Mar 2026 18:22:14 +0100 Subject: [PATCH 07/93] i18n: update catalogs AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- locale/de_DE/LC_MESSAGES/petersql.mo | Bin 8534 -> 8756 bytes locale/de_DE/LC_MESSAGES/petersql.po | 647 ++++++++++++++++---------- locale/en_US/LC_MESSAGES/petersql.mo | Bin 396 -> 2349 bytes locale/en_US/LC_MESSAGES/petersql.po | 643 +++++++++++++++---------- locale/es_ES/LC_MESSAGES/petersql.mo | Bin 8524 -> 8746 bytes locale/es_ES/LC_MESSAGES/petersql.po | 651 ++++++++++++++++---------- locale/fr_FR/LC_MESSAGES/petersql.mo | Bin 8728 -> 8539 bytes locale/fr_FR/LC_MESSAGES/petersql.po | 647 ++++++++++++++++---------- locale/it_IT/LC_MESSAGES/petersql.mo | Bin 8491 -> 8639 bytes locale/it_IT/LC_MESSAGES/petersql.po | 671 ++++++++++++++++----------- locale/petersql.pot | 604 ++++++++++++++---------- 11 files changed, 2342 insertions(+), 1521 deletions(-) diff --git a/locale/de_DE/LC_MESSAGES/petersql.mo b/locale/de_DE/LC_MESSAGES/petersql.mo index 120161d5e0c3f9375fcd6a9e2612ce22d0f0974b..72d1446b221636195e8d0c59ccda113074da239f 100644 GIT binary patch delta 4025 zcmYk-eQZ_r9mnxsA6BL1p|qg16?%%Zw9poDAW~4Ff&z9}TXgau<$|Zdq8!7Kll=Wa{3ZbH8q$^mAUn z-#NeYdpHlb=bL`IIQ_?x`~!|{kX%QiUvchZJXXk#?Uu35<={Nz&&@ZNBTd~Z%)@pZ zg&lT&6P8lnj^l7Ys>_$G{$u3&wEH=Q3Qn9t{@h3G^6?Vtg3DNd|3M9yM{lZ+Le-1S zYs~9V6Paq(;RNb8<5hUK*%q31ZViR2IniOpQ62OmW4bhs#(k&`4x=XaG8W?-$e(+Y zohEkHj-N-h`x|OOS5WL-EKxCJ$V7f~x8L|s3On)utO_V1N&|25zT9MH`FVqQeO)&D@f16NQJ%;A+* zhvP68J=CMS-mJ%Ksn10{f_qQ{#!w4+*xZI1Z)ci_fe+2a!K_jGbowOH@ba zunFHst)PO&diGP1Il6hMj+;>nyBD>9wWxl2a3MZ|;iEfgg)^uN&!blQ2h@x&+VLx> z4o6S}=1$B`pv?483z=s1dUH1F4$VjPzXUarDXSwA0*+lc*m+-QqV;10P4N z_#|qe)2Nl5LEV}6QT<%7`bAW`%NR}^HIZCquXdxci1FP-3Yu9J>Vj qUjX$59;w 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 diff --git a/locale/de_DE/LC_MESSAGES/petersql.po b/locale/de_DE/LC_MESSAGES/petersql.po index 9ce809a..b138456 100644 --- a/locale/de_DE/LC_MESSAGES/petersql.po +++ b/locale/de_DE/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-10 18:23+0100\n" +"POT-Creation-Date: 2026-03-12 19:04+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: de_DE\n" @@ -43,38 +43,83 @@ msgctxt "unit" msgid "TB" msgstr "TB" -#: structures/ssh_tunnel.py:166 +#: structures/ssh_tunnel.py:177 msgid "OpenSSH client not found." msgstr "OpenSSH-Client nicht gefunden." -#: windows/dialogs/connections/view.py:395 -#: windows/dialogs/connections/view.py:738 windows/main/controller.py:296 +#: structures/engines/mariadb/context.py:592 +#: structures/engines/mysql/context.py:563 +#: structures/engines/postgresql/context.py:579 +#: structures/engines/sqlite/context.py:518 +#, python-brace-format +msgid "Table{table_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:620 +#: structures/engines/mysql/context.py:591 +#: structures/engines/postgresql/context.py:604 +#: structures/engines/sqlite/context.py:542 +#, python-brace-format +msgid "Column{column_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:638 +#: structures/engines/mysql/context.py:609 +#: structures/engines/postgresql/context.py:622 +#: structures/engines/sqlite/context.py:560 +#, python-brace-format +msgid "Index{index_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:678 +#: structures/engines/mysql/context.py:649 +#: structures/engines/postgresql/context.py:662 +#: structures/engines/sqlite/context.py:602 +#, python-brace-format +msgid "ForeignKey{foreign_key_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:711 +#: structures/engines/mysql/context.py:680 +#: structures/engines/postgresql/context.py:692 +#: structures/engines/sqlite/context.py:630 +#, python-brace-format +msgid "View{view_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:762 +#, python-brace-format +msgid "Trigger{trigger_index:03}" +msgstr "" + +#: windows/dialogs/connections/view.py:402 +#: windows/dialogs/connections/view.py:739 windows/main/controller.py:606 #: windows/views.py:33 msgid "Connection" msgstr "Verbindung" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:956 -#: windows/views.py:1306 windows/views.py:1413 windows/views.py:1796 -#: windows/views.py:2707 windows/views.py:2730 windows/views.py:2731 -#: windows/views.py:2732 windows/views.py:2733 windows/views.py:2734 -#: windows/views.py:2735 windows/views.py:2736 windows/views.py:2737 -#: windows/views.py:2738 windows/views.py:2742 windows/views.py:2936 -#: windows/views.py:3137 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1003 +#: windows/views.py:1353 windows/views.py:1460 windows/views.py:1843 +#: windows/views.py:2788 windows/views.py:2811 windows/views.py:2812 +#: windows/views.py:2813 windows/views.py:2814 windows/views.py:2815 +#: windows/views.py:2816 windows/views.py:2817 windows/views.py:2818 +#: windows/views.py:2819 windows/views.py:2823 windows/views.py:3017 +#: windows/views.py:3218 msgid "Name" msgstr "Name" -#: windows/views.py:48 windows/views.py:381 +#: windows/views.py:48 windows/views.py:428 msgid "Last connection" msgstr "Letzte Verbindung" -#: windows/dialogs/connections/view.py:631 windows/views.py:61 +#: windows/dialogs/connections/view.py:640 windows/views.py:61 msgid "New directory" msgstr "Neues Verzeichnis" -#: windows/dialogs/connections/model.py:187 -#: windows/dialogs/connections/view.py:591 windows/views.py:65 +#: windows/dialogs/connections/model.py:206 +#: windows/dialogs/connections/view.py:600 windows/views.py:65 msgid "New connection" msgstr "Neue Verbindung" @@ -88,14 +133,14 @@ msgstr "Name" msgid "Clone connection" msgstr "Neue Verbindung" -#: windows/views.py:81 windows/views.py:556 windows/views.py:1290 -#: windows/views.py:1331 windows/views.py:1706 windows/views.py:1738 -#: windows/views.py:1997 windows/views.py:3273 windows/views.py:3305 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1337 +#: windows/views.py:1378 windows/views.py:1753 windows/views.py:1785 +#: windows/views.py:2044 windows/views.py:3354 windows/views.py:3386 msgid "Delete" msgstr "Löschen" -#: windows/views.py:111 windows/views.py:1311 windows/views.py:1468 -#: windows/views.py:2747 windows/views.py:3192 +#: windows/views.py:111 windows/views.py:1358 windows/views.py:1515 +#: windows/views.py:2828 windows/views.py:3273 msgid "Engine" msgstr "Engine" @@ -107,542 +152,562 @@ msgstr "Host + Port" msgid "Username" msgstr "Benutzername" -#: windows/views.py:161 windows/views.py:1100 +#: windows/views.py:161 windows/views.py:1147 msgid "Password" msgstr "Passwort" -#: windows/views.py:177 +#: windows/views.py:174 +#, fuzzy +msgid "Connection timeout" +msgstr "Verbindung verloren" + +#: windows/views.py:192 msgid "Use TLS" msgstr "TLS verwenden" -#: windows/views.py:180 +#: windows/views.py:203 msgid "Use SSH tunnel" msgstr "SSH-Tunnel verwenden" -#: windows/views.py:202 windows/views.py:2668 +#: windows/views.py:214 +msgid "Compressed client/server protocol" +msgstr "" + +#: windows/views.py:233 windows/views.py:2749 msgid "Filename" msgstr "Dateiname" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2673 -#: windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:2754 +#: windows/views.py:2937 msgid "Select a file" msgstr "Datei auswählen" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:2937 #, fuzzy msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:221 windows/views.py:1313 windows/views.py:1426 -#: windows/views.py:2748 windows/views.py:3034 windows/views.py:3150 +#: windows/views.py:255 windows/views.py:1360 windows/views.py:1473 +#: windows/views.py:2829 windows/views.py:3115 windows/views.py:3231 msgid "Comments" msgstr "Kommentare" -#: windows/main/controller.py:219 windows/views.py:235 windows/views.py:683 -#: windows/views.py:837 +#: windows/main/controller.py:236 windows/views.py:269 windows/views.py:730 +#: windows/views.py:884 msgid "Settings" msgstr "Einstellungen" -#: windows/views.py:244 +#: windows/views.py:278 msgid "SSH executable" msgstr "SSH-Executable" -#: windows/views.py:249 +#: windows/views.py:283 msgid "ssh" msgstr "ssh" -#: windows/views.py:257 +#: windows/views.py:291 msgid "SSH host + port" msgstr "SSH-Host + Port" -#: windows/views.py:269 +#: windows/views.py:303 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "SSH-Host + Port (der SSH-Server, der den Verkehr zur DB weiterleitet)" -#: windows/views.py:278 +#: windows/views.py:312 msgid "SSH username" msgstr "SSH-Benutzername" -#: windows/views.py:291 +#: windows/views.py:325 msgid "SSH password" msgstr "SSH-Passwort" -#: windows/views.py:304 +#: windows/views.py:338 msgid "Local port" msgstr "Lokaler Port" -#: windows/views.py:310 +#: windows/views.py:344 msgid "if the value is set to 0, the first available port will be used" msgstr "wenn der Wert auf 0 gesetzt ist, wird der erste verfügbare Port verwendet" -#: windows/views.py:319 +#: windows/views.py:353 msgid "Identity file" msgstr "Identitätsdatei" -#: windows/views.py:335 +#: windows/views.py:369 #, fuzzy msgid "Remote host + port" msgstr "Host + Port" -#: windows/views.py:347 +#: windows/views.py:381 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "Remote-Host/Port ist das eigentliche DB-Ziel (standardmäßig DB-Host/Port)." -#: windows/views.py:358 +#: windows/views.py:390 +msgid "SSH extra args" +msgstr "" + +#: windows/views.py:405 msgid "SSH Tunnel" msgstr "SSH-Tunnel" -#: windows/views.py:364 windows/views.py:1309 windows/views.py:2745 +#: windows/views.py:411 windows/views.py:1356 windows/views.py:2826 msgid "Created at" msgstr "Erstellt am" -#: windows/views.py:398 +#: windows/views.py:445 msgid "Successful connections" msgstr "Erfolgreiche Verbindungen" -#: windows/views.py:415 +#: windows/views.py:462 #, fuzzy msgid "Last successful connection" msgstr "Erfolgreiche Verbindungen" -#: windows/views.py:432 +#: windows/views.py:479 msgid "Unsuccessful connections" msgstr "Erfolglose Verbindungen" -#: windows/views.py:449 +#: windows/views.py:496 msgid "Last failure reason" msgstr "" -#: windows/views.py:466 +#: windows/views.py:513 #, fuzzy msgid "Total connection attempts" msgstr "Letzte Verbindung" -#: windows/views.py:483 +#: windows/views.py:530 #, fuzzy -msgid " Average connection time (ms)" +msgid "Average connection time (ms)" msgstr "Wiederverbindung fehlgeschlagen:" -#: windows/views.py:500 +#: windows/views.py:547 #, fuzzy -msgid " Most recent connection duration" +msgid "Most recent connection duration" msgstr "Verbindungsmanager öffnen" -#: windows/views.py:519 +#: windows/views.py:566 msgid "Statistics" msgstr "Statistiken" -#: windows/views.py:537 windows/views.py:1672 +#: windows/views.py:584 windows/views.py:1719 msgid "Create" msgstr "Erstellen" -#: windows/views.py:541 +#: windows/views.py:588 #, fuzzy msgid "Create connection" msgstr "Letzte Verbindung" -#: windows/views.py:544 +#: windows/views.py:591 #, fuzzy msgid "Create directory" msgstr "Neues Verzeichnis" -#: windows/views.py:573 windows/views.py:797 windows/views.py:1328 -#: windows/views.py:1741 windows/views.py:2002 windows/views.py:2078 -#: windows/views.py:3081 windows/views.py:3308 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1375 +#: windows/views.py:1788 windows/views.py:2049 windows/views.py:2152 +#: windows/views.py:3162 windows/views.py:3389 msgid "Cancel" msgstr "Abbrechen" -#: windows/views.py:578 windows/views.py:2007 windows/views.py:3091 -#: windows/views.py:3313 +#: windows/views.py:625 windows/views.py:2054 windows/views.py:3172 +#: windows/views.py:3394 msgid "Save" msgstr "Speichern" -#: windows/views.py:585 +#: windows/views.py:632 msgid "Test" msgstr "Testen" -#: windows/views.py:592 +#: windows/views.py:639 msgid "Connect" msgstr "Verbinden" -#: windows/views.py:695 +#: windows/views.py:742 msgid "Language" msgstr "Sprache" -#: windows/views.py:700 +#: windows/views.py:747 msgid "English" msgstr "Englisch" -#: windows/views.py:700 +#: windows/views.py:747 msgid "Italian" msgstr "Italienisch" -#: windows/views.py:700 +#: windows/views.py:747 msgid "French" msgstr "Französisch" -#: windows/views.py:712 +#: windows/views.py:759 msgid "Locale" msgstr "Lokale" -#: windows/views.py:733 +#: windows/views.py:780 msgid "Edit Value" msgstr "Wert bearbeiten" -#: windows/views.py:743 +#: windows/views.py:790 msgid "Syntax" msgstr "Syntax" -#: windows/views.py:800 +#: windows/views.py:847 msgid "Ok" msgstr "Ok" -#: windows/views.py:831 +#: windows/views.py:878 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:840 +#: windows/views.py:887 msgid "File" msgstr "Datei" -#: windows/views.py:843 +#: windows/views.py:890 msgid "About" msgstr "Über" -#: windows/views.py:846 +#: windows/views.py:893 msgid "Help" msgstr "Hilfe" -#: windows/views.py:851 +#: windows/views.py:898 msgid "Open connection manager" msgstr "Verbindungsmanager öffnen" -#: windows/views.py:853 +#: windows/views.py:900 msgid "Disconnect from server" msgstr "Vom Server trennen" -#: windows/views.py:857 +#: windows/views.py:904 msgid "tool" msgstr "Werkzeug" -#: windows/views.py:857 +#: windows/views.py:904 msgid "Refresh" msgstr "Aktualisieren" -#: windows/views.py:861 windows/views.py:863 +#: windows/views.py:908 windows/views.py:910 msgid "Add" msgstr "Hinzufügen" -#: windows/views.py:897 windows/views.py:901 windows/views.py:2166 -#: windows/views.py:2654 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2232 +#: windows/views.py:2735 msgid "MyMenuItem" msgstr "MeinMenüElement" -#: windows/views.py:904 windows/views.py:1769 windows/views.py:3336 +#: windows/views.py:951 windows/views.py:1816 windows/views.py:3417 msgid "MyMenu" msgstr "MeinMenü" -#: windows/views.py:919 windows/views.py:1350 windows/views.py:1357 -#: windows/views.py:1364 +#: windows/views.py:966 windows/views.py:1397 windows/views.py:1404 +#: windows/views.py:1411 msgid "MyLabel" msgstr "MeinLabel" -#: windows/views.py:925 +#: windows/views.py:972 msgid "Databases" msgstr "Datenbanken" -#: windows/views.py:926 windows/views.py:1308 windows/views.py:2716 -#: windows/views.py:2744 +#: windows/views.py:973 windows/views.py:1355 windows/views.py:2797 +#: windows/views.py:2825 msgid "Size" msgstr "Größe" -#: windows/views.py:927 +#: windows/views.py:974 msgid "Elements" msgstr "Elemente" -#: windows/views.py:928 +#: windows/views.py:975 msgid "Modified at" msgstr "Geändert am" -#: windows/views.py:929 +#: windows/views.py:976 msgid "Tables" msgstr "Tabellen" -#: windows/views.py:936 +#: windows/views.py:983 msgid "System" msgstr "System" -#: windows/views.py:979 +#: windows/views.py:1026 #, fuzzy msgid "Character set" msgstr "Erstellt am" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1000 -#: windows/views.py:1312 windows/views.py:2980 +#: windows/components/dataview.py:89 windows/views.py:1047 +#: windows/views.py:1359 windows/views.py:3061 msgid "Collation" msgstr "Sortierung" -#: windows/views.py:1026 windows/views.py:2862 +#: windows/views.py:1073 windows/views.py:2943 msgid "Encryption" msgstr "" -#: windows/views.py:1038 +#: windows/views.py:1085 msgid "Read Only" msgstr "" -#: windows/views.py:1055 +#: windows/views.py:1102 #, fuzzy msgid "Tablespace" msgstr "Tabellen" -#: windows/views.py:1076 +#: windows/views.py:1123 #, fuzzy msgid "Connection limit" msgstr "Verbindung verloren" -#: windows/views.py:1119 +#: windows/views.py:1166 #, fuzzy msgid "Profile" msgstr "Datei" -#: windows/views.py:1145 +#: windows/views.py:1192 #, fuzzy msgid "Default tablespace" msgstr "Tabelle löschen" -#: windows/views.py:1166 +#: windows/views.py:1213 #, fuzzy msgid "Temporary tablespace" msgstr "Temporär" -#: windows/views.py:1192 +#: windows/views.py:1239 msgid "Quota" msgstr "" -#: windows/views.py:1211 +#: windows/views.py:1258 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1228 +#: windows/views.py:1275 msgid "Account status" msgstr "" -#: windows/views.py:1249 +#: windows/views.py:1296 #, fuzzy msgid "Password expire" msgstr "Passwort" -#: windows/views.py:1270 +#: windows/views.py:1317 msgid "Table:" msgstr "Tabelle:" -#: windows/views.py:1278 windows/views.py:1551 windows/views.py:1595 -#: windows/views.py:1701 windows/views.py:3268 +#: windows/views.py:1325 windows/views.py:1598 windows/views.py:1642 +#: windows/views.py:1748 windows/views.py:3349 msgid "Insert" msgstr "Einfügen" -#: windows/views.py:1283 +#: windows/views.py:1330 msgid "Clone" msgstr "Klonen" -#: windows/views.py:1307 +#: windows/views.py:1354 msgid "Rows" msgstr "Zeilen" -#: windows/views.py:1310 windows/views.py:2746 +#: windows/views.py:1357 windows/views.py:2827 msgid "Updated at" msgstr "Aktualisiert am" -#: windows/views.py:1334 windows/views.py:1746 windows/views.py:2085 -#: windows/views.py:2140 +#: windows/views.py:1381 windows/views.py:1793 windows/views.py:2159 +#: windows/views.py:2206 msgid "Apply" msgstr "Anwenden" -#: windows/views.py:1344 windows/views.py:1503 windows/views.py:1956 -#: windows/views.py:3225 +#: windows/views.py:1391 windows/views.py:1550 windows/views.py:2003 +#: windows/views.py:3306 msgid "Options" msgstr "Optionen" -#: windows/views.py:1375 +#: windows/views.py:1422 msgid "Diagram" msgstr "Diagramm" -#: windows/views.py:1386 windows/views.py:2715 +#: windows/views.py:1433 windows/views.py:2796 msgid "Database" msgstr "Datenbank" -#: windows/views.py:1441 windows/views.py:3165 +#: windows/views.py:1488 windows/views.py:3246 msgid "Base" msgstr "Basis" -#: windows/views.py:1455 windows/views.py:3179 +#: windows/views.py:1502 windows/views.py:3260 msgid "Auto Increment" msgstr "Auto Inkrement" -#: windows/views.py:1483 windows/views.py:3207 +#: windows/views.py:1530 windows/views.py:3288 msgid "Default Collation" msgstr "Standard-Sortierung" -#: windows/views.py:1515 windows/views.py:1556 windows/views.py:1600 +#: windows/views.py:1562 windows/views.py:1603 windows/views.py:1647 msgid "Remove" msgstr "Entfernen" -#: windows/views.py:1522 windows/views.py:1563 windows/views.py:1607 +#: windows/views.py:1569 windows/views.py:1610 windows/views.py:1654 msgid "Clear" msgstr "Löschen" -#: windows/views.py:1537 windows/views.py:3239 +#: windows/views.py:1584 windows/views.py:3320 msgid "Indexes" msgstr "Indizes" -#: windows/views.py:1581 +#: windows/views.py:1628 msgid "Foreign Keys" msgstr "Fremdschlüssel" -#: windows/views.py:1625 +#: windows/views.py:1672 msgid "Checks" msgstr "Prüfungen" -#: windows/views.py:1693 windows/views.py:3260 +#: windows/views.py:1740 windows/views.py:3341 msgid "Columns:" msgstr "Spalten:" -#: windows/views.py:1713 windows/views.py:3280 +#: windows/views.py:1760 windows/views.py:3361 msgid "Up" msgstr "Hoch" -#: windows/views.py:1720 windows/views.py:3287 +#: windows/views.py:1767 windows/views.py:3368 msgid "Down" msgstr "Runter" -#: windows/views.py:1759 windows/views.py:1766 windows/views.py:3326 -#: windows/views.py:3333 +#: windows/views.py:1806 windows/views.py:1813 windows/views.py:3407 +#: windows/views.py:3414 msgid "Add Index" msgstr "Index hinzufügen" -#: windows/views.py:1763 windows/views.py:3330 +#: windows/views.py:1810 windows/views.py:3411 msgid "Add PrimaryKey" msgstr "Primärschlüssel hinzufügen" -#: windows/views.py:1780 +#: windows/views.py:1827 msgid "Table" msgstr "Tabelle" -#: windows/views.py:1816 +#: windows/views.py:1863 #, fuzzy msgid "Definer" msgstr "Einfügen" -#: windows/views.py:1836 +#: windows/views.py:1883 msgid "Schema" msgstr "" -#: windows/views.py:1862 +#: windows/views.py:1909 msgid "SQL security" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1916 #, fuzzy msgid "DEFINER" msgstr "Einfügen" -#: windows/views.py:1869 +#: windows/views.py:1916 #, fuzzy msgid "INVOKER" msgstr "Einfügen" -#: windows/views.py:1881 windows/views.py:2558 windows/views.py:2577 -#: windows/views.py:2820 +#: windows/views.py:1928 windows/views.py:2639 windows/views.py:2658 +#: windows/views.py:2901 msgid "Algorithm" msgstr "" -#: windows/views.py:1883 windows/views.py:2543 windows/views.py:2576 -#: windows/views.py:2825 +#: windows/views.py:1930 windows/views.py:2624 windows/views.py:2657 +#: windows/views.py:2906 #, fuzzy msgid "UNDEFINED" msgstr "Ohne Vorzeichen" -#: windows/views.py:1886 windows/views.py:2546 windows/views.py:2576 -#: windows/views.py:2828 +#: windows/views.py:1933 windows/views.py:2627 windows/views.py:2657 +#: windows/views.py:2909 msgid "MERGE" msgstr "" -#: windows/views.py:1889 windows/views.py:2555 windows/views.py:2576 -#: windows/views.py:2831 +#: windows/views.py:1936 windows/views.py:2636 windows/views.py:2657 +#: windows/views.py:2912 #, fuzzy msgid "TEMPTABLE" msgstr "Tabelle" -#: windows/views.py:1899 windows/views.py:2582 +#: windows/views.py:1946 windows/views.py:2663 msgid "View constraint" msgstr "" -#: windows/views.py:1901 windows/views.py:2581 +#: windows/views.py:1948 windows/views.py:2662 #, fuzzy msgid "None" msgstr "Klonen" -#: windows/views.py:1904 windows/views.py:2581 +#: windows/views.py:1951 windows/views.py:2662 #, fuzzy msgid "LOCAL" msgstr "Lokale" -#: windows/views.py:1907 +#: windows/views.py:1954 #, fuzzy msgid "CASCADE" msgstr "Abbrechen" -#: windows/views.py:1910 +#: windows/views.py:1957 #, fuzzy msgid "CHECK ONLY" msgstr "Prüfen" -#: windows/views.py:1913 windows/views.py:2581 +#: windows/views.py:1960 windows/views.py:2662 msgid "READ ONLY" msgstr "" -#: windows/views.py:1925 +#: windows/views.py:1972 msgid "Force" msgstr "" -#: windows/views.py:1937 +#: windows/views.py:1984 msgid "Security barrier" msgstr "" -#: windows/views.py:2019 +#: windows/views.py:2066 msgid "Views" msgstr "Ansichten" -#: windows/views.py:2027 +#: windows/views.py:2074 msgid "Triggers" msgstr "Trigger" -#: windows/views.py:2039 -#, python-format -msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#: windows/views.py:2086 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" -"Tabelle `%(database_name)s`.`%(table_name)s`: %(total_rows) Zeilen " -"insgesamt" -#: windows/views.py:2049 +#: windows/views.py:2094 +#, fuzzy +msgid "First" +msgstr "Filter" + +#: windows/views.py:2112 +msgid "Last" +msgstr "" + +#: windows/views.py:2123 msgid "Insert record" msgstr "Datensatz einfügen" -#: windows/views.py:2054 +#: windows/views.py:2128 msgid "Duplicate record" msgstr "Datensatz duplizieren" -#: windows/views.py:2061 +#: windows/views.py:2135 msgid "Delete record" msgstr "Datensatz löschen" -#: windows/views.py:2071 +#: windows/views.py:2145 msgid "Apply changes automatically" msgstr "Änderungen automatisch anwenden" -#: windows/views.py:2073 windows/views.py:2074 +#: windows/views.py:2147 windows/views.py:2148 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -650,76 +715,72 @@ msgstr "" "Wenn aktiviert, werden Tabellenbearbeitungen sofort angewendet, ohne auf " "Anwenden oder Abbrechen zu drücken" -#: windows/views.py:2095 -msgid "Next" -msgstr "Weiter" - -#: windows/views.py:2103 +#: windows/views.py:2169 msgid "Filters" msgstr "Filter" -#: windows/views.py:2143 +#: windows/views.py:2209 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2163 +#: windows/views.py:2229 msgid "Insert row" msgstr "Zeile einfügen" -#: windows/views.py:2171 +#: windows/views.py:2237 msgid "Data" msgstr "Daten" -#: windows/views.py:2225 windows/views.py:2275 +#: windows/views.py:2291 windows/views.py:2341 msgid "New" msgstr "Neu" -#: windows/views.py:2252 +#: windows/views.py:2318 msgid "Query" msgstr "Abfrage" -#: windows/views.py:2272 +#: windows/views.py:2338 msgid "Close" msgstr "Schließen" -#: windows/views.py:2285 +#: windows/views.py:2351 msgid "Query #2" msgstr "Abfrage #2" -#: windows/views.py:2537 +#: windows/views.py:2618 msgid "Column5" msgstr "Spalte5" -#: windows/views.py:2548 +#: windows/views.py:2629 msgid "Import" msgstr "Importieren" -#: windows/views.py:2573 +#: windows/views.py:2654 msgid "Read only" msgstr "" -#: windows/views.py:2581 +#: windows/views.py:2662 msgid "CASCADED" msgstr "" -#: windows/views.py:2581 +#: windows/views.py:2662 #, fuzzy msgid "CHECK OPTION" msgstr "Verbindung" -#: windows/views.py:2613 +#: windows/views.py:2694 msgid "collapsible" msgstr "zusammenklappbar" -#: windows/views.py:2629 +#: windows/views.py:2710 msgid "Column3" msgstr "Spalte3" -#: windows/views.py:2630 +#: windows/views.py:2711 msgid "Column4" msgstr "Spalte4" -#: windows/views.py:2673 +#: windows/views.py:2754 msgid "" "Database " "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" @@ -727,78 +788,78 @@ msgstr "" "Database " "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" -#: windows/views.py:2685 +#: windows/views.py:2766 msgid "Port" msgstr "Port" -#: windows/views.py:2708 +#: windows/views.py:2789 msgid "Usage" msgstr "Verwendung" -#: windows/views.py:2719 +#: windows/views.py:2800 #, python-format msgid "%(total_rows)s" msgstr "%(total_rows)s" -#: windows/views.py:2724 +#: windows/views.py:2805 msgid "rows total" msgstr "Zeilen insgesamt" -#: windows/views.py:2743 +#: windows/views.py:2824 msgid "Lines" msgstr "Zeilen" -#: windows/views.py:2775 +#: windows/views.py:2856 msgid "Temporary" msgstr "Temporär" -#: windows/views.py:2786 +#: windows/views.py:2867 #, fuzzy msgid "Engine options" msgstr "Optionen" -#: windows/views.py:2845 +#: windows/views.py:2926 msgid "RadioBtn" msgstr "" -#: windows/views.py:2928 +#: windows/views.py:3009 msgid "Edit Column" msgstr "Spalte bearbeiten" -#: windows/views.py:2944 +#: windows/views.py:3025 msgid "Datatype" msgstr "Datentyp" -#: windows/components/dataview.py:121 windows/views.py:2959 +#: windows/components/dataview.py:121 windows/views.py:3040 msgid "Length/Set" msgstr "Länge/Menge" -#: windows/components/dataview.py:51 windows/views.py:2998 +#: windows/components/dataview.py:51 windows/views.py:3079 msgid "Unsigned" msgstr "Ohne Vorzeichen" #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3004 +#: windows/components/dataview.py:75 windows/views.py:3085 msgid "Allow NULL" msgstr "NULL erlauben" -#: windows/views.py:3010 +#: windows/views.py:3091 msgid "Zero Fill" msgstr "Nullfüllung" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3021 +#: windows/components/dataview.py:78 windows/views.py:3102 msgid "Default" msgstr "Standard" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3047 +#: windows/components/dataview.py:82 windows/views.py:3128 msgid "Virtuality" msgstr "Virtualität" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3062 +#: windows/views.py:3143 msgid "Expression" msgstr "Ausdruck" @@ -870,11 +931,11 @@ msgstr "Bei UPDATE" msgid "On DELETE" msgstr "Bei DELETE" -#: windows/components/dataview.py:299 +#: windows/components/dataview.py:298 msgid "Add foreign key" msgstr "Fremdschlüssel hinzufügen" -#: windows/components/dataview.py:305 +#: windows/components/dataview.py:304 msgid "Remove foreign key" msgstr "Fremdschlüssel entfernen" @@ -894,75 +955,111 @@ msgstr "AUTO INCREMENT" msgid "Text/Expression" msgstr "Text/Ausdruck" -#: windows/dialogs/connections/view.py:119 windows/main/tabs/query.py:376 +#: windows/dialogs/connections/view.py:122 windows/main/tabs/query.py:387 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:394 +#: windows/dialogs/connections/view.py:401 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:407 +#: windows/dialogs/connections/view.py:413 +#, python-brace-format +msgid "Do you want save the connection {connection_name}?" +msgstr "" + +#: windows/dialogs/connections/view.py:416 msgid "Confirm save" msgstr "Speichern bestätigen" -#: windows/dialogs/connections/view.py:459 +#: windows/dialogs/connections/view.py:468 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:461 +#: windows/dialogs/connections/view.py:470 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:736 +#: windows/dialogs/connections/view.py:737 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" #: windows/dialogs/connections/view.py:762 +#, fuzzy, python-brace-format +msgid "" +"Connection error:\n" +"{error}" +msgstr "Verbindungsfehler" + +#: windows/dialogs/connections/view.py:763 msgid "Connection error" msgstr "Verbindungsfehler" -#: windows/dialogs/connections/view.py:788 -#: windows/dialogs/connections/view.py:803 +#: windows/dialogs/connections/view.py:789 +#, fuzzy, python-brace-format +msgid "Do you want to delete the connection '{connection_name}'?" +msgstr "Möchten Sie die Datensätze löschen?" + +#: windows/dialogs/connections/view.py:792 +#: windows/dialogs/connections/view.py:809 msgid "Confirm delete" msgstr "Löschen bestätigen" -#: windows/main/controller.py:172 +#: windows/dialogs/connections/view.py:806 +#, fuzzy, python-brace-format +msgid "Do you want to delete the directory '{directory_name}'?" +msgstr "Möchten Sie die Datensätze löschen?" + +#: windows/main/controller.py:189 msgid "days" msgstr "Tage" -#: windows/main/controller.py:173 +#: windows/main/controller.py:190 msgid "hours" msgstr "Stunden" -#: windows/main/controller.py:174 +#: windows/main/controller.py:191 msgid "minutes" msgstr "Minuten" -#: windows/main/controller.py:175 +#: windows/main/controller.py:192 msgid "seconds" msgstr "Sekunden" -#: windows/main/controller.py:183 +#: windows/main/controller.py:200 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Verwendeter Speicher: {used} ({percentage:.2%})" -#: windows/main/controller.py:219 +#: windows/main/controller.py:236 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:298 +#: windows/main/controller.py:441 +#, python-brace-format +msgid "~{estimated} (Loading...)" +msgstr "" + +#: windows/main/controller.py:443 +msgid "~ (Loading...)" +msgstr "" + +#: windows/main/controller.py:608 msgid "Version" msgstr "Version" -#: windows/main/controller.py:300 +#: windows/main/controller.py:610 msgid "Uptime" msgstr "Betriebszeit" -#: windows/main/controller.py:399 +#: windows/main/controller.py:678 +#, python-brace-format +msgid "Do you want discard the change to {database_name}?" +msgstr "" + +#: windows/main/controller.py:711 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -972,37 +1069,52 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:404 windows/main/controller.py:425 +#: windows/main/controller.py:716 windows/main/controller.py:737 #, fuzzy msgid "Delete database" msgstr "Tabelle löschen" -#: windows/main/controller.py:410 +#: windows/main/controller.py:722 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:411 +#: windows/main/controller.py:723 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:424 +#: windows/main/controller.py:736 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:439 +#: windows/main/controller.py:751 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:440 windows/main/tabs/view.py:253 +#: windows/main/controller.py:752 windows/main/tabs/view.py:253 #: windows/main/tabs/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:582 +#: windows/main/controller.py:871 +#, python-brace-format +msgid "Do you want discard the change to {table_name}?" +msgstr "" + +#: windows/main/controller.py:897 +#, fuzzy, python-brace-format +msgid "Do you want delete the table {table_name}?" +msgstr "Möchten Sie die Datensätze löschen?" + +#: windows/main/controller.py:900 msgid "Delete table" msgstr "Tabelle löschen" -#: windows/main/controller.py:699 +#: windows/main/controller.py:919 +#, python-brace-format +msgid "{table_name} (COPY)" +msgstr "" + +#: windows/main/controller.py:1017 msgid "Do you want delete the records?" msgstr "Möchten Sie die Datensätze löschen?" @@ -1022,52 +1134,52 @@ msgstr "Verbindung verloren" msgid "Reconnection failed:" msgstr "Wiederverbindung fehlgeschlagen:" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:450 +#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:489 #: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 msgid "Error" msgstr "Fehler" -#: windows/main/tabs/query.py:305 +#: windows/main/tabs/query.py:308 #, python-brace-format -msgid "{} rows affected" +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/query.py:309 windows/main/tabs/query.py:331 -#, fuzzy, python-brace-format -msgid "Query {}" -msgstr "Abfrage" +#: windows/main/tabs/query.py:315 windows/main/tabs/query.py:339 +#, python-brace-format +msgid "Query {query_number}" +msgstr "" -#: windows/main/tabs/query.py:314 +#: windows/main/tabs/query.py:320 #, python-brace-format -msgid "Query {} (Error)" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/query.py:326 +#: windows/main/tabs/query.py:334 #, python-brace-format -msgid "Query {} ({} rows × {} cols)" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/query.py:353 -#, fuzzy, python-brace-format -msgid "{} rows" -msgstr "Zeilen" +#: windows/main/tabs/query.py:360 +#, python-brace-format +msgid "{rows_count} rows" +msgstr "" -#: windows/main/tabs/query.py:355 +#: windows/main/tabs/query.py:362 #, python-brace-format -msgid "{:.1f} ms" +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/query.py:358 +#: windows/main/tabs/query.py:366 #, python-brace-format -msgid "{} warnings" +msgid "{warnings_count} warnings" msgstr "" -#: windows/main/tabs/query.py:370 +#: windows/main/tabs/query.py:381 #, fuzzy msgid "Error:" msgstr "Fehler" -#: windows/main/tabs/query.py:449 +#: windows/main/tabs/query.py:488 #, fuzzy msgid "No active database connection" msgstr "Neue Verbindung" @@ -1140,3 +1252,32 @@ msgstr "" #~ msgid "directory" #~ msgstr "Verzeichnis" +#~ msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#~ msgstr "" +#~ "Tabelle `%(database_name)s`.`%(table_name)s`: " +#~ "%(total_rows) Zeilen insgesamt" + +#~ msgid "Next" +#~ msgstr "Weiter" + +#~ msgid "{} rows affected" +#~ msgstr "" + +#~ msgid "Query {}" +#~ msgstr "Abfrage" + +#~ msgid "Query {} (Error)" +#~ msgstr "" + +#~ msgid "Query {} ({} rows × {} cols)" +#~ msgstr "" + +#~ msgid "{} rows" +#~ msgstr "Zeilen" + +#~ msgid "{:.1f} ms" +#~ msgstr "" + +#~ msgid "{} warnings" +#~ msgstr "" + diff --git a/locale/en_US/LC_MESSAGES/petersql.mo b/locale/en_US/LC_MESSAGES/petersql.mo index 7e69f11e6219a515e38693f75c2e81b63fc9b6db..a950b1ad439600a78124ebd4dcdaa02abc194cc5 100644 GIT binary patch literal 2349 zcmeH`&2Jk;7{;et!J&-aneYj zO7s>XB#wUh_oofzLp`4Px{pMm#)Ux4?4 zUxN>T--Fcu8Jq%t1Mdfa2f6Q0@Nw`M7HQ`+I1SE$W$-*me{X`ca~-7qyCCg<46cJ; zg7<=dr2Gq{{o{9x?VbRsHv`@UX4Cm|;2OqjAVLw>K>E85a^FG9_fvkD@>6gNHT)c; z-rrDS-Ht$sc}#eZIJuk16j9E zK-&2RWZk|4S+}1+`a6k5+L-~VzXa0$d2k)9fvnp>$`3%=zX{?KU!kMkw;)W4AJX|B zLDuaTkoljw8@Yos;8Ack<(ZW8DVIR*1Cln2{;~9*U~ErAX!F+gXgV2|b47NM+^rBk-Aom}vBPmMx{ z#fa0)VY!L@i&BnL_9y@U3_&99WTbv%N1--=f zt_nw*6^M~C{N;20RC$a`DPK<6NZBrn zrj6b}h+fV2Uez|nH7dy_u8D2EQkS`GKAX#IX`Y1Fi2H%>l}G*9YeywnZCt3;*Dq8m zTh&%8)4bws>D_?(UIiDfBn!F1IWPCTm(R(3u~f*P&7oy@rM$LPCJq&@Nl9YH^hc@> z4Q*cAZv8j*+FA|LN%>;Ez8(hI$o5~#G{ccqp|=s)L0pn%m<;39!ipUKt}YlkyDIaG zE15>EQ9VqHx!`3uTt~0HKZHl8ue#-7r~Oo^{S>D`8d3i vEOr8yxulJ@d{|z2za-15qeEH9=8JG4ZokdjZ}b1(W?A4x;xXbK;tBo(!#o~d delta 66 xcmZ20)Wd9XPl#nI0}wC*u?!Ha05LNV>i{tbSO9SlP|^}egVeyl=9R1=i~#FS1^WO1 diff --git a/locale/en_US/LC_MESSAGES/petersql.po b/locale/en_US/LC_MESSAGES/petersql.po index 89d304b..9d65ccf 100644 --- a/locale/en_US/LC_MESSAGES/petersql.po +++ b/locale/en_US/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-10 18:23+0100\n" +"POT-Creation-Date: 2026-03-12 19:04+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: en_US\n" @@ -43,38 +43,83 @@ msgctxt "unit" msgid "TB" msgstr "TB" -#: structures/ssh_tunnel.py:166 +#: structures/ssh_tunnel.py:177 msgid "OpenSSH client not found." msgstr "OpenSSH client not found." -#: windows/dialogs/connections/view.py:395 -#: windows/dialogs/connections/view.py:738 windows/main/controller.py:296 +#: structures/engines/mariadb/context.py:592 +#: structures/engines/mysql/context.py:563 +#: structures/engines/postgresql/context.py:579 +#: structures/engines/sqlite/context.py:518 +#, python-brace-format +msgid "Table{table_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:620 +#: structures/engines/mysql/context.py:591 +#: structures/engines/postgresql/context.py:604 +#: structures/engines/sqlite/context.py:542 +#, python-brace-format +msgid "Column{column_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:638 +#: structures/engines/mysql/context.py:609 +#: structures/engines/postgresql/context.py:622 +#: structures/engines/sqlite/context.py:560 +#, python-brace-format +msgid "Index{index_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:678 +#: structures/engines/mysql/context.py:649 +#: structures/engines/postgresql/context.py:662 +#: structures/engines/sqlite/context.py:602 +#, python-brace-format +msgid "ForeignKey{foreign_key_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:711 +#: structures/engines/mysql/context.py:680 +#: structures/engines/postgresql/context.py:692 +#: structures/engines/sqlite/context.py:630 +#, python-brace-format +msgid "View{view_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:762 +#, python-brace-format +msgid "Trigger{trigger_index:03}" +msgstr "" + +#: windows/dialogs/connections/view.py:402 +#: windows/dialogs/connections/view.py:739 windows/main/controller.py:606 #: windows/views.py:33 msgid "Connection" msgstr "Connection" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:956 -#: windows/views.py:1306 windows/views.py:1413 windows/views.py:1796 -#: windows/views.py:2707 windows/views.py:2730 windows/views.py:2731 -#: windows/views.py:2732 windows/views.py:2733 windows/views.py:2734 -#: windows/views.py:2735 windows/views.py:2736 windows/views.py:2737 -#: windows/views.py:2738 windows/views.py:2742 windows/views.py:2936 -#: windows/views.py:3137 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1003 +#: windows/views.py:1353 windows/views.py:1460 windows/views.py:1843 +#: windows/views.py:2788 windows/views.py:2811 windows/views.py:2812 +#: windows/views.py:2813 windows/views.py:2814 windows/views.py:2815 +#: windows/views.py:2816 windows/views.py:2817 windows/views.py:2818 +#: windows/views.py:2819 windows/views.py:2823 windows/views.py:3017 +#: windows/views.py:3218 msgid "Name" msgstr "Name" -#: windows/views.py:48 windows/views.py:381 +#: windows/views.py:48 windows/views.py:428 msgid "Last connection" msgstr "Last connection" -#: windows/dialogs/connections/view.py:631 windows/views.py:61 +#: windows/dialogs/connections/view.py:640 windows/views.py:61 msgid "New directory" msgstr "New directory" -#: windows/dialogs/connections/model.py:187 -#: windows/dialogs/connections/view.py:591 windows/views.py:65 +#: windows/dialogs/connections/model.py:206 +#: windows/dialogs/connections/view.py:600 windows/views.py:65 msgid "New connection" msgstr "New connection" @@ -86,14 +131,14 @@ msgstr "Rename" msgid "Clone connection" msgstr "Clone connection" -#: windows/views.py:81 windows/views.py:556 windows/views.py:1290 -#: windows/views.py:1331 windows/views.py:1706 windows/views.py:1738 -#: windows/views.py:1997 windows/views.py:3273 windows/views.py:3305 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1337 +#: windows/views.py:1378 windows/views.py:1753 windows/views.py:1785 +#: windows/views.py:2044 windows/views.py:3354 windows/views.py:3386 msgid "Delete" msgstr "Delete" -#: windows/views.py:111 windows/views.py:1311 windows/views.py:1468 -#: windows/views.py:2747 windows/views.py:3192 +#: windows/views.py:111 windows/views.py:1358 windows/views.py:1515 +#: windows/views.py:2828 windows/views.py:3273 msgid "Engine" msgstr "Engine" @@ -105,666 +150,684 @@ msgstr "Host + port" msgid "Username" msgstr "Username" -#: windows/views.py:161 windows/views.py:1100 +#: windows/views.py:161 windows/views.py:1147 msgid "Password" msgstr "Password" -#: windows/views.py:177 +#: windows/views.py:174 +#, fuzzy +msgid "Connection timeout" +msgstr "Connection" + +#: windows/views.py:192 msgid "Use TLS" msgstr "Use TLS" -#: windows/views.py:180 +#: windows/views.py:203 msgid "Use SSH tunnel" msgstr "Use SSH tunnel" -#: windows/views.py:202 windows/views.py:2668 +#: windows/views.py:214 +msgid "Compressed client/server protocol" +msgstr "" + +#: windows/views.py:233 windows/views.py:2749 msgid "Filename" msgstr "Filename" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2673 -#: windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:2754 +#: windows/views.py:2937 msgid "Select a file" msgstr "Select a file" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:2937 msgid "*.*" msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:221 windows/views.py:1313 windows/views.py:1426 -#: windows/views.py:2748 windows/views.py:3034 windows/views.py:3150 +#: windows/views.py:255 windows/views.py:1360 windows/views.py:1473 +#: windows/views.py:2829 windows/views.py:3115 windows/views.py:3231 msgid "Comments" msgstr "Comments" -#: windows/main/controller.py:219 windows/views.py:235 windows/views.py:683 -#: windows/views.py:837 +#: windows/main/controller.py:236 windows/views.py:269 windows/views.py:730 +#: windows/views.py:884 msgid "Settings" msgstr "Settings" -#: windows/views.py:244 +#: windows/views.py:278 msgid "SSH executable" msgstr "SSH executable" -#: windows/views.py:249 +#: windows/views.py:283 msgid "ssh" msgstr "ssh" -#: windows/views.py:257 +#: windows/views.py:291 msgid "SSH host + port" msgstr "SSH host + port" -#: windows/views.py:269 +#: windows/views.py:303 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "SSH host + port (the SSH server that forwards traffic to the DB)" -#: windows/views.py:278 +#: windows/views.py:312 msgid "SSH username" msgstr "SSH username" -#: windows/views.py:291 +#: windows/views.py:325 msgid "SSH password" msgstr "SSH password" -#: windows/views.py:304 +#: windows/views.py:338 msgid "Local port" msgstr "Local port" -#: windows/views.py:310 +#: windows/views.py:344 msgid "if the value is set to 0, the first available port will be used" msgstr "if the value is set to 0, the first available port will be used" -#: windows/views.py:319 +#: windows/views.py:353 msgid "Identity file" msgstr "Identity file" -#: windows/views.py:335 +#: windows/views.py:369 msgid "Remote host + port" msgstr "Remote host + port" -#: windows/views.py:347 +#: windows/views.py:381 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "Remote host/port is the real DB target (defaults to DB Host/Port)." -#: windows/views.py:358 +#: windows/views.py:390 +msgid "SSH extra args" +msgstr "" + +#: windows/views.py:405 msgid "SSH Tunnel" msgstr "SSH Tunnel" -#: windows/views.py:364 windows/views.py:1309 windows/views.py:2745 +#: windows/views.py:411 windows/views.py:1356 windows/views.py:2826 msgid "Created at" msgstr "Created at" -#: windows/views.py:398 +#: windows/views.py:445 msgid "Successful connections" msgstr "" -#: windows/views.py:415 +#: windows/views.py:462 msgid "Last successful connection" msgstr "" -#: windows/views.py:432 +#: windows/views.py:479 msgid "Unsuccessful connections" msgstr "" -#: windows/views.py:449 +#: windows/views.py:496 msgid "Last failure reason" msgstr "" -#: windows/views.py:466 +#: windows/views.py:513 msgid "Total connection attempts" msgstr "" -#: windows/views.py:483 -msgid " Average connection time (ms)" -msgstr "" +#: windows/views.py:530 +#, fuzzy +msgid "Average connection time (ms)" +msgstr "Connection" -#: windows/views.py:500 -msgid " Most recent connection duration" +#: windows/views.py:547 +msgid "Most recent connection duration" msgstr "" -#: windows/views.py:519 +#: windows/views.py:566 msgid "Statistics" msgstr "" -#: windows/views.py:537 windows/views.py:1672 +#: windows/views.py:584 windows/views.py:1719 msgid "Create" msgstr "" -#: windows/views.py:541 +#: windows/views.py:588 msgid "Create connection" msgstr "" -#: windows/views.py:544 +#: windows/views.py:591 msgid "Create directory" msgstr "" -#: windows/views.py:573 windows/views.py:797 windows/views.py:1328 -#: windows/views.py:1741 windows/views.py:2002 windows/views.py:2078 -#: windows/views.py:3081 windows/views.py:3308 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1375 +#: windows/views.py:1788 windows/views.py:2049 windows/views.py:2152 +#: windows/views.py:3162 windows/views.py:3389 msgid "Cancel" msgstr "" -#: windows/views.py:578 windows/views.py:2007 windows/views.py:3091 -#: windows/views.py:3313 +#: windows/views.py:625 windows/views.py:2054 windows/views.py:3172 +#: windows/views.py:3394 msgid "Save" msgstr "" -#: windows/views.py:585 +#: windows/views.py:632 msgid "Test" msgstr "" -#: windows/views.py:592 +#: windows/views.py:639 msgid "Connect" msgstr "" -#: windows/views.py:695 +#: windows/views.py:742 msgid "Language" msgstr "" -#: windows/views.py:700 +#: windows/views.py:747 msgid "English" msgstr "" -#: windows/views.py:700 +#: windows/views.py:747 msgid "Italian" msgstr "" -#: windows/views.py:700 +#: windows/views.py:747 msgid "French" msgstr "" -#: windows/views.py:712 +#: windows/views.py:759 msgid "Locale" msgstr "" -#: windows/views.py:733 +#: windows/views.py:780 msgid "Edit Value" msgstr "" -#: windows/views.py:743 +#: windows/views.py:790 msgid "Syntax" msgstr "" -#: windows/views.py:800 +#: windows/views.py:847 msgid "Ok" msgstr "" -#: windows/views.py:831 +#: windows/views.py:878 msgid "PeterSQL" msgstr "" -#: windows/views.py:840 +#: windows/views.py:887 msgid "File" msgstr "" -#: windows/views.py:843 +#: windows/views.py:890 msgid "About" msgstr "" -#: windows/views.py:846 +#: windows/views.py:893 msgid "Help" msgstr "" -#: windows/views.py:851 +#: windows/views.py:898 msgid "Open connection manager" msgstr "" -#: windows/views.py:853 +#: windows/views.py:900 msgid "Disconnect from server" msgstr "" -#: windows/views.py:857 +#: windows/views.py:904 msgid "tool" msgstr "" -#: windows/views.py:857 +#: windows/views.py:904 msgid "Refresh" msgstr "" -#: windows/views.py:861 windows/views.py:863 +#: windows/views.py:908 windows/views.py:910 msgid "Add" msgstr "" -#: windows/views.py:897 windows/views.py:901 windows/views.py:2166 -#: windows/views.py:2654 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2232 +#: windows/views.py:2735 msgid "MyMenuItem" msgstr "" -#: windows/views.py:904 windows/views.py:1769 windows/views.py:3336 +#: windows/views.py:951 windows/views.py:1816 windows/views.py:3417 msgid "MyMenu" msgstr "" -#: windows/views.py:919 windows/views.py:1350 windows/views.py:1357 -#: windows/views.py:1364 +#: windows/views.py:966 windows/views.py:1397 windows/views.py:1404 +#: windows/views.py:1411 msgid "MyLabel" msgstr "" -#: windows/views.py:925 +#: windows/views.py:972 msgid "Databases" msgstr "" -#: windows/views.py:926 windows/views.py:1308 windows/views.py:2716 -#: windows/views.py:2744 +#: windows/views.py:973 windows/views.py:1355 windows/views.py:2797 +#: windows/views.py:2825 msgid "Size" msgstr "" -#: windows/views.py:927 +#: windows/views.py:974 msgid "Elements" msgstr "" -#: windows/views.py:928 +#: windows/views.py:975 msgid "Modified at" msgstr "" -#: windows/views.py:929 +#: windows/views.py:976 msgid "Tables" msgstr "" -#: windows/views.py:936 +#: windows/views.py:983 msgid "System" msgstr "" -#: windows/views.py:979 +#: windows/views.py:1026 msgid "Character set" msgstr "" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1000 -#: windows/views.py:1312 windows/views.py:2980 +#: windows/components/dataview.py:89 windows/views.py:1047 +#: windows/views.py:1359 windows/views.py:3061 msgid "Collation" msgstr "" -#: windows/views.py:1026 windows/views.py:2862 +#: windows/views.py:1073 windows/views.py:2943 msgid "Encryption" msgstr "" -#: windows/views.py:1038 +#: windows/views.py:1085 msgid "Read Only" msgstr "" -#: windows/views.py:1055 +#: windows/views.py:1102 msgid "Tablespace" msgstr "" -#: windows/views.py:1076 +#: windows/views.py:1123 msgid "Connection limit" msgstr "" -#: windows/views.py:1119 +#: windows/views.py:1166 msgid "Profile" msgstr "" -#: windows/views.py:1145 +#: windows/views.py:1192 msgid "Default tablespace" msgstr "" -#: windows/views.py:1166 +#: windows/views.py:1213 msgid "Temporary tablespace" msgstr "" -#: windows/views.py:1192 +#: windows/views.py:1239 msgid "Quota" msgstr "" -#: windows/views.py:1211 +#: windows/views.py:1258 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1228 +#: windows/views.py:1275 msgid "Account status" msgstr "" -#: windows/views.py:1249 +#: windows/views.py:1296 msgid "Password expire" msgstr "" -#: windows/views.py:1270 +#: windows/views.py:1317 msgid "Table:" msgstr "" -#: windows/views.py:1278 windows/views.py:1551 windows/views.py:1595 -#: windows/views.py:1701 windows/views.py:3268 +#: windows/views.py:1325 windows/views.py:1598 windows/views.py:1642 +#: windows/views.py:1748 windows/views.py:3349 msgid "Insert" msgstr "" -#: windows/views.py:1283 +#: windows/views.py:1330 msgid "Clone" msgstr "" -#: windows/views.py:1307 +#: windows/views.py:1354 msgid "Rows" msgstr "" -#: windows/views.py:1310 windows/views.py:2746 +#: windows/views.py:1357 windows/views.py:2827 msgid "Updated at" msgstr "" -#: windows/views.py:1334 windows/views.py:1746 windows/views.py:2085 -#: windows/views.py:2140 +#: windows/views.py:1381 windows/views.py:1793 windows/views.py:2159 +#: windows/views.py:2206 msgid "Apply" msgstr "" -#: windows/views.py:1344 windows/views.py:1503 windows/views.py:1956 -#: windows/views.py:3225 +#: windows/views.py:1391 windows/views.py:1550 windows/views.py:2003 +#: windows/views.py:3306 msgid "Options" msgstr "" -#: windows/views.py:1375 +#: windows/views.py:1422 msgid "Diagram" msgstr "" -#: windows/views.py:1386 windows/views.py:2715 +#: windows/views.py:1433 windows/views.py:2796 msgid "Database" msgstr "" -#: windows/views.py:1441 windows/views.py:3165 +#: windows/views.py:1488 windows/views.py:3246 msgid "Base" msgstr "" -#: windows/views.py:1455 windows/views.py:3179 +#: windows/views.py:1502 windows/views.py:3260 msgid "Auto Increment" msgstr "" -#: windows/views.py:1483 windows/views.py:3207 +#: windows/views.py:1530 windows/views.py:3288 msgid "Default Collation" msgstr "" -#: windows/views.py:1515 windows/views.py:1556 windows/views.py:1600 +#: windows/views.py:1562 windows/views.py:1603 windows/views.py:1647 msgid "Remove" msgstr "" -#: windows/views.py:1522 windows/views.py:1563 windows/views.py:1607 +#: windows/views.py:1569 windows/views.py:1610 windows/views.py:1654 msgid "Clear" msgstr "" -#: windows/views.py:1537 windows/views.py:3239 +#: windows/views.py:1584 windows/views.py:3320 msgid "Indexes" msgstr "" -#: windows/views.py:1581 +#: windows/views.py:1628 msgid "Foreign Keys" msgstr "" -#: windows/views.py:1625 +#: windows/views.py:1672 msgid "Checks" msgstr "" -#: windows/views.py:1693 windows/views.py:3260 +#: windows/views.py:1740 windows/views.py:3341 msgid "Columns:" msgstr "" -#: windows/views.py:1713 windows/views.py:3280 +#: windows/views.py:1760 windows/views.py:3361 msgid "Up" msgstr "" -#: windows/views.py:1720 windows/views.py:3287 +#: windows/views.py:1767 windows/views.py:3368 msgid "Down" msgstr "" -#: windows/views.py:1759 windows/views.py:1766 windows/views.py:3326 -#: windows/views.py:3333 +#: windows/views.py:1806 windows/views.py:1813 windows/views.py:3407 +#: windows/views.py:3414 msgid "Add Index" msgstr "" -#: windows/views.py:1763 windows/views.py:3330 +#: windows/views.py:1810 windows/views.py:3411 msgid "Add PrimaryKey" msgstr "" -#: windows/views.py:1780 +#: windows/views.py:1827 msgid "Table" msgstr "" -#: windows/views.py:1816 +#: windows/views.py:1863 msgid "Definer" msgstr "" -#: windows/views.py:1836 +#: windows/views.py:1883 msgid "Schema" msgstr "" -#: windows/views.py:1862 +#: windows/views.py:1909 msgid "SQL security" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1916 msgid "DEFINER" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1916 msgid "INVOKER" msgstr "" -#: windows/views.py:1881 windows/views.py:2558 windows/views.py:2577 -#: windows/views.py:2820 +#: windows/views.py:1928 windows/views.py:2639 windows/views.py:2658 +#: windows/views.py:2901 msgid "Algorithm" msgstr "" -#: windows/views.py:1883 windows/views.py:2543 windows/views.py:2576 -#: windows/views.py:2825 +#: windows/views.py:1930 windows/views.py:2624 windows/views.py:2657 +#: windows/views.py:2906 msgid "UNDEFINED" msgstr "" -#: windows/views.py:1886 windows/views.py:2546 windows/views.py:2576 -#: windows/views.py:2828 +#: windows/views.py:1933 windows/views.py:2627 windows/views.py:2657 +#: windows/views.py:2909 msgid "MERGE" msgstr "" -#: windows/views.py:1889 windows/views.py:2555 windows/views.py:2576 -#: windows/views.py:2831 +#: windows/views.py:1936 windows/views.py:2636 windows/views.py:2657 +#: windows/views.py:2912 msgid "TEMPTABLE" msgstr "" -#: windows/views.py:1899 windows/views.py:2582 +#: windows/views.py:1946 windows/views.py:2663 msgid "View constraint" msgstr "" -#: windows/views.py:1901 windows/views.py:2581 +#: windows/views.py:1948 windows/views.py:2662 msgid "None" msgstr "" -#: windows/views.py:1904 windows/views.py:2581 +#: windows/views.py:1951 windows/views.py:2662 msgid "LOCAL" msgstr "" -#: windows/views.py:1907 +#: windows/views.py:1954 msgid "CASCADE" msgstr "" -#: windows/views.py:1910 +#: windows/views.py:1957 msgid "CHECK ONLY" msgstr "" -#: windows/views.py:1913 windows/views.py:2581 +#: windows/views.py:1960 windows/views.py:2662 msgid "READ ONLY" msgstr "" -#: windows/views.py:1925 +#: windows/views.py:1972 msgid "Force" msgstr "" -#: windows/views.py:1937 +#: windows/views.py:1984 msgid "Security barrier" msgstr "" -#: windows/views.py:2019 +#: windows/views.py:2066 msgid "Views" msgstr "" -#: windows/views.py:2027 +#: windows/views.py:2074 msgid "Triggers" msgstr "" -#: windows/views.py:2039 -#, python-format -msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#: windows/views.py:2086 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" + +#: windows/views.py:2094 +msgid "First" +msgstr "" + +#: windows/views.py:2112 +msgid "Last" msgstr "" -#: windows/views.py:2049 +#: windows/views.py:2123 msgid "Insert record" msgstr "" -#: windows/views.py:2054 +#: windows/views.py:2128 msgid "Duplicate record" msgstr "" -#: windows/views.py:2061 +#: windows/views.py:2135 msgid "Delete record" msgstr "" -#: windows/views.py:2071 +#: windows/views.py:2145 msgid "Apply changes automatically" msgstr "" -#: windows/views.py:2073 windows/views.py:2074 +#: windows/views.py:2147 windows/views.py:2148 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" msgstr "" -#: windows/views.py:2095 -msgid "Next" -msgstr "" - -#: windows/views.py:2103 +#: windows/views.py:2169 msgid "Filters" msgstr "" -#: windows/views.py:2143 +#: windows/views.py:2209 msgid "CTRL+ENTER" msgstr "" -#: windows/views.py:2163 +#: windows/views.py:2229 msgid "Insert row" msgstr "" -#: windows/views.py:2171 +#: windows/views.py:2237 msgid "Data" msgstr "" -#: windows/views.py:2225 windows/views.py:2275 +#: windows/views.py:2291 windows/views.py:2341 msgid "New" msgstr "" -#: windows/views.py:2252 +#: windows/views.py:2318 msgid "Query" msgstr "" -#: windows/views.py:2272 +#: windows/views.py:2338 msgid "Close" msgstr "" -#: windows/views.py:2285 +#: windows/views.py:2351 msgid "Query #2" msgstr "" -#: windows/views.py:2537 +#: windows/views.py:2618 msgid "Column5" msgstr "" -#: windows/views.py:2548 +#: windows/views.py:2629 msgid "Import" msgstr "" -#: windows/views.py:2573 +#: windows/views.py:2654 msgid "Read only" msgstr "" -#: windows/views.py:2581 +#: windows/views.py:2662 msgid "CASCADED" msgstr "" -#: windows/views.py:2581 +#: windows/views.py:2662 msgid "CHECK OPTION" msgstr "" -#: windows/views.py:2613 +#: windows/views.py:2694 msgid "collapsible" msgstr "" -#: windows/views.py:2629 +#: windows/views.py:2710 msgid "Column3" msgstr "" -#: windows/views.py:2630 +#: windows/views.py:2711 msgid "Column4" msgstr "" -#: windows/views.py:2673 +#: windows/views.py:2754 msgid "" "Database " "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" msgstr "" -#: windows/views.py:2685 +#: windows/views.py:2766 msgid "Port" msgstr "" -#: windows/views.py:2708 +#: windows/views.py:2789 msgid "Usage" msgstr "" -#: windows/views.py:2719 +#: windows/views.py:2800 #, python-format msgid "%(total_rows)s" msgstr "" -#: windows/views.py:2724 +#: windows/views.py:2805 msgid "rows total" msgstr "" -#: windows/views.py:2743 +#: windows/views.py:2824 msgid "Lines" msgstr "" -#: windows/views.py:2775 +#: windows/views.py:2856 msgid "Temporary" msgstr "" -#: windows/views.py:2786 +#: windows/views.py:2867 msgid "Engine options" msgstr "" -#: windows/views.py:2845 +#: windows/views.py:2926 msgid "RadioBtn" msgstr "" -#: windows/views.py:2928 +#: windows/views.py:3009 msgid "Edit Column" msgstr "" -#: windows/views.py:2944 +#: windows/views.py:3025 msgid "Datatype" msgstr "" -#: windows/components/dataview.py:121 windows/views.py:2959 +#: windows/components/dataview.py:121 windows/views.py:3040 msgid "Length/Set" msgstr "" -#: windows/components/dataview.py:51 windows/views.py:2998 +#: windows/components/dataview.py:51 windows/views.py:3079 msgid "Unsigned" msgstr "" #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3004 +#: windows/components/dataview.py:75 windows/views.py:3085 msgid "Allow NULL" msgstr "" -#: windows/views.py:3010 +#: windows/views.py:3091 msgid "Zero Fill" msgstr "" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3021 +#: windows/components/dataview.py:78 windows/views.py:3102 msgid "Default" msgstr "" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3047 +#: windows/components/dataview.py:82 windows/views.py:3128 msgid "Virtuality" msgstr "" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3062 +#: windows/views.py:3143 msgid "Expression" msgstr "" @@ -836,11 +899,11 @@ msgstr "" msgid "On DELETE" msgstr "" -#: windows/components/dataview.py:299 +#: windows/components/dataview.py:298 msgid "Add foreign key" msgstr "" -#: windows/components/dataview.py:305 +#: windows/components/dataview.py:304 msgid "Remove foreign key" msgstr "" @@ -860,75 +923,111 @@ msgstr "" msgid "Text/Expression" msgstr "" -#: windows/dialogs/connections/view.py:119 windows/main/tabs/query.py:376 +#: windows/dialogs/connections/view.py:122 windows/main/tabs/query.py:387 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:394 +#: windows/dialogs/connections/view.py:401 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:407 +#: windows/dialogs/connections/view.py:413 +#, python-brace-format +msgid "Do you want save the connection {connection_name}?" +msgstr "" + +#: windows/dialogs/connections/view.py:416 msgid "Confirm save" msgstr "" -#: windows/dialogs/connections/view.py:459 +#: windows/dialogs/connections/view.py:468 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:461 +#: windows/dialogs/connections/view.py:470 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:736 +#: windows/dialogs/connections/view.py:737 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" #: windows/dialogs/connections/view.py:762 +#, python-brace-format +msgid "" +"Connection error:\n" +"{error}" +msgstr "" + +#: windows/dialogs/connections/view.py:763 msgid "Connection error" msgstr "" -#: windows/dialogs/connections/view.py:788 -#: windows/dialogs/connections/view.py:803 +#: windows/dialogs/connections/view.py:789 +#, python-brace-format +msgid "Do you want to delete the connection '{connection_name}'?" +msgstr "" + +#: windows/dialogs/connections/view.py:792 +#: windows/dialogs/connections/view.py:809 msgid "Confirm delete" msgstr "" -#: windows/main/controller.py:172 +#: windows/dialogs/connections/view.py:806 +#, python-brace-format +msgid "Do you want to delete the directory '{directory_name}'?" +msgstr "" + +#: windows/main/controller.py:189 msgid "days" msgstr "" -#: windows/main/controller.py:173 +#: windows/main/controller.py:190 msgid "hours" msgstr "" -#: windows/main/controller.py:174 +#: windows/main/controller.py:191 msgid "minutes" msgstr "" -#: windows/main/controller.py:175 +#: windows/main/controller.py:192 msgid "seconds" msgstr "" -#: windows/main/controller.py:183 +#: windows/main/controller.py:200 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "" -#: windows/main/controller.py:219 +#: windows/main/controller.py:236 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:298 +#: windows/main/controller.py:441 +#, python-brace-format +msgid "~{estimated} (Loading...)" +msgstr "" + +#: windows/main/controller.py:443 +msgid "~ (Loading...)" +msgstr "" + +#: windows/main/controller.py:608 msgid "Version" msgstr "" -#: windows/main/controller.py:300 +#: windows/main/controller.py:610 msgid "Uptime" msgstr "" -#: windows/main/controller.py:399 +#: windows/main/controller.py:678 +#, python-brace-format +msgid "Do you want discard the change to {database_name}?" +msgstr "" + +#: windows/main/controller.py:711 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -938,36 +1037,51 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:404 windows/main/controller.py:425 +#: windows/main/controller.py:716 windows/main/controller.py:737 msgid "Delete database" msgstr "" -#: windows/main/controller.py:410 +#: windows/main/controller.py:722 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:411 +#: windows/main/controller.py:723 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:424 +#: windows/main/controller.py:736 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:439 +#: windows/main/controller.py:751 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:440 windows/main/tabs/view.py:253 +#: windows/main/controller.py:752 windows/main/tabs/view.py:253 #: windows/main/tabs/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:582 +#: windows/main/controller.py:871 +#, python-brace-format +msgid "Do you want discard the change to {table_name}?" +msgstr "" + +#: windows/main/controller.py:897 +#, python-brace-format +msgid "Do you want delete the table {table_name}?" +msgstr "" + +#: windows/main/controller.py:900 msgid "Delete table" msgstr "" -#: windows/main/controller.py:699 +#: windows/main/controller.py:919 +#, python-brace-format +msgid "{table_name} (COPY)" +msgstr "" + +#: windows/main/controller.py:1017 msgid "Do you want delete the records?" msgstr "" @@ -987,51 +1101,51 @@ msgstr "" msgid "Reconnection failed:" msgstr "" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:450 +#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:489 #: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 msgid "Error" msgstr "" -#: windows/main/tabs/query.py:305 +#: windows/main/tabs/query.py:308 #, python-brace-format -msgid "{} rows affected" +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/query.py:309 windows/main/tabs/query.py:331 +#: windows/main/tabs/query.py:315 windows/main/tabs/query.py:339 #, python-brace-format -msgid "Query {}" +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/query.py:314 +#: windows/main/tabs/query.py:320 #, python-brace-format -msgid "Query {} (Error)" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/query.py:326 +#: windows/main/tabs/query.py:334 #, python-brace-format -msgid "Query {} ({} rows × {} cols)" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/query.py:353 +#: windows/main/tabs/query.py:360 #, python-brace-format -msgid "{} rows" +msgid "{rows_count} rows" msgstr "" -#: windows/main/tabs/query.py:355 +#: windows/main/tabs/query.py:362 #, python-brace-format -msgid "{:.1f} ms" +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/query.py:358 +#: windows/main/tabs/query.py:366 #, python-brace-format -msgid "{} warnings" +msgid "{warnings_count} warnings" msgstr "" -#: windows/main/tabs/query.py:370 +#: windows/main/tabs/query.py:381 msgid "Error:" msgstr "" -#: windows/main/tabs/query.py:449 +#: windows/main/tabs/query.py:488 msgid "No active database connection" msgstr "" @@ -1105,3 +1219,36 @@ 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 "" + diff --git a/locale/es_ES/LC_MESSAGES/petersql.mo b/locale/es_ES/LC_MESSAGES/petersql.mo index eca26497c2d02e4695a25f8f23610744892b8654..d1a10627380f680fcc7d536ebffa21c21c546d75 100644 GIT binary patch delta 4054 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 diff --git a/locale/es_ES/LC_MESSAGES/petersql.po b/locale/es_ES/LC_MESSAGES/petersql.po index 9cf4c67..595b0fc 100644 --- a/locale/es_ES/LC_MESSAGES/petersql.po +++ b/locale/es_ES/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-10 18:23+0100\n" +"POT-Creation-Date: 2026-03-12 19:04+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: es_ES\n" @@ -43,38 +43,83 @@ msgctxt "unit" msgid "TB" msgstr "TB" -#: structures/ssh_tunnel.py:166 +#: structures/ssh_tunnel.py:177 msgid "OpenSSH client not found." msgstr "Cliente OpenSSH no encontrado." -#: windows/dialogs/connections/view.py:395 -#: windows/dialogs/connections/view.py:738 windows/main/controller.py:296 +#: structures/engines/mariadb/context.py:592 +#: structures/engines/mysql/context.py:563 +#: structures/engines/postgresql/context.py:579 +#: structures/engines/sqlite/context.py:518 +#, python-brace-format +msgid "Table{table_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:620 +#: structures/engines/mysql/context.py:591 +#: structures/engines/postgresql/context.py:604 +#: structures/engines/sqlite/context.py:542 +#, python-brace-format +msgid "Column{column_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:638 +#: structures/engines/mysql/context.py:609 +#: structures/engines/postgresql/context.py:622 +#: structures/engines/sqlite/context.py:560 +#, python-brace-format +msgid "Index{index_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:678 +#: structures/engines/mysql/context.py:649 +#: structures/engines/postgresql/context.py:662 +#: structures/engines/sqlite/context.py:602 +#, python-brace-format +msgid "ForeignKey{foreign_key_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:711 +#: structures/engines/mysql/context.py:680 +#: structures/engines/postgresql/context.py:692 +#: structures/engines/sqlite/context.py:630 +#, python-brace-format +msgid "View{view_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:762 +#, python-brace-format +msgid "Trigger{trigger_index:03}" +msgstr "" + +#: windows/dialogs/connections/view.py:402 +#: windows/dialogs/connections/view.py:739 windows/main/controller.py:606 #: windows/views.py:33 msgid "Connection" msgstr "Conexión" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:956 -#: windows/views.py:1306 windows/views.py:1413 windows/views.py:1796 -#: windows/views.py:2707 windows/views.py:2730 windows/views.py:2731 -#: windows/views.py:2732 windows/views.py:2733 windows/views.py:2734 -#: windows/views.py:2735 windows/views.py:2736 windows/views.py:2737 -#: windows/views.py:2738 windows/views.py:2742 windows/views.py:2936 -#: windows/views.py:3137 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1003 +#: windows/views.py:1353 windows/views.py:1460 windows/views.py:1843 +#: windows/views.py:2788 windows/views.py:2811 windows/views.py:2812 +#: windows/views.py:2813 windows/views.py:2814 windows/views.py:2815 +#: windows/views.py:2816 windows/views.py:2817 windows/views.py:2818 +#: windows/views.py:2819 windows/views.py:2823 windows/views.py:3017 +#: windows/views.py:3218 msgid "Name" msgstr "Nombre" -#: windows/views.py:48 windows/views.py:381 +#: windows/views.py:48 windows/views.py:428 msgid "Last connection" msgstr "Última conexión" -#: windows/dialogs/connections/view.py:631 windows/views.py:61 +#: windows/dialogs/connections/view.py:640 windows/views.py:61 msgid "New directory" msgstr "Nuevo directorio" -#: windows/dialogs/connections/model.py:187 -#: windows/dialogs/connections/view.py:591 windows/views.py:65 +#: windows/dialogs/connections/model.py:206 +#: windows/dialogs/connections/view.py:600 windows/views.py:65 msgid "New connection" msgstr "Nueva conexión" @@ -88,14 +133,14 @@ msgstr "Nombre" msgid "Clone connection" msgstr "Nueva conexión" -#: windows/views.py:81 windows/views.py:556 windows/views.py:1290 -#: windows/views.py:1331 windows/views.py:1706 windows/views.py:1738 -#: windows/views.py:1997 windows/views.py:3273 windows/views.py:3305 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1337 +#: windows/views.py:1378 windows/views.py:1753 windows/views.py:1785 +#: windows/views.py:2044 windows/views.py:3354 windows/views.py:3386 msgid "Delete" msgstr "Eliminar" -#: windows/views.py:111 windows/views.py:1311 windows/views.py:1468 -#: windows/views.py:2747 windows/views.py:3192 +#: windows/views.py:111 windows/views.py:1358 windows/views.py:1515 +#: windows/views.py:2828 windows/views.py:3273 msgid "Engine" msgstr "Motor" @@ -107,540 +152,564 @@ msgstr "Host + puerto" msgid "Username" msgstr "Nombre de usuario" -#: windows/views.py:161 windows/views.py:1100 +#: windows/views.py:161 windows/views.py:1147 msgid "Password" msgstr "Contraseña" -#: windows/views.py:177 +#: windows/views.py:174 +#, fuzzy +msgid "Connection timeout" +msgstr "Conexión perdida" + +#: windows/views.py:192 msgid "Use TLS" msgstr "Usar TLS" -#: windows/views.py:180 +#: windows/views.py:203 msgid "Use SSH tunnel" msgstr "Usar túnel SSH" -#: windows/views.py:202 windows/views.py:2668 +#: windows/views.py:214 +msgid "Compressed client/server protocol" +msgstr "" + +#: windows/views.py:233 windows/views.py:2749 msgid "Filename" msgstr "Nombre de archivo" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2673 -#: windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:2754 +#: windows/views.py:2937 msgid "Select a file" msgstr "Seleccionar un archivo" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:2937 #, fuzzy msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:221 windows/views.py:1313 windows/views.py:1426 -#: windows/views.py:2748 windows/views.py:3034 windows/views.py:3150 +#: windows/views.py:255 windows/views.py:1360 windows/views.py:1473 +#: windows/views.py:2829 windows/views.py:3115 windows/views.py:3231 msgid "Comments" msgstr "Comentarios" -#: windows/main/controller.py:219 windows/views.py:235 windows/views.py:683 -#: windows/views.py:837 +#: windows/main/controller.py:236 windows/views.py:269 windows/views.py:730 +#: windows/views.py:884 msgid "Settings" msgstr "Configuraciones" -#: windows/views.py:244 +#: windows/views.py:278 msgid "SSH executable" msgstr "Ejecutable SSH" -#: windows/views.py:249 +#: windows/views.py:283 msgid "ssh" msgstr "ssh" -#: windows/views.py:257 +#: windows/views.py:291 msgid "SSH host + port" msgstr "Host SSH + puerto" -#: windows/views.py:269 +#: windows/views.py:303 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "Host SSH + puerto (el servidor SSH que reenvía el tráfico a la BD)" -#: windows/views.py:278 +#: windows/views.py:312 msgid "SSH username" msgstr "Nombre de usuario SSH" -#: windows/views.py:291 +#: windows/views.py:325 msgid "SSH password" msgstr "Contraseña SSH" -#: windows/views.py:304 +#: windows/views.py:338 msgid "Local port" msgstr "Puerto local" -#: windows/views.py:310 +#: windows/views.py:344 msgid "if the value is set to 0, the first available port will be used" msgstr "si el valor se establece en 0, se utilizará el primer puerto disponible" -#: windows/views.py:319 +#: windows/views.py:353 msgid "Identity file" msgstr "Archivo de identidad" -#: windows/views.py:335 +#: windows/views.py:369 #, fuzzy msgid "Remote host + port" msgstr "Host + puerto" -#: windows/views.py:347 +#: windows/views.py:381 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." -msgstr "Host/puerto remoto es el objetivo real de la BD (por defecto Host/Puerto BD)." +msgstr "" +"Host/puerto remoto es el objetivo real de la BD (por defecto Host/Puerto " +"BD)." -#: windows/views.py:358 +#: windows/views.py:390 +msgid "SSH extra args" +msgstr "" + +#: windows/views.py:405 msgid "SSH Tunnel" msgstr "Túnel SSH" -#: windows/views.py:364 windows/views.py:1309 windows/views.py:2745 +#: windows/views.py:411 windows/views.py:1356 windows/views.py:2826 msgid "Created at" msgstr "Creado en" -#: windows/views.py:398 +#: windows/views.py:445 msgid "Successful connections" msgstr "Conexiones exitosas" -#: windows/views.py:415 +#: windows/views.py:462 #, fuzzy msgid "Last successful connection" msgstr "Conexiones exitosas" -#: windows/views.py:432 +#: windows/views.py:479 msgid "Unsuccessful connections" msgstr "Conexiones fallidas" -#: windows/views.py:449 +#: windows/views.py:496 msgid "Last failure reason" msgstr "" -#: windows/views.py:466 +#: windows/views.py:513 #, fuzzy msgid "Total connection attempts" msgstr "Última conexión" -#: windows/views.py:483 +#: windows/views.py:530 #, fuzzy -msgid " Average connection time (ms)" +msgid "Average connection time (ms)" msgstr "Reconexión fallida:" -#: windows/views.py:500 +#: windows/views.py:547 #, fuzzy -msgid " Most recent connection duration" +msgid "Most recent connection duration" msgstr "Abrir administrador de conexiones" -#: windows/views.py:519 +#: windows/views.py:566 msgid "Statistics" msgstr "Estadísticas" -#: windows/views.py:537 windows/views.py:1672 +#: windows/views.py:584 windows/views.py:1719 msgid "Create" msgstr "Crear" -#: windows/views.py:541 +#: windows/views.py:588 #, fuzzy msgid "Create connection" msgstr "Última conexión" -#: windows/views.py:544 +#: windows/views.py:591 #, fuzzy msgid "Create directory" msgstr "Nuevo directorio" -#: windows/views.py:573 windows/views.py:797 windows/views.py:1328 -#: windows/views.py:1741 windows/views.py:2002 windows/views.py:2078 -#: windows/views.py:3081 windows/views.py:3308 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1375 +#: windows/views.py:1788 windows/views.py:2049 windows/views.py:2152 +#: windows/views.py:3162 windows/views.py:3389 msgid "Cancel" msgstr "Cancelar" -#: windows/views.py:578 windows/views.py:2007 windows/views.py:3091 -#: windows/views.py:3313 +#: windows/views.py:625 windows/views.py:2054 windows/views.py:3172 +#: windows/views.py:3394 msgid "Save" msgstr "Guardar" -#: windows/views.py:585 +#: windows/views.py:632 msgid "Test" msgstr "Probar" -#: windows/views.py:592 +#: windows/views.py:639 msgid "Connect" msgstr "Conectar" -#: windows/views.py:695 +#: windows/views.py:742 msgid "Language" msgstr "Idioma" -#: windows/views.py:700 +#: windows/views.py:747 msgid "English" msgstr "Inglés" -#: windows/views.py:700 +#: windows/views.py:747 msgid "Italian" msgstr "Italiano" -#: windows/views.py:700 +#: windows/views.py:747 msgid "French" msgstr "Francés" -#: windows/views.py:712 +#: windows/views.py:759 msgid "Locale" msgstr "Localización" -#: windows/views.py:733 +#: windows/views.py:780 msgid "Edit Value" msgstr "Editar valor" -#: windows/views.py:743 +#: windows/views.py:790 msgid "Syntax" msgstr "Sintaxis" -#: windows/views.py:800 +#: windows/views.py:847 msgid "Ok" msgstr "Ok" -#: windows/views.py:831 +#: windows/views.py:878 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:840 +#: windows/views.py:887 msgid "File" msgstr "Archivo" -#: windows/views.py:843 +#: windows/views.py:890 msgid "About" msgstr "Acerca de" -#: windows/views.py:846 +#: windows/views.py:893 msgid "Help" msgstr "Ayuda" -#: windows/views.py:851 +#: windows/views.py:898 msgid "Open connection manager" msgstr "Abrir administrador de conexiones" -#: windows/views.py:853 +#: windows/views.py:900 msgid "Disconnect from server" msgstr "Desconectar del servidor" -#: windows/views.py:857 +#: windows/views.py:904 msgid "tool" msgstr "herramienta" -#: windows/views.py:857 +#: windows/views.py:904 msgid "Refresh" msgstr "Actualizar" -#: windows/views.py:861 windows/views.py:863 +#: windows/views.py:908 windows/views.py:910 msgid "Add" msgstr "Agregar" -#: windows/views.py:897 windows/views.py:901 windows/views.py:2166 -#: windows/views.py:2654 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2232 +#: windows/views.py:2735 msgid "MyMenuItem" msgstr "MiElementoMenu" -#: windows/views.py:904 windows/views.py:1769 windows/views.py:3336 +#: windows/views.py:951 windows/views.py:1816 windows/views.py:3417 msgid "MyMenu" msgstr "MiMenu" -#: windows/views.py:919 windows/views.py:1350 windows/views.py:1357 -#: windows/views.py:1364 +#: windows/views.py:966 windows/views.py:1397 windows/views.py:1404 +#: windows/views.py:1411 msgid "MyLabel" msgstr "MiEtiqueta" -#: windows/views.py:925 +#: windows/views.py:972 msgid "Databases" msgstr "Bases de datos" -#: windows/views.py:926 windows/views.py:1308 windows/views.py:2716 -#: windows/views.py:2744 +#: windows/views.py:973 windows/views.py:1355 windows/views.py:2797 +#: windows/views.py:2825 msgid "Size" msgstr "Tamaño" -#: windows/views.py:927 +#: windows/views.py:974 msgid "Elements" msgstr "Elementos" -#: windows/views.py:928 +#: windows/views.py:975 msgid "Modified at" msgstr "Modificado en" -#: windows/views.py:929 +#: windows/views.py:976 msgid "Tables" msgstr "Tablas" -#: windows/views.py:936 +#: windows/views.py:983 msgid "System" msgstr "Sistema" -#: windows/views.py:979 +#: windows/views.py:1026 #, fuzzy msgid "Character set" msgstr "Creado en" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1000 -#: windows/views.py:1312 windows/views.py:2980 +#: windows/components/dataview.py:89 windows/views.py:1047 +#: windows/views.py:1359 windows/views.py:3061 msgid "Collation" msgstr "Intercalación" -#: windows/views.py:1026 windows/views.py:2862 +#: windows/views.py:1073 windows/views.py:2943 msgid "Encryption" msgstr "" -#: windows/views.py:1038 +#: windows/views.py:1085 msgid "Read Only" msgstr "" -#: windows/views.py:1055 +#: windows/views.py:1102 #, fuzzy msgid "Tablespace" msgstr "Tablas" -#: windows/views.py:1076 +#: windows/views.py:1123 #, fuzzy msgid "Connection limit" msgstr "Conexión perdida" -#: windows/views.py:1119 +#: windows/views.py:1166 #, fuzzy msgid "Profile" msgstr "Archivo" -#: windows/views.py:1145 +#: windows/views.py:1192 #, fuzzy msgid "Default tablespace" msgstr "Eliminar tabla" -#: windows/views.py:1166 +#: windows/views.py:1213 #, fuzzy msgid "Temporary tablespace" msgstr "Temporal" -#: windows/views.py:1192 +#: windows/views.py:1239 msgid "Quota" msgstr "" -#: windows/views.py:1211 +#: windows/views.py:1258 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1228 +#: windows/views.py:1275 msgid "Account status" msgstr "" -#: windows/views.py:1249 +#: windows/views.py:1296 #, fuzzy msgid "Password expire" msgstr "Contraseña" -#: windows/views.py:1270 +#: windows/views.py:1317 msgid "Table:" msgstr "Tabla:" -#: windows/views.py:1278 windows/views.py:1551 windows/views.py:1595 -#: windows/views.py:1701 windows/views.py:3268 +#: windows/views.py:1325 windows/views.py:1598 windows/views.py:1642 +#: windows/views.py:1748 windows/views.py:3349 msgid "Insert" msgstr "Insertar" -#: windows/views.py:1283 +#: windows/views.py:1330 msgid "Clone" msgstr "Clonar" -#: windows/views.py:1307 +#: windows/views.py:1354 msgid "Rows" msgstr "Filas" -#: windows/views.py:1310 windows/views.py:2746 +#: windows/views.py:1357 windows/views.py:2827 msgid "Updated at" msgstr "Actualizado en" -#: windows/views.py:1334 windows/views.py:1746 windows/views.py:2085 -#: windows/views.py:2140 +#: windows/views.py:1381 windows/views.py:1793 windows/views.py:2159 +#: windows/views.py:2206 msgid "Apply" msgstr "Aplicar" -#: windows/views.py:1344 windows/views.py:1503 windows/views.py:1956 -#: windows/views.py:3225 +#: windows/views.py:1391 windows/views.py:1550 windows/views.py:2003 +#: windows/views.py:3306 msgid "Options" msgstr "Opciones" -#: windows/views.py:1375 +#: windows/views.py:1422 msgid "Diagram" msgstr "Diagrama" -#: windows/views.py:1386 windows/views.py:2715 +#: windows/views.py:1433 windows/views.py:2796 msgid "Database" msgstr "Base de datos" -#: windows/views.py:1441 windows/views.py:3165 +#: windows/views.py:1488 windows/views.py:3246 msgid "Base" msgstr "Base" -#: windows/views.py:1455 windows/views.py:3179 +#: windows/views.py:1502 windows/views.py:3260 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1483 windows/views.py:3207 +#: windows/views.py:1530 windows/views.py:3288 msgid "Default Collation" msgstr "Intercalación predeterminada" -#: windows/views.py:1515 windows/views.py:1556 windows/views.py:1600 +#: windows/views.py:1562 windows/views.py:1603 windows/views.py:1647 msgid "Remove" msgstr "Eliminar" -#: windows/views.py:1522 windows/views.py:1563 windows/views.py:1607 +#: windows/views.py:1569 windows/views.py:1610 windows/views.py:1654 msgid "Clear" msgstr "Limpiar" -#: windows/views.py:1537 windows/views.py:3239 +#: windows/views.py:1584 windows/views.py:3320 msgid "Indexes" msgstr "Índices" -#: windows/views.py:1581 +#: windows/views.py:1628 msgid "Foreign Keys" msgstr "Claves foráneas" -#: windows/views.py:1625 +#: windows/views.py:1672 msgid "Checks" msgstr "Comprobaciones" -#: windows/views.py:1693 windows/views.py:3260 +#: windows/views.py:1740 windows/views.py:3341 msgid "Columns:" msgstr "Columnas:" -#: windows/views.py:1713 windows/views.py:3280 +#: windows/views.py:1760 windows/views.py:3361 msgid "Up" msgstr "Arriba" -#: windows/views.py:1720 windows/views.py:3287 +#: windows/views.py:1767 windows/views.py:3368 msgid "Down" msgstr "Abajo" -#: windows/views.py:1759 windows/views.py:1766 windows/views.py:3326 -#: windows/views.py:3333 +#: windows/views.py:1806 windows/views.py:1813 windows/views.py:3407 +#: windows/views.py:3414 msgid "Add Index" msgstr "Agregar índice" -#: windows/views.py:1763 windows/views.py:3330 +#: windows/views.py:1810 windows/views.py:3411 msgid "Add PrimaryKey" msgstr "Agregar clave primaria" -#: windows/views.py:1780 +#: windows/views.py:1827 msgid "Table" msgstr "Tabla" -#: windows/views.py:1816 +#: windows/views.py:1863 #, fuzzy msgid "Definer" msgstr "Insertar" -#: windows/views.py:1836 +#: windows/views.py:1883 msgid "Schema" msgstr "" -#: windows/views.py:1862 +#: windows/views.py:1909 msgid "SQL security" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1916 #, fuzzy msgid "DEFINER" msgstr "Insertar" -#: windows/views.py:1869 +#: windows/views.py:1916 #, fuzzy msgid "INVOKER" msgstr "Insertar" -#: windows/views.py:1881 windows/views.py:2558 windows/views.py:2577 -#: windows/views.py:2820 +#: windows/views.py:1928 windows/views.py:2639 windows/views.py:2658 +#: windows/views.py:2901 msgid "Algorithm" msgstr "" -#: windows/views.py:1883 windows/views.py:2543 windows/views.py:2576 -#: windows/views.py:2825 +#: windows/views.py:1930 windows/views.py:2624 windows/views.py:2657 +#: windows/views.py:2906 #, fuzzy msgid "UNDEFINED" msgstr "Sin signo" -#: windows/views.py:1886 windows/views.py:2546 windows/views.py:2576 -#: windows/views.py:2828 +#: windows/views.py:1933 windows/views.py:2627 windows/views.py:2657 +#: windows/views.py:2909 msgid "MERGE" msgstr "" -#: windows/views.py:1889 windows/views.py:2555 windows/views.py:2576 -#: windows/views.py:2831 +#: windows/views.py:1936 windows/views.py:2636 windows/views.py:2657 +#: windows/views.py:2912 #, fuzzy msgid "TEMPTABLE" msgstr "Tabla" -#: windows/views.py:1899 windows/views.py:2582 +#: windows/views.py:1946 windows/views.py:2663 msgid "View constraint" msgstr "" -#: windows/views.py:1901 windows/views.py:2581 +#: windows/views.py:1948 windows/views.py:2662 #, fuzzy msgid "None" msgstr "Clonar" -#: windows/views.py:1904 windows/views.py:2581 +#: windows/views.py:1951 windows/views.py:2662 #, fuzzy msgid "LOCAL" msgstr "Localización" -#: windows/views.py:1907 +#: windows/views.py:1954 #, fuzzy msgid "CASCADE" msgstr "Cancelar" -#: windows/views.py:1910 +#: windows/views.py:1957 #, fuzzy msgid "CHECK ONLY" msgstr "Verificar" -#: windows/views.py:1913 windows/views.py:2581 +#: windows/views.py:1960 windows/views.py:2662 msgid "READ ONLY" msgstr "" -#: windows/views.py:1925 +#: windows/views.py:1972 msgid "Force" msgstr "" -#: windows/views.py:1937 +#: windows/views.py:1984 msgid "Security barrier" msgstr "" -#: windows/views.py:2019 +#: windows/views.py:2066 msgid "Views" msgstr "Vistas" -#: windows/views.py:2027 +#: windows/views.py:2074 msgid "Triggers" msgstr "Disparadores" -#: windows/views.py:2039 -#, python-format -msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" -msgstr "Tabla `%(database_name)s`.`%(table_name)s`: %(total_rows) filas en total" +#: windows/views.py:2086 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" + +#: windows/views.py:2094 +#, fuzzy +msgid "First" +msgstr "Filtros" + +#: windows/views.py:2112 +msgid "Last" +msgstr "" -#: windows/views.py:2049 +#: windows/views.py:2123 msgid "Insert record" msgstr "Insertar registro" -#: windows/views.py:2054 +#: windows/views.py:2128 msgid "Duplicate record" msgstr "Duplicar registro" -#: windows/views.py:2061 +#: windows/views.py:2135 msgid "Delete record" msgstr "Eliminar registro" -#: windows/views.py:2071 +#: windows/views.py:2145 msgid "Apply changes automatically" msgstr "Aplicar cambios automáticamente" -#: windows/views.py:2073 windows/views.py:2074 +#: windows/views.py:2147 windows/views.py:2148 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -648,76 +717,72 @@ msgstr "" "Si está habilitado, las ediciones de la tabla se aplican inmediatamente " "sin presionar Aplicar o Cancelar" -#: windows/views.py:2095 -msgid "Next" -msgstr "Siguiente" - -#: windows/views.py:2103 +#: windows/views.py:2169 msgid "Filters" msgstr "Filtros" -#: windows/views.py:2143 +#: windows/views.py:2209 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2163 +#: windows/views.py:2229 msgid "Insert row" msgstr "Insertar fila" -#: windows/views.py:2171 +#: windows/views.py:2237 msgid "Data" msgstr "Datos" -#: windows/views.py:2225 windows/views.py:2275 +#: windows/views.py:2291 windows/views.py:2341 msgid "New" msgstr "Nuevo" -#: windows/views.py:2252 +#: windows/views.py:2318 msgid "Query" msgstr "Consulta" -#: windows/views.py:2272 +#: windows/views.py:2338 msgid "Close" msgstr "Cerrar" -#: windows/views.py:2285 +#: windows/views.py:2351 msgid "Query #2" msgstr "Consulta #2" -#: windows/views.py:2537 +#: windows/views.py:2618 msgid "Column5" msgstr "Columna5" -#: windows/views.py:2548 +#: windows/views.py:2629 msgid "Import" msgstr "Importar" -#: windows/views.py:2573 +#: windows/views.py:2654 msgid "Read only" msgstr "" -#: windows/views.py:2581 +#: windows/views.py:2662 msgid "CASCADED" msgstr "" -#: windows/views.py:2581 +#: windows/views.py:2662 #, fuzzy msgid "CHECK OPTION" msgstr "conexión" -#: windows/views.py:2613 +#: windows/views.py:2694 msgid "collapsible" msgstr "colapsable" -#: windows/views.py:2629 +#: windows/views.py:2710 msgid "Column3" msgstr "Columna3" -#: windows/views.py:2630 +#: windows/views.py:2711 msgid "Column4" msgstr "Columna4" -#: windows/views.py:2673 +#: windows/views.py:2754 msgid "" "Database " "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" @@ -725,78 +790,78 @@ msgstr "" "Database " "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" -#: windows/views.py:2685 +#: windows/views.py:2766 msgid "Port" msgstr "Puerto" -#: windows/views.py:2708 +#: windows/views.py:2789 msgid "Usage" msgstr "Uso" -#: windows/views.py:2719 +#: windows/views.py:2800 #, python-format msgid "%(total_rows)s" msgstr "%(total_rows)s" -#: windows/views.py:2724 +#: windows/views.py:2805 msgid "rows total" msgstr "filas en total" -#: windows/views.py:2743 +#: windows/views.py:2824 msgid "Lines" msgstr "Líneas" -#: windows/views.py:2775 +#: windows/views.py:2856 msgid "Temporary" msgstr "Temporal" -#: windows/views.py:2786 +#: windows/views.py:2867 #, fuzzy msgid "Engine options" msgstr "Opciones" -#: windows/views.py:2845 +#: windows/views.py:2926 msgid "RadioBtn" msgstr "" -#: windows/views.py:2928 +#: windows/views.py:3009 msgid "Edit Column" msgstr "Editar columna" -#: windows/views.py:2944 +#: windows/views.py:3025 msgid "Datatype" msgstr "Tipo de datos" -#: windows/components/dataview.py:121 windows/views.py:2959 +#: windows/components/dataview.py:121 windows/views.py:3040 msgid "Length/Set" msgstr "Longitud/Conjunto" -#: windows/components/dataview.py:51 windows/views.py:2998 +#: windows/components/dataview.py:51 windows/views.py:3079 msgid "Unsigned" msgstr "Sin signo" #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3004 +#: windows/components/dataview.py:75 windows/views.py:3085 msgid "Allow NULL" msgstr "Permitir NULL" -#: windows/views.py:3010 +#: windows/views.py:3091 msgid "Zero Fill" msgstr "Relleno cero" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3021 +#: windows/components/dataview.py:78 windows/views.py:3102 msgid "Default" msgstr "Predeterminado" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3047 +#: windows/components/dataview.py:82 windows/views.py:3128 msgid "Virtuality" msgstr "Virtualidad" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3062 +#: windows/views.py:3143 msgid "Expression" msgstr "Expresión" @@ -868,11 +933,11 @@ msgstr "En UPDATE" msgid "On DELETE" msgstr "En DELETE" -#: windows/components/dataview.py:299 +#: windows/components/dataview.py:298 msgid "Add foreign key" msgstr "Agregar clave foránea" -#: windows/components/dataview.py:305 +#: windows/components/dataview.py:304 msgid "Remove foreign key" msgstr "Eliminar clave foránea" @@ -892,75 +957,111 @@ msgstr "AUTO INCREMENTO" msgid "Text/Expression" msgstr "Texto/Expresión" -#: windows/dialogs/connections/view.py:119 windows/main/tabs/query.py:376 +#: windows/dialogs/connections/view.py:122 windows/main/tabs/query.py:387 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:394 +#: windows/dialogs/connections/view.py:401 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:407 +#: windows/dialogs/connections/view.py:413 +#, python-brace-format +msgid "Do you want save the connection {connection_name}?" +msgstr "" + +#: windows/dialogs/connections/view.py:416 msgid "Confirm save" msgstr "Confirmar guardar" -#: windows/dialogs/connections/view.py:459 +#: windows/dialogs/connections/view.py:468 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:461 +#: windows/dialogs/connections/view.py:470 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:736 +#: windows/dialogs/connections/view.py:737 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" #: windows/dialogs/connections/view.py:762 +#, fuzzy, python-brace-format +msgid "" +"Connection error:\n" +"{error}" +msgstr "Error de conexión" + +#: windows/dialogs/connections/view.py:763 msgid "Connection error" msgstr "Error de conexión" -#: windows/dialogs/connections/view.py:788 -#: windows/dialogs/connections/view.py:803 +#: windows/dialogs/connections/view.py:789 +#, fuzzy, python-brace-format +msgid "Do you want to delete the connection '{connection_name}'?" +msgstr "¿Quieres eliminar los registros?" + +#: windows/dialogs/connections/view.py:792 +#: windows/dialogs/connections/view.py:809 msgid "Confirm delete" msgstr "Confirmar eliminar" -#: windows/main/controller.py:172 +#: windows/dialogs/connections/view.py:806 +#, fuzzy, python-brace-format +msgid "Do you want to delete the directory '{directory_name}'?" +msgstr "¿Quieres eliminar los registros?" + +#: windows/main/controller.py:189 msgid "days" msgstr "días" -#: windows/main/controller.py:173 +#: windows/main/controller.py:190 msgid "hours" msgstr "horas" -#: windows/main/controller.py:174 +#: windows/main/controller.py:191 msgid "minutes" msgstr "minutos" -#: windows/main/controller.py:175 +#: windows/main/controller.py:192 msgid "seconds" msgstr "segundos" -#: windows/main/controller.py:183 +#: windows/main/controller.py:200 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizada: {used} ({percentage:.2%})" -#: windows/main/controller.py:219 +#: windows/main/controller.py:236 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:298 +#: windows/main/controller.py:441 +#, python-brace-format +msgid "~{estimated} (Loading...)" +msgstr "" + +#: windows/main/controller.py:443 +msgid "~ (Loading...)" +msgstr "" + +#: windows/main/controller.py:608 msgid "Version" msgstr "Versión" -#: windows/main/controller.py:300 +#: windows/main/controller.py:610 msgid "Uptime" msgstr "Tiempo de actividad" -#: windows/main/controller.py:399 +#: windows/main/controller.py:678 +#, python-brace-format +msgid "Do you want discard the change to {database_name}?" +msgstr "" + +#: windows/main/controller.py:711 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -970,37 +1071,52 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:404 windows/main/controller.py:425 +#: windows/main/controller.py:716 windows/main/controller.py:737 #, fuzzy msgid "Delete database" msgstr "Eliminar tabla" -#: windows/main/controller.py:410 +#: windows/main/controller.py:722 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:411 +#: windows/main/controller.py:723 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:424 +#: windows/main/controller.py:736 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:439 +#: windows/main/controller.py:751 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:440 windows/main/tabs/view.py:253 +#: windows/main/controller.py:752 windows/main/tabs/view.py:253 #: windows/main/tabs/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:582 +#: windows/main/controller.py:871 +#, python-brace-format +msgid "Do you want discard the change to {table_name}?" +msgstr "" + +#: windows/main/controller.py:897 +#, fuzzy, python-brace-format +msgid "Do you want delete the table {table_name}?" +msgstr "¿Quieres eliminar los registros?" + +#: windows/main/controller.py:900 msgid "Delete table" msgstr "Eliminar tabla" -#: windows/main/controller.py:699 +#: windows/main/controller.py:919 +#, python-brace-format +msgid "{table_name} (COPY)" +msgstr "" + +#: windows/main/controller.py:1017 msgid "Do you want delete the records?" msgstr "¿Quieres eliminar los registros?" @@ -1020,52 +1136,52 @@ msgstr "Conexión perdida" msgid "Reconnection failed:" msgstr "Reconexión fallida:" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:450 +#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:489 #: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 msgid "Error" msgstr "Error" -#: windows/main/tabs/query.py:305 +#: windows/main/tabs/query.py:308 #, python-brace-format -msgid "{} rows affected" +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/query.py:309 windows/main/tabs/query.py:331 -#, fuzzy, python-brace-format -msgid "Query {}" -msgstr "Consulta" +#: windows/main/tabs/query.py:315 windows/main/tabs/query.py:339 +#, python-brace-format +msgid "Query {query_number}" +msgstr "" -#: windows/main/tabs/query.py:314 +#: windows/main/tabs/query.py:320 #, python-brace-format -msgid "Query {} (Error)" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/query.py:326 +#: windows/main/tabs/query.py:334 #, python-brace-format -msgid "Query {} ({} rows × {} cols)" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/query.py:353 -#, fuzzy, python-brace-format -msgid "{} rows" -msgstr "Filas" +#: windows/main/tabs/query.py:360 +#, python-brace-format +msgid "{rows_count} rows" +msgstr "" -#: windows/main/tabs/query.py:355 +#: windows/main/tabs/query.py:362 #, python-brace-format -msgid "{:.1f} ms" +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/query.py:358 +#: windows/main/tabs/query.py:366 #, python-brace-format -msgid "{} warnings" +msgid "{warnings_count} warnings" msgstr "" -#: windows/main/tabs/query.py:370 +#: windows/main/tabs/query.py:381 #, fuzzy msgid "Error:" msgstr "Error" -#: windows/main/tabs/query.py:449 +#: windows/main/tabs/query.py:488 #, fuzzy msgid "No active database connection" msgstr "Nueva conexión" @@ -1138,3 +1254,32 @@ msgstr "" #~ msgid "directory" #~ msgstr "directorio" +#~ msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#~ msgstr "" +#~ "Tabla `%(database_name)s`.`%(table_name)s`: %(total_rows)" +#~ " filas en total" + +#~ msgid "Next" +#~ msgstr "Siguiente" + +#~ msgid "{} rows affected" +#~ msgstr "" + +#~ msgid "Query {}" +#~ msgstr "Consulta" + +#~ msgid "Query {} (Error)" +#~ msgstr "" + +#~ msgid "Query {} ({} rows × {} cols)" +#~ msgstr "" + +#~ msgid "{} rows" +#~ msgstr "Filas" + +#~ msgid "{:.1f} ms" +#~ msgstr "" + +#~ msgid "{} warnings" +#~ msgstr "" + diff --git a/locale/fr_FR/LC_MESSAGES/petersql.mo b/locale/fr_FR/LC_MESSAGES/petersql.mo index 13d3905146b96f61259f9fedade3e355509ed9a7..18340790dd95918fe3dd85cad7e9c076edab477a 100644 GIT binary patch delta 3288 zcmYM#d2AL%7{~E>fl}y8&lhY_mQTqHI>aV7F%Ng)vhC_0QSLH%tduM z&aNjS&j;OH3Mt&EKz?ourxv&e^*}AQ#G|MILw5a6aU)=MKy}a+Y3ur88yt%2pa7Lv3C7_w$j{B^q(p;OUx{kB6E&eaRKIVc z#yf_S8Q&eZ8!7ar8(mQYWnw(`M|C_3S#?*4N~jFgZVo<#3o#8hU^2dq8u)YM=T39- z<7HH0H!-LIf2W`U?x8x=s9Ne)sD?49j*?OBQ&H`@qY~+hdVUD1-Eh=Eqi`IKxBG`s z&mFbv_u^T9&FBOb8t9BQ_y#r5HDqzzO}qamDlwN3Zr>6$a6ERyB-Fq;$Xr|j>iJ2i zfy+_t7oo;mnZWuh<4P*zCe&`;hI;o^s00t9Iy{EW@H5oAY%niiBG;EuOMeG7pr21b z6A73dP~&t(O{7=QZe*c$=|JS?#&S~TQ&Al)L}k7L^{&?-W4IbrheuE|dKZ=W3Domv zQLpHVc?;G4E^1=IdlZymB%|v_9IB&ssDaW^1818jJYFwIZdcex{)kn1wxX z8ETwb^Bqjm_aCC5B|V24_yTIcE2x34qh@pq)xlq=75UfhN45(;7mZ3F3DvF>YA^Ig zC6tZoZwTgK9yWgeYbj{pjg1w~RipO6UetrNsDuvN{d)6#^JB9Ci}+J{9`zS9jSZp1 zGt4Yhf;p%O)WlP#t`Lde;r84!*_S_#KG*6CP!lS_pbAf0gDGY?G6}a7wUk@T9p--2KyRToR|qxGN#y6waw0c(!|ETH z5zLb9=3-G_Q|DyfzY=(a3T?K*sE&$J15dJsGt4D8nfmp}p8|K=>hGi8wZDD1pF~tY znW+CQ15hhC-s)!}KUdM7_198v<|-9GM7{eIkgIa-BYzXbzc+_%K(I7&` zL>JURImn*`SBe^F9%_k~BR{v!tV6BTDU4!#cbbAeqpwjjykXu&eFYI|;Q>=nySuwx z=b}0qjY_x#mB3`wL>8kGUX6N%l~%tQ^;{K31u5*dio>V`>aibwf_epipl0OK!wLCO zE0TfQT-{Kg+oPzJ8H4(oN>O`Y0V<)jsOL7K+HJ+48tkSZ>rpfL6qWHgRHhek6kbMk z)F~spV!co^9E92n!%@#Yjy@cNdiRB9u{ptfD#HzGY*i-L=`cG(5#jcn~2SX zj_fdQuh$UOFL?=9+pWe!rN4ui=EeI1$!qM+N>fZEYP`q%X~VWrdV|mu%LyHO2(A4K z#8{#S(R56pu&C(*4|#k2DLtQ~G}~&oqTZDDvpy!auqz>SJnLQeCr0e1^pf|FKfB#5 zN_oUCLW`&46)(R{O4$KQ`jnOtbBLWp)6t*8IHHm$B-RmnbuSZ5#}*3f?P4v?wDJO6 z?44?poj;e-a-t7$kkB!nm`W@prW31(3PLZK4d`AYbnFk)_&@Y{Z&-AqZ@yO=oigxs z>R%V!Z delta 3472 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 IXHccT0UxDm?EnA( diff --git a/locale/it_IT/LC_MESSAGES/petersql.po b/locale/it_IT/LC_MESSAGES/petersql.po index 94bfe10..840dd2c 100644 --- a/locale/it_IT/LC_MESSAGES/petersql.po +++ b/locale/it_IT/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-10 18:23+0100\n" +"POT-Creation-Date: 2026-03-12 19:04+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: it_IT\n" @@ -43,59 +43,102 @@ msgctxt "unit" msgid "TB" msgstr "TB" -#: structures/ssh_tunnel.py:166 +#: structures/ssh_tunnel.py:177 msgid "OpenSSH client not found." msgstr "Client OpenSSH non trovato." -#: windows/dialogs/connections/view.py:395 -#: windows/dialogs/connections/view.py:738 windows/main/controller.py:296 +#: structures/engines/mariadb/context.py:592 +#: structures/engines/mysql/context.py:563 +#: structures/engines/postgresql/context.py:579 +#: structures/engines/sqlite/context.py:518 +#, python-brace-format +msgid "Table{table_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:620 +#: structures/engines/mysql/context.py:591 +#: structures/engines/postgresql/context.py:604 +#: structures/engines/sqlite/context.py:542 +#, python-brace-format +msgid "Column{column_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:638 +#: structures/engines/mysql/context.py:609 +#: structures/engines/postgresql/context.py:622 +#: structures/engines/sqlite/context.py:560 +#, python-brace-format +msgid "Index{index_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:678 +#: structures/engines/mysql/context.py:649 +#: structures/engines/postgresql/context.py:662 +#: structures/engines/sqlite/context.py:602 +#, python-brace-format +msgid "ForeignKey{foreign_key_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:711 +#: structures/engines/mysql/context.py:680 +#: structures/engines/postgresql/context.py:692 +#: structures/engines/sqlite/context.py:630 +#, python-brace-format +msgid "View{view_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:762 +#, python-brace-format +msgid "Trigger{trigger_index:03}" +msgstr "" + +#: windows/dialogs/connections/view.py:402 +#: windows/dialogs/connections/view.py:739 windows/main/controller.py:606 #: windows/views.py:33 msgid "Connection" msgstr "Connessione" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:956 -#: windows/views.py:1306 windows/views.py:1413 windows/views.py:1796 -#: windows/views.py:2707 windows/views.py:2730 windows/views.py:2731 -#: windows/views.py:2732 windows/views.py:2733 windows/views.py:2734 -#: windows/views.py:2735 windows/views.py:2736 windows/views.py:2737 -#: windows/views.py:2738 windows/views.py:2742 windows/views.py:2936 -#: windows/views.py:3137 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1003 +#: windows/views.py:1353 windows/views.py:1460 windows/views.py:1843 +#: windows/views.py:2788 windows/views.py:2811 windows/views.py:2812 +#: windows/views.py:2813 windows/views.py:2814 windows/views.py:2815 +#: windows/views.py:2816 windows/views.py:2817 windows/views.py:2818 +#: windows/views.py:2819 windows/views.py:2823 windows/views.py:3017 +#: windows/views.py:3218 msgid "Name" msgstr "Nome" -#: windows/views.py:48 windows/views.py:381 +#: windows/views.py:48 windows/views.py:428 msgid "Last connection" msgstr "Ultima connessione" -#: windows/dialogs/connections/view.py:631 windows/views.py:61 +#: windows/dialogs/connections/view.py:640 windows/views.py:61 msgid "New directory" msgstr "Nuova directory" -#: windows/dialogs/connections/model.py:187 -#: windows/dialogs/connections/view.py:591 windows/views.py:65 +#: windows/dialogs/connections/model.py:206 +#: windows/dialogs/connections/view.py:600 windows/views.py:65 msgid "New connection" msgstr "Nuova connessione" #: windows/views.py:71 -#, fuzzy msgid "Rename" -msgstr "Nome" +msgstr "Rinomina" #: windows/views.py:76 -#, fuzzy msgid "Clone connection" -msgstr "Nuova connessione" +msgstr "Chiudi connessione" -#: windows/views.py:81 windows/views.py:556 windows/views.py:1290 -#: windows/views.py:1331 windows/views.py:1706 windows/views.py:1738 -#: windows/views.py:1997 windows/views.py:3273 windows/views.py:3305 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1337 +#: windows/views.py:1378 windows/views.py:1753 windows/views.py:1785 +#: windows/views.py:2044 windows/views.py:3354 windows/views.py:3386 msgid "Delete" msgstr "Elimina" -#: windows/views.py:111 windows/views.py:1311 windows/views.py:1468 -#: windows/views.py:2747 windows/views.py:3192 +#: windows/views.py:111 windows/views.py:1358 windows/views.py:1515 +#: windows/views.py:2828 windows/views.py:3273 msgid "Engine" msgstr "Motore" @@ -107,539 +150,553 @@ msgstr "Host + porta" msgid "Username" msgstr "Nome utente" -#: windows/views.py:161 windows/views.py:1100 +#: windows/views.py:161 windows/views.py:1147 msgid "Password" msgstr "Password" -#: windows/views.py:177 +#: windows/views.py:174 +msgid "Connection timeout" +msgstr "Timeout connessione" + +#: windows/views.py:192 msgid "Use TLS" msgstr "" -#: windows/views.py:180 +#: windows/views.py:203 msgid "Use SSH tunnel" msgstr "Usa tunnel SSH" -#: windows/views.py:202 windows/views.py:2668 +#: windows/views.py:214 +msgid "Compressed client/server protocol" +msgstr "" + +#: windows/views.py:233 windows/views.py:2749 msgid "Filename" msgstr "Nome file" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2673 -#: windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:2754 +#: windows/views.py:2937 msgid "Select a file" msgstr "Seleziona un file" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:2937 msgid "*.*" msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:221 windows/views.py:1313 windows/views.py:1426 -#: windows/views.py:2748 windows/views.py:3034 windows/views.py:3150 +#: windows/views.py:255 windows/views.py:1360 windows/views.py:1473 +#: windows/views.py:2829 windows/views.py:3115 windows/views.py:3231 msgid "Comments" msgstr "Commenti" -#: windows/main/controller.py:219 windows/views.py:235 windows/views.py:683 -#: windows/views.py:837 +#: windows/main/controller.py:236 windows/views.py:269 windows/views.py:730 +#: windows/views.py:884 msgid "Settings" msgstr "Impostazioni" -#: windows/views.py:244 +#: windows/views.py:278 msgid "SSH executable" msgstr "Eseguibile SSH" -#: windows/views.py:249 +#: windows/views.py:283 msgid "ssh" msgstr "ssh" -#: windows/views.py:257 +#: windows/views.py:291 msgid "SSH host + port" msgstr "Host SSH + porta" -#: windows/views.py:269 +#: windows/views.py:303 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "" -#: windows/views.py:278 +#: windows/views.py:312 msgid "SSH username" msgstr "Nome utente SSH" -#: windows/views.py:291 +#: windows/views.py:325 msgid "SSH password" msgstr "Password SSH" -#: windows/views.py:304 +#: windows/views.py:338 msgid "Local port" msgstr "Porta locale" -#: windows/views.py:310 +#: windows/views.py:344 msgid "if the value is set to 0, the first available port will be used" msgstr "se il valore è impostato a 0, verrà utilizzata la prima porta disponibile" -#: windows/views.py:319 +#: windows/views.py:353 msgid "Identity file" msgstr "" -#: windows/views.py:335 -#, fuzzy +#: windows/views.py:369 msgid "Remote host + port" -msgstr "Host + porta" +msgstr "Remoto host + porta" -#: windows/views.py:347 +#: windows/views.py:381 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "" -#: windows/views.py:358 +#: windows/views.py:390 +msgid "SSH extra args" +msgstr "" + +#: windows/views.py:405 msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:364 windows/views.py:1309 windows/views.py:2745 +#: windows/views.py:411 windows/views.py:1356 windows/views.py:2826 msgid "Created at" msgstr "Creato il" -#: windows/views.py:398 +#: windows/views.py:445 msgid "Successful connections" msgstr "Connessioni riuscite" -#: windows/views.py:415 -#, fuzzy +#: windows/views.py:462 msgid "Last successful connection" -msgstr "Connessioni riuscite" +msgstr "Ultima connessione riuscita" -#: windows/views.py:432 +#: windows/views.py:479 msgid "Unsuccessful connections" msgstr "Connessioni non riuscite" -#: windows/views.py:449 +#: windows/views.py:496 msgid "Last failure reason" msgstr "" -#: windows/views.py:466 -#, fuzzy +#: windows/views.py:513 msgid "Total connection attempts" -msgstr "Ultima connessione" +msgstr "Totale connessioni" -#: windows/views.py:483 -#, fuzzy -msgid " Average connection time (ms)" -msgstr "Riconnessione fallita:" +#: windows/views.py:530 +msgid "Average connection time (ms)" +msgstr "Media in ms del tempo di connessione" -#: windows/views.py:500 -#, fuzzy -msgid " Most recent connection duration" -msgstr "Apri gestore connessioni" +#: windows/views.py:547 +msgid "Most recent connection duration" +msgstr "" -#: windows/views.py:519 +#: windows/views.py:566 msgid "Statistics" msgstr "Statistiche" -#: windows/views.py:537 windows/views.py:1672 +#: windows/views.py:584 windows/views.py:1719 msgid "Create" msgstr "Crea" -#: windows/views.py:541 -#, fuzzy +#: windows/views.py:588 msgid "Create connection" -msgstr "Ultima connessione" +msgstr "Nuova connessione" -#: windows/views.py:544 -#, fuzzy +#: windows/views.py:591 msgid "Create directory" msgstr "Nuova directory" -#: windows/views.py:573 windows/views.py:797 windows/views.py:1328 -#: windows/views.py:1741 windows/views.py:2002 windows/views.py:2078 -#: windows/views.py:3081 windows/views.py:3308 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1375 +#: windows/views.py:1788 windows/views.py:2049 windows/views.py:2152 +#: windows/views.py:3162 windows/views.py:3389 msgid "Cancel" msgstr "Annulla" -#: windows/views.py:578 windows/views.py:2007 windows/views.py:3091 -#: windows/views.py:3313 +#: windows/views.py:625 windows/views.py:2054 windows/views.py:3172 +#: windows/views.py:3394 msgid "Save" msgstr "Salva" -#: windows/views.py:585 +#: windows/views.py:632 msgid "Test" msgstr "Testa" -#: windows/views.py:592 +#: windows/views.py:639 msgid "Connect" msgstr "Connetti" -#: windows/views.py:695 +#: windows/views.py:742 msgid "Language" msgstr "Lingua" -#: windows/views.py:700 +#: windows/views.py:747 msgid "English" msgstr "Inglese" -#: windows/views.py:700 +#: windows/views.py:747 msgid "Italian" msgstr "Italiano" -#: windows/views.py:700 +#: windows/views.py:747 msgid "French" msgstr "Francese" -#: windows/views.py:712 +#: windows/views.py:759 msgid "Locale" msgstr "Localizzazione" -#: windows/views.py:733 +#: windows/views.py:780 msgid "Edit Value" msgstr "Modifica valore" -#: windows/views.py:743 +#: windows/views.py:790 msgid "Syntax" msgstr "Sintassi" -#: windows/views.py:800 +#: windows/views.py:847 msgid "Ok" msgstr "Ok" -#: windows/views.py:831 +#: windows/views.py:878 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:840 +#: windows/views.py:887 msgid "File" msgstr "File" -#: windows/views.py:843 +#: windows/views.py:890 msgid "About" msgstr "Informazioni" -#: windows/views.py:846 +#: windows/views.py:893 msgid "Help" msgstr "Aiuto" -#: windows/views.py:851 +#: windows/views.py:898 msgid "Open connection manager" msgstr "Apri gestore connessioni" -#: windows/views.py:853 +#: windows/views.py:900 msgid "Disconnect from server" msgstr "Disconnetti dal server" -#: windows/views.py:857 +#: windows/views.py:904 msgid "tool" msgstr "strumento" -#: windows/views.py:857 +#: windows/views.py:904 msgid "Refresh" msgstr "Aggiorna" -#: windows/views.py:861 windows/views.py:863 +#: windows/views.py:908 windows/views.py:910 msgid "Add" msgstr "Aggiungi" -#: windows/views.py:897 windows/views.py:901 windows/views.py:2166 -#: windows/views.py:2654 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2232 +#: windows/views.py:2735 msgid "MyMenuItem" msgstr "IlMioElementoMenu" -#: windows/views.py:904 windows/views.py:1769 windows/views.py:3336 +#: windows/views.py:951 windows/views.py:1816 windows/views.py:3417 msgid "MyMenu" msgstr "IlMioMenu" -#: windows/views.py:919 windows/views.py:1350 windows/views.py:1357 -#: windows/views.py:1364 +#: windows/views.py:966 windows/views.py:1397 windows/views.py:1404 +#: windows/views.py:1411 msgid "MyLabel" msgstr "LaMiaEtichetta" -#: windows/views.py:925 +#: windows/views.py:972 msgid "Databases" msgstr "Database" -#: windows/views.py:926 windows/views.py:1308 windows/views.py:2716 -#: windows/views.py:2744 +#: windows/views.py:973 windows/views.py:1355 windows/views.py:2797 +#: windows/views.py:2825 msgid "Size" msgstr "Dimensione" -#: windows/views.py:927 +#: windows/views.py:974 msgid "Elements" msgstr "Elementi" -#: windows/views.py:928 +#: windows/views.py:975 msgid "Modified at" msgstr "Modificato il" -#: windows/views.py:929 +#: windows/views.py:976 msgid "Tables" msgstr "Tabelle" -#: windows/views.py:936 +#: windows/views.py:983 msgid "System" msgstr "Sistema" -#: windows/views.py:979 +#: windows/views.py:1026 #, fuzzy msgid "Character set" msgstr "Creato il" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1000 -#: windows/views.py:1312 windows/views.py:2980 +#: windows/components/dataview.py:89 windows/views.py:1047 +#: windows/views.py:1359 windows/views.py:3061 msgid "Collation" msgstr "Ordinamento" -#: windows/views.py:1026 windows/views.py:2862 +#: windows/views.py:1073 windows/views.py:2943 msgid "Encryption" msgstr "" -#: windows/views.py:1038 +#: windows/views.py:1085 msgid "Read Only" msgstr "" -#: windows/views.py:1055 +#: windows/views.py:1102 #, fuzzy msgid "Tablespace" msgstr "Tabelle" -#: windows/views.py:1076 +#: windows/views.py:1123 #, fuzzy msgid "Connection limit" msgstr "Connessione persa" -#: windows/views.py:1119 +#: windows/views.py:1166 #, fuzzy msgid "Profile" msgstr "File" -#: windows/views.py:1145 +#: windows/views.py:1192 #, fuzzy msgid "Default tablespace" msgstr "Elimina tabella" -#: windows/views.py:1166 +#: windows/views.py:1213 #, fuzzy msgid "Temporary tablespace" msgstr "Temporaneo" -#: windows/views.py:1192 +#: windows/views.py:1239 msgid "Quota" msgstr "" -#: windows/views.py:1211 +#: windows/views.py:1258 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1228 +#: windows/views.py:1275 msgid "Account status" msgstr "" -#: windows/views.py:1249 +#: windows/views.py:1296 #, fuzzy msgid "Password expire" msgstr "Password" -#: windows/views.py:1270 +#: windows/views.py:1317 msgid "Table:" msgstr "Tabella:" -#: windows/views.py:1278 windows/views.py:1551 windows/views.py:1595 -#: windows/views.py:1701 windows/views.py:3268 +#: windows/views.py:1325 windows/views.py:1598 windows/views.py:1642 +#: windows/views.py:1748 windows/views.py:3349 msgid "Insert" msgstr "Inserisci" -#: windows/views.py:1283 +#: windows/views.py:1330 msgid "Clone" msgstr "Clona" -#: windows/views.py:1307 +#: windows/views.py:1354 msgid "Rows" msgstr "Righe" -#: windows/views.py:1310 windows/views.py:2746 +#: windows/views.py:1357 windows/views.py:2827 msgid "Updated at" msgstr "Aggiornato il" -#: windows/views.py:1334 windows/views.py:1746 windows/views.py:2085 -#: windows/views.py:2140 +#: windows/views.py:1381 windows/views.py:1793 windows/views.py:2159 +#: windows/views.py:2206 msgid "Apply" msgstr "Applica" -#: windows/views.py:1344 windows/views.py:1503 windows/views.py:1956 -#: windows/views.py:3225 +#: windows/views.py:1391 windows/views.py:1550 windows/views.py:2003 +#: windows/views.py:3306 msgid "Options" msgstr "Opzioni" -#: windows/views.py:1375 +#: windows/views.py:1422 msgid "Diagram" msgstr "Diagramma" -#: windows/views.py:1386 windows/views.py:2715 +#: windows/views.py:1433 windows/views.py:2796 msgid "Database" msgstr "Database" -#: windows/views.py:1441 windows/views.py:3165 +#: windows/views.py:1488 windows/views.py:3246 msgid "Base" msgstr "Base" -#: windows/views.py:1455 windows/views.py:3179 +#: windows/views.py:1502 windows/views.py:3260 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1483 windows/views.py:3207 +#: windows/views.py:1530 windows/views.py:3288 msgid "Default Collation" msgstr "Ordinamento predefinito" -#: windows/views.py:1515 windows/views.py:1556 windows/views.py:1600 +#: windows/views.py:1562 windows/views.py:1603 windows/views.py:1647 msgid "Remove" msgstr "Rimuovi" -#: windows/views.py:1522 windows/views.py:1563 windows/views.py:1607 +#: windows/views.py:1569 windows/views.py:1610 windows/views.py:1654 msgid "Clear" msgstr "Pulisci" -#: windows/views.py:1537 windows/views.py:3239 +#: windows/views.py:1584 windows/views.py:3320 msgid "Indexes" msgstr "Indici" -#: windows/views.py:1581 +#: windows/views.py:1628 msgid "Foreign Keys" msgstr "Chiavi esterne" -#: windows/views.py:1625 +#: windows/views.py:1672 msgid "Checks" msgstr "Vincoli" -#: windows/views.py:1693 windows/views.py:3260 +#: windows/views.py:1740 windows/views.py:3341 msgid "Columns:" msgstr "Colonne:" -#: windows/views.py:1713 windows/views.py:3280 +#: windows/views.py:1760 windows/views.py:3361 msgid "Up" msgstr "Su" -#: windows/views.py:1720 windows/views.py:3287 +#: windows/views.py:1767 windows/views.py:3368 msgid "Down" msgstr "Giù" -#: windows/views.py:1759 windows/views.py:1766 windows/views.py:3326 -#: windows/views.py:3333 +#: windows/views.py:1806 windows/views.py:1813 windows/views.py:3407 +#: windows/views.py:3414 msgid "Add Index" msgstr "Aggiungi indice" -#: windows/views.py:1763 windows/views.py:3330 +#: windows/views.py:1810 windows/views.py:3411 msgid "Add PrimaryKey" msgstr "Aggiungi chiave primaria" -#: windows/views.py:1780 +#: windows/views.py:1827 msgid "Table" msgstr "Tabella" -#: windows/views.py:1816 +#: windows/views.py:1863 #, fuzzy msgid "Definer" msgstr "Inserisci" -#: windows/views.py:1836 +#: windows/views.py:1883 msgid "Schema" msgstr "" -#: windows/views.py:1862 +#: windows/views.py:1909 msgid "SQL security" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1916 #, fuzzy msgid "DEFINER" msgstr "Inserisci" -#: windows/views.py:1869 +#: windows/views.py:1916 #, fuzzy msgid "INVOKER" msgstr "Inserisci" -#: windows/views.py:1881 windows/views.py:2558 windows/views.py:2577 -#: windows/views.py:2820 +#: windows/views.py:1928 windows/views.py:2639 windows/views.py:2658 +#: windows/views.py:2901 msgid "Algorithm" msgstr "" -#: windows/views.py:1883 windows/views.py:2543 windows/views.py:2576 -#: windows/views.py:2825 +#: windows/views.py:1930 windows/views.py:2624 windows/views.py:2657 +#: windows/views.py:2906 #, fuzzy msgid "UNDEFINED" msgstr "Senza segno" -#: windows/views.py:1886 windows/views.py:2546 windows/views.py:2576 -#: windows/views.py:2828 +#: windows/views.py:1933 windows/views.py:2627 windows/views.py:2657 +#: windows/views.py:2909 msgid "MERGE" msgstr "" -#: windows/views.py:1889 windows/views.py:2555 windows/views.py:2576 -#: windows/views.py:2831 +#: windows/views.py:1936 windows/views.py:2636 windows/views.py:2657 +#: windows/views.py:2912 #, fuzzy msgid "TEMPTABLE" msgstr "Tabella" -#: windows/views.py:1899 windows/views.py:2582 +#: windows/views.py:1946 windows/views.py:2663 msgid "View constraint" msgstr "" -#: windows/views.py:1901 windows/views.py:2581 +#: windows/views.py:1948 windows/views.py:2662 #, fuzzy msgid "None" msgstr "Clona" -#: windows/views.py:1904 windows/views.py:2581 +#: windows/views.py:1951 windows/views.py:2662 #, fuzzy msgid "LOCAL" msgstr "Localizzazione" -#: windows/views.py:1907 +#: windows/views.py:1954 #, fuzzy msgid "CASCADE" msgstr "Annulla" -#: windows/views.py:1910 +#: windows/views.py:1957 #, fuzzy msgid "CHECK ONLY" msgstr "Verifica" -#: windows/views.py:1913 windows/views.py:2581 +#: windows/views.py:1960 windows/views.py:2662 msgid "READ ONLY" msgstr "" -#: windows/views.py:1925 +#: windows/views.py:1972 msgid "Force" msgstr "" -#: windows/views.py:1937 +#: windows/views.py:1984 msgid "Security barrier" msgstr "" -#: windows/views.py:2019 +#: windows/views.py:2066 msgid "Views" msgstr "Viste" -#: windows/views.py:2027 +#: windows/views.py:2074 msgid "Triggers" msgstr "Trigger" -#: windows/views.py:2039 -#, python-format -msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" -msgstr "Tabella `%(database_name)s`.`%(table_name)s`: %(total_rows) righe totali" +#: windows/views.py:2086 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" + +#: windows/views.py:2094 +#, fuzzy +msgid "First" +msgstr "Filtri" -#: windows/views.py:2049 +#: windows/views.py:2112 +msgid "Last" +msgstr "" + +#: windows/views.py:2123 msgid "Insert record" msgstr "Inserisci record" -#: windows/views.py:2054 +#: windows/views.py:2128 msgid "Duplicate record" msgstr "Duplica record" -#: windows/views.py:2061 +#: windows/views.py:2135 msgid "Delete record" msgstr "Elimina record" -#: windows/views.py:2071 +#: windows/views.py:2145 msgid "Apply changes automatically" msgstr "Applica modifiche automaticamente" -#: windows/views.py:2073 windows/views.py:2074 +#: windows/views.py:2147 windows/views.py:2148 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -647,76 +704,72 @@ msgstr "" "Se abilitato, le modifiche alla tabella vengono applicate immediatamente " "senza premere Applica o Annulla" -#: windows/views.py:2095 -msgid "Next" -msgstr "Avanti" - -#: windows/views.py:2103 +#: windows/views.py:2169 msgid "Filters" msgstr "Filtri" -#: windows/views.py:2143 +#: windows/views.py:2209 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2163 +#: windows/views.py:2229 msgid "Insert row" msgstr "Inserisci riga" -#: windows/views.py:2171 +#: windows/views.py:2237 msgid "Data" msgstr "Dati" -#: windows/views.py:2225 windows/views.py:2275 +#: windows/views.py:2291 windows/views.py:2341 msgid "New" msgstr "Nuovo" -#: windows/views.py:2252 +#: windows/views.py:2318 msgid "Query" msgstr "Query" -#: windows/views.py:2272 +#: windows/views.py:2338 msgid "Close" msgstr "Chiudi" -#: windows/views.py:2285 +#: windows/views.py:2351 msgid "Query #2" msgstr "Query #2" -#: windows/views.py:2537 +#: windows/views.py:2618 msgid "Column5" msgstr "Colonna5" -#: windows/views.py:2548 +#: windows/views.py:2629 msgid "Import" msgstr "Importa" -#: windows/views.py:2573 +#: windows/views.py:2654 msgid "Read only" msgstr "" -#: windows/views.py:2581 +#: windows/views.py:2662 msgid "CASCADED" msgstr "" -#: windows/views.py:2581 +#: windows/views.py:2662 #, fuzzy msgid "CHECK OPTION" msgstr "connessione" -#: windows/views.py:2613 +#: windows/views.py:2694 msgid "collapsible" msgstr "collassabile" -#: windows/views.py:2629 +#: windows/views.py:2710 msgid "Column3" msgstr "Colonna3" -#: windows/views.py:2630 +#: windows/views.py:2711 msgid "Column4" msgstr "Colonna4" -#: windows/views.py:2673 +#: windows/views.py:2754 msgid "" "Database " "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" @@ -724,78 +777,78 @@ msgstr "" "Database " "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" -#: windows/views.py:2685 +#: windows/views.py:2766 msgid "Port" msgstr "Porta" -#: windows/views.py:2708 +#: windows/views.py:2789 msgid "Usage" msgstr "Utilizzo" -#: windows/views.py:2719 +#: windows/views.py:2800 #, python-format msgid "%(total_rows)s" msgstr "%(total_rows)s" -#: windows/views.py:2724 +#: windows/views.py:2805 msgid "rows total" msgstr "righe totali" -#: windows/views.py:2743 +#: windows/views.py:2824 msgid "Lines" msgstr "Righe" -#: windows/views.py:2775 +#: windows/views.py:2856 msgid "Temporary" msgstr "Temporaneo" -#: windows/views.py:2786 +#: windows/views.py:2867 #, fuzzy msgid "Engine options" msgstr "Opzioni" -#: windows/views.py:2845 +#: windows/views.py:2926 msgid "RadioBtn" msgstr "" -#: windows/views.py:2928 +#: windows/views.py:3009 msgid "Edit Column" msgstr "Modifica colonna" -#: windows/views.py:2944 +#: windows/views.py:3025 msgid "Datatype" msgstr "Tipo di dati" -#: windows/components/dataview.py:121 windows/views.py:2959 +#: windows/components/dataview.py:121 windows/views.py:3040 msgid "Length/Set" msgstr "Lunghezza/Insieme" -#: windows/components/dataview.py:51 windows/views.py:2998 +#: windows/components/dataview.py:51 windows/views.py:3079 msgid "Unsigned" msgstr "Senza segno" #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3004 +#: windows/components/dataview.py:75 windows/views.py:3085 msgid "Allow NULL" msgstr "Consenti NULL" -#: windows/views.py:3010 +#: windows/views.py:3091 msgid "Zero Fill" msgstr "Riempimento zero" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3021 +#: windows/components/dataview.py:78 windows/views.py:3102 msgid "Default" msgstr "Predefinito" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3047 +#: windows/components/dataview.py:82 windows/views.py:3128 msgid "Virtuality" msgstr "Virtualità" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3062 +#: windows/views.py:3143 msgid "Expression" msgstr "Espressione" @@ -867,11 +920,11 @@ msgstr "Su AGGIORNAMENTO" msgid "On DELETE" msgstr "Su ELIMINA" -#: windows/components/dataview.py:299 +#: windows/components/dataview.py:298 msgid "Add foreign key" msgstr "Aggiungi chiave esterna" -#: windows/components/dataview.py:305 +#: windows/components/dataview.py:304 msgid "Remove foreign key" msgstr "Rimuovi chiave esterna" @@ -891,75 +944,111 @@ msgstr "AUTO INCREMENTO" msgid "Text/Expression" msgstr "Testo/Espressione" -#: windows/dialogs/connections/view.py:119 windows/main/tabs/query.py:376 +#: windows/dialogs/connections/view.py:122 windows/main/tabs/query.py:387 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:394 +#: windows/dialogs/connections/view.py:401 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:407 +#: windows/dialogs/connections/view.py:413 +#, python-brace-format +msgid "Do you want save the connection {connection_name}?" +msgstr "" + +#: windows/dialogs/connections/view.py:416 msgid "Confirm save" msgstr "Conferma salvataggio" -#: windows/dialogs/connections/view.py:459 +#: windows/dialogs/connections/view.py:468 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:461 +#: windows/dialogs/connections/view.py:470 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:736 +#: windows/dialogs/connections/view.py:737 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" #: windows/dialogs/connections/view.py:762 +#, fuzzy, python-brace-format +msgid "" +"Connection error:\n" +"{error}" +msgstr "Errore di connessione" + +#: windows/dialogs/connections/view.py:763 msgid "Connection error" msgstr "Errore di connessione" -#: windows/dialogs/connections/view.py:788 -#: windows/dialogs/connections/view.py:803 +#: windows/dialogs/connections/view.py:789 +#, fuzzy, python-brace-format +msgid "Do you want to delete the connection '{connection_name}'?" +msgstr "Vuoi eliminare i record?" + +#: windows/dialogs/connections/view.py:792 +#: windows/dialogs/connections/view.py:809 msgid "Confirm delete" msgstr "Conferma eliminazione" -#: windows/main/controller.py:172 +#: windows/dialogs/connections/view.py:806 +#, fuzzy, python-brace-format +msgid "Do you want to delete the directory '{directory_name}'?" +msgstr "Vuoi eliminare i record?" + +#: windows/main/controller.py:189 msgid "days" msgstr "giorni" -#: windows/main/controller.py:173 +#: windows/main/controller.py:190 msgid "hours" msgstr "ore" -#: windows/main/controller.py:174 +#: windows/main/controller.py:191 msgid "minutes" msgstr "minuti" -#: windows/main/controller.py:175 +#: windows/main/controller.py:192 msgid "seconds" msgstr "secondi" -#: windows/main/controller.py:183 +#: windows/main/controller.py:200 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizzata: {used} ({percentage:.2%})" -#: windows/main/controller.py:219 +#: windows/main/controller.py:236 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:298 +#: windows/main/controller.py:441 +#, python-brace-format +msgid "~{estimated} (Loading...)" +msgstr "" + +#: windows/main/controller.py:443 +msgid "~ (Loading...)" +msgstr "" + +#: windows/main/controller.py:608 msgid "Version" msgstr "Versione" -#: windows/main/controller.py:300 +#: windows/main/controller.py:610 msgid "Uptime" msgstr "Tempo di attività" -#: windows/main/controller.py:399 +#: windows/main/controller.py:678 +#, python-brace-format +msgid "Do you want discard the change to {database_name}?" +msgstr "" + +#: windows/main/controller.py:711 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -969,37 +1058,52 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:404 windows/main/controller.py:425 +#: windows/main/controller.py:716 windows/main/controller.py:737 #, fuzzy msgid "Delete database" msgstr "Elimina tabella" -#: windows/main/controller.py:410 +#: windows/main/controller.py:722 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:411 +#: windows/main/controller.py:723 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:424 +#: windows/main/controller.py:736 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:439 +#: windows/main/controller.py:751 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:440 windows/main/tabs/view.py:253 +#: windows/main/controller.py:752 windows/main/tabs/view.py:253 #: windows/main/tabs/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:582 +#: windows/main/controller.py:871 +#, python-brace-format +msgid "Do you want discard the change to {table_name}?" +msgstr "" + +#: windows/main/controller.py:897 +#, fuzzy, python-brace-format +msgid "Do you want delete the table {table_name}?" +msgstr "Vuoi eliminare i record?" + +#: windows/main/controller.py:900 msgid "Delete table" msgstr "Elimina tabella" -#: windows/main/controller.py:699 +#: windows/main/controller.py:919 +#, python-brace-format +msgid "{table_name} (COPY)" +msgstr "" + +#: windows/main/controller.py:1017 msgid "Do you want delete the records?" msgstr "Vuoi eliminare i record?" @@ -1019,52 +1123,52 @@ msgstr "Connessione persa" msgid "Reconnection failed:" msgstr "Riconnessione fallita:" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:450 +#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:489 #: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 msgid "Error" msgstr "Errore" -#: windows/main/tabs/query.py:305 +#: windows/main/tabs/query.py:308 #, python-brace-format -msgid "{} rows affected" +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/query.py:309 windows/main/tabs/query.py:331 -#, fuzzy, python-brace-format -msgid "Query {}" -msgstr "Query" +#: windows/main/tabs/query.py:315 windows/main/tabs/query.py:339 +#, python-brace-format +msgid "Query {query_number}" +msgstr "" -#: windows/main/tabs/query.py:314 +#: windows/main/tabs/query.py:320 #, python-brace-format -msgid "Query {} (Error)" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/query.py:326 +#: windows/main/tabs/query.py:334 #, python-brace-format -msgid "Query {} ({} rows × {} cols)" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/query.py:353 -#, fuzzy, python-brace-format -msgid "{} rows" -msgstr "Righe" +#: windows/main/tabs/query.py:360 +#, python-brace-format +msgid "{rows_count} rows" +msgstr "" -#: windows/main/tabs/query.py:355 +#: windows/main/tabs/query.py:362 #, python-brace-format -msgid "{:.1f} ms" +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/query.py:358 +#: windows/main/tabs/query.py:366 #, python-brace-format -msgid "{} warnings" +msgid "{warnings_count} warnings" msgstr "" -#: windows/main/tabs/query.py:370 +#: windows/main/tabs/query.py:381 #, fuzzy msgid "Error:" msgstr "Errore" -#: windows/main/tabs/query.py:449 +#: windows/main/tabs/query.py:488 #, fuzzy msgid "No active database connection" msgstr "Nuova connessione" @@ -1131,3 +1235,32 @@ msgstr "" #~ msgid "directory" #~ msgstr "directory" +#~ msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#~ msgstr "" +#~ "Tabella `%(database_name)s`.`%(table_name)s`: " +#~ "%(total_rows) righe totali" + +#~ msgid "Next" +#~ msgstr "Avanti" + +#~ msgid "{} rows affected" +#~ msgstr "" + +#~ msgid "Query {}" +#~ msgstr "Query" + +#~ msgid "Query {} (Error)" +#~ msgstr "" + +#~ msgid "Query {} ({} rows × {} cols)" +#~ msgstr "" + +#~ msgid "{} rows" +#~ msgstr "Righe" + +#~ msgid "{:.1f} ms" +#~ msgstr "" + +#~ msgid "{} warnings" +#~ msgstr "" + diff --git a/locale/petersql.pot b/locale/petersql.pot index da6114d..f6b6181 100644 --- a/locale/petersql.pot +++ b/locale/petersql.pot @@ -23,38 +23,83 @@ msgctxt "unit" msgid "TB" msgstr "" -#: structures/ssh_tunnel.py:166 +#: structures/ssh_tunnel.py:177 msgid "OpenSSH client not found." msgstr "" -#: windows/dialogs/connections/view.py:395 -#: windows/dialogs/connections/view.py:738 windows/main/controller.py:296 +#: structures/engines/mariadb/context.py:592 +#: structures/engines/mysql/context.py:563 +#: structures/engines/postgresql/context.py:579 +#: structures/engines/sqlite/context.py:518 +#, python-brace-format +msgid "Table{table_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:620 +#: structures/engines/mysql/context.py:591 +#: structures/engines/postgresql/context.py:604 +#: structures/engines/sqlite/context.py:542 +#, python-brace-format +msgid "Column{column_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:638 +#: structures/engines/mysql/context.py:609 +#: structures/engines/postgresql/context.py:622 +#: structures/engines/sqlite/context.py:560 +#, python-brace-format +msgid "Index{index_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:678 +#: structures/engines/mysql/context.py:649 +#: structures/engines/postgresql/context.py:662 +#: structures/engines/sqlite/context.py:602 +#, python-brace-format +msgid "ForeignKey{foreign_key_number:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:711 +#: structures/engines/mysql/context.py:680 +#: structures/engines/postgresql/context.py:692 +#: structures/engines/sqlite/context.py:630 +#, python-brace-format +msgid "View{view_index:03}" +msgstr "" + +#: structures/engines/mariadb/context.py:762 +#, python-brace-format +msgid "Trigger{trigger_index:03}" +msgstr "" + +#: windows/dialogs/connections/view.py:402 +#: windows/dialogs/connections/view.py:739 windows/main/controller.py:606 #: windows/views.py:33 msgid "Connection" msgstr "" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:956 -#: windows/views.py:1306 windows/views.py:1413 windows/views.py:1796 -#: windows/views.py:2707 windows/views.py:2730 windows/views.py:2731 -#: windows/views.py:2732 windows/views.py:2733 windows/views.py:2734 -#: windows/views.py:2735 windows/views.py:2736 windows/views.py:2737 -#: windows/views.py:2738 windows/views.py:2742 windows/views.py:2936 -#: windows/views.py:3137 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1003 +#: windows/views.py:1353 windows/views.py:1460 windows/views.py:1843 +#: windows/views.py:2788 windows/views.py:2811 windows/views.py:2812 +#: windows/views.py:2813 windows/views.py:2814 windows/views.py:2815 +#: windows/views.py:2816 windows/views.py:2817 windows/views.py:2818 +#: windows/views.py:2819 windows/views.py:2823 windows/views.py:3017 +#: windows/views.py:3218 msgid "Name" msgstr "" -#: windows/views.py:48 windows/views.py:381 +#: windows/views.py:48 windows/views.py:428 msgid "Last connection" msgstr "" -#: windows/dialogs/connections/view.py:631 windows/views.py:61 +#: windows/dialogs/connections/view.py:640 windows/views.py:61 msgid "New directory" msgstr "" -#: windows/dialogs/connections/model.py:187 -#: windows/dialogs/connections/view.py:591 windows/views.py:65 +#: windows/dialogs/connections/model.py:206 +#: windows/dialogs/connections/view.py:600 windows/views.py:65 msgid "New connection" msgstr "" @@ -66,14 +111,14 @@ msgstr "" msgid "Clone connection" msgstr "" -#: windows/views.py:81 windows/views.py:556 windows/views.py:1290 -#: windows/views.py:1331 windows/views.py:1706 windows/views.py:1738 -#: windows/views.py:1997 windows/views.py:3273 windows/views.py:3305 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1337 +#: windows/views.py:1378 windows/views.py:1753 windows/views.py:1785 +#: windows/views.py:2044 windows/views.py:3354 windows/views.py:3386 msgid "Delete" msgstr "" -#: windows/views.py:111 windows/views.py:1311 windows/views.py:1468 -#: windows/views.py:2747 windows/views.py:3192 +#: windows/views.py:111 windows/views.py:1358 windows/views.py:1515 +#: windows/views.py:2828 windows/views.py:3273 msgid "Engine" msgstr "" @@ -85,666 +130,682 @@ msgstr "" msgid "Username" msgstr "" -#: windows/views.py:161 windows/views.py:1100 +#: windows/views.py:161 windows/views.py:1147 msgid "Password" msgstr "" -#: windows/views.py:177 +#: windows/views.py:174 +msgid "Connection timeout" +msgstr "" + +#: windows/views.py:192 msgid "Use TLS" msgstr "" -#: windows/views.py:180 +#: windows/views.py:203 msgid "Use SSH tunnel" msgstr "" -#: windows/views.py:202 windows/views.py:2668 +#: windows/views.py:214 +msgid "Compressed client/server protocol" +msgstr "" + +#: windows/views.py:233 windows/views.py:2749 msgid "Filename" msgstr "" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2673 -#: windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:2754 +#: windows/views.py:2937 msgid "Select a file" msgstr "" -#: windows/views.py:207 windows/views.py:324 windows/views.py:2856 +#: windows/views.py:238 windows/views.py:358 windows/views.py:2937 msgid "*.*" msgstr "" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:221 windows/views.py:1313 windows/views.py:1426 -#: windows/views.py:2748 windows/views.py:3034 windows/views.py:3150 +#: windows/views.py:255 windows/views.py:1360 windows/views.py:1473 +#: windows/views.py:2829 windows/views.py:3115 windows/views.py:3231 msgid "Comments" msgstr "" -#: windows/main/controller.py:219 windows/views.py:235 windows/views.py:683 -#: windows/views.py:837 +#: windows/main/controller.py:236 windows/views.py:269 windows/views.py:730 +#: windows/views.py:884 msgid "Settings" msgstr "" -#: windows/views.py:244 +#: windows/views.py:278 msgid "SSH executable" msgstr "" -#: windows/views.py:249 +#: windows/views.py:283 msgid "ssh" msgstr "" -#: windows/views.py:257 +#: windows/views.py:291 msgid "SSH host + port" msgstr "" -#: windows/views.py:269 +#: windows/views.py:303 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "" -#: windows/views.py:278 +#: windows/views.py:312 msgid "SSH username" msgstr "" -#: windows/views.py:291 +#: windows/views.py:325 msgid "SSH password" msgstr "" -#: windows/views.py:304 +#: windows/views.py:338 msgid "Local port" msgstr "" -#: windows/views.py:310 +#: windows/views.py:344 msgid "if the value is set to 0, the first available port will be used" msgstr "" -#: windows/views.py:319 +#: windows/views.py:353 msgid "Identity file" msgstr "" -#: windows/views.py:335 +#: windows/views.py:369 msgid "Remote host + port" msgstr "" -#: windows/views.py:347 +#: windows/views.py:381 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "" -#: windows/views.py:358 +#: windows/views.py:390 +msgid "SSH extra args" +msgstr "" + +#: windows/views.py:405 msgid "SSH Tunnel" msgstr "" -#: windows/views.py:364 windows/views.py:1309 windows/views.py:2745 +#: windows/views.py:411 windows/views.py:1356 windows/views.py:2826 msgid "Created at" msgstr "" -#: windows/views.py:398 +#: windows/views.py:445 msgid "Successful connections" msgstr "" -#: windows/views.py:415 +#: windows/views.py:462 msgid "Last successful connection" msgstr "" -#: windows/views.py:432 +#: windows/views.py:479 msgid "Unsuccessful connections" msgstr "" -#: windows/views.py:449 +#: windows/views.py:496 msgid "Last failure reason" msgstr "" -#: windows/views.py:466 +#: windows/views.py:513 msgid "Total connection attempts" msgstr "" -#: windows/views.py:483 -msgid " Average connection time (ms)" +#: windows/views.py:530 +msgid "Average connection time (ms)" msgstr "" -#: windows/views.py:500 -msgid " Most recent connection duration" +#: windows/views.py:547 +msgid "Most recent connection duration" msgstr "" -#: windows/views.py:519 +#: windows/views.py:566 msgid "Statistics" msgstr "" -#: windows/views.py:537 windows/views.py:1672 +#: windows/views.py:584 windows/views.py:1719 msgid "Create" msgstr "" -#: windows/views.py:541 +#: windows/views.py:588 msgid "Create connection" msgstr "" -#: windows/views.py:544 +#: windows/views.py:591 msgid "Create directory" msgstr "" -#: windows/views.py:573 windows/views.py:797 windows/views.py:1328 -#: windows/views.py:1741 windows/views.py:2002 windows/views.py:2078 -#: windows/views.py:3081 windows/views.py:3308 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1375 +#: windows/views.py:1788 windows/views.py:2049 windows/views.py:2152 +#: windows/views.py:3162 windows/views.py:3389 msgid "Cancel" msgstr "" -#: windows/views.py:578 windows/views.py:2007 windows/views.py:3091 -#: windows/views.py:3313 +#: windows/views.py:625 windows/views.py:2054 windows/views.py:3172 +#: windows/views.py:3394 msgid "Save" msgstr "" -#: windows/views.py:585 +#: windows/views.py:632 msgid "Test" msgstr "" -#: windows/views.py:592 +#: windows/views.py:639 msgid "Connect" msgstr "" -#: windows/views.py:695 +#: windows/views.py:742 msgid "Language" msgstr "" -#: windows/views.py:700 +#: windows/views.py:747 msgid "English" msgstr "" -#: windows/views.py:700 +#: windows/views.py:747 msgid "Italian" msgstr "" -#: windows/views.py:700 +#: windows/views.py:747 msgid "French" msgstr "" -#: windows/views.py:712 +#: windows/views.py:759 msgid "Locale" msgstr "" -#: windows/views.py:733 +#: windows/views.py:780 msgid "Edit Value" msgstr "" -#: windows/views.py:743 +#: windows/views.py:790 msgid "Syntax" msgstr "" -#: windows/views.py:800 +#: windows/views.py:847 msgid "Ok" msgstr "" -#: windows/views.py:831 +#: windows/views.py:878 msgid "PeterSQL" msgstr "" -#: windows/views.py:840 +#: windows/views.py:887 msgid "File" msgstr "" -#: windows/views.py:843 +#: windows/views.py:890 msgid "About" msgstr "" -#: windows/views.py:846 +#: windows/views.py:893 msgid "Help" msgstr "" -#: windows/views.py:851 +#: windows/views.py:898 msgid "Open connection manager" msgstr "" -#: windows/views.py:853 +#: windows/views.py:900 msgid "Disconnect from server" msgstr "" -#: windows/views.py:857 +#: windows/views.py:904 msgid "tool" msgstr "" -#: windows/views.py:857 +#: windows/views.py:904 msgid "Refresh" msgstr "" -#: windows/views.py:861 windows/views.py:863 +#: windows/views.py:908 windows/views.py:910 msgid "Add" msgstr "" -#: windows/views.py:897 windows/views.py:901 windows/views.py:2166 -#: windows/views.py:2654 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2232 +#: windows/views.py:2735 msgid "MyMenuItem" msgstr "" -#: windows/views.py:904 windows/views.py:1769 windows/views.py:3336 +#: windows/views.py:951 windows/views.py:1816 windows/views.py:3417 msgid "MyMenu" msgstr "" -#: windows/views.py:919 windows/views.py:1350 windows/views.py:1357 -#: windows/views.py:1364 +#: windows/views.py:966 windows/views.py:1397 windows/views.py:1404 +#: windows/views.py:1411 msgid "MyLabel" msgstr "" -#: windows/views.py:925 +#: windows/views.py:972 msgid "Databases" msgstr "" -#: windows/views.py:926 windows/views.py:1308 windows/views.py:2716 -#: windows/views.py:2744 +#: windows/views.py:973 windows/views.py:1355 windows/views.py:2797 +#: windows/views.py:2825 msgid "Size" msgstr "" -#: windows/views.py:927 +#: windows/views.py:974 msgid "Elements" msgstr "" -#: windows/views.py:928 +#: windows/views.py:975 msgid "Modified at" msgstr "" -#: windows/views.py:929 +#: windows/views.py:976 msgid "Tables" msgstr "" -#: windows/views.py:936 +#: windows/views.py:983 msgid "System" msgstr "" -#: windows/views.py:979 +#: windows/views.py:1026 msgid "Character set" msgstr "" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1000 -#: windows/views.py:1312 windows/views.py:2980 +#: windows/components/dataview.py:89 windows/views.py:1047 +#: windows/views.py:1359 windows/views.py:3061 msgid "Collation" msgstr "" -#: windows/views.py:1026 windows/views.py:2862 +#: windows/views.py:1073 windows/views.py:2943 msgid "Encryption" msgstr "" -#: windows/views.py:1038 +#: windows/views.py:1085 msgid "Read Only" msgstr "" -#: windows/views.py:1055 +#: windows/views.py:1102 msgid "Tablespace" msgstr "" -#: windows/views.py:1076 +#: windows/views.py:1123 msgid "Connection limit" msgstr "" -#: windows/views.py:1119 +#: windows/views.py:1166 msgid "Profile" msgstr "" -#: windows/views.py:1145 +#: windows/views.py:1192 msgid "Default tablespace" msgstr "" -#: windows/views.py:1166 +#: windows/views.py:1213 msgid "Temporary tablespace" msgstr "" -#: windows/views.py:1192 +#: windows/views.py:1239 msgid "Quota" msgstr "" -#: windows/views.py:1211 +#: windows/views.py:1258 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1228 +#: windows/views.py:1275 msgid "Account status" msgstr "" -#: windows/views.py:1249 +#: windows/views.py:1296 msgid "Password expire" msgstr "" -#: windows/views.py:1270 +#: windows/views.py:1317 msgid "Table:" msgstr "" -#: windows/views.py:1278 windows/views.py:1551 windows/views.py:1595 -#: windows/views.py:1701 windows/views.py:3268 +#: windows/views.py:1325 windows/views.py:1598 windows/views.py:1642 +#: windows/views.py:1748 windows/views.py:3349 msgid "Insert" msgstr "" -#: windows/views.py:1283 +#: windows/views.py:1330 msgid "Clone" msgstr "" -#: windows/views.py:1307 +#: windows/views.py:1354 msgid "Rows" msgstr "" -#: windows/views.py:1310 windows/views.py:2746 +#: windows/views.py:1357 windows/views.py:2827 msgid "Updated at" msgstr "" -#: windows/views.py:1334 windows/views.py:1746 windows/views.py:2085 -#: windows/views.py:2140 +#: windows/views.py:1381 windows/views.py:1793 windows/views.py:2159 +#: windows/views.py:2206 msgid "Apply" msgstr "" -#: windows/views.py:1344 windows/views.py:1503 windows/views.py:1956 -#: windows/views.py:3225 +#: windows/views.py:1391 windows/views.py:1550 windows/views.py:2003 +#: windows/views.py:3306 msgid "Options" msgstr "" -#: windows/views.py:1375 +#: windows/views.py:1422 msgid "Diagram" msgstr "" -#: windows/views.py:1386 windows/views.py:2715 +#: windows/views.py:1433 windows/views.py:2796 msgid "Database" msgstr "" -#: windows/views.py:1441 windows/views.py:3165 +#: windows/views.py:1488 windows/views.py:3246 msgid "Base" msgstr "" -#: windows/views.py:1455 windows/views.py:3179 +#: windows/views.py:1502 windows/views.py:3260 msgid "Auto Increment" msgstr "" -#: windows/views.py:1483 windows/views.py:3207 +#: windows/views.py:1530 windows/views.py:3288 msgid "Default Collation" msgstr "" -#: windows/views.py:1515 windows/views.py:1556 windows/views.py:1600 +#: windows/views.py:1562 windows/views.py:1603 windows/views.py:1647 msgid "Remove" msgstr "" -#: windows/views.py:1522 windows/views.py:1563 windows/views.py:1607 +#: windows/views.py:1569 windows/views.py:1610 windows/views.py:1654 msgid "Clear" msgstr "" -#: windows/views.py:1537 windows/views.py:3239 +#: windows/views.py:1584 windows/views.py:3320 msgid "Indexes" msgstr "" -#: windows/views.py:1581 +#: windows/views.py:1628 msgid "Foreign Keys" msgstr "" -#: windows/views.py:1625 +#: windows/views.py:1672 msgid "Checks" msgstr "" -#: windows/views.py:1693 windows/views.py:3260 +#: windows/views.py:1740 windows/views.py:3341 msgid "Columns:" msgstr "" -#: windows/views.py:1713 windows/views.py:3280 +#: windows/views.py:1760 windows/views.py:3361 msgid "Up" msgstr "" -#: windows/views.py:1720 windows/views.py:3287 +#: windows/views.py:1767 windows/views.py:3368 msgid "Down" msgstr "" -#: windows/views.py:1759 windows/views.py:1766 windows/views.py:3326 -#: windows/views.py:3333 +#: windows/views.py:1806 windows/views.py:1813 windows/views.py:3407 +#: windows/views.py:3414 msgid "Add Index" msgstr "" -#: windows/views.py:1763 windows/views.py:3330 +#: windows/views.py:1810 windows/views.py:3411 msgid "Add PrimaryKey" msgstr "" -#: windows/views.py:1780 +#: windows/views.py:1827 msgid "Table" msgstr "" -#: windows/views.py:1816 +#: windows/views.py:1863 msgid "Definer" msgstr "" -#: windows/views.py:1836 +#: windows/views.py:1883 msgid "Schema" msgstr "" -#: windows/views.py:1862 +#: windows/views.py:1909 msgid "SQL security" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1916 msgid "DEFINER" msgstr "" -#: windows/views.py:1869 +#: windows/views.py:1916 msgid "INVOKER" msgstr "" -#: windows/views.py:1881 windows/views.py:2558 windows/views.py:2577 -#: windows/views.py:2820 +#: windows/views.py:1928 windows/views.py:2639 windows/views.py:2658 +#: windows/views.py:2901 msgid "Algorithm" msgstr "" -#: windows/views.py:1883 windows/views.py:2543 windows/views.py:2576 -#: windows/views.py:2825 +#: windows/views.py:1930 windows/views.py:2624 windows/views.py:2657 +#: windows/views.py:2906 msgid "UNDEFINED" msgstr "" -#: windows/views.py:1886 windows/views.py:2546 windows/views.py:2576 -#: windows/views.py:2828 +#: windows/views.py:1933 windows/views.py:2627 windows/views.py:2657 +#: windows/views.py:2909 msgid "MERGE" msgstr "" -#: windows/views.py:1889 windows/views.py:2555 windows/views.py:2576 -#: windows/views.py:2831 +#: windows/views.py:1936 windows/views.py:2636 windows/views.py:2657 +#: windows/views.py:2912 msgid "TEMPTABLE" msgstr "" -#: windows/views.py:1899 windows/views.py:2582 +#: windows/views.py:1946 windows/views.py:2663 msgid "View constraint" msgstr "" -#: windows/views.py:1901 windows/views.py:2581 +#: windows/views.py:1948 windows/views.py:2662 msgid "None" msgstr "" -#: windows/views.py:1904 windows/views.py:2581 +#: windows/views.py:1951 windows/views.py:2662 msgid "LOCAL" msgstr "" -#: windows/views.py:1907 +#: windows/views.py:1954 msgid "CASCADE" msgstr "" -#: windows/views.py:1910 +#: windows/views.py:1957 msgid "CHECK ONLY" msgstr "" -#: windows/views.py:1913 windows/views.py:2581 +#: windows/views.py:1960 windows/views.py:2662 msgid "READ ONLY" msgstr "" -#: windows/views.py:1925 +#: windows/views.py:1972 msgid "Force" msgstr "" -#: windows/views.py:1937 +#: windows/views.py:1984 msgid "Security barrier" msgstr "" -#: windows/views.py:2019 +#: windows/views.py:2066 msgid "Views" msgstr "" -#: windows/views.py:2027 +#: windows/views.py:2074 msgid "Triggers" msgstr "" -#: windows/views.py:2039 -#, python-format -msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" +#: windows/views.py:2086 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" + +#: windows/views.py:2094 +msgid "First" +msgstr "" + +#: windows/views.py:2112 +msgid "Last" msgstr "" -#: windows/views.py:2049 +#: windows/views.py:2123 msgid "Insert record" msgstr "" -#: windows/views.py:2054 +#: windows/views.py:2128 msgid "Duplicate record" msgstr "" -#: windows/views.py:2061 +#: windows/views.py:2135 msgid "Delete record" msgstr "" -#: windows/views.py:2071 +#: windows/views.py:2145 msgid "Apply changes automatically" msgstr "" -#: windows/views.py:2073 windows/views.py:2074 +#: windows/views.py:2147 windows/views.py:2148 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" msgstr "" -#: windows/views.py:2095 -msgid "Next" -msgstr "" - -#: windows/views.py:2103 +#: windows/views.py:2169 msgid "Filters" msgstr "" -#: windows/views.py:2143 +#: windows/views.py:2209 msgid "CTRL+ENTER" msgstr "" -#: windows/views.py:2163 +#: windows/views.py:2229 msgid "Insert row" msgstr "" -#: windows/views.py:2171 +#: windows/views.py:2237 msgid "Data" msgstr "" -#: windows/views.py:2225 windows/views.py:2275 +#: windows/views.py:2291 windows/views.py:2341 msgid "New" msgstr "" -#: windows/views.py:2252 +#: windows/views.py:2318 msgid "Query" msgstr "" -#: windows/views.py:2272 +#: windows/views.py:2338 msgid "Close" msgstr "" -#: windows/views.py:2285 +#: windows/views.py:2351 msgid "Query #2" msgstr "" -#: windows/views.py:2537 +#: windows/views.py:2618 msgid "Column5" msgstr "" -#: windows/views.py:2548 +#: windows/views.py:2629 msgid "Import" msgstr "" -#: windows/views.py:2573 +#: windows/views.py:2654 msgid "Read only" msgstr "" -#: windows/views.py:2581 +#: windows/views.py:2662 msgid "CASCADED" msgstr "" -#: windows/views.py:2581 +#: windows/views.py:2662 msgid "CHECK OPTION" msgstr "" -#: windows/views.py:2613 +#: windows/views.py:2694 msgid "collapsible" msgstr "" -#: windows/views.py:2629 +#: windows/views.py:2710 msgid "Column3" msgstr "" -#: windows/views.py:2630 +#: windows/views.py:2711 msgid "Column4" msgstr "" -#: windows/views.py:2673 +#: windows/views.py:2754 msgid "" "Database " "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" msgstr "" -#: windows/views.py:2685 +#: windows/views.py:2766 msgid "Port" msgstr "" -#: windows/views.py:2708 +#: windows/views.py:2789 msgid "Usage" msgstr "" -#: windows/views.py:2719 +#: windows/views.py:2800 #, python-format msgid "%(total_rows)s" msgstr "" -#: windows/views.py:2724 +#: windows/views.py:2805 msgid "rows total" msgstr "" -#: windows/views.py:2743 +#: windows/views.py:2824 msgid "Lines" msgstr "" -#: windows/views.py:2775 +#: windows/views.py:2856 msgid "Temporary" msgstr "" -#: windows/views.py:2786 +#: windows/views.py:2867 msgid "Engine options" msgstr "" -#: windows/views.py:2845 +#: windows/views.py:2926 msgid "RadioBtn" msgstr "" -#: windows/views.py:2928 +#: windows/views.py:3009 msgid "Edit Column" msgstr "" -#: windows/views.py:2944 +#: windows/views.py:3025 msgid "Datatype" msgstr "" -#: windows/components/dataview.py:121 windows/views.py:2959 +#: windows/components/dataview.py:121 windows/views.py:3040 msgid "Length/Set" msgstr "" -#: windows/components/dataview.py:51 windows/views.py:2998 +#: windows/components/dataview.py:51 windows/views.py:3079 msgid "Unsigned" msgstr "" #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3004 +#: windows/components/dataview.py:75 windows/views.py:3085 msgid "Allow NULL" msgstr "" -#: windows/views.py:3010 +#: windows/views.py:3091 msgid "Zero Fill" msgstr "" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3021 +#: windows/components/dataview.py:78 windows/views.py:3102 msgid "Default" msgstr "" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3047 +#: windows/components/dataview.py:82 windows/views.py:3128 msgid "Virtuality" msgstr "" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3062 +#: windows/views.py:3143 msgid "Expression" msgstr "" @@ -816,11 +877,11 @@ msgstr "" msgid "On DELETE" msgstr "" -#: windows/components/dataview.py:299 +#: windows/components/dataview.py:298 msgid "Add foreign key" msgstr "" -#: windows/components/dataview.py:305 +#: windows/components/dataview.py:304 msgid "Remove foreign key" msgstr "" @@ -840,75 +901,111 @@ msgstr "" msgid "Text/Expression" msgstr "" -#: windows/dialogs/connections/view.py:119 windows/main/tabs/query.py:376 +#: windows/dialogs/connections/view.py:122 windows/main/tabs/query.py:387 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:394 +#: windows/dialogs/connections/view.py:401 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:407 +#: windows/dialogs/connections/view.py:413 +#, python-brace-format +msgid "Do you want save the connection {connection_name}?" +msgstr "" + +#: windows/dialogs/connections/view.py:416 msgid "Confirm save" msgstr "" -#: windows/dialogs/connections/view.py:459 +#: windows/dialogs/connections/view.py:468 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:461 +#: windows/dialogs/connections/view.py:470 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:736 +#: windows/dialogs/connections/view.py:737 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" #: windows/dialogs/connections/view.py:762 +#, python-brace-format +msgid "" +"Connection error:\n" +"{error}" +msgstr "" + +#: windows/dialogs/connections/view.py:763 msgid "Connection error" msgstr "" -#: windows/dialogs/connections/view.py:788 -#: windows/dialogs/connections/view.py:803 +#: windows/dialogs/connections/view.py:789 +#, python-brace-format +msgid "Do you want to delete the connection '{connection_name}'?" +msgstr "" + +#: windows/dialogs/connections/view.py:792 +#: windows/dialogs/connections/view.py:809 msgid "Confirm delete" msgstr "" -#: windows/main/controller.py:172 +#: windows/dialogs/connections/view.py:806 +#, python-brace-format +msgid "Do you want to delete the directory '{directory_name}'?" +msgstr "" + +#: windows/main/controller.py:189 msgid "days" msgstr "" -#: windows/main/controller.py:173 +#: windows/main/controller.py:190 msgid "hours" msgstr "" -#: windows/main/controller.py:174 +#: windows/main/controller.py:191 msgid "minutes" msgstr "" -#: windows/main/controller.py:175 +#: windows/main/controller.py:192 msgid "seconds" msgstr "" -#: windows/main/controller.py:183 +#: windows/main/controller.py:200 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "" -#: windows/main/controller.py:219 +#: windows/main/controller.py:236 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:298 +#: windows/main/controller.py:441 +#, python-brace-format +msgid "~{estimated} (Loading...)" +msgstr "" + +#: windows/main/controller.py:443 +msgid "~ (Loading...)" +msgstr "" + +#: windows/main/controller.py:608 msgid "Version" msgstr "" -#: windows/main/controller.py:300 +#: windows/main/controller.py:610 msgid "Uptime" msgstr "" -#: windows/main/controller.py:399 +#: windows/main/controller.py:678 +#, python-brace-format +msgid "Do you want discard the change to {database_name}?" +msgstr "" + +#: windows/main/controller.py:711 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -918,36 +1015,51 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:404 windows/main/controller.py:425 +#: windows/main/controller.py:716 windows/main/controller.py:737 msgid "Delete database" msgstr "" -#: windows/main/controller.py:410 +#: windows/main/controller.py:722 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:411 +#: windows/main/controller.py:723 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:424 +#: windows/main/controller.py:736 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:439 +#: windows/main/controller.py:751 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:440 windows/main/tabs/view.py:253 +#: windows/main/controller.py:752 windows/main/tabs/view.py:253 #: windows/main/tabs/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:582 +#: windows/main/controller.py:871 +#, python-brace-format +msgid "Do you want discard the change to {table_name}?" +msgstr "" + +#: windows/main/controller.py:897 +#, python-brace-format +msgid "Do you want delete the table {table_name}?" +msgstr "" + +#: windows/main/controller.py:900 msgid "Delete table" msgstr "" -#: windows/main/controller.py:699 +#: windows/main/controller.py:919 +#, python-brace-format +msgid "{table_name} (COPY)" +msgstr "" + +#: windows/main/controller.py:1017 msgid "Do you want delete the records?" msgstr "" @@ -967,51 +1079,51 @@ msgstr "" msgid "Reconnection failed:" msgstr "" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:450 +#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:489 #: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 msgid "Error" msgstr "" -#: windows/main/tabs/query.py:305 +#: windows/main/tabs/query.py:308 #, python-brace-format -msgid "{} rows affected" +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/query.py:309 windows/main/tabs/query.py:331 +#: windows/main/tabs/query.py:315 windows/main/tabs/query.py:339 #, python-brace-format -msgid "Query {}" +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/query.py:314 +#: windows/main/tabs/query.py:320 #, python-brace-format -msgid "Query {} (Error)" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/query.py:326 +#: windows/main/tabs/query.py:334 #, python-brace-format -msgid "Query {} ({} rows × {} cols)" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/query.py:353 +#: windows/main/tabs/query.py:360 #, python-brace-format -msgid "{} rows" +msgid "{rows_count} rows" msgstr "" -#: windows/main/tabs/query.py:355 +#: windows/main/tabs/query.py:362 #, python-brace-format -msgid "{:.1f} ms" +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/query.py:358 +#: windows/main/tabs/query.py:366 #, python-brace-format -msgid "{} warnings" +msgid "{warnings_count} warnings" msgstr "" -#: windows/main/tabs/query.py:370 +#: windows/main/tabs/query.py:381 msgid "Error:" msgstr "" -#: windows/main/tabs/query.py:449 +#: windows/main/tabs/query.py:488 msgid "No active database connection" msgstr "" From 7d16e354d63c3021389c81d83bfbfb9f63e3f5b9 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 14 Mar 2026 18:22:28 +0100 Subject: [PATCH 08/93] chore(ui): update wxFormBuilder project layout AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- PeterSQL.fbp | 217 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 146 insertions(+), 71 deletions(-) diff --git a/PeterSQL.fbp b/PeterSQL.fbp index 2b5d944..87c4072 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -4754,7 +4754,7 @@ 0 0 wxID_ANY - Average connection time (ms) + Average connection time (ms) 0 0 @@ -4889,7 +4889,7 @@ 0 0 wxID_ANY - Most recent connection duration + Most recent connection duration 0 0 @@ -6831,8 +6831,8 @@ - - + + 1 1 1 @@ -6884,7 +6884,7 @@ wxTAB_TRAVERSAL - + bSizer24 wxHORIZONTAL @@ -7331,7 +7331,7 @@ Load From File; icons/16x16/database.png Database 0 - + 1 1 1 @@ -7383,16 +7383,16 @@ wxTAB_TRAVERSAL - + bSizer27 wxVERTICAL none - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -7446,11 +7446,11 @@ - + Options 0 - + 1 1 1 @@ -7502,16 +7502,16 @@ wxTAB_TRAVERSAL - + bSizer80 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -7569,8 +7569,8 @@ - - + + 1 1 1 @@ -7622,7 +7622,7 @@ wxTAB_TRAVERSAL - + bSizer158 wxVERTICAL @@ -8169,20 +8169,20 @@ - + 5 wxEXPAND 0 - + bSizer13911 wxHORIZONTAL none - + 0 wxALIGN_CENTER 1 - + 1 1 1 @@ -8234,16 +8234,16 @@ wxTAB_TRAVERSAL - + bSizer1391 wxHORIZONTAL none - + 5 wxALL 0 - + 1 1 1 @@ -10280,8 +10280,8 @@ - - + + 1 1 1 @@ -10333,7 +10333,7 @@ wxTAB_TRAVERSAL - + bSizer154 wxVERTICAL @@ -10656,11 +10656,11 @@ - + 5 wxEXPAND 1 - + bSizer152 wxVERTICAL @@ -10805,11 +10805,11 @@ bSizer138 wxHORIZONTAL none - + 5 wxALL 0 - + 1 1 1 @@ -10880,11 +10880,11 @@ on_cancel_database - + 5 wxALL 0 - + 1 1 1 @@ -10955,11 +10955,11 @@ on_delete_database - + 5 wxALL 0 - + 1 1 1 @@ -11035,11 +11035,11 @@ - + Diagram 0 - + 1 1 1 @@ -11096,11 +11096,11 @@ bSizer82 wxVERTICAL none - + 5 wxALL 0 - + 1 1 1 @@ -11158,11 +11158,11 @@ -1 - + 5 wxALL 0 - + 1 1 1 @@ -11220,11 +11220,11 @@ -1 - + 5 wxALL 0 - + 1 1 1 @@ -16414,8 +16414,8 @@ Load From File; icons/16x16/text_columns.png Data - 1 - + 0 + 1 1 1 @@ -16467,16 +16467,16 @@ wxTAB_TRAVERSAL - + bSizer61 wxVERTICAL none - + 5 wxEXPAND 0 - + bSizer94 wxHORIZONTAL @@ -16553,11 +16553,11 @@ 0 - + 5 wxALL|wxEXPAND 0 - + 1 1 1 @@ -16628,11 +16628,11 @@ on_first_records - + 5 wxALL|wxEXPAND 0 - + 1 1 1 @@ -16703,11 +16703,11 @@ on_prev_records - + 5 wxALL 0 - + 1 1 1 @@ -16841,11 +16841,11 @@ on_next_records - + 5 wxALL|wxEXPAND 0 - + 1 1 1 @@ -16918,11 +16918,11 @@ - + 5 wxEXPAND 0 - + bSizer83 wxHORIZONTAL @@ -17671,7 +17671,7 @@ - + MyMenu m_menu10 protected @@ -17704,11 +17704,11 @@ - + Load From File; icons/16x16/arrow_right.png Query - 0 - + 1 + 1 1 1 @@ -17760,16 +17760,16 @@ wxTAB_TRAVERSAL - + bSizer26 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -17827,8 +17827,8 @@ - - + + 1 1 1 @@ -17880,11 +17880,86 @@ wxTAB_TRAVERSAL - + bSizer125 wxVERTICAL none + + 5 + wxALIGN_RIGHT|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 + cancel_query_execution + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_cancel_query_execution + + 5 wxEXPAND | wxALL @@ -18030,8 +18105,8 @@ - - + + 1 1 1 @@ -18083,7 +18158,7 @@ wxTAB_TRAVERSAL - + bSizer1261 wxVERTICAL From 969f0a43b670f95d8372b994335c538eca377d97 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 16 Mar 2026 15:42:10 +0100 Subject: [PATCH 09/93] refactor(settings): move Settings to helpers and restructure key schema AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- helpers/settings.py | 69 ++++++++++ main.py | 32 ++--- settings.yml | 69 ++++++---- .../stc/autocomplete/auto_complete.py | 12 +- .../stc/autocomplete/autocomplete_popup.py | 4 +- windows/dialogs/connections/view.py | 21 ++- windows/dialogs/settings/controller.py | 123 ++++++++---------- windows/dialogs/settings/repository.py | 29 +---- 8 files changed, 199 insertions(+), 160 deletions(-) create mode 100644 helpers/settings.py diff --git a/helpers/settings.py b/helpers/settings.py new file mode 100644 index 0000000..5013ecb --- /dev/null +++ b/helpers/settings.py @@ -0,0 +1,69 @@ +from pathlib import Path +from typing import Any, Optional + +from helpers.repository import YamlRepository +from helpers.observables import ObservableObject + + +class Settings(ObservableObject): + _default_unset = object() + + def _ensure_root(self) -> dict[str, Any]: + value = super()._get_value() + if isinstance(value, dict): + return value + + value = {} + super()._set_value(value) + + return value + + def _persist_default(self, attributes: tuple[str, ...], default: Any) -> Any: + if not attributes: + return super()._get_value() + + root = self._ensure_root() + current = root + + for attribute in attributes[:-1]: + nested = current.get(attribute) + if not isinstance(nested, dict): + nested = {} + current[attribute] = nested + current = nested + + last_attribute = attributes[-1] + if current.get(last_attribute) is None: + current[last_attribute] = default + super()._set_value(root) + + return current.get(last_attribute) + + def get_value(self, *attributes: str, default: Any = _default_unset) -> Any: + value = super().get_value(*attributes) + if value is not None: + return value + + if default is self._default_unset: + return None + + return self._persist_default(attributes, default) + + +class SettingsRepository(YamlRepository[Settings]): + def __init__(self, config_file: Path): + super().__init__(config_file) + self.settings: Optional[Settings] = None + + def _write(self) -> None: + if self.settings is None: + return + + data = dict(self.settings.get_value(default={})) + self._write_yaml(data) + + def load(self) -> Settings: + data = self._read_yaml() + self.settings = Settings(data) + self.settings.subscribe(lambda _: self._write()) + return self.settings \ No newline at end of file diff --git a/main.py b/main.py index 9fb84e8..325e4f3 100755 --- a/main.py +++ b/main.py @@ -11,9 +11,7 @@ from helpers.loader import Loader from helpers.logger import logger -from helpers.observables import ObservableObject - -from windows.dialogs.settings.repository import SettingsRepository +from helpers.settings import Settings, SettingsRepository from windows.components.stc.styles import apply_stc_theme, set_theme_loader from windows.components.stc.themes import ThemeManager @@ -26,7 +24,7 @@ class PeterSQL(wx.App): locale: wx.Locale = wx.Locale() settings_repository = SettingsRepository(WORKDIR / "settings.yml") - settings: ObservableObject = settings_repository.load() + settings: Settings = settings_repository.load() main_frame: wx.Frame = None @@ -53,7 +51,7 @@ def OnInit(self) -> bool: return True def _init_theme_loader(self) -> None: - theme_name = self.settings.get_value("theme", "current") or "petersql" + theme_name = self.settings.get_value("ui", "appearance", "theme", default="petersql") self.theme_loader = ThemeLoader(WORKDIR / "themes") try: self.theme_loader.load_theme(theme_name) @@ -64,13 +62,7 @@ def _init_theme_loader(self) -> None: logger.error(f"Error loading theme: {ex}", exc_info=True) def _init_locale(self): - _locale = self.settings.get_value("locale") - - if _locale is None: - _locale = locale.getlocale()[0] - - if not _locale: - _locale = "en_US" + _locale = self.settings.get_value("language", default="en_US") translation = gettext.translation( 'petersql', @@ -104,16 +96,12 @@ def open_main_frame(self) -> None: from windows.main.controller import MainFrameController self.main_frame = MainFrameController() - size = wx.Size( - *list(map(int, self.settings.get_value("window", "size").split(","))) - ) + size_values = self.settings.get_value("ui", "window", "size", default=[1920, 1080]) + size = wx.Size(*list(map(int, size_values))) self.main_frame.SetSize(width=size.width, height=size.height) - position = wx.Point( - *list( - map(int, self.settings.get_value("window", "position").split(",")) - ) - ) + position_values = self.settings.get_value("ui", "window", "position", default=[0, 0]) + position = wx.Point(*list(map(int, position_values))) self.main_frame.SetPosition(position) self.main_frame.Layout() self.main_frame.SetIcon( @@ -128,13 +116,13 @@ def open_main_frame(self) -> None: def _on_size(self, event: wx.SizeEvent) -> None: size = event.GetSize() - self.settings.set_value("window", "size", value=f"{size.Width},{size.Height}") + self.settings.set_value("ui", "window", "size", value=[size.Width, size.Height]) self.main_frame.Layout() def _on_move(self, event: wx.MouseEvent) -> None: position = event.GetPosition() self.settings.set_value( - "window", "position", value=f"{position.x},{position.y}" + "ui", "window", "position", value=[position.x, position.y] ) self.main_frame.Layout() diff --git a/settings.yml b/settings.yml index ae75cb9..4bda072 100755 --- a/settings.yml +++ b/settings.yml @@ -1,35 +1,50 @@ -window: - size: 1920,1080 - position: 0,0 -appearance: - theme: petersql - mode: auto -shortcuts: - autocomplete: - force_show: Ctrl+Space - complete: Tab,Enter - cancel: Escape - query_editor: - execute: F5 - execute_selection: Ctrl+Enter -autocomplete: - debounce_ms: 80 - min_prefix_length: 1 - add_space_after_completion: true - popup_width: 300 - popup_max_height: 10 language: en_US -query_editor: +ui: + window: + size: + - 1920 + - 1048 + position: + - 0 + - 0 + appearance: + theme: petersql + mode: auto + dialogs: + connections: + expanded_directories: + - - 0 + - - 3 + shortcuts: + query: + execute_current: Ctrl+Enter + execute_all: Ctrl+Shift+Enter + stop: Esc + new_query: Ctrl+T + close_query: Ctrl+W + save: Ctrl+S + save_as: Ctrl+Shift+S +editor: statement_separator: ; trim_whitespace: false execute_selected_only: false - autocomplete: true autoformat: true -advanced: + autocomplete: + enabled: true + debounce_ms: 80 + min_prefix_length: 1 + add_space_after_completion: true + popup_width: 300 + popup_max_height: 10 + shortcuts: + execute: F5 + execute_selection: Ctrl+Enter + force_autocomplete: Ctrl+Space + complete: Tab,Enter + cancel: Escape +runtime: connection_timeout: 10 query_timeout: 10 logging_level: INFO -connections_dialog: - expanded_directories: - - - 3 -records: {} +records: + limit: 100 diff --git a/windows/components/stc/autocomplete/auto_complete.py b/windows/components/stc/autocomplete/auto_complete.py index 067292d..45f787e 100644 --- a/windows/components/stc/autocomplete/auto_complete.py +++ b/windows/components/stc/autocomplete/auto_complete.py @@ -126,18 +126,14 @@ def __init__( if settings: self._debounce_ms = ( - settings.get_value("autocomplete", "debounce_ms") - or debounce_ms + settings.get_value("editor", "autocomplete", "debounce_ms", default=debounce_ms) ) self._min_prefix_length = ( - settings.get_value("autocomplete", "min_prefix_length") - or min_prefix_length + settings.get_value("editor", "autocomplete", "min_prefix_length", default=min_prefix_length) ) self._add_space_after_completion = settings.get_value( - "autocomplete", "add_space_after_completion" + "editor", "autocomplete", "add_space_after_completion", default=True ) - if self._add_space_after_completion is None: - self._add_space_after_completion = True else: self._debounce_ms = debounce_ms self._min_prefix_length = min_prefix_length @@ -161,7 +157,7 @@ def set_enabled(self, is_enabled: bool) -> None: def get_effective_separator(self) -> str: if self._settings: - separator = self._settings.get_value("query_editor", "statement_separator") + separator = self._settings.get_value("editor", "statement_separator", default=";") if separator: return separator diff --git a/windows/components/stc/autocomplete/autocomplete_popup.py b/windows/components/stc/autocomplete/autocomplete_popup.py index 86687e5..a60db3e 100644 --- a/windows/components/stc/autocomplete/autocomplete_popup.py +++ b/windows/components/stc/autocomplete/autocomplete_popup.py @@ -16,8 +16,8 @@ def __init__(self, parent: wx.Window, settings: object = None, theme_loader: The self._theme_loader = theme_loader if settings: - self._popup_width = settings.get_value("autocomplete", "popup_width") or 300 - self._popup_max_height = settings.get_value("autocomplete", "popup_max_height") or 10 + self._popup_width = settings.get_value("editor", "autocomplete", "popup_width", default=300) + self._popup_max_height = settings.get_value("editor", "autocomplete", "popup_max_height", default=10) else: self._popup_width = 300 self._popup_max_height = 10 diff --git a/windows/dialogs/connections/view.py b/windows/dialogs/connections/view.py index 16272bf..d0a570a 100644 --- a/windows/dialogs/connections/view.py +++ b/windows/dialogs/connections/view.py @@ -31,7 +31,9 @@ class ConnectionsManager(ConnectionsDialog): - _SETTINGS_SECTION = "connections_dialog" + _SETTINGS_UI = "ui" + _SETTINGS_DIALOGS = "dialogs" + _SETTINGS_CONNECTIONS = "connections" _SETTINGS_EXPANDED_DIRECTORIES = "expanded_directories" def __init__(self, parent): @@ -291,19 +293,28 @@ def _save_expanded_directory_paths_to_settings(self) -> None: expanded_paths = self._capture_expanded_directory_paths() serialized_paths = self._serialize_expanded_directory_paths(expanded_paths) - if self._app.settings.get_value(self._SETTINGS_SECTION) is None: - self._app.settings.set_value(self._SETTINGS_SECTION, value={}) + self._app.settings.get_value( + self._SETTINGS_UI, + self._SETTINGS_DIALOGS, + self._SETTINGS_CONNECTIONS, + default={}, + ) self._app.settings.set_value( - self._SETTINGS_SECTION, + self._SETTINGS_UI, + self._SETTINGS_DIALOGS, + self._SETTINGS_CONNECTIONS, self._SETTINGS_EXPANDED_DIRECTORIES, value=serialized_paths, ) def _restore_expanded_directory_paths_from_settings(self) -> None: raw_paths = self._app.settings.get_value( - self._SETTINGS_SECTION, + self._SETTINGS_UI, + self._SETTINGS_DIALOGS, + self._SETTINGS_CONNECTIONS, self._SETTINGS_EXPANDED_DIRECTORIES, + default=[], ) expanded_paths = self._deserialize_expanded_directory_paths(raw_paths) self._restore_expanded_directory_paths(expanded_paths) diff --git a/windows/dialogs/settings/controller.py b/windows/dialogs/settings/controller.py index f608583..dda60a1 100644 --- a/windows/dialogs/settings/controller.py +++ b/windows/dialogs/settings/controller.py @@ -4,8 +4,8 @@ import wx.dataview from constants import Language, LogLevel +from helpers.settings import Settings -from windows.dialogs.settings.repository import Settings from windows.views import SettingsDialog @@ -36,16 +36,10 @@ def _populate_languages(self) -> None: def _populate_shortcuts(self) -> None: self.dialog.shortcuts_list.DeleteAllItems() - - shortcuts = self.settings.get_value("shortcuts") or {} - for action, shortcut_data in shortcuts.items(): - if isinstance(shortcut_data, dict): - shortcut = shortcut_data.get("key", "") - context = shortcut_data.get("context", "Global") - else: - shortcut = str(shortcut_data) - context = "Global" - + + shortcuts = self.settings.get_value("editor", "shortcuts", default={}) + for action, shortcut in shortcuts.items(): + context = "Global" self.dialog.shortcuts_list.AppendItem([action, shortcut, context]) def _populate_themes(self) -> None: @@ -71,57 +65,57 @@ def _load_settings(self) -> None: def _load_advanced_settings(self) -> None: self.dialog.advanced_connection_timeout.SetValue( - self.settings.get_value("advanced", "connection_timeout") or 60 + self.settings.get_value("runtime", "connection_timeout", default=60) ) self.dialog.advanced_query_timeout.SetValue( - self.settings.get_value("advanced", "query_timeout") or 60 + self.settings.get_value("runtime", "query_timeout", default=60) ) - + levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARNING, LogLevel.ERROR] - logging_level = self.settings.get_value("advanced", "logging_level") or "INFO" + logging_level = self.settings.get_value("runtime", "logging_level", default="INFO") try: selection = next(i for i, level in enumerate(levels) if level.value == logging_level) except StopIteration: selection = 1 self.dialog.advanced_logging_level.SetSelection(selection) - + def _load_appearance_settings(self) -> None: - current_theme = self.settings.get_value("appearance", "theme") or "" + current_theme = self.settings.get_value("ui", "appearance", "theme", default="") if current_theme: idx = self.dialog.theme.FindString(current_theme) if idx != wx.NOT_FOUND: self.dialog.theme.SetSelection(idx) - - appearance_mode = self.settings.get_value("appearance", "mode") or "auto" + + appearance_mode = self.settings.get_value("ui", "appearance", "mode", default="auto") if appearance_mode == "auto": self.dialog.appearance_mode_auto.SetValue(True) elif appearance_mode == "light": self.dialog.appearance_mode_light.SetValue(True) elif appearance_mode == "dark": self.dialog.appearance_mode_dark.SetValue(True) - + def _load_general_settings(self) -> None: language_map = {lang.code: idx for idx, lang in enumerate(Language)} - language = self.settings.get_value("language") or "en_US" + language = self.settings.get_value("language", default="en_US") self.dialog.language.SetSelection(language_map.get(language, 0)) - + def _load_query_editor_settings(self) -> None: self.dialog.query_editor_statement_separator.SetValue( - self.settings.get_value("query_editor", "statement_separator") or ";" + self.settings.get_value("editor", "statement_separator", default=";") ) self.dialog.query_editor_trim_whitespace.SetValue( - self.settings.get_value("query_editor", "trim_whitespace") or False + self.settings.get_value("editor", "trim_whitespace", default=False) ) self.dialog.query_editor_execute_selected_only.SetValue( - self.settings.get_value("query_editor", "execute_selected_only") or False + self.settings.get_value("editor", "execute_selected_only", default=False) ) - - autocomplete = self.settings.get_value("query_editor", "autocomplete") + + autocomplete = self.settings.get_value("editor", "autocomplete", "enabled", default=True) self.dialog.query_editor_autocomplete.SetValue( autocomplete if autocomplete is not None else True ) - - autoformat = self.settings.get_value("query_editor", "autoformat") + + autoformat = self.settings.get_value("editor", "autoformat", default=True) self.dialog.query_editor_format.SetValue( autoformat if autoformat is not None else True ) @@ -133,61 +127,59 @@ def _save_settings(self) -> None: self._save_advanced_settings() def _save_advanced_settings(self) -> None: - if not self.settings.get_value("advanced"): - self.settings.set_value("advanced", value={}) - - self.settings.set_value("advanced", "connection_timeout", + self.settings.get_value("runtime", default={}) + + self.settings.set_value("runtime", "connection_timeout", value=self.dialog.advanced_connection_timeout.GetValue() ) - self.settings.set_value("advanced", "query_timeout", + self.settings.set_value("runtime", "query_timeout", value=self.dialog.advanced_query_timeout.GetValue() ) - + levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARNING, LogLevel.ERROR] selection = self.dialog.advanced_logging_level.GetSelection() - self.settings.set_value("advanced", "logging_level", + self.settings.set_value("runtime", "logging_level", value=levels[selection].value if 0 <= selection < len(levels) else LogLevel.INFO.value ) - + def _save_appearance_settings(self) -> None: - if not self.settings.get_value("appearance"): - self.settings.set_value("appearance", value={}) - + self.settings.get_value("ui", "appearance", default={}) + theme_idx = self.dialog.theme.GetSelection() if theme_idx != wx.NOT_FOUND: - self.settings.set_value("appearance", "theme", value=self.dialog.theme.GetString(theme_idx)) - + self.settings.set_value("ui", "appearance", "theme", value=self.dialog.theme.GetString(theme_idx)) + if self.dialog.appearance_mode_auto.GetValue(): appearance_mode = "auto" elif self.dialog.appearance_mode_light.GetValue(): appearance_mode = "light" else: appearance_mode = "dark" - self.settings.set_value("appearance", "mode", value=appearance_mode) - + self.settings.set_value("ui", "appearance", "mode", value=appearance_mode) + def _save_general_settings(self) -> None: language_map = {idx: lang.code for idx, lang in enumerate(Language)} self.settings.set_value("language", value=language_map.get( self.dialog.language.GetSelection(), "en_US" )) - + def _save_query_editor_settings(self) -> None: - if not self.settings.get_value("query_editor"): - self.settings.set_value("query_editor", value={}) - - self.settings.set_value("query_editor", "statement_separator", + self.settings.get_value("editor", default={}) + self.settings.get_value("editor", "autocomplete", default={}) + + self.settings.set_value("editor", "statement_separator", value=self.dialog.query_editor_statement_separator.GetValue() ) - self.settings.set_value("query_editor", "trim_whitespace", + self.settings.set_value("editor", "trim_whitespace", value=self.dialog.query_editor_trim_whitespace.GetValue() ) - self.settings.set_value("query_editor", "execute_selected_only", + self.settings.set_value("editor", "execute_selected_only", value=self.dialog.query_editor_execute_selected_only.GetValue() ) - self.settings.set_value("query_editor", "autocomplete", + self.settings.set_value("editor", "autocomplete", "enabled", value=self.dialog.query_editor_autocomplete.GetValue() ) - self.settings.set_value("query_editor", "autoformat", + self.settings.set_value("editor", "autoformat", value=self.dialog.query_editor_format.GetValue() ) @@ -200,22 +192,17 @@ def _on_cancel(self, event: wx.Event) -> None: def _on_filter_shortcuts(self, event: wx.Event) -> None: filter_text = self.dialog.shortcuts_filter.GetValue().lower() - + self.dialog.shortcuts_list.DeleteAllItems() - - shortcuts = self.settings.get_value("shortcuts") or {} - - for action, shortcut_data in shortcuts.items(): - if isinstance(shortcut_data, dict): - shortcut = shortcut_data.get("key", "") - context = shortcut_data.get("context", "Global") - else: - shortcut = str(shortcut_data) - context = "Global" - - if (not filter_text or - filter_text in action.lower() or - filter_text in shortcut.lower() or + + shortcuts = self.settings.get_value("editor", "shortcuts", default={}) + + for action, shortcut in shortcuts.items(): + context = "Global" + + if (not filter_text or + filter_text in action.lower() or + filter_text in shortcut.lower() or filter_text in context.lower()): self.dialog.shortcuts_list.AppendItem([action, shortcut, context]) diff --git a/windows/dialogs/settings/repository.py b/windows/dialogs/settings/repository.py index b3ee7b1..b9d001f 100644 --- a/windows/dialogs/settings/repository.py +++ b/windows/dialogs/settings/repository.py @@ -1,28 +1 @@ -from pathlib import Path -from typing import Optional - -from helpers.observables import ObservableObject -from helpers.repository import YamlRepository - - -class SettingsRepository(YamlRepository[ObservableObject]): - def __init__(self, config_file: Path): - super().__init__(config_file) - self.settings: Optional[ObservableObject] = None - - def _write(self) -> None: - if self.settings is None: - return - - data = dict(self.settings.get_value()) - self._write_yaml(data) - - def load(self) -> ObservableObject: - data = self._read_yaml() - self.settings = ObservableObject(data) - self.settings.subscribe(lambda _: self._write()) - return self.settings - - -class Settings(SettingsRepository): - pass +from helpers.settings import Settings, SettingsRepository From 191ca25dc7d1c6c5991b0cdac983d4e3b905eb9d Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 16 Mar 2026 15:43:11 +0100 Subject: [PATCH 10/93] docs(style): fix contradictions and merge duplicate rule sections AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- CODE_STYLE.md | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/CODE_STYLE.md b/CODE_STYLE.md index b6190a5..c074b74 100644 --- a/CODE_STYLE.md +++ b/CODE_STYLE.md @@ -15,31 +15,18 @@ If a requested change conflicts with these rules, the change must **stop** and c --- -## Core Rules (Quick Reference) +## Mandatory Rules (Quick Reference) -The following rules are the most critical and must always be respected: +The following rules are strict and MUST NOT be violated. When generating or modifying code, tools and agents MUST consult this file before producing changes. 1. All code, comments, documentation, commit messages, and user-facing text MUST be written in English. 2. Python typing rules MUST be respected (PEP 585 generics; `Optional[T]`, not `T | None`). -3. Import ordering and grouping rules MUST be followed exactly. -4. Functions and methods MUST NOT exceed 50 lines. -5. Code changes MUST avoid modifying unrelated code. -6. Naming MUST remain explicit and descriptive (no aggressive abbreviations). -7. Code MUST remain mypy-friendly whenever possible. - -When generating or modifying code, tools and agents MUST consult this file before producing changes. - ---- - -## Mandatory Rules - -The following rules are strict and MUST NOT be violated: - -- English must always be used for code and documentation. -- `typing.Optional[T]` MUST be used instead of `T | None`. -- `from __future__ import annotations` MUST NOT be used. -- Import ordering rules MUST be respected exactly. -- Functions MUST NOT exceed the maximum size limit. +3. `from __future__ import annotations` MUST NOT be used. +4. Import ordering and grouping rules MUST be followed exactly. +5. Functions and methods MUST NOT exceed 50 lines. +6. Code changes MUST avoid modifying unrelated code. +7. Naming MUST remain explicit and descriptive (no aggressive abbreviations). +8. Code MUST remain mypy-friendly whenever possible. --- @@ -431,9 +418,10 @@ This rule applies only to builtin modules. import os import sys +import gettext as _ + import numpy as np import pandas as pd -import gettext as _ ``` #### Bad example @@ -448,7 +436,7 @@ import sys When importing multiple symbols from the same module: -- Parenthesized multiline `from ... import (...)` MUST NOT be used for functions or methods. +- Parenthesized multiline `from ... import (...)` MUST NOT be used. - Imports MUST NOT be split into one line per symbol. - Prefer a single `from ... import ...` line whenever possible. - If a `from ... import ...` statement would exceed the maximum line width: @@ -639,7 +627,7 @@ class Example: ## 10. Walrus Operator ( := ) -- The walrus operator MAY be used when it improves clarity and avoids redundant calls. +- The walrus operator MUST be used whenever it avoids redundant calls or repeated expressions. - It MUST NOT be used when it makes the control flow harder to read. #### Good examples From ba6bd42d5c0208c3ac835e1e42e10228947cc96d Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 16 Mar 2026 15:43:18 +0100 Subject: [PATCH 11/93] feat(engines): support skip_before/after_connect in context.connect() AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- structures/engines/mariadb/context.py | 8 ++++++-- structures/engines/mysql/context.py | 8 ++++++-- structures/engines/postgresql/context.py | 9 +++++++-- structures/engines/sqlite/context.py | 8 +++++++- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/structures/engines/mariadb/context.py b/structures/engines/mariadb/context.py index e5db6bf..979f04e 100755 --- a/structures/engines/mariadb/context.py +++ b/structures/engines/mariadb/context.py @@ -152,8 +152,12 @@ def get_result_column_datatypes( return datatypes def connect(self, **connect_kwargs) -> None: + skip_after_connect = bool(connect_kwargs.pop("skip_after_connect", False)) + skip_before_connect = bool(connect_kwargs.pop("skip_before_connect", False)) + if self._connection is None: - self.before_connect() + if not skip_before_connect: + self.before_connect() connect_timeout_override = connect_kwargs.pop("connect_timeout", None) compressed_protocol_override = connect_kwargs.pop("compress", None) @@ -231,7 +235,7 @@ def connect(self, **connect_kwargs) -> None: logger.error(f"Failed to connect to MariaDB: {e}", exc_info=True) raise - if self._cursor is not None: + if self._cursor is not None and not skip_after_connect: self.after_connect() def set_database(self, database: SQLDatabase) -> None: diff --git a/structures/engines/mysql/context.py b/structures/engines/mysql/context.py index 68b01ce..f1260e7 100644 --- a/structures/engines/mysql/context.py +++ b/structures/engines/mysql/context.py @@ -152,8 +152,12 @@ def get_result_column_datatypes( return datatypes def connect(self, **connect_kwargs) -> None: + skip_after_connect = bool(connect_kwargs.pop("skip_after_connect", False)) + skip_before_connect = bool(connect_kwargs.pop("skip_before_connect", False)) + if self._connection is None: - self.before_connect() + if not skip_before_connect: + self.before_connect() connect_timeout_override = connect_kwargs.pop("connect_timeout", None) compressed_protocol_override = connect_kwargs.pop("compress", None) @@ -231,7 +235,7 @@ def connect(self, **connect_kwargs) -> None: logger.error(f"Failed to connect to MySQL: {e}") raise - if self._cursor is not None: + if self._cursor is not None and not skip_after_connect: self.after_connect() def set_database(self, database: SQLDatabase) -> None: diff --git a/structures/engines/postgresql/context.py b/structures/engines/postgresql/context.py index d6f9b2f..1aa246d 100644 --- a/structures/engines/postgresql/context.py +++ b/structures/engines/postgresql/context.py @@ -164,9 +164,13 @@ def get_result_column_datatypes( return datatypes def connect(self, **connect_kwargs) -> None: + skip_after_connect = bool(connect_kwargs.pop("skip_after_connect", False)) + skip_before_connect = bool(connect_kwargs.pop("skip_before_connect", False)) + if self._connection is None: try: - self.before_connect() + if not skip_before_connect: + self.before_connect() database = connect_kwargs.pop("database", "postgres") connect_timeout_override = connect_kwargs.pop("connect_timeout", None) connect_timeout = ( @@ -202,7 +206,8 @@ def connect(self, **connect_kwargs) -> None: logger.error(f"Failed to connect to PostgreSQL: {e}", exc_info=True) raise else: - self.after_connect() + if not skip_after_connect: + self.after_connect() def after_disconnect(self): self._current_database = None diff --git a/structures/engines/sqlite/context.py b/structures/engines/sqlite/context.py index 35291cf..a68e05c 100755 --- a/structures/engines/sqlite/context.py +++ b/structures/engines/sqlite/context.py @@ -96,8 +96,13 @@ def after_connect(self, *args, **kwargs): # self.execute("PRAGMA page_size = 4096") def connect(self, **connect_kwargs) -> None: + skip_after_connect = bool(connect_kwargs.pop("skip_after_connect", False)) + skip_before_connect = bool(connect_kwargs.pop("skip_before_connect", False)) + if self._connection is None: try: + if not skip_before_connect: + self.before_connect() self._connection = sqlite3.connect(self.filename) except Exception as e: @@ -106,7 +111,8 @@ def connect(self, **connect_kwargs) -> None: else: self._connection.row_factory = sqlite3.Row self._cursor = self._connection.cursor() - self.after_connect() + if not skip_after_connect: + self.after_connect() def set_database(self, database: SQLDatabase) -> None: pass From c8e9e9580798648890f769733633885839e4a3db Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 16 Mar 2026 15:43:59 +0100 Subject: [PATCH 12/93] refactor(query): expose extract_all_statements in StatementExtractor AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- .../stc/autocomplete/statement_extractor.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/windows/components/stc/autocomplete/statement_extractor.py b/windows/components/stc/autocomplete/statement_extractor.py index 82d3804..b2f766b 100644 --- a/windows/components/stc/autocomplete/statement_extractor.py +++ b/windows/components/stc/autocomplete/statement_extractor.py @@ -38,6 +38,32 @@ def extract_current_statement(text: str, cursor_pos: int) -> tuple[str, int]: return text, cursor_pos + @staticmethod + def extract_all_statements(text: str) -> list[tuple[str, int, int]]: + """Return all statements as (text, start_pos, end_pos) tuples.""" + if not text.strip(): + return [] + + cleaned = StatementExtractor._remove_strings_and_comments(text) + + boundaries = [0] + for i, char in enumerate(cleaned): + if char == ';': + boundaries.append(i + 1) + boundaries.append(len(text)) + + results = [] + for i in range(len(boundaries) - 1): + start, end = boundaries[i], boundaries[i + 1] + statement = text[start:end] + if statement.endswith(';'): + statement = statement[:-1] + statement = statement.strip() + if statement: + results.append((statement, start, end)) + + return results + @staticmethod def _remove_strings_and_comments(text: str) -> str: text = StatementExtractor._string_pattern.sub(lambda m: ' ' * len(m.group(0)), text) From ff00625d909a5bf3d4609276aa3ae8148dd24ddd Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 16 Mar 2026 15:45:02 +0100 Subject: [PATCH 13/93] feat(ui): add multi-tab query editor with configurable shortcuts AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- PeterSQL.fbp | 777 ++++++++++++++++++++++++++++++++----- windows/main/controller.py | 508 ++++++++++++++++++++++-- windows/main/tabs/query.py | 250 ++++++------ windows/views.py | 136 ++++++- 4 files changed, 1418 insertions(+), 253 deletions(-) diff --git a/PeterSQL.fbp b/PeterSQL.fbp index 87c4072..d6ddad7 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -6371,7 +6371,7 @@ - + 0 @@ -6401,7 +6401,7 @@ wxTAB_TRAVERSAL 1 do_close - + 1 @@ -6427,7 +6427,7 @@ File m_menu2 protected - + 0 1 @@ -6442,7 +6442,7 @@ on_settings - + Help m_menu4 protected @@ -6583,16 +6583,16 @@ - + bSizer19 wxVERTICAL none - + 0 wxEXPAND | wxALL 1 - + 1 1 1 @@ -6644,16 +6644,16 @@ wxTAB_TRAVERSAL - + bSizer21 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -6711,8 +6711,8 @@ - - + + 1 1 1 @@ -6764,16 +6764,16 @@ wxTAB_TRAVERSAL - + bSizer72 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -6831,8 +6831,8 @@ - - + + 1 1 1 @@ -6884,7 +6884,7 @@ wxTAB_TRAVERSAL - + bSizer24 wxHORIZONTAL @@ -7001,8 +7001,8 @@ - - + + 1 1 1 @@ -7054,16 +7054,16 @@ wxTAB_TRAVERSAL - + bSizer25 wxVERTICAL none - + 5 wxALL|wxEXPAND 1 - + 1 1 1 @@ -7327,7 +7327,7 @@ - + Load From File; icons/16x16/database.png Database 0 @@ -16411,7 +16411,7 @@ wxTAB_TRAVERSAL - + Load From File; icons/16x16/text_columns.png Data 0 @@ -17704,11 +17704,11 @@ - + Load From File; icons/16x16/arrow_right.png Query 1 - + 1 1 1 @@ -17760,16 +17760,16 @@ wxTAB_TRAVERSAL - + bSizer26 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -17827,8 +17827,8 @@ - - + + 1 1 1 @@ -17880,16 +17880,16 @@ wxTAB_TRAVERSAL - + bSizer125 wxVERTICAL none - + 5 - wxALIGN_RIGHT|wxALL + wxEXPAND 0 - + 1 1 1 @@ -17898,35 +17898,28 @@ 0 0 - 0 - Load From File; icons/16x16/cancel.png + 1 0 1 1 - - 0 0 - Dock 0 Left 0 - 0 + 1 1 - 0 0 wxID_ANY - Cancel - 0 0 @@ -17934,30 +17927,103 @@ 0 1 - cancel_query_execution + m_toolBar2 + 1 1 protected 1 - - Resizable + 5 1 - wxBORDER_NONE + wxTB_HORIZONTAL ; ; forward_declare 0 - - wxFILTER_NONE - wxDefaultValidator - - on_cancel_query_execution + + Load From File; icons/16x16/add.png + 0 + wxID_ANY + wxITEM_NORMAL + New query + new_query + protected + + New query + on_new_query + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Close query + close_query + protected + + Close query + on_close_query + + + protected + + + Load From File; icons/16x16/arrow_right.png + 0 + wxID_ANY + wxITEM_NORMAL + Execute + execute_statement + protected + + Execute + on_execute_statement + + + Load From File; icons/16x16/arrows_lefttoright.png + 0 + wxID_ANY + wxITEM_NORMAL + Execute 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 + tool + save + protected + + + on_save + @@ -18105,8 +18171,8 @@ - - + + 1 1 1 @@ -18158,7 +18224,7 @@ wxTAB_TRAVERSAL - + bSizer1261 wxVERTICAL @@ -18699,7 +18765,7 @@ - + 0 wxAUI_MGR_DEFAULT @@ -18723,7 +18789,7 @@ wxTAB_TRAVERSAL - + bSizer90 wxVERTICAL @@ -18793,11 +18859,86 @@ - + + 5 + wxALIGN_RIGHT|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 + cancel_query_execution + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_cancel_query_execution + + + 5 wxALIGN_CENTER|wxALL 0 - + 1 1 1 @@ -18852,20 +18993,20 @@ - + 5 wxEXPAND 1 - + bSizer93 wxVERTICAL none - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -18926,11 +19067,11 @@ - + 5 wxEXPAND 1 - + bSizer129 wxVERTICAL @@ -19211,11 +19352,11 @@ -1 - + 5 wxALL|wxEXPAND 1 - + 2 wxBOTH @@ -19508,11 +19649,11 @@ - + 5 wxALL 1 - + 1 1 1 @@ -19768,11 +19909,11 @@ - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -19833,11 +19974,11 @@ - + 5 wxALL|wxEXPAND 1 - + 1 1 1 @@ -19957,11 +20098,11 @@ - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -20158,20 +20299,20 @@ - + 0 wxEXPAND 0 - + bSizer51 wxVERTICAL none - + 0 wxEXPAND | wxALL 0 - + 1 1 1 @@ -20223,7 +20364,7 @@ wxTAB_TRAVERSAL - + bSizer48 wxVERTICAL @@ -20289,7 +20430,7 @@ - + MyMenu m_menu3 protected @@ -20309,11 +20450,11 @@ - + 0 wxEXPAND | wxALL 0 - + 1 1 1 @@ -20365,16 +20506,16 @@ wxTAB_TRAVERSAL - + bSizer52 wxVERTICAL none - + 0 wxEXPAND 0 - + bSizer1212 wxHORIZONTAL @@ -21489,11 +21630,11 @@ - + 5 wxEXPAND 1 - + bSizer871 wxHORIZONTAL @@ -22068,11 +22209,11 @@ - + 5 wxEXPAND 1 - + 2 0 @@ -22080,11 +22221,11 @@ none 0 0 - + 5 wxEXPAND 1 - + bSizer8712 wxHORIZONTAL @@ -22426,11 +22567,11 @@ - + 5 wxEXPAND 0 - + bSizer86 wxHORIZONTAL @@ -22561,11 +22702,11 @@ - + 5 wxALL 0 - + 1 1 1 @@ -26338,5 +26479,445 @@ + + + + wxBOTH + + 1 + 0 + 1 + impl_virtual + + + + 0 + wxID_ANY + + + MyWizard1 + + + wxDEFAULT_DIALOG_STYLE + ; ; forward_declare + + + 0 + + + + + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 0 + 1 + impl_virtual + + + + 0 + wxID_ANY + + 320,200 + SaveStatments + + + wxDEFAULT_DIALOG_STYLE + ; ; forward_declare + Save Starments + + 0 + + + + + + bSizer163 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + + bSizer164 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Location + 0 + + 0 + + + 0 + 150,-1 + 1 + m_staticText86 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + Select a file + + 0 + + 1 + m_filePicker5 + 1 + + + protected + 1 + + Resizable + 1 + + wxFLP_DEFAULT_STYLE|wxFLP_SAVE|wxFLP_SMALL|wxFLP_USE_TEXTCTRL + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + *.sql + + + + + + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 5 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_staticline7 + 1 + + + protected + 1 + + Resizable + 1 + + wxLI_HORIZONTAL + ; ; forward_declare + 0 + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer165 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Cancel + + 0 + + 0 + + + 0 + + 1 + m_button57 + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Save + + 0 + + 0 + + + 0 + + 1 + m_button58 + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + diff --git a/windows/main/controller.py b/windows/main/controller.py index 82bfd3a..a5d0244 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -53,6 +53,10 @@ def __init__(self): super().__init__(None) self.styled_text_ctrls_name = ["sql_query_logs", "stc_view_select", "sql_query_filters", "sql_create_table", "sql_query_editor"] + self._query_pages: list[wx.Panel] = [] + self._query_page_counter = 1 + self._query_page_meta: dict[wx.Panel, dict[str, Any]] = {} + self._query_shortcuts = self._load_query_shortcuts() self.edit_table_model = EditTableModel() self.edit_table_model.bind_controls( @@ -76,11 +80,7 @@ def __init__(self): self.controller_list_table_check = TableCheckController(self.dv_table_checks) self.controller_list_table_foreign_key = TableForeignKeyController(self.dv_table_foreign_keys) - self.controller_query_records = QueryResultsController( - self.sql_query_editor, - self.notebook_sql_results, - cancel_button=self.cancel_query_execution, - ) + self._setup_query_pages() self.controller_view_editor = ViewEditorController(self) @@ -115,32 +115,427 @@ def on_sys_colour_changed(self, event): event.Skip() def _setup_query_editors(self): + editors = set() + for styled_text_ctrl_name in self.styled_text_ctrls_name: styled_text_ctrl = getattr(self, styled_text_ctrl_name) + self._setup_sql_editor(styled_text_ctrl) + editors.add(styled_text_ctrl) - styled_text_ctrl.EmptyUndoBuffer() + for meta in self._query_page_meta.values(): + styled_text_ctrl = meta["editor"] + if styled_text_ctrl in editors: + continue - wx.GetApp().theme_manager.register(styled_text_ctrl, lambda: wx.GetApp().syntax_registry.get("sql")) + self._setup_sql_editor(styled_text_ctrl) - apply_stc_theme(styled_text_ctrl, SQL) + def _setup_sql_editor(self, styled_text_ctrl: wx.stc.StyledTextCtrl) -> None: + styled_text_ctrl.EmptyUndoBuffer() - sql_completion_provider = SQLCompletionProvider( - get_database=lambda: CURRENT_DATABASE.get_value(), - get_current_table=lambda: CURRENT_TABLE.get_value(), - ) + wx.GetApp().theme_manager.register(styled_text_ctrl, lambda: wx.GetApp().syntax_registry.get("sql")) - sql_autocomplete_controller = SQLAutoCompleteController( - editor=styled_text_ctrl, - provider=sql_completion_provider, - settings=wx.GetApp().settings, - theme_loader=wx.GetApp().theme_loader, - ) - - sql_template_menu = SQLTemplateMenuController( - editor=styled_text_ctrl, - get_database=lambda: CURRENT_DATABASE.get_value(), - get_current_table=lambda: CURRENT_TABLE.get_value(), - ) + apply_stc_theme(styled_text_ctrl, SQL) + + sql_completion_provider = SQLCompletionProvider( + get_database=lambda: CURRENT_DATABASE.get_value(), + get_current_table=lambda: CURRENT_TABLE.get_value(), + ) + + SQLAutoCompleteController( + editor=styled_text_ctrl, + provider=sql_completion_provider, + settings=wx.GetApp().settings, + theme_loader=wx.GetApp().theme_loader, + ) + + SQLTemplateMenuController( + editor=styled_text_ctrl, + get_database=lambda: CURRENT_DATABASE.get_value(), + get_current_table=lambda: CURRENT_TABLE.get_value(), + ) + + def _build_query_editor(self, parent: wx.Window) -> wx.stc.StyledTextCtrl: + editor = wx.stc.StyledTextCtrl(parent, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0) + editor.SetUseTabs(True) + editor.SetTabWidth(4) + editor.SetIndent(4) + editor.SetTabIndents(True) + editor.SetBackSpaceUnIndents(True) + editor.SetViewEOL(False) + editor.SetViewWhiteSpace(False) + editor.SetMarginWidth(2, 0) + editor.SetIndentationGuides(True) + editor.SetReadOnly(False) + editor.SetMarginType(1, wx.stc.STC_MARGIN_SYMBOL) + editor.SetMarginMask(1, wx.stc.STC_MASK_FOLDERS) + editor.SetMarginWidth(1, 16) + editor.SetMarginSensitive(1, True) + editor.SetProperty("fold", "1") + editor.SetFoldFlags(wx.stc.STC_FOLDFLAG_LINEBEFORE_CONTRACTED | wx.stc.STC_FOLDFLAG_LINEAFTER_CONTRACTED) + editor.SetMarginType(0, wx.stc.STC_MARGIN_NUMBER) + editor.SetMarginWidth(0, editor.TextWidth(wx.stc.STC_STYLE_LINENUMBER, "_99999")) + editor.SetSelBackground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)) + editor.SetSelForeground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT)) + return editor + + def _load_query_shortcuts(self) -> dict[str, str]: + settings = wx.GetApp().settings + return { + "execute_current": settings.get_value("ui", "shortcuts", "query", "execute_current", default="Ctrl+Enter"), + "execute_all": settings.get_value("ui", "shortcuts", "query", "execute_all", default="Ctrl+Shift+Enter"), + "stop": settings.get_value("ui", "shortcuts", "query", "stop", default="Esc"), + "new_query": settings.get_value("ui", "shortcuts", "query", "new_query", default="Ctrl+T"), + "close_query": settings.get_value("ui", "shortcuts", "query", "close_query", default="Ctrl+W"), + "save": settings.get_value("ui", "shortcuts", "query", "save", default="Ctrl+S"), + "save_as": settings.get_value("ui", "shortcuts", "query", "save_as", default="Ctrl+Shift+S"), + } + + def _with_shortcut(self, text: str, shortcut_key: str) -> str: + shortcut = self._query_shortcuts.get(shortcut_key) + if not shortcut: + return text + + return _("{text} ({shortcut})").format(text=text, shortcut=shortcut) + + def _apply_query_toolbar_shortcuts(self, toolbar: wx.ToolBar, tool_ids: dict[str, int]) -> None: + toolbar.SetToolShortHelp(tool_ids["new"], self._with_shortcut(_("New query"), "new_query")) + toolbar.SetToolShortHelp(tool_ids["close"], self._with_shortcut(_("Close query"), "close_query")) + toolbar.SetToolShortHelp(tool_ids["execute"], self._with_shortcut(_("Execute"), "execute_current")) + toolbar.SetToolShortHelp(tool_ids["execute_all"], self._with_shortcut(_("Execute all"), "execute_all")) + toolbar.SetToolShortHelp(tool_ids["stop"], self._with_shortcut(_("Stop"), "stop")) + toolbar.SetToolShortHelp(tool_ids["save"], self._with_shortcut(_("Save"), "save")) + + def _build_query_toolbar(self, parent: wx.Window) -> tuple[wx.ToolBar, dict[str, int]]: + toolbar = wx.ToolBar(parent, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL) + new_query = toolbar.AddTool(wx.ID_ANY, _("New query"), wx.Bitmap("icons/16x16/add.png", wx.BITMAP_TYPE_ANY), + wx.NullBitmap, wx.ITEM_NORMAL, _("New query"), wx.EmptyString, None) + close_query = toolbar.AddTool(wx.ID_ANY, _("Close query"), wx.Bitmap("icons/16x16/delete.png", wx.BITMAP_TYPE_ANY), + wx.NullBitmap, wx.ITEM_NORMAL, _("Close query"), wx.EmptyString, None) + toolbar.AddSeparator() + execute_statement = toolbar.AddTool(wx.ID_ANY, _("Execute"), wx.Bitmap("icons/16x16/arrow_right.png", wx.BITMAP_TYPE_ANY), + wx.NullBitmap, wx.ITEM_NORMAL, _("Execute"), wx.EmptyString, None) + execute_all = toolbar.AddTool(wx.ID_ANY, _("Execute all"), wx.Bitmap("icons/16x16/arrows_lefttoright.png", wx.BITMAP_TYPE_ANY), + wx.NullBitmap, wx.ITEM_NORMAL, _("Execute all statements"), wx.EmptyString, None) + toolbar.AddSeparator() + stop_statements = toolbar.AddTool(wx.ID_ANY, _("Stop"), wx.Bitmap("icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY), + wx.NullBitmap, wx.ITEM_NORMAL, _("Stop"), wx.EmptyString, None) + toolbar.AddSeparator() + save_query = toolbar.AddTool(wx.ID_ANY, _("Save"), wx.Bitmap("icons/16x16/disk.png", wx.BITMAP_TYPE_ANY), + wx.NullBitmap, wx.ITEM_NORMAL, _("Save"), wx.EmptyString, None) + toolbar.Realize() + + tool_ids = { + "new": new_query.GetId(), + "close": close_query.GetId(), + "execute": execute_statement.GetId(), + "execute_all": execute_all.GetId(), + "stop": stop_statements.GetId(), + "save": save_query.GetId(), + } + + self._apply_query_toolbar_shortcuts(toolbar, tool_ids) + + toolbar.Bind(wx.EVT_TOOL, self.on_new_query, id=new_query.GetId()) + toolbar.Bind(wx.EVT_TOOL, self.on_close_query, id=close_query.GetId()) + toolbar.Bind(wx.EVT_TOOL, self.on_execute_statement, id=execute_statement.GetId()) + toolbar.Bind(wx.EVT_TOOL, self.on_execute_statements, id=execute_all.GetId()) + toolbar.Bind(wx.EVT_TOOL, self.on_stop_statements, id=stop_statements.GetId()) + toolbar.Bind(wx.EVT_TOOL, self.on_save, id=save_query.GetId()) + return toolbar, tool_ids + + def _build_query_page(self) -> tuple[wx.Panel, wx.stc.StyledTextCtrl, wx.Window, wx.ToolBar, dict[str, int]]: + panel_query = wx.Panel(self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL) + query_sizer = wx.BoxSizer(wx.VERTICAL) + splitter = wx.SplitterWindow(panel_query, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_3D) + + panel_top = wx.Panel(splitter, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL) + top_sizer = wx.BoxSizer(wx.VERTICAL) + toolbar, tool_ids = self._build_query_toolbar(panel_top) + editor = self._build_query_editor(panel_top) + top_sizer.Add(toolbar, 0, wx.EXPAND, 5) + top_sizer.Add(editor, 1, wx.EXPAND | wx.ALL, 5) + panel_top.SetSizer(top_sizer) + + panel_bottom = wx.Panel(splitter, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL) + bottom_sizer = wx.BoxSizer(wx.VERTICAL) + results_notebook_class = self.notebook_sql_results.__class__ + results_notebook = results_notebook_class(panel_bottom, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0) + bottom_sizer.Add(results_notebook, 1, wx.EXPAND | wx.ALL, 5) + panel_bottom.SetSizer(bottom_sizer) + + splitter.SplitHorizontally(panel_top, panel_bottom, -300) + query_sizer.Add(splitter, 1, wx.EXPAND, 5) + panel_query.SetSizer(query_sizer) + return panel_query, editor, results_notebook, toolbar, tool_ids + + def _get_active_query_controller(self) -> Optional[QueryResultsController]: + page = self.MainFrameNotebook.GetCurrentPage() + if page is None: + return None + + meta = self._query_page_meta.get(page) + if meta is None: + return None + + return meta["controller"] + + def _register_query_page( + self, + panel: wx.Panel, + editor: wx.stc.StyledTextCtrl, + results_notebook: wx.Window, + toolbar: wx.ToolBar, + tool_ids: dict[str, int], + display_name: str, + ) -> None: + controller = QueryResultsController( + editor, + results_notebook, + cancel_button=None, + on_new_query=self.on_new_query, + on_close_query=self.on_close_query, + on_save_query=self.on_save, + on_save_as_query=self.on_save_as_query, + on_stop_state_changed=lambda enabled: self._set_query_stop_enabled(panel, enabled), + ) + self._query_pages.append(panel) + self._query_page_meta[panel] = { + "editor": editor, + "toolbar": toolbar, + "controller": controller, + "tool_ids": tool_ids, + "file_path": None, + "is_dirty": False, + "display_name": display_name, + } + self._bind_query_editor_events(panel, editor) + self._set_query_stop_enabled(panel, enabled=False) + + def _set_query_stop_enabled(self, page: wx.Panel, enabled: bool) -> None: + meta = self._query_page_meta.get(page) + if meta is None: + return + + toolbar = meta["toolbar"] + tool_ids = meta["tool_ids"] + toolbar.EnableTool(tool_ids["stop"], enabled) + toolbar.EnableTool(tool_ids["execute"], not enabled) + toolbar.EnableTool(tool_ids["execute_all"], not enabled) + + def _bind_query_editor_events(self, page: wx.Panel, editor: wx.stc.StyledTextCtrl) -> None: + editor.Bind(wx.stc.EVT_STC_CHANGE, lambda event: self._on_query_editor_changed(page, event)) + + def _on_query_editor_changed(self, page: wx.Panel, event: wx.Event) -> None: + self._set_query_dirty(page, is_dirty=True) + event.Skip() + + def _set_query_dirty(self, page: wx.Panel, is_dirty: bool) -> None: + meta = self._query_page_meta.get(page) + if meta is None: + return + + if meta["is_dirty"] == is_dirty: + return + + meta["is_dirty"] = is_dirty + self._update_query_page_title(page) + + def _update_query_page_title(self, page: wx.Panel) -> None: + meta = self._query_page_meta.get(page) + if meta is None: + return + + page_index = self.MainFrameNotebook.FindPage(page) + if page_index < 0: + return + + title = meta["display_name"] + if meta["is_dirty"]: + title = f"{title} *" + + self.MainFrameNotebook.SetPageText(page_index, title) + + def _setup_query_pages(self) -> None: + template_index = self.MainFrameNotebook.FindPage(self.QueryPanelTpl) + if template_index >= 0: + self.MainFrameNotebook.DeletePage(template_index) + + query_index = self.MainFrameNotebook.FindPage(self.panel_query) + self.MainFrameNotebook.SetPageText(query_index, _("Query (1)")) + + self._register_query_page( + panel=self.panel_query, + editor=self.sql_query_editor, + results_notebook=self.notebook_sql_results, + toolbar=self.m_toolBar2, + tool_ids={ + "new": self.new_query.GetId(), + "close": self.close_query.GetId(), + "execute": self.execute_statement.GetId(), + "execute_all": self.execute_all_statements.GetId(), + "stop": self.stop_statements.GetId(), + "save": self.save.GetId(), + }, + display_name=_("Query (1)"), + ) + + self._apply_query_toolbar_shortcuts(self.m_toolBar2, self._query_page_meta[self.panel_query]["tool_ids"]) + + self.controller_query_records = self._query_page_meta[self.panel_query]["controller"] + self._update_query_close_tools_state() + + def _update_query_close_tools_state(self) -> None: + can_close = len(self._query_pages) > 1 + for meta in self._query_page_meta.values(): + toolbar = meta["toolbar"] + close_query_tool_id = meta["tool_ids"]["close"] + toolbar.EnableTool(close_query_tool_id, can_close) + + def _create_new_query_page(self) -> None: + self._query_page_counter += 1 + label = _("Query ({query_number})").format(query_number=self._query_page_counter) + + panel, editor, results_notebook, toolbar, tool_ids = self._build_query_page() + self.MainFrameNotebook.AddPage(panel, label, select=True) + + self._register_query_page( + panel=panel, + editor=editor, + results_notebook=results_notebook, + toolbar=toolbar, + tool_ids=tool_ids, + display_name=label, + ) + + self._setup_sql_editor(editor) + self._update_query_close_tools_state() + + def _confirm_close_query_page(self, page: wx.Panel) -> bool: + meta = self._query_page_meta.get(page) + if meta is None or not meta["is_dirty"]: + return True + + result = wx.MessageDialog( + None, + message=_("You have unsaved changes. Save before closing?"), + caption=_("Unsaved query"), + style=wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION, + ).ShowModal() + + if result == wx.ID_YES: + return self._save_query_page(page, force_save_as=False) + + if result == wx.ID_NO: + return True + + return False + + def _close_active_query_page(self) -> None: + if len(self._query_pages) <= 1: + return + + page = self.MainFrameNotebook.GetCurrentPage() + if page is None or page not in self._query_page_meta: + return + + if not self._confirm_close_query_page(page): + return + + meta = self._query_page_meta.pop(page) + self._query_pages.remove(page) + + controller = meta["controller"] + controller.cancel_execution(wx.CommandEvent()) + + query_page_index = self.MainFrameNotebook.FindPage(page) + if query_page_index >= 0: + self.MainFrameNotebook.DeletePage(query_page_index) + + self._update_query_close_tools_state() + + active_controller = self._get_active_query_controller() + if active_controller is not None: + self.controller_query_records = active_controller + + def _ask_query_save_path(self, file_path: Optional[str] = None) -> Optional[str]: + default_dir = os.path.dirname(file_path) if file_path else os.getcwd() + default_name = os.path.basename(file_path) if file_path else "query.sql" + + dialog = wx.FileDialog( + self, + message=_("Save query"), + defaultDir=default_dir, + defaultFile=default_name, + wildcard=_("SQL files (*.sql)|*.sql|All files (*.*)|*.*"), + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, + ) + + if dialog.ShowModal() != wx.ID_OK: + return None + + return dialog.GetPath() + + @staticmethod + def _write_query_file(file_path: str, content: str) -> None: + with open(file_path, "w", encoding="utf-8") as file_obj: + file_obj.write(content) + + @staticmethod + def _get_query_autosave_path() -> str: + query_dir = os.path.join(os.getcwd(), ".queries") + os.makedirs(query_dir, exist_ok=True) + return os.path.join(query_dir, f"query_{time.strftime('%Y%m%d_%H%M%S')}_{time.time_ns()}.sql") + + def _save_query_page(self, page: wx.Panel, force_save_as: bool) -> bool: + meta = self._query_page_meta.get(page) + if meta is None: + return False + + file_path = meta["file_path"] + if force_save_as or not file_path: + file_path = self._ask_query_save_path(file_path) + if not file_path: + return False + + editor = meta["editor"] + + try: + self._write_query_file(file_path, editor.GetText()) + except Exception as ex: + logger.error(str(ex), exc_info=True) + wx.MessageDialog(None, str(ex), _("Error"), wx.OK | wx.ICON_ERROR).ShowModal() + return False + + meta["file_path"] = file_path + meta["display_name"] = os.path.basename(file_path) + self._set_query_dirty(page, is_dirty=False) + QUERY_LOGS.append(_("Saved query to {file_path}").format(file_path=file_path)) + return True + + def _autosave_query_page_before_execute(self, page: wx.Panel) -> bool: + meta = self._query_page_meta.get(page) + if meta is None: + return False + + if meta["file_path"] is not None: + return True + + editor = meta["editor"] + if not editor.GetText().strip(): + return True + + file_path = self._get_query_autosave_path() + try: + self._write_query_file(file_path, editor.GetText()) + except Exception as ex: + logger.error(str(ex), exc_info=True) + wx.MessageDialog(None, str(ex), _("Error"), wx.OK | wx.ICON_ERROR).ShowModal() + return False + + meta["file_path"] = file_path + self._set_query_dirty(page, is_dirty=False) + QUERY_LOGS.append(_("Autosaved query to {file_path}").format(file_path=file_path)) + return True def _setup_subscribers(self): self.toggle_panel() @@ -395,7 +790,7 @@ def _build_records_count_connection(self, session: Session) -> Connection: return connection def _format_records_number(self, value: int) -> str: - locale = wx.GetApp().settings.get_value("locale") or wx.GetApp().settings.get_value("language") or "en_US" + locale = wx.GetApp().settings.get_value("language", default="en_US") try: return babel.numbers.format_decimal(value, locale=locale) except Exception: @@ -403,17 +798,13 @@ def _format_records_number(self, value: int) -> str: def _load_records_limit_from_settings(self) -> int: settings = wx.GetApp().settings - if settings.get_value("records") is None: - settings.set_value("records", value={}) max_limit = 1000 if hasattr(self, "limit_records"): with contextlib.suppress(Exception): max_limit = max(1, int(self.limit_records.GetMax())) - saved_limit = settings.get_value("records", "limit") - if saved_limit is None: - return min(100, max_limit) + saved_limit = settings.get_value("records", "limit", default=100) try: return min(max(1, int(saved_limit)), max_limit) @@ -1064,8 +1455,63 @@ def on_apply_filters(self, event): self._records_offset = 0 self._load_records_page() + def on_new_query(self, event): + self._create_new_query_page() + + def on_close_query(self, event): + self._close_active_query_page() + + def on_save(self, event): + page = self.MainFrameNotebook.GetCurrentPage() + if page is None: + return + + self._save_query_page(page, force_save_as=False) + + def on_save_as_query(self, event): + page = self.MainFrameNotebook.GetCurrentPage() + if page is None: + return + + self._save_query_page(page, force_save_as=True) + + def on_execute_statement(self, event): + controller = self._get_active_query_controller() + if controller is not None: + page = self.MainFrameNotebook.GetCurrentPage() + if page is None: + return + + if not self._autosave_query_page_before_execute(page): + return + + self.controller_query_records = controller + controller.execute_current(event) + + def on_execute_statements(self, event): + controller = self._get_active_query_controller() + if controller is not None: + page = self.MainFrameNotebook.GetCurrentPage() + if page is None: + return + + if not self._autosave_query_page_before_execute(page): + return + + self.controller_query_records = controller + controller.execute_all(event) + + def on_stop_statements(self, event): + controller = self._get_active_query_controller() + if controller is not None: + self.controller_query_records = controller + controller.cancel_execution(event) + def on_cancel_query_execution(self, event): - self.controller_query_records.cancel_execution(event) + controller = self._get_active_query_controller() + if controller is not None: + self.controller_query_records = controller + controller.cancel_execution(event) # def on_clear_record(self, event): # self.controller_list_table_records.on_row_clear() diff --git a/windows/main/tabs/query.py b/windows/main/tabs/query.py index 4a696b4..e68e5d1 100644 --- a/windows/main/tabs/query.py +++ b/windows/main/tabs/query.py @@ -11,6 +11,7 @@ import wx import wx.dataview +from helpers.loader import Loader from helpers.logger import logger from helpers.dataview import BaseDataViewListModel @@ -18,6 +19,7 @@ from structures.connection import Connection, ConnectionEngine from structures.engines.datatype import DataTypeCategory, SQLDataType +from windows.components.stc.autocomplete.statement_extractor import StatementExtractor from windows.components.popup import PopupCalendar, PopupCalendarTime from windows.components.renders import AdvancedTextRenderer, FloatRenderer, IntegerRenderer, PopupRenderer, TextRenderer, TimeRenderer from windows.components.dataview import QueryEditorResultsDataViewCtrl @@ -77,94 +79,10 @@ def __init__(self, engine: ConnectionEngine): self.engine = engine def parse(self, sql_text: str) -> list[ParsedStatement]: - if not sql_text.strip(): - return [] - - statements = [] - statement_index = 0 - current_start = 0 - i = 0 - length = len(sql_text) - - in_single_quote = False - in_double_quote = False - in_line_comment = False - in_block_comment = False - - while i < length: - char = sql_text[i] - - if in_line_comment: - if char == '\n': - in_line_comment = False - i += 1 - continue - - if in_block_comment: - if i + 1 < length and sql_text[i:i + 2] == '*/': - in_block_comment = False - i += 2 - continue - i += 1 - continue - - if not in_single_quote and not in_double_quote: - if self._is_line_comment_start(sql_text, i): - in_line_comment = True - i += 2 - continue - - if self._is_block_comment_start(sql_text, i): - in_block_comment = True - i += 2 - continue - - if char == "'" and not in_double_quote: - if i + 1 < length and sql_text[i + 1] == "'": - i += 2 - continue - in_single_quote = not in_single_quote - - elif char == '"' and not in_single_quote: - if i + 1 < length and sql_text[i + 1] == '"': - i += 2 - continue - in_double_quote = not in_double_quote - - elif char == ';' and not in_single_quote and not in_double_quote: - statement_text = sql_text[current_start:i].strip() - if statement_text: - statements.append(ParsedStatement( - text=statement_text, - start_pos=current_start, - end_pos=i, - statement_index=statement_index - )) - statement_index += 1 - current_start = i + 1 - - i += 1 - - final_statement = sql_text[current_start:].strip() - if final_statement: - statements.append(ParsedStatement( - text=final_statement, - start_pos=current_start, - end_pos=length, - statement_index=statement_index - )) - - return statements - - def _is_line_comment_start(self, text: str, pos: int) -> bool: - if pos + 1 >= len(text): - return False - return text[pos:pos + 2] in ('--', '# ') - - def _is_block_comment_start(self, text: str, pos: int) -> bool: - if pos + 1 >= len(text): - return False - return text[pos:pos + 2] == '/*' + return [ + ParsedStatement(text=text, start_pos=start, end_pos=end, statement_index=i) + for i, (text, start, end) in enumerate(StatementExtractor.extract_all_statements(sql_text)) + ] class StatementSelector: @@ -260,28 +178,29 @@ def _execute_worker( summary = ExecutionSummary(total_statements=len(statements)) try: - context = self._create_worker_context(current_database) - self._set_worker_context(context) + with Loader.cursor_wait(): + context = self._create_worker_context(current_database) + self._set_worker_context(context) - for stmt in statements: - if self._cancel_requested: - summary.cancelled = True - break + for stmt in statements: + if self._cancel_requested: + summary.cancelled = True + break - summary.last_statement = stmt - result = self._execute_single(context, stmt) + summary.last_statement = stmt + result = self._execute_single(context, stmt) - if result.success: - summary.completed_statements += 1 - summary.successful_statements += 1 - elif not result.cancelled: - summary.completed_statements += 1 - summary.failed_statements += 1 + if result.success: + summary.completed_statements += 1 + summary.successful_statements += 1 + elif not result.cancelled: + summary.completed_statements += 1 + summary.failed_statements += 1 - self._dispatch_statement_result(on_statement_complete, result) + self._dispatch_statement_result(on_statement_complete, result) - if not result.success and stop_on_error: - break + if not result.success and stop_on_error: + break except Exception as ex: logger.error(f"Execution worker error: {ex}", exc_info=True) @@ -379,7 +298,20 @@ def _build_worker_connection(self) -> Connection: def _create_worker_context(self, current_database: Optional[Any]) -> Any: context = self.session._get_context_class()(self._build_worker_connection()) - context.connect() + + if self.session.engine == ConnectionEngine.POSTGRESQL: + connect_kwargs = { + "skip_before_connect": True, + "skip_after_connect": True, + } + + if current_database is not None and hasattr(current_database, "name"): + connect_kwargs["database"] = current_database.name + + context.connect(**connect_kwargs) + return context + + context.connect(skip_before_connect=True, skip_after_connect=True) if current_database is not None: with contextlib.suppress(Exception): @@ -694,44 +626,113 @@ def __init__( session_provider: Callable[[], Optional[Session]], database_provider: Optional[Callable[[], Optional[Any]]] = None, cancel_button: Optional[wx.Button] = None, + on_new_query: Optional[Callable[[wx.Event], None]] = None, + on_close_query: Optional[Callable[[wx.Event], None]] = None, + on_save_query: Optional[Callable[[wx.Event], None]] = None, + on_save_as_query: Optional[Callable[[wx.Event], None]] = None, + on_stop_state_changed: Optional[Callable[[bool], None]] = None, ): self.editor = stc_editor self.notebook = results_notebook self.get_session = session_provider self.get_database = database_provider or (lambda: None) self.cancel_button = cancel_button + self.on_new_query = on_new_query + self.on_close_query = on_close_query + self.on_save_query = on_save_query + self.on_save_as_query = on_save_as_query + self.on_stop_state_changed = on_stop_state_changed self.parser: Optional[SQLStatementParser] = None self.selector = StatementSelector(stc_editor) self.executor: Optional[QueryExecutor] = None self.renderer: Optional[QueryResultsRenderer] = None self._cancel_feedback_pending = False + self._shortcuts = self._load_shortcuts() self._bind_shortcuts() + self._set_cancel_button_enabled(False) + + def _load_shortcuts(self) -> dict[str, str]: + settings = wx.GetApp().settings + return { + "execute_current": settings.get_value("ui", "shortcuts", "query", "execute_current", default="Ctrl+Enter"), + "execute_all": settings.get_value("ui", "shortcuts", "query", "execute_all", default="Ctrl+Shift+Enter"), + "stop": settings.get_value("ui", "shortcuts", "query", "stop", default="Esc"), + "new_query": settings.get_value("ui", "shortcuts", "query", "new_query", default="Ctrl+T"), + "close_query": settings.get_value("ui", "shortcuts", "query", "close_query", default="Ctrl+W"), + "save": settings.get_value("ui", "shortcuts", "query", "save", default="Ctrl+S"), + "save_as": settings.get_value("ui", "shortcuts", "query", "save_as", default="Ctrl+Shift+S"), + } + + def get_shortcuts(self) -> dict[str, str]: + return dict(self._shortcuts) + + def _matches_shortcut(self, event: wx.KeyEvent, shortcut: str) -> bool: + parts = [part.strip().lower() for part in shortcut.split("+") if part.strip()] + if not parts: + return False + + key_name = parts[-1] + modifiers = set(parts[:-1]) + + if event.ControlDown() != ("ctrl" in modifiers): + return False + + if event.ShiftDown() != ("shift" in modifiers): + return False + + if event.AltDown() != ("alt" in modifiers): + return False + + key_code = event.GetKeyCode() + return self._matches_shortcut_key(key_name, key_code) + + @staticmethod + def _matches_shortcut_key(key_name: str, key_code: int) -> bool: + if key_name == "enter": + return key_code in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER] + + if key_name == "esc": + return key_code == wx.WXK_ESCAPE + + if len(key_name) == 1: + return key_code == ord(key_name.upper()) + + return False def _bind_shortcuts(self) -> None: self.editor.Bind(wx.EVT_KEY_DOWN, self._on_key_down) def _on_key_down(self, event: wx.KeyEvent) -> None: - key_code = event.GetKeyCode() - ctrl_down = event.ControlDown() - shift_down = event.ShiftDown() - - if key_code == wx.WXK_F5: - if shift_down: - self.cancel_execution(event) - else: - self.execute_all(event) + if self._matches_shortcut(event, self._shortcuts["execute_current"]): + self.execute_current(event) return - if ctrl_down and key_code == wx.WXK_RETURN: - self.execute_current(event) + if self._matches_shortcut(event, self._shortcuts["execute_all"]): + self.execute_all(event) return - if ctrl_down and shift_down and key_code == ord('C'): + if self._matches_shortcut(event, self._shortcuts["stop"]): self.cancel_execution(event) return + if self._matches_shortcut(event, self._shortcuts["new_query"]) and self.on_new_query is not None: + self.on_new_query(event) + return + + if self._matches_shortcut(event, self._shortcuts["close_query"]) and self.on_close_query is not None: + self.on_close_query(event) + return + + if self._matches_shortcut(event, self._shortcuts["save_as"]) and self.on_save_as_query is not None: + self.on_save_as_query(event) + return + + if self._matches_shortcut(event, self._shortcuts["save"]) and self.on_save_query is not None: + self.on_save_query(event) + return + event.Skip() def execute_all(self, event: wx.Event) -> None: @@ -800,6 +801,9 @@ def _set_cancel_button_enabled(self, enabled: bool) -> None: if self.cancel_button is not None: self.cancel_button.Enable(enabled) + if self.on_stop_state_changed is not None: + self.on_stop_state_changed(enabled) + def _format_elapsed(self, elapsed_ms: float) -> str: if elapsed_ms < 1000: return _("{elapsed_ms:.0f} ms").format(elapsed_ms=elapsed_ms) @@ -846,6 +850,11 @@ def __init__( stc_sql_query: wx.stc.StyledTextCtrl, notebook_sql_results: wx.Notebook, cancel_button: Optional[wx.Button] = None, + on_new_query: Optional[Callable[[wx.Event], None]] = None, + on_close_query: Optional[Callable[[wx.Event], None]] = None, + on_save_query: Optional[Callable[[wx.Event], None]] = None, + on_save_as_query: Optional[Callable[[wx.Event], None]] = None, + on_stop_state_changed: Optional[Callable[[bool], None]] = None, ): from windows.main import CURRENT_DATABASE, CURRENT_SESSION # Lazy import: unavoidable circular dependency. @@ -855,4 +864,9 @@ def __init__( session_provider=lambda: CURRENT_SESSION.get_value(), database_provider=lambda: CURRENT_DATABASE.get_value(), cancel_button=cancel_button, + on_new_query=on_new_query, + on_close_query=on_close_query, + on_save_query=on_save_query, + on_save_as_query=on_save_as_query, + on_stop_state_changed=on_stop_state_changed, ) diff --git a/windows/views.py b/windows/views.py index 7771316..570cb8e 100755 --- a/windows/views.py +++ b/windows/views.py @@ -19,6 +19,7 @@ import wx.stc import wx.lib.agw.hypertreelist import wx.aui +import wx.adv import gettext _ = gettext.gettext @@ -2250,12 +2251,26 @@ def __init__( self, parent ): self.m_panel52 = wx.Panel( self.m_splitter6, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer125 = wx.BoxSizer( wx.VERTICAL ) - self.cancel_query_execution = wx.Button( self.m_panel52, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.m_toolBar2 = wx.ToolBar( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL ) + self.new_query = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"New query"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"New query"), wx.EmptyString, None ) - self.cancel_query_execution.SetBitmap( wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ) ) - self.cancel_query_execution.Enable( False ) + self.close_query = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Close query"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Close query"), wx.EmptyString, None ) + + self.m_toolBar2.AddSeparator() + + self.execute_statement = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Execute"), wx.Bitmap( u"icons/16x16/arrow_right.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Execute"), wx.EmptyString, None ) + + self.execute_all_statements = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Execute all"), wx.Bitmap( u"icons/16x16/arrows_lefttoright.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Execute all statements"), wx.EmptyString, None ) + + self.stop_statements = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Stop"), wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Stop"), wx.EmptyString, None ) - bSizer125.Add( self.cancel_query_execution, 0, wx.ALIGN_RIGHT|wx.ALL, 5 ) + self.m_toolBar2.AddSeparator() + + self.save = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"tool"), wx.Bitmap( u"icons/16x16/disk.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + + self.m_toolBar2.Realize() + + bSizer125.Add( self.m_toolBar2, 0, wx.EXPAND, 5 ) self.sql_query_editor = wx.stc.StyledTextCtrl( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0) self.sql_query_editor.SetUseTabs ( True ) @@ -2464,7 +2479,12 @@ def __init__( self, parent ): self.chb_auto_apply.Bind( wx.EVT_CHECKBOX, self.on_auto_apply ) 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.cancel_query_execution.Bind( wx.EVT_BUTTON, self.on_cancel_query_execution ) + self.Bind( wx.EVT_TOOL, self.on_new_query, id = self.new_query.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_close_query, id = self.close_query.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_execute_statement, id = self.execute_statement.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_execute_statements, id = self.execute_all_statements.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_stop_statements, id = self.stop_statements.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_save, id = self.save.GetId() ) def __del__( self ): pass @@ -2574,7 +2594,22 @@ def on_collapsible_pane_changed( self, event ): def on_apply_filters( self, event ): event.Skip() - def on_cancel_query_execution( self, event ): + def on_new_query( self, event ): + event.Skip() + + def on_close_query( self, event ): + event.Skip() + + def on_execute_statement( self, event ): + event.Skip() + + def on_execute_statements( self, event ): + event.Skip() + + def on_stop_statements( self, event ): + event.Skip() + + def on_save( self, event ): event.Skip() def m_splitter51OnIdle( self, event ): @@ -2620,6 +2655,13 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. self.m_textCtrl221 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) bSizer90.Add( self.m_textCtrl221, 1, wx.ALL|wx.EXPAND, 5 ) + self.cancel_query_execution = wx.Button( self, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + + self.cancel_query_execution.SetBitmap( wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ) ) + self.cancel_query_execution.Enable( False ) + + bSizer90.Add( self.cancel_query_execution, 0, wx.ALIGN_RIGHT|wx.ALL, 5 ) + self.total_rows_loading = wx.StaticBitmap( self, wx.ID_ANY, wx.Bitmap( u"icons/16x16/hourglass.png", wx.BITMAP_TYPE_ANY ), wx.DefaultPosition, wx.DefaultSize, 0 ) bSizer90.Add( self.total_rows_loading, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) @@ -2966,6 +3008,7 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. # Connect Events + self.cancel_query_execution.Bind( wx.EVT_BUTTON, self.on_cancel_query_execution ) self.Bind( wx.EVT_MENU, self.on_import, id = self.m_menuItem10.GetId() ) self.tree_ctrl_sessions.Bind( wx.EVT_TREE_ITEM_RIGHT_CLICK, self.show_tree_ctrl_menu ) @@ -2974,6 +3017,9 @@ def __del__( self ): # Virtual event handlers, override them in your derived class + def on_cancel_query_execution( self, event ): + event.Skip() + def on_import( self, event ): event.Skip() @@ -3479,3 +3525,81 @@ def panel_table_columnsOnContextMenu( self, event ): self.panel_table_columns.PopupMenu( self.menu_table_columns, event.GetPosition() ) +########################################################################### +## Class MyWizard1 +########################################################################### + +class MyWizard1 ( wx.adv.Wizard ): + + def __init__( self, parent ): + wx.adv.Wizard.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, bitmap = wx.NullBitmap, pos = wx.DefaultPosition, style = wx.DEFAULT_DIALOG_STYLE ) + + self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) + self.m_pages = [] + + self.Centre( wx.BOTH ) + + + def __del__( self ): + pass + + +########################################################################### +## Class SaveStatments +########################################################################### + +class SaveStatments ( wx.Dialog ): + + def __init__( self, parent ): + wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Save Starments"), pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.DEFAULT_DIALOG_STYLE ) + + self.SetSizeHints( wx.Size( 320,200 ), wx.DefaultSize ) + + bSizer163 = wx.BoxSizer( wx.VERTICAL ) + + bSizer164 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText86 = wx.StaticText( self, wx.ID_ANY, _(u"Location"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText86.Wrap( -1 ) + + self.m_staticText86.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer164.Add( self.m_staticText86, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + self.m_filePicker5 = wx.FilePickerCtrl( self, wx.ID_ANY, wx.EmptyString, _(u"Select a file"), _(u"*.sql"), wx.DefaultPosition, wx.DefaultSize, wx.FLP_DEFAULT_STYLE|wx.FLP_SAVE|wx.FLP_SMALL|wx.FLP_USE_TEXTCTRL ) + bSizer164.Add( self.m_filePicker5, 1, wx.ALL, 5 ) + + + bSizer163.Add( bSizer164, 0, wx.EXPAND, 5 ) + + + bSizer163.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + + self.m_staticline7 = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) + bSizer163.Add( self.m_staticline7, 0, wx.EXPAND | wx.ALL, 5 ) + + bSizer165 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_button57 = wx.Button( self, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer165.Add( self.m_button57, 0, wx.ALL, 5 ) + + + bSizer165.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + + self.m_button58 = wx.Button( self, wx.ID_ANY, _(u"Save"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer165.Add( self.m_button58, 0, wx.ALL, 5 ) + + + bSizer163.Add( bSizer165, 0, wx.EXPAND, 5 ) + + + self.SetSizer( bSizer163 ) + self.Layout() + bSizer163.Fit( self ) + + self.Centre( wx.BOTH ) + + def __del__( self ): + pass + + From e040e2e7b6521a2a4320c8d079091b6bd3cf8496 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 16 Mar 2026 16:06:35 +0100 Subject: [PATCH 14/93] docs: merge ROADMAP into PROJECT_STATUS and update progress AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- PROJECT_STATUS.md | 160 +++++++++++++++++++++++++++------------------- ROADMAP.md | 152 ------------------------------------------- 2 files changed, 95 insertions(+), 217 deletions(-) delete mode 100644 ROADMAP.md diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 7c0e50c..7c15d9b 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -1,44 +1,33 @@ # PeterSQL — Project Status -> **Last Updated:** 2026-03-10 -> **Validation Policy:** new engine features are marked **PARTIAL** until broader integration validation is complete. +> **Last Updated:** 2026-03-16 +> **Status Rule:** newly implemented features are tracked as **PARTIAL** until validated across supported versions. +> **Definition of DONE:** engine methods implemented, integration tests pass on target versions, UI workflow exists (if user-facing), no known regressions, documentation updated. --- -## 1. Executive Summary +## Priority Matrix -### ✅ Solid and Stable Areas +| Priority | Focus | Target | +|----------|-------|--------| +| 🔴 **P0 - Validation Now** | stabilize newly added engine features | 1-2 weeks | +| 🟡 **P1 - Engine Gaps** | close remaining CRUD parity gaps | 2-4 weeks | +| 🟢 **P2 - UI Completeness** | add missing editors for exposed objects | 1-2 months | +| 🔵 **P3 - Advanced Features** | schema/security/import-export roadmap | 2-3 months | + +--- + +## 1. Solid and Stable Areas | Area | Status | |------|--------| | **SQLite Engine** | Most mature path with complete day-to-day table/record workflows. | | **MySQL/MariaDB Core** | Strong parity for tables, columns, indexes, foreign keys, records, views, triggers, functions. | | **UI Core Editors** | Table, columns, indexes, foreign keys, records, and view editor are operational. | +| **Multi-tab Query Editor** | Multi-tab editor with per-tab dirty tracking, autosave, cancelable execution, and configurable shortcuts. | | **Explorer Navigation** | Databases, tables, views, triggers, procedures, functions, and events are visible in tree explorer. | | **SSH Tunnel Support** | Implemented for MySQL, MariaDB, and PostgreSQL. | -### 🟡 Partial / Under Validation - -| Area | Current State | -|------|---------------| -| **MySQL Procedure** | Class + CRUD methods exist, context introspection exists, integration tests added, broader validation still ongoing. | -| **MariaDB Procedure** | Class + CRUD methods exist, context introspection exists, integration tests added, broader validation still ongoing. | -| **PostgreSQL Function** | Class + CRUD methods exist, context introspection exists, integration tests now cover create/alter/drop across supported PG versions; broader validation still ongoing. | -| **PostgreSQL Procedure** | Class + CRUD methods exist, context introspection exists, integration tests now cover create/alter/drop across supported PG versions; broader validation still ongoing. | -| **Check Constraints (MySQL/MariaDB/PostgreSQL)** | Engine classes and introspection exist, cross-version validation still needed. | -| **Connection Reliability Features** | Persistent connection statistics, empty DB password support, and TLS auto-retry are implemented and need longer real-world validation. | -| **SQL Dump / Backup** | `SQLDatabase.dump()` produces object-driven `.sql` dumps (schema + records); restore/import workflow is still missing. | -| **Database Lifecycle (Create/Drop)** | Engine database objects expose lifecycle methods, but context/UI workflow parity is still incomplete. | - -### ❌ Missing / Not Implemented - -| Area | Notes | -|------|-------| -| **Function/Procedure UI Editors** | Explorer lists objects, but dedicated create/edit UI is still missing. | -| **Database Create/Drop UI** | No complete create/drop workflow across engines. | -| **Schema/Sequence Management** | PostgreSQL schema/sequence CRUD is not available. | -| **User/Role/Grants** | Not implemented for any engine. | - --- ## 2. Engine Capability Matrix @@ -112,68 +101,109 @@ | Table / Column / Index / Foreign Key | ✅ | ✅ | ✅ | ✅ | Main table editor workflow complete. | | Check Constraint | ✅ | 🟡 | 🟡 | 🟡 | `TableCheckController` exists; broader multi-engine UX validation pending. | | View | ✅ | ✅ | ✅ | ✅ | Dedicated view editor is available. | -| Trigger | ✅ | ❌ | ❌ | ❌ | Explorer only; no dedicated trigger editor panel yet. | +| Trigger | ✅ | ❌ | ❌ | ❌ | Explorer only; no dedicated editor yet. | | Function | ✅ | ❌ | ❌ | ❌ | Explorer only; no dedicated editor yet. | | Procedure | ✅ | ❌ | ❌ | ❌ | Explorer only; no dedicated editor yet. | | Event | ✅ | ❌ | ❌ | ❌ | Explorer only. | | Records | ✅ | ✅ | ✅ | ✅ | Insert/update/delete/duplicate in table records tab. | +| Query Editor | ✅ | ✅ | ✅ | ✅ | Multi-tab, cancelable execution, configurable shortcuts, autosave. | --- -## 4. Cross-Cutting Notes +## 4. Feature Backlog -### Recently Added +### 🔴 P0 - Validation Now -- Persistent connection statistics in connection model and dialog. -- Empty database password accepted in connection validation. -- Automatic TLS retry path for MySQL/MariaDB when server requires TLS. -- Unit reliability coverage for MySQL/MariaDB TLS auto-retry and SSH tunnel lifecycle contracts. -- SQL dump/backup pipeline is now object-driven via `SQLDatabase.dump()` + per-object `raw_create()`. -- CI workflow split into `test`, `update` (nightly), and `release` jobs. +- [x] **PostgreSQL Function engine implementation** (PARTIAL) + - **Files:** `structures/engines/postgresql/database.py`, `structures/engines/postgresql/context.py` + - **Next:** long-run/manual workflow validation + broader regression checks. -### Main Remaining Risks +- [x] **PostgreSQL Procedure engine implementation** (PARTIAL) + - **Files:** `structures/engines/postgresql/database.py`, `structures/engines/postgresql/context.py` + - **Next:** long-run/manual workflow validation + introspection consistency checks. -- PostgreSQL Function/Procedure now have integration coverage for create/alter/drop, but still need broader long-run/manual validation. -- Check constraints across MySQL/MariaDB/PostgreSQL need more cross-version coverage. -- SQL dump/backup still needs broader cross-engine manual restore validation. -- SSH tunnel integration validation with testcontainers remains blocked (existing SSH integration suites are still skipped). -- UI parity lags engine parity for Trigger/Function/Procedure editors. +- [x] **Check constraint implementations for MySQL/MariaDB/PostgreSQL** (PARTIAL) + - **Files:** `structures/engines/mysql/`, `structures/engines/mariadb/`, `structures/engines/postgresql/` + - **Next:** cross-version validation matrix. ---- +- [x] **Connection reliability updates** (PARTIAL) + - **Scope:** persistent connection statistics, empty DB password support, TLS auto-retry (MySQL/MariaDB). + - **Files:** `structures/connection.py`, `windows/dialogs/connections/` + - **Next:** SSH testcontainers integration validation (currently skipped) + long-run behavioral validation. + +- [x] **SQL dump/backup object-driven flow** (PARTIAL) + - **Scope:** `SQLDatabase.dump()` generates SQL dump files through domain objects (`raw_create()`). + - **Files:** `structures/engines/database.py`, `structures/engines/dump.py`, per-engine `database.py` + - **Next:** cross-engine manual restore/import verification from produced dumps. -## 5. Actionable Backlog (High Signal) +### 🟡 P1 - Engine Gaps -### Priority A — Validate Newly Implemented Features +- [x] **MySQL Procedure implementation** (PARTIAL) + - **Files:** `structures/engines/mysql/context.py`, `structures/engines/mysql/database.py` -1. PostgreSQL Function/Procedure long-run validation (manual workflows + regression suites after integration coverage). -2. Check constraints validation matrix for MySQL, MariaDB, PostgreSQL. -3. Connection statistics + TLS auto-retry robustness checks. +- [x] **MariaDB Procedure implementation** (PARTIAL) + - **Files:** `structures/engines/mariadb/context.py`, `structures/engines/mariadb/database.py` -### Priority B — Close Engine Gaps +- [ ] **Database lifecycle parity (context + UI wiring)** + - **Current state:** engine database objects expose create/alter/drop, but context/UI workflow remains read/list oriented. + - **Files:** `structures/engines/*/context.py` -1. Complete context/UI wiring for database lifecycle (create/drop) across engines. +### 🟢 P2 - UI Completeness -### Priority C — UI Completeness +- [x] **View Create/Edit Dialog** — DONE. (`windows/main/tabs/view.py`, `helpers/sql.py`) +- [x] **Multi-tab query editor** — DONE. (`windows/main/controller.py`, `windows/main/tabs/query.py`) +- [x] **Cancelable query execution with richer result metadata** — DONE. (`windows/main/tabs/query.py`) +- [x] **Configurable keyboard shortcuts for query editor** — DONE. (`windows/main/controller.py`, `windows/dialogs/settings/`) +- [ ] **Trigger Create/Edit UI** — Explorer visibility exists, editor panel missing. +- [ ] **Function Create/Edit UI** — Explorer visibility exists, editor panel missing. +- [ ] **Procedure Create/Edit UI** — Explorer visibility exists, editor panel missing. +- [ ] **Database Create/Drop UI** — Depends on engine create/drop API parity. -1. Trigger create/edit UI. -2. Function create/edit UI. -3. Procedure create/edit UI. +### 🔵 P3 - Advanced Features -### Priority D — Future Platform Features +- [ ] PostgreSQL schema CRUD +- [ ] PostgreSQL sequence CRUD +- [ ] User/role management +- [ ] Privileges/grants management +- [ ] Restore + structured import/export workflows +- [ ] PostgreSQL advanced objects (materialized views, partitioning, extensions) -1. PostgreSQL schema CRUD. -2. PostgreSQL sequence CRUD. -3. User/role/grants management. -4. Restore and structured import/export workflows. +--- + +## 5. Progress Snapshot + +- **P0 implemented (partial):** 5/5 +- **P1 gaps closed:** 2/3 +- **P2 UI tasks complete:** 4/8 +- **P3 advanced tasks complete:** 0/6 + +--- + +## 6. Recently Added + +- Multi-tab query editor with per-tab dirty tracking, autosave before execution, and close/save confirmation dialogs. +- Cancelable query execution with background thread, per-statement result rendering, and execution summary. +- Configurable keyboard shortcuts for all query editor actions (execute, stop, new tab, close tab, save, save-as). +- `save` toolbar tool is disabled when the query has no unsaved changes. +- Settings module moved to `helpers/settings.py` with restructured key schema. +- `skip_before_connect` / `skip_after_connect` support added to all engine contexts. +- Persistent connection statistics in connection model and dialog. +- Empty database password accepted in connection validation. +- Automatic TLS retry path for MySQL/MariaDB when server requires TLS. +- Unit reliability coverage for MySQL/MariaDB TLS auto-retry and SSH tunnel lifecycle contracts. +- SQL dump/backup pipeline is now object-driven via `SQLDatabase.dump()` + per-object `raw_create()`. +- CI workflow split into `test`, `update` (nightly), and `release` jobs. --- -## 6. Definition of DONE +## 7. Main Remaining Risks -A capability is treated as **DONE** only when: +- PostgreSQL Function/Procedure still need broader long-run/manual validation. +- Check constraints across MySQL/MariaDB/PostgreSQL need more cross-version coverage. +- SQL dump/backup still needs broader cross-engine manual restore validation. +- SSH tunnel integration validation with testcontainers remains blocked. +- UI parity lags engine parity for Trigger/Function/Procedure editors. + +--- -- Engine methods are implemented (`create/read/update/delete` where applicable). -- Integration tests pass on target engine versions. -- UI workflow exists (if feature is user-facing in explorer). -- No known regression in existing suites. -- Documentation is updated (`README`, `PROJECT_STATUS`, `ROADMAP`). +*This document is a living reference and should be updated whenever a PARTIAL item is validated or a new gap is identified.* \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 9096d56..0000000 --- a/ROADMAP.md +++ /dev/null @@ -1,152 +0,0 @@ -# PeterSQL — Development Roadmap - -> **Last Updated:** 2026-03-10 -> **Status Rule:** newly implemented features are tracked as **PARTIAL** until validated across supported versions. - ---- - -## 🎯 Overview - -This roadmap reflects the current project state and separates: - -1. features already implemented but still under validation, -2. true implementation gaps, -3. UI parity work. - ---- - -## 📊 Priority Matrix - -| Priority | Focus | Target | -|----------|-------|--------| -| 🔴 **P0 - Validation Now** | stabilize newly added engine features | 1-2 weeks | -| 🟡 **P1 - Engine Gaps** | close remaining CRUD parity gaps | 2-4 weeks | -| 🟢 **P2 - UI Completeness** | add missing editors for exposed objects | 1-2 months | -| 🔵 **P3 - Advanced Features** | schema/security/import-export roadmap | 2-3 months | - ---- - -## 🔴 P0 - Validation Now - -### Implemented recently (still PARTIAL) - -- [x] **PostgreSQL Function engine implementation** (PARTIAL) - - **Files:** `structures/engines/postgresql/database.py`, `structures/engines/postgresql/context.py` - - **Status:** integration coverage for create/alter/drop is now in place across supported PostgreSQL variants. - - **Next:** long-run/manual workflow validation + broader regression checks. - -- [x] **PostgreSQL Procedure engine implementation** (PARTIAL) - - **Files:** `structures/engines/postgresql/database.py`, `structures/engines/postgresql/context.py` - - **Status:** integration coverage for create/alter/drop is now in place across supported PostgreSQL variants. - - **Next:** long-run/manual workflow validation + introspection consistency checks. - -- [x] **Check constraint implementations for MySQL/MariaDB/PostgreSQL** (PARTIAL) - - **Files:** - - `structures/engines/mysql/database.py`, `structures/engines/mysql/context.py` - - `structures/engines/mariadb/database.py`, `structures/engines/mariadb/context.py` - - `structures/engines/postgresql/database.py`, `structures/engines/postgresql/context.py` - - **Next:** cross-version validation matrix. - -- [x] **Connection reliability updates** (PARTIAL) - - **Scope:** persistent connection statistics, empty DB password support, TLS auto-retry (MySQL/MariaDB). - - **Files:** - - `structures/connection.py` - - `windows/dialogs/connections/model.py` - - `windows/dialogs/connections/view.py` - - **Validation status:** unit tests now cover connection statistics updates, MySQL/MariaDB TLS retry behavior, and SSH tunnel context lifecycle contracts. - - **Next:** unblock and run SSH testcontainers integration validation (currently skipped) + long-run behavioral validation. - -- [x] **SQL dump/backup object-driven flow** (PARTIAL) - - **Scope:** `SQLDatabase.dump()` now generates SQL dump files through domain objects (`raw_create()`), with ordered schema + records sections. - - **Files:** - - `structures/engines/database.py` - - `structures/engines/dump.py` - - `structures/engines/mysql/database.py` - - `structures/engines/mariadb/database.py` - - `structures/engines/postgresql/database.py` - - `structures/engines/sqlite/database.py` - - **Validation status:** unit suite is green in serial and xdist runs. - - **Next:** cross-engine manual restore/import verification from produced dumps. - ---- - -## 🟡 P1 - Engine Gaps - -- [x] **MySQL Procedure implementation** (PARTIAL) - - **Status:** Engine CRUD + introspection implemented, integration tests added. - - **Files:** `structures/engines/mysql/context.py`, `structures/engines/mysql/database.py`, `tests/engines/mysql/test_integration_suite.py`, `tests/engines/base_procedure_tests.py` - -- [x] **MariaDB Procedure implementation** (PARTIAL) - - **Status:** Engine CRUD + introspection implemented, integration tests added. - - **Files:** `structures/engines/mariadb/context.py`, `structures/engines/mariadb/database.py`, `tests/engines/mariadb/test_integration_suite.py`, `tests/engines/base_procedure_tests.py` - -- [ ] **Database lifecycle parity (context + UI wiring)** - - **Current state:** engine database objects expose create/alter/drop, but context/UI workflow remains read/list oriented. - - **Files:** `structures/engines/*/context.py` - ---- - -## 🟢 P2 - UI Completeness - -- [x] **View Create/Edit Dialog** - - **Status:** DONE - - **Files:** `windows/main/tabs/view.py`, `helpers/sql.py` - -- [ ] **Trigger Create/Edit UI** - - **Current state:** explorer visibility exists, editor panel missing. - -- [ ] **Function Create/Edit UI** - - **Current state:** explorer visibility exists, editor panel missing. - -- [ ] **Procedure Create/Edit UI** - - **Current state:** explorer visibility exists, editor panel missing. - -- [ ] **Database Create/Drop UI** - - **Dependency:** engine create/drop API parity. - ---- - -## 🔵 P3 - Advanced Features - -- [ ] PostgreSQL schema CRUD -- [ ] PostgreSQL sequence CRUD -- [ ] User/role management -- [ ] Privileges/grants management -- [ ] Restore + structured import/export workflows -- [ ] PostgreSQL advanced objects (materialized views, partitioning, extensions) - ---- - -## 🛠️ Validation and Completion Criteria - -Before moving a PARTIAL item to DONE: - -- [ ] Integration tests pass on supported versions. -- [ ] Behavior is stable in repeated manual workflows. -- [ ] No regressions in current engine/UI suites. -- [ ] Documentation is aligned (`README`, `PROJECT_STATUS`, `ROADMAP`). - ---- - -## 📈 Progress Snapshot - -### Current Status - -- **P0 implemented (partial):** 5/5 -- **P1 gaps closed:** 2/3 -- **P2 UI tasks complete:** 1/5 -- **P3 advanced tasks complete:** 0/6 - -### Recent Highlights - -- PostgreSQL Function and Procedure engine classes added. -- PostgreSQL Function/Procedure integration tests now include ALTER coverage (create/alter/drop). -- Check constraint support added for MySQL, MariaDB, PostgreSQL. -- Connection statistics and TLS auto-retry behavior added in connection manager. -- SQL dump/backup pipeline refactored to object-driven generation (`SQLDatabase.dump()` + `raw_create()`). -- SSH tunnel unit contract tests added for context lifecycle and process stop behavior. -- CI workflow split into test/nightly-update/release lanes. - ---- - -*This roadmap is a living document and should be updated whenever a PARTIAL item is validated or a new gap is identified.* From 079dd56750365a460754e886b9f3e97cbacbc62f Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 17 Mar 2026 09:19:52 +0100 Subject: [PATCH 15/93] refactor(autocomplete): remove dead code and fix typing violations AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- tests/autocomplete/test_autocomplete_basic.py | 16 --- .../stc/autocomplete/autocomplete_popup.py | 10 +- .../stc/autocomplete/context_detector.py | 127 ------------------ .../stc/autocomplete/query_scope.py | 4 +- .../dialogs/advanced_cell_editor/__init__.py | 3 - windows/dialogs/column_content/__init__.py | 3 + .../controller.py | 6 +- windows/main/controller.py | 11 ++ 8 files changed, 25 insertions(+), 155 deletions(-) delete mode 100644 windows/dialogs/advanced_cell_editor/__init__.py create mode 100644 windows/dialogs/column_content/__init__.py rename windows/dialogs/{advanced_cell_editor => column_content}/controller.py (94%) diff --git a/tests/autocomplete/test_autocomplete_basic.py b/tests/autocomplete/test_autocomplete_basic.py index 582062f..445db85 100644 --- a/tests/autocomplete/test_autocomplete_basic.py +++ b/tests/autocomplete/test_autocomplete_basic.py @@ -91,8 +91,6 @@ def test_empty_context(): assert "INSERT" in item_names assert "UPDATE" in item_names - print("✓ GT-010 EMPTY context test passed") - def test_single_token(): database = create_mock_database() @@ -109,8 +107,6 @@ def test_single_token(): item_names = [item.name for item in result.items] assert "SELECT" in item_names - print("✓ GT-011 SINGLE_TOKEN test passed") - def test_select_without_from(): database = create_mock_database() @@ -128,8 +124,6 @@ def test_select_without_from(): assert "SUM" in item_names assert "*" in item_names - print("✓ GT-020 SELECT without FROM test passed") - def test_select_with_from(): database = create_mock_database() @@ -148,8 +142,6 @@ def test_select_with_from(): assert "users.name" in item_names assert "COUNT" in item_names - print("✓ GT-021 SELECT with FROM test passed") - def test_where_basic(): database = create_mock_database() @@ -168,8 +160,6 @@ def test_where_basic(): assert "name" in item_names assert "COUNT" in item_names - print("✓ GT-030 WHERE basic test passed") - def test_from_clause(): database = create_mock_database() @@ -186,8 +176,6 @@ def test_from_clause(): assert "users" in item_names assert "orders" in item_names - print("✓ FROM clause test passed") - def test_dot_completion(): database = create_mock_database() @@ -208,8 +196,6 @@ def test_dot_completion(): for name in item_names: assert "users." not in name - print("✓ GT-002 Dot completion test passed") - def test_multi_query(): database = create_mock_database() @@ -230,8 +216,6 @@ def test_multi_query(): assert "id" in item_names assert "user_id" in item_names - print("✓ GT-001 Multi-query test passed") - if __name__ == "__main__": test_empty_context() diff --git a/windows/components/stc/autocomplete/autocomplete_popup.py b/windows/components/stc/autocomplete/autocomplete_popup.py index a60db3e..f77396e 100644 --- a/windows/components/stc/autocomplete/autocomplete_popup.py +++ b/windows/components/stc/autocomplete/autocomplete_popup.py @@ -1,3 +1,5 @@ +from typing import Callable, Optional + import wx import wx.dataview @@ -11,7 +13,7 @@ def __init__(self, parent: wx.Window, settings: object = None, theme_loader: The self._selected_index: int = 0 self._items: list[CompletionItem] = [] - self._on_item_selected: callable = None + self._on_item_selected: Optional[Callable] = None self._settings = settings self._theme_loader = theme_loader @@ -156,10 +158,10 @@ def _complete_with_item(self, item: CompletionItem) -> None: self._on_item_selected(item) self.Hide() - def set_on_item_selected(self, callback: callable) -> None: + def set_on_item_selected(self, callback: Callable) -> None: self._on_item_selected = callback - - def get_selected_item(self) -> CompletionItem: + + def get_selected_item(self) -> Optional[CompletionItem]: row = self._list_ctrl.GetFirstSelected() if row != wx.NOT_FOUND and row < len(self._items): return self._items[row] diff --git a/windows/components/stc/autocomplete/context_detector.py b/windows/components/stc/autocomplete/context_detector.py index fdee062..487d10a 100644 --- a/windows/components/stc/autocomplete/context_detector.py +++ b/windows/components/stc/autocomplete/context_detector.py @@ -2,9 +2,6 @@ from typing import Optional -import sqlglot -from sqlglot import exp - from helpers.logger import logger from windows.components.stc.autocomplete.query_scope import ( @@ -349,64 +346,6 @@ def _extract_from_qualifier(self, left_text: str) -> Optional[str]: return table_name - def _extract_scope_from_select( - self, parsed: exp.Select, database: Optional[SQLDatabase] - ) -> QueryScope: - from_tables = [] - join_tables = [] - aliases = {} - - if from_clause := parsed.args.get("from"): - if isinstance(from_clause, exp.From): - for table_exp in from_clause.find_all(exp.Table): - table_name = table_exp.name - alias = ( - table_exp.alias - if hasattr(table_exp, "alias") and table_exp.alias - else None - ) - - table_obj = ( - self._find_table_in_database(table_name, database) - if database - else None - ) - ref = TableReference(name=table_name, alias=alias, table=table_obj) - from_tables.append(ref) - - if alias: - aliases[alias.lower()] = ref - aliases[table_name.lower()] = ref - - for join_exp in parsed.find_all(exp.Join): - if table_exp := join_exp.this: - if isinstance(table_exp, exp.Table): - table_name = table_exp.name - alias = ( - table_exp.alias - if hasattr(table_exp, "alias") and table_exp.alias - else None - ) - - table_obj = ( - self._find_table_in_database(table_name, database) - if database - else None - ) - ref = TableReference(name=table_name, alias=alias, table=table_obj) - join_tables.append(ref) - - if alias: - aliases[alias.lower()] = ref - aliases[table_name.lower()] = ref - - return QueryScope( - from_tables=from_tables, - join_tables=join_tables, - current_table=None, - aliases=aliases, - ) - def _extract_scope_from_text( self, text: str, database: Optional[SQLDatabase] ) -> QueryScope: @@ -693,51 +632,6 @@ def _find_table_in_database( pass return None - def _is_in_where(self, text: str) -> bool: - upper = text.upper() - where_pos = upper.rfind("WHERE") - if where_pos == -1: - return False - - after_where = upper[where_pos:] - return ( - "ORDER BY" not in after_where - and "GROUP BY" not in after_where - and "LIMIT" not in after_where - ) - - def _is_after_from(self, text: str) -> bool: - upper = text.upper() - from_pos = upper.rfind("FROM") - if from_pos == -1: - return False - - after_from = upper[from_pos + 4 :].strip() - return len(after_from) == 0 or ( - len(after_from) > 0 and after_from[-1] in [" ", "\n", "\t"] - ) - - def _is_after_on(self, text: str) -> bool: - upper = text.upper() - on_pos = upper.rfind(" ON ") - if on_pos == -1: - return False - - after_on = upper[on_pos + 4 :].strip() - return len(after_on) == 0 or ( - len(after_on) > 0 - and not after_on.endswith(("WHERE", "ORDER", "GROUP", "LIMIT")) - ) - - def _is_after_order_by(self, text: str) -> bool: - upper = text.upper() - order_by_pos = upper.rfind("ORDER BY") - if order_by_pos == -1: - return False - - after_order_by = upper[order_by_pos + 8 :].strip() - return "LIMIT" not in after_order_by - def _is_after_limit_number( self, left_text: str, limit_pos: int, prefix: str ) -> bool: @@ -794,24 +688,3 @@ def _is_after_order_by_column( return True return False - def _is_after_group_by(self, text: str) -> bool: - upper = text.upper() - group_by_pos = upper.rfind("GROUP BY") - if group_by_pos == -1: - return False - - after_group_by = upper[group_by_pos + 8 :].strip() - return ( - "HAVING" not in after_group_by - and "ORDER BY" not in after_group_by - and "LIMIT" not in after_group_by - ) - - def _is_in_having(self, text: str) -> bool: - upper = text.upper() - having_pos = upper.rfind("HAVING") - if having_pos == -1: - return False - - after_having = upper[having_pos:] - return "ORDER BY" not in after_having and "LIMIT" not in after_having diff --git a/windows/components/stc/autocomplete/query_scope.py b/windows/components/stc/autocomplete/query_scope.py index c2a0814..55961c9 100644 --- a/windows/components/stc/autocomplete/query_scope.py +++ b/windows/components/stc/autocomplete/query_scope.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field -from typing import Optional +from typing import Optional, Union from structures.engines.database import SQLTable @@ -20,7 +20,7 @@ class VirtualTable: class TableReference: name: str alias: Optional[str] = None - table: Optional[SQLTable | VirtualTable] = None + table: Optional[Union[SQLTable, VirtualTable]] = None @dataclass diff --git a/windows/dialogs/advanced_cell_editor/__init__.py b/windows/dialogs/advanced_cell_editor/__init__.py deleted file mode 100644 index c844cb4..0000000 --- a/windows/dialogs/advanced_cell_editor/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from windows.dialogs.advanced_cell_editor.controller import AdvancedCellEditorController - -__all__ = ["AdvancedCellEditorController"] diff --git a/windows/dialogs/column_content/__init__.py b/windows/dialogs/column_content/__init__.py new file mode 100644 index 0000000..689438b --- /dev/null +++ b/windows/dialogs/column_content/__init__.py @@ -0,0 +1,3 @@ +from windows.dialogs.column_content.controller import ColumnContentDialogController + +__all__ = ["ColumnContentDialogController"] \ No newline at end of file diff --git a/windows/dialogs/advanced_cell_editor/controller.py b/windows/dialogs/column_content/controller.py similarity index 94% rename from windows/dialogs/advanced_cell_editor/controller.py rename to windows/dialogs/column_content/controller.py index db2a036..b09d799 100644 --- a/windows/dialogs/advanced_cell_editor/controller.py +++ b/windows/dialogs/column_content/controller.py @@ -5,10 +5,10 @@ from windows.components.stc.detectors import detect_syntax_id from windows.components.stc.profiles import SyntaxProfile from windows.components.stc.styles import apply_stc_theme -from windows.views import AdvancedCellEditorDialog +from windows.views import ColumnContentDialog -class AdvancedCellEditorController(AdvancedCellEditorDialog): +class ColumnContentDialogController(ColumnContentDialog): app = wx.GetApp() def __init__(self, parent, value: str, read_only: bool = False): @@ -82,4 +82,4 @@ def _replace_text_undo_friendly(self, new_text: str): self.advanced_stc_editor.EndUndoAction() def get_value(self) -> Optional[str]: - return self.advanced_stc_editor.GetText() + return self.advanced_stc_editor.GetText() \ No newline at end of file diff --git a/windows/main/controller.py b/windows/main/controller.py index a5d0244..f2d5eb5 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -310,6 +310,16 @@ def _register_query_page( } self._bind_query_editor_events(panel, editor) self._set_query_stop_enabled(panel, enabled=False) + self._set_query_save_enabled(panel, enabled=False) + + def _set_query_save_enabled(self, page: wx.Panel, enabled: bool) -> None: + meta = self._query_page_meta.get(page) + if meta is None: + return + + toolbar = meta["toolbar"] + tool_ids = meta["tool_ids"] + toolbar.EnableTool(tool_ids["save"], enabled) def _set_query_stop_enabled(self, page: wx.Panel, enabled: bool) -> None: meta = self._query_page_meta.get(page) @@ -339,6 +349,7 @@ def _set_query_dirty(self, page: wx.Panel, is_dirty: bool) -> None: meta["is_dirty"] = is_dirty self._update_query_page_title(page) + self._set_query_save_enabled(page, enabled=is_dirty) def _update_query_page_title(self, page: wx.Panel) -> None: meta = self._query_page_meta.get(page) From 01f6e5f4b4228eda888f0027e1ad4bd05380af07 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 17 Mar 2026 09:24:51 +0100 Subject: [PATCH 16/93] feat(database): update action buttons state based on option changes AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- windows/main/controller.py | 74 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/windows/main/controller.py b/windows/main/controller.py index f2d5eb5..59f7252 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -3,6 +3,7 @@ import threading import time import contextlib +import dataclasses from collections import defaultdict from gettext import gettext as _ @@ -99,6 +100,8 @@ def __init__(self): self._setup_query_editors() + self._setup_database_action_buttons_bindings() + self._setup_subscribers() # Memory update timer @@ -108,6 +111,75 @@ def __init__(self): self.Bind(wx.EVT_SYS_COLOUR_CHANGED, self.on_sys_colour_changed) + def _setup_database_action_buttons_bindings(self) -> None: + model = self.controller_database_options.model + + database_observables = [ + model.database_name, + model.database_character_set, + model.database_collation, + model.database_encryption, + model.database_read_only, + model.database_tablespace, + model.database_connection_limit, + model.database_password, + model.database_profile, + model.database_default_tablespace, + model.database_temporary_tablespace, + model.database_quota, + model.database_unlimited_quota, + model.database_account_status, + model.database_password_expire, + ] + + for observable in database_observables: + observable.subscribe(self._on_database_options_changed) + + CURRENT_SESSION.subscribe(self._on_database_options_changed) + + def _on_database_options_changed(self, _=None) -> None: + self._update_database_action_buttons() + + @staticmethod + def _database_has_changes(database: Optional[SQLDatabase]) -> bool: + if database is None: + return False + + if database.is_new: + return True + + session = CURRENT_SESSION.get_value() + if session is None: + return False + + original_database = next( + (db for db in session.context.databases.get_value() if db.id == database.id), + None, + ) + + if original_database is None: + return True + + for field in dataclasses.fields(database): + if not field.compare: + continue + + if getattr(database, field.name, None) != getattr(original_database, field.name, None): + return True + + return False + + def _update_database_action_buttons(self) -> None: + database = CURRENT_DATABASE.get_value() + + has_database = database is not None + has_changes = self._database_has_changes(database) + is_persisted = bool(database is not None and not database.is_new) + + self.btn_apply_database.Enable(has_database and has_changes) + self.btn_cancel_database.Enable(is_persisted and has_changes) + self.btn_delete_database.Enable(is_persisted) + def on_sys_colour_changed(self, event): self._setup_query_editors() @@ -1038,6 +1110,8 @@ def _on_current_session(self, session: Session): def _on_current_database(self, database: SQLDatabase): self.toggle_panel(database) + self._update_database_action_buttons() + if database: self.table_engine.Enable(len(database.context.ENGINES) > 1) self.table_engine.SetItems(database.context.ENGINES) From e4926b30f2f2cec74441babda66be070cb8a6679 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 17 Mar 2026 09:26:10 +0100 Subject: [PATCH 17/93] refactor(records): replace advanced cell editor with column content dialog AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- PeterSQL.fbp | 5901 +------------------------------- windows/components/dataview.py | 4 +- windows/main/tabs/records.py | 4 +- windows/views.py | 615 +--- 4 files changed, 69 insertions(+), 6455 deletions(-) diff --git a/PeterSQL.fbp b/PeterSQL.fbp index d6ddad7..c9e09d0 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -5966,12 +5966,12 @@ wxID_ANY 640,480 - AdvancedCellEditorDialog + ColumnContentDialog 900,550 wxDEFAULT_DIALOG_STYLE ; ; forward_declare - Edit Value + Column content 0 @@ -6371,7 +6371,7 @@ - + 0 @@ -6583,16 +6583,16 @@ - + bSizer19 wxVERTICAL none - + 0 wxEXPAND | wxALL 1 - + 1 1 1 @@ -6644,16 +6644,16 @@ wxTAB_TRAVERSAL - + bSizer21 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -6711,8 +6711,8 @@ - - + + 1 1 1 @@ -6764,16 +6764,16 @@ wxTAB_TRAVERSAL - + bSizer72 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -7001,8 +7001,8 @@ - - + + 1 1 1 @@ -7054,16 +7054,16 @@ wxTAB_TRAVERSAL - + bSizer25 wxVERTICAL none - + 5 wxALL|wxEXPAND 1 - + 1 1 1 @@ -7327,11 +7327,11 @@ - + Load From File; icons/16x16/database.png Database - 0 - + 1 + 1 1 1 @@ -7383,16 +7383,16 @@ wxTAB_TRAVERSAL - + bSizer27 wxVERTICAL none - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -7446,11 +7446,11 @@ - + Options 0 - + 1 1 1 @@ -7502,16 +7502,16 @@ wxTAB_TRAVERSAL - + bSizer80 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -7543,7 +7543,7 @@ 0 - 0 + 200 0 @@ -7557,7 +7557,7 @@ Resizable 0.0 - -1 + 200 -1 1 @@ -7569,8 +7569,8 @@ - - + + 1 1 1 @@ -7622,7 +7622,7 @@ wxTAB_TRAVERSAL - + bSizer158 wxVERTICAL @@ -10836,7 +10836,7 @@ 0 Left 0 - 1 + 0 1 @@ -10911,7 +10911,7 @@ 0 Left 0 - 1 + 0 1 @@ -10986,7 +10986,7 @@ 0 Left 0 - 1 + 0 1 @@ -11295,11 +11295,11 @@ - + Load From File; icons/16x16/table.png Table 0 - + 1 1 1 @@ -11351,16 +11351,16 @@ wxTAB_TRAVERSAL - + bSizer251 wxVERTICAL none - + 0 wxEXPAND 1 - + 1 1 1 @@ -14207,11 +14207,11 @@ - + Load From File; icons/16x16/view.png Views 0 - + 1 1 1 @@ -17707,7 +17707,7 @@ Load From File; icons/16x16/arrow_right.png Query - 1 + 0 1 1 @@ -18789,5815 +18789,6 @@ wxTAB_TRAVERSAL - - - bSizer90 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl221 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALIGN_RIGHT|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 - cancel_query_execution - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_cancel_query_execution - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - Load From File; icons/16x16/hourglass.png - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - total_rows_loading - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer93 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - tree_ctrl_explorer____ - 1 - - - protected - 1 - - Resizable - 1 - - wxTL_DEFAULT_STYLE - ; ; forward_declare - 0 - - - - - - wxALIGN_LEFT - wxCOL_RESIZABLE - Column5 - wxCOL_WIDTH_DEFAULT - - - - - 5 - wxEXPAND - 1 - - - bSizer129 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - UNDEFINED - - 0 - - - 0 - - 1 - m_radioBtn11 - 1 - - - protected - 1 - - Resizable - 1 - - wxRB_GROUP - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - MERGE - - 0 - - - 0 - - 1 - m_radioBtn21 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - MyMenu - m_menu13 - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Import - m_menuItem10 - none - - - on_import - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - TEMPTABLE - - 0 - - - 0 - - 1 - m_radioBtn31 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Algorithm - 0 - - 0 - - - 0 - - 1 - m_staticText4011 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 1 - - 2 - wxBOTH - - - 0 - - fgSizer1 - wxFLEX_GROWMODE_NONE - none - 3 - 0 - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Read only - - 0 - - - 0 - - 1 - m_checkBox7 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - "UNDEFINED" "MERGE" "TEMPTABLE" - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Algorithm - 1 - - 0 - - - 0 - - 1 - rad_view_algorithm - 1 - - - protected - 1 - - Resizable - 0 - 1 - - wxRA_SPECIFY_COLS - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - "None" "LOCAL" "CASCADED" "CHECK OPTION" "READ ONLY" - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - View constraint - 1 - - 0 - - - 0 - -1,-1 - 1 - rad_view_constraint - 1 - - - protected - 1 - - Resizable - 0 - 1 - - wxRA_SPECIFY_COLS - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - MyMenu - m_menu15 - protected - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl10 - 1 - - - protected - 1 - - Resizable - 1 - - wxTE_MULTILINE|wxTE_RICH|wxTE_RICH2 - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl361 - 1 - - - protected - 1 - - Resizable - 1 - - wxTE_PASSWORD - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - notebook_sql_results - 1 - - - protected - 1 - - Resizable - 1 - - wxAUI_NB_DEFAULT_STYLE|wxAUI_NB_MIDDLE_CLICK_CLOSE - ; ; forward_declare - -1 - 0 - - - - - - - - - 5 - wxALIGN_CENTER|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - ssh_tunnel_password1 - 1 - - - protected - 1 - - Resizable - 1 - - wxTE_PASSWORD - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_encryption_old - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - 0 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - collapsible - - 0 - - - 0 - - 1 - m_collapsiblePane2 - 1 - - - protected - 1 - - Resizable - 1 - - wxCP_DEFAULT_STYLE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - tree_ctrl_sessions - 1 - - - protected - 1 - - Resizable - 1 - - wxTR_DEFAULT_STYLE|wxTR_FULL_ROW_HIGHLIGHT|wxTR_HAS_BUTTONS|wxTR_HIDE_ROOT|wxTR_TWIST_BUTTONS - ; ; forward_declare - 0 - - - - - show_tree_ctrl_menu - - MyMenu - m_menu12 - protected - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_treeListCtrl3 - 1 - - - protected - 1 - - Resizable - 1 - - wxTL_DEFAULT_STYLE - ; ; forward_declare - 0 - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - tree_ctrl_sessions1 - 1 - - - protected - 1 - - Resizable - 1 - - wxTL_DEFAULT_STYLE - ; ; forward_declare - 0 - - - - - - wxALIGN_LEFT - wxCOL_RESIZABLE - Column3 - wxCOL_WIDTH_DEFAULT - - - wxALIGN_LEFT - wxCOL_RESIZABLE - Column4 - wxCOL_WIDTH_DEFAULT - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - table_collationdd - 1 - - - protected - 1 - - Resizable - 1 - - - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl21 - 1 - - - protected - 1 - - Resizable - 1 - - wxTE_MULTILINE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 0 - wxEXPAND - 0 - - - bSizer51 - wxVERTICAL - none - - 0 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - panel_credentials - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer48 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_notebook8 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - - - - - MyMenu - m_menu3 - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - MyMenuItem - m_menuItem3 - none - - - - - - - - 0 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 1 - wxID_ANY - - 0 - - - 0 - - 1 - panel_source - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer52 - wxVERTICAL - none - - 0 - wxEXPAND - 0 - - - bSizer1212 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Filename - 0 - - 0 - - - 0 - -1,-1 - 1 - m_staticText212 - 1 - - - protected - 1 - - Resizable - 1 - 150,-1 - - - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - Select a file - - 0 - - 1 - filename - 1 - - - protected - 1 - - Resizable - 1 - - wxFLP_CHANGE_DIR|wxFLP_USE_TEXTCTRL - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - Database (*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3 - - - - - - - - - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Port - 0 - - 0 - - - 0 - -1,-1 - 1 - m_staticText2211 - 1 - - - protected - 1 - - Resizable - 1 - 150,-1 - - - 0 - - - - - -1 - - - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - ssh_tunnel_port - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALIGN_CENTER|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - ssh_tunnel_local_port - 1 - - - protected - 1 - - Resizable - 1 - - - - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 1 - wxID_ANY - - 0 - - - 0 - - 1 - tree_ctrl_sessions2 - 1 - - - protected - 1 - - Resizable - 1 - - wxTR_DEFAULT_STYLE - ; ; forward_declare - 0 - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 1 - wxID_ANY - - 0 - - - 0 - - 1 - tree_ctrl_sessions_bkp3 - 1 - - - protected - 1 - - Resizable - 1 - - wxTL_DEFAULT_STYLE|wxTL_SINGLE - ; ; forward_declare - 0 - - - - - - wxALIGN_LEFT - wxCOL_RESIZABLE - Name - wxCOL_WIDTH_DEFAULT - - - wxALIGN_LEFT - wxCOL_RESIZABLE - Usage - wxCOL_WIDTH_DEFAULT - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 1 - wxID_ANY - - - tree_ctrl_sessions_bkp - protected - - - wxDV_SINGLE - ; ; forward_declare - - - - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Database - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn1 - protected - IconText - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Size - wxDATAVIEW_CELL_INERT - 1 - m_dataViewColumn3 - protected - Progress - 50 - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - %(total_rows)s - 0 - - 0 - - - 0 - - 1 - rows_database_table - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - rows total - 0 - - 0 - - - 0 - - 1 - m_staticText44 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 0 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - ____list_ctrl_database_tables - protected - - - - ; ; forward_declare - - - - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn5 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn6 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn7 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn8 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn9 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn10 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn11 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn20 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn21 - protected - Text - -1 - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - ___list_ctrl_database_tables - protected - - - - - - - - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Name - wxDATAVIEW_CELL_INERT - m_dataViewListColumn6 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Lines - wxDATAVIEW_CELL_INERT - m_dataViewListColumn7 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Size - wxDATAVIEW_CELL_INERT - m_dataViewListColumn8 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Created at - wxDATAVIEW_CELL_INERT - m_dataViewListColumn9 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Updated at - wxDATAVIEW_CELL_INERT - m_dataViewListColumn10 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Engine - wxDATAVIEW_CELL_INERT - m_dataViewListColumn11 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE - Comments - wxDATAVIEW_CELL_INERT - m_dataViewListColumn12 - protected - Text - -1 - - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_gauge1 - 1 - - - protected - 1 - - 100 - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxEXPAND - 0 - - 0 - protected - 150 - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 1 - wxID_ANY - - - tree_ctrl_explorer__ - protected - - - - ; ; forward_declare - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_vlistBox1 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_listBox1 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer871 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Temporary - 0 - - 0 - - - 0 - - 1 - m_staticText401 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - - 0 - - - 0 - - 1 - m_checkBox5 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - 0 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Engine options - - 0 - - - 0 - - 1 - m_collapsiblePane3 - 1 - - - protected - 1 - - Resizable - 1 - - wxCP_DEFAULT_STYLE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - bSizer115 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel41 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel42 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel43 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl2211 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl2212 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_comboBox11 - 1 - - - protected - 1 - - Resizable - -1 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxEXPAND - 1 - - 2 - 0 - - gSizer3 - none - 0 - 0 - - 5 - wxEXPAND - 1 - - - bSizer8712 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Algorithm - 0 - - 0 - - - 0 - - 1 - m_staticText4012 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - UNDEFINED - - 0 - - - 0 - - 1 - m_radioBtn1 - 1 - - - protected - 1 - - Resizable - 1 - - wxRB_GROUP - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - MERGE - - 0 - - - 0 - - 1 - m_radioBtn2 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - TEMPTABLE - - 0 - - - 0 - - 1 - m_radioBtn3 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer12211 - wxHORIZONTAL - none - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - RadioBtn - - 0 - - - 0 - - 1 - m_radioBtn10 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - - - - - 5 - wxEXPAND - 0 - - - bSizer86 - wxHORIZONTAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel44 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - Select a file - - 0 - - 1 - filename1 - 1 - - - protected - 1 - - Resizable - 1 - - wxFLP_CHANGE_DIR|wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - *.* - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl351 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - 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 - Encryption - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText701 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - - MyMenu - m_menu11 - protected - - - - 0 - wxAUI_MGR_DEFAULT - - - 1 - 0 - 1 - impl_virtual - - - 0 - wxID_ANY - - - MyPanel1 - - 500,300 - ; ; forward_declare - - 0 - - - wxTAB_TRAVERSAL - - - 0 - wxAUI_MGR_DEFAULT - - wxBOTH - - 1 - 0 - 1 - impl_virtual - - - - 0 - wxID_ANY - - - EditColumnView - - 600,600 - wxDEFAULT_DIALOG_STYLE|wxSTAY_ON_TOP - - Edit Column - - 1 - - - - - - bSizer98 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - - bSizer52 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Name - 0 - - 0 - - - 0 - - 1 - m_staticText26 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - wxST_NO_AUTORESIZE - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - column_name - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Datatype - 0 - - 0 - - - 0 - - 1 - m_staticText261 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - column_datatype - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer5211 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Length/Set - 0 - - 0 - - - 0 - - 1 - m_staticText2611 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxEXPAND - 1 - - - bSizer60 - wxHORIZONTAL - none - - 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 - column_set - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - 0 - 65536 - - 0 - - 0 - - 0 - - 1 - column_length - 1 - - - protected - 1 - - Resizable - 1 - - wxSP_ARROW_KEYS - ; ; forward_declare - 0 - - - - - - - - - 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 - 65536 - - 0 - - 0 - - 0 - - 1 - column_scale - 1 - - - protected - 1 - - Resizable - 1 - - wxSP_WRAP - ; ; forward_declare - 0 - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Collation - 0 - - 0 - - - 0 - - 1 - m_staticText261111112 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - column_collation - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer52111 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Unsigned - - 0 - - - 0 - - 1 - column_unsigned - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Allow NULL - - 0 - - - 0 - - 1 - column_allow_null - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 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 - Zero Fill - - 0 - - - 0 - - 1 - column_zero_fill - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - - - 5 - 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 - Default - 0 - - 0 - - - 0 - - 1 - m_staticText26111111 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - column_default - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer531 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Comments - 0 - - 0 - - - 0 - - 1 - m_staticText261111111 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - column_comments - 1 - - - protected - 1 - - Resizable - 1 - -1,100 - wxTE_MULTILINE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer532 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Virtuality - 0 - - 0 - - - 0 - - 1 - m_staticText261111113 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - column_virtuality - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer5311 - 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 - Expression - 0 - - 0 - - - 0 - - 1 - m_staticText2611111111 - 1 - - - protected - 1 - - Resizable - 1 - 100,-1 - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - column_expression - 1 - - - protected - 1 - - Resizable - 1 - -1,100 - wxTE_MULTILINE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 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_staticline2 - 1 - - - protected - 1 - - Resizable - 1 - - wxLI_HORIZONTAL - ; ; forward_declare - 0 - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer64 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/cancel.png - - 1 - 0 - 1 - - 1 - - 1 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Cancel - - 0 - - 0 - - - 0 - - 1 - m_button16 - 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 - - - Load From File; icons/16x16/disk.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Save - - 0 - - 0 - - - 0 - - 1 - m_button15 - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 0 diff --git a/windows/components/dataview.py b/windows/components/dataview.py index 37ea226..e0b1d43 100644 --- a/windows/components/dataview.py +++ b/windows/components/dataview.py @@ -330,8 +330,8 @@ def __init__(self, *args, **kwargs): CURRENT_TABLE.subscribe(self._load_table) def make_advanced_dialog(self, parent, value: str, read_only : bool = False): - from windows.dialogs.advanced_cell_editor import AdvancedCellEditorController - return AdvancedCellEditorController(parent, value, read_only) + from windows.dialogs.column_content import ColumnContentDialogController + return ColumnContentDialogController(parent, value, read_only) def _get_column_renderer(self, column: SQLColumn) -> wx.dataview.DataViewRenderer: for foreign_key in column.table.foreign_keys: diff --git a/windows/main/tabs/records.py b/windows/main/tabs/records.py index 669e476..1c21b16 100644 --- a/windows/main/tabs/records.py +++ b/windows/main/tabs/records.py @@ -15,7 +15,7 @@ from windows.views import TableRecordsDataViewCtrl -from windows.dialogs.advanced_cell_editor import AdvancedCellEditorController +from windows.dialogs.column_content import ColumnContentDialogController from windows.main import CURRENT_TABLE, CURRENT_SESSION, CURRENT_DATABASE, AUTO_APPLY, CURRENT_RECORDS @@ -166,7 +166,7 @@ def _on_selection_changed(self, event: wx.dataview.DataViewEvent): event.Skip() def make_advanced_dialog(self, parent, value: str): - dialog = AdvancedCellEditorController(parent, value) + dialog = ColumnContentDialogController(parent, value) return dialog diff --git a/windows/views.py b/windows/views.py index 570cb8e..72adb78 100755 --- a/windows/views.py +++ b/windows/views.py @@ -18,7 +18,6 @@ import wx.dataview import wx.stc import wx.lib.agw.hypertreelist -import wx.aui import wx.adv import gettext @@ -772,13 +771,13 @@ def __del__( self ): ########################################################################### -## Class AdvancedCellEditorDialog +## Class ColumnContentDialog ########################################################################### -class AdvancedCellEditorDialog ( wx.Dialog ): +class ColumnContentDialog ( wx.Dialog ): def __init__( self, parent ): - wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Edit Value"), pos = wx.DefaultPosition, size = wx.Size( 900,550 ), style = wx.DEFAULT_DIALOG_STYLE ) + wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Column content"), pos = wx.DefaultPosition, size = wx.Size( 900,550 ), style = wx.DEFAULT_DIALOG_STYLE ) self.SetSizeHints( wx.Size( 640,480 ), wx.DefaultSize ) @@ -996,6 +995,9 @@ def __init__( self, parent ): bSizer80 = wx.BoxSizer( wx.VERTICAL ) self.m_splitter7 = wx.SplitterWindow( self.m_panel30, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_3D ) + self.m_splitter7.Bind( wx.EVT_IDLE, self.m_splitter7OnIdle ) + self.m_splitter7.SetMinimumPaneSize( 200 ) + self.m_panel54 = wx.Panel( self.m_splitter7, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer158 = wx.BoxSizer( wx.VERTICAL ) @@ -1368,18 +1370,24 @@ def __init__( self, parent ): self.m_panel55.SetSizer( bSizer154 ) self.m_panel55.Layout() bSizer154.Fit( self.m_panel55 ) - self.m_splitter7.SplitHorizontally( self.m_panel54, self.m_panel55, -1 ) + self.m_splitter7.SplitHorizontally( self.m_panel54, self.m_panel55, 200 ) bSizer80.Add( self.m_splitter7, 1, wx.EXPAND, 5 ) bSizer138 = wx.BoxSizer( wx.HORIZONTAL ) self.btn_cancel_database = wx.Button( self.m_panel30, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btn_cancel_database.Enable( False ) + bSizer138.Add( self.btn_cancel_database, 0, wx.ALL, 5 ) self.btn_delete_database = wx.Button( self.m_panel30, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btn_delete_database.Enable( False ) + bSizer138.Add( self.btn_delete_database, 0, wx.ALL, 5 ) self.btn_apply_database = wx.Button( self.m_panel30, wx.ID_ANY, _(u"Apply"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btn_apply_database.Enable( False ) + bSizer138.Add( self.btn_apply_database, 0, wx.ALL, 5 ) @@ -1431,7 +1439,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 ) @@ -2337,7 +2345,7 @@ def __init__( self, parent ): self.panel_query.SetSizer( bSizer26 ) self.panel_query.Layout() bSizer26.Fit( self.panel_query ) - self.MainFrameNotebook.AddPage( self.panel_query, _(u"Query"), True ) + self.MainFrameNotebook.AddPage( self.panel_query, _(u"Query"), False ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/arrow_right.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2623,6 +2631,10 @@ def m_splitter4OnIdle( self, event ): def m_panel14OnContextMenu( self, event ): self.m_panel14.PopupMenu( self.m_menu5, event.GetPosition() ) + def m_splitter7OnIdle( self, event ): + self.m_splitter7.SetSashPosition( 200 ) + self.m_splitter7.Unbind( wx.EVT_IDLE ) + def panel_databaseOnContextMenu( self, event ): self.panel_database.PopupMenu( self.m_menu15, event.GetPosition() ) @@ -2650,595 +2662,6 @@ class Trash ( wx.Panel ): def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.Size( 500,300 ), style = wx.TAB_TRAVERSAL, name = wx.EmptyString ): wx.Panel.__init__ ( self, parent, id = id, pos = pos, size = size, style = style, name = name ) - bSizer90 = wx.BoxSizer( wx.VERTICAL ) - - self.m_textCtrl221 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.m_textCtrl221, 1, wx.ALL|wx.EXPAND, 5 ) - - self.cancel_query_execution = wx.Button( self, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.cancel_query_execution.SetBitmap( wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ) ) - self.cancel_query_execution.Enable( False ) - - bSizer90.Add( self.cancel_query_execution, 0, wx.ALIGN_RIGHT|wx.ALL, 5 ) - - self.total_rows_loading = wx.StaticBitmap( self, wx.ID_ANY, wx.Bitmap( u"icons/16x16/hourglass.png", wx.BITMAP_TYPE_ANY ), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.total_rows_loading, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - bSizer93 = wx.BoxSizer( wx.VERTICAL ) - - self.tree_ctrl_explorer____ = wx.dataview.TreeListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.TL_DEFAULT_STYLE ) - self.tree_ctrl_explorer____.AppendColumn( _(u"Column5"), wx.COL_WIDTH_DEFAULT, wx.ALIGN_LEFT, wx.COL_RESIZABLE ) - - bSizer93.Add( self.tree_ctrl_explorer____, 1, wx.EXPAND | wx.ALL, 5 ) - - bSizer129 = wx.BoxSizer( wx.VERTICAL ) - - self.m_radioBtn11 = wx.RadioButton( self, wx.ID_ANY, _(u"UNDEFINED"), wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP ) - bSizer129.Add( self.m_radioBtn11, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_radioBtn21 = wx.RadioButton( self, wx.ID_ANY, _(u"MERGE"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_menu13 = wx.Menu() - self.m_menuItem10 = wx.MenuItem( self.m_menu13, wx.ID_ANY, _(u"Import"), wx.EmptyString, wx.ITEM_NORMAL ) - self.m_menu13.Append( self.m_menuItem10 ) - - self.m_radioBtn21.Bind( wx.EVT_RIGHT_DOWN, self.m_radioBtn21OnContextMenu ) - - bSizer129.Add( self.m_radioBtn21, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_radioBtn31 = wx.RadioButton( self, wx.ID_ANY, _(u"TEMPTABLE"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer129.Add( self.m_radioBtn31, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_staticText4011 = wx.StaticText( self, wx.ID_ANY, _(u"Algorithm"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText4011.Wrap( -1 ) - - bSizer129.Add( self.m_staticText4011, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - fgSizer1 = wx.FlexGridSizer( 3, 2, 0, 0 ) - fgSizer1.SetFlexibleDirection( wx.BOTH ) - fgSizer1.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_NONE ) - - - fgSizer1.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - - bSizer129.Add( fgSizer1, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_checkBox7 = wx.CheckBox( self, wx.ID_ANY, _(u"Read only"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer129.Add( self.m_checkBox7, 0, wx.ALL, 5 ) - - rad_view_algorithmChoices = [ _(u"UNDEFINED"), _(u"MERGE"), _(u"TEMPTABLE") ] - self.rad_view_algorithm = wx.RadioBox( self, wx.ID_ANY, _(u"Algorithm"), wx.DefaultPosition, wx.DefaultSize, rad_view_algorithmChoices, 1, wx.RA_SPECIFY_COLS ) - self.rad_view_algorithm.SetSelection( 0 ) - bSizer129.Add( self.rad_view_algorithm, 0, wx.ALL|wx.EXPAND, 5 ) - - rad_view_constraintChoices = [ _(u"None"), _(u"LOCAL"), _(u"CASCADED"), _(u"CHECK OPTION"), _(u"READ ONLY") ] - self.rad_view_constraint = wx.RadioBox( self, wx.ID_ANY, _(u"View constraint"), wx.DefaultPosition, wx.DefaultSize, rad_view_constraintChoices, 1, wx.RA_SPECIFY_COLS ) - self.rad_view_constraint.SetSelection( 0 ) - self.m_menu15 = wx.Menu() - self.rad_view_constraint.Bind( wx.EVT_RIGHT_DOWN, self.rad_view_constraintOnContextMenu ) - - bSizer129.Add( self.rad_view_constraint, 0, wx.ALL|wx.EXPAND, 5 ) - - self.m_textCtrl10 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE|wx.TE_RICH|wx.TE_RICH2 ) - bSizer129.Add( self.m_textCtrl10, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_textCtrl361 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_PASSWORD ) - bSizer129.Add( self.m_textCtrl361, 1, wx.ALL, 5 ) - - - bSizer93.Add( bSizer129, 1, wx.EXPAND, 5 ) - - self.notebook_sql_results = wx.aui.AuiNotebook( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.aui.AUI_NB_DEFAULT_STYLE|wx.aui.AUI_NB_MIDDLE_CLICK_CLOSE ) - - bSizer93.Add( self.notebook_sql_results, 1, wx.EXPAND | wx.ALL, 5 ) - - self.ssh_tunnel_password1 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_PASSWORD ) - bSizer93.Add( self.ssh_tunnel_password1, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) - - database_encryption_oldChoices = [] - self.database_encryption_old = wx.Choice( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, database_encryption_oldChoices, 0 ) - self.database_encryption_old.SetSelection( 0 ) - bSizer93.Add( self.database_encryption_old, 1, wx.ALL, 5 ) - - - bSizer90.Add( bSizer93, 1, wx.EXPAND, 5 ) - - self.m_collapsiblePane2 = wx.CollapsiblePane( self, wx.ID_ANY, _(u"collapsible"), wx.DefaultPosition, wx.DefaultSize, wx.CP_DEFAULT_STYLE ) - self.m_collapsiblePane2.Collapse( False ) - - bSizer90.Add( self.m_collapsiblePane2, 1, wx.EXPAND | wx.ALL, 5 ) - - self.tree_ctrl_sessions = wx.TreeCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TR_DEFAULT_STYLE|wx.TR_FULL_ROW_HIGHLIGHT|wx.TR_HAS_BUTTONS|wx.TR_HIDE_ROOT|wx.TR_TWIST_BUTTONS ) - self.m_menu12 = wx.Menu() - self.tree_ctrl_sessions.Bind( wx.EVT_RIGHT_DOWN, self.tree_ctrl_sessionsOnContextMenu ) - - bSizer90.Add( self.tree_ctrl_sessions, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_treeListCtrl3 = wx.dataview.TreeListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.TL_DEFAULT_STYLE ) - - bSizer90.Add( self.m_treeListCtrl3, 1, wx.EXPAND | wx.ALL, 5 ) - - self.tree_ctrl_sessions1 = wx.dataview.TreeListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.TL_DEFAULT_STYLE ) - self.tree_ctrl_sessions1.AppendColumn( _(u"Column3"), wx.COL_WIDTH_DEFAULT, wx.ALIGN_LEFT, wx.COL_RESIZABLE ) - self.tree_ctrl_sessions1.AppendColumn( _(u"Column4"), wx.COL_WIDTH_DEFAULT, wx.ALIGN_LEFT, wx.COL_RESIZABLE ) - - bSizer90.Add( self.tree_ctrl_sessions1, 1, wx.EXPAND | wx.ALL, 5 ) - - self.table_collationdd = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.table_collationdd, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_textCtrl21 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE ) - bSizer90.Add( self.m_textCtrl21, 1, wx.ALL|wx.EXPAND, 5 ) - - bSizer51 = wx.BoxSizer( wx.VERTICAL ) - - self.panel_credentials = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer48 = wx.BoxSizer( wx.VERTICAL ) - - self.m_notebook8 = wx.Notebook( self.panel_credentials, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - - bSizer48.Add( self.m_notebook8, 1, wx.EXPAND | wx.ALL, 5 ) - - - self.panel_credentials.SetSizer( bSizer48 ) - self.panel_credentials.Layout() - bSizer48.Fit( self.panel_credentials ) - self.m_menu3 = wx.Menu() - self.m_menuItem3 = wx.MenuItem( self.m_menu3, wx.ID_ANY, _(u"MyMenuItem"), wx.EmptyString, wx.ITEM_NORMAL ) - self.m_menu3.Append( self.m_menuItem3 ) - - self.panel_credentials.Bind( wx.EVT_RIGHT_DOWN, self.panel_credentialsOnContextMenu ) - - bSizer51.Add( self.panel_credentials, 0, wx.EXPAND | wx.ALL, 0 ) - - self.panel_source = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - self.panel_source.Hide() - - bSizer52 = wx.BoxSizer( wx.VERTICAL ) - - bSizer1212 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText212 = wx.StaticText( self.panel_source, wx.ID_ANY, _(u"Filename"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) - self.m_staticText212.Wrap( -1 ) - - bSizer1212.Add( self.m_staticText212, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.filename = wx.FilePickerCtrl( self.panel_source, wx.ID_ANY, wx.EmptyString, _(u"Select a file"), _(u"Database (*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3"), wx.DefaultPosition, wx.DefaultSize, wx.FLP_CHANGE_DIR|wx.FLP_USE_TEXTCTRL ) - bSizer1212.Add( self.filename, 1, wx.ALL, 5 ) - - - bSizer52.Add( bSizer1212, 0, wx.EXPAND, 0 ) - - - self.panel_source.SetSizer( bSizer52 ) - self.panel_source.Layout() - bSizer52.Fit( self.panel_source ) - bSizer51.Add( self.panel_source, 0, wx.EXPAND | wx.ALL, 0 ) - - self.m_staticText2211 = wx.StaticText( self, wx.ID_ANY, _(u"Port"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 ) - self.m_staticText2211.Wrap( -1 ) - - bSizer51.Add( self.m_staticText2211, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - - bSizer90.Add( bSizer51, 0, wx.EXPAND, 0 ) - - self.ssh_tunnel_port = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.ssh_tunnel_port, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.ssh_tunnel_local_port = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.ssh_tunnel_local_port, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.tree_ctrl_sessions2 = wx.TreeCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TR_DEFAULT_STYLE ) - self.tree_ctrl_sessions2.Hide() - - bSizer90.Add( self.tree_ctrl_sessions2, 1, wx.ALL|wx.EXPAND, 5 ) - - self.tree_ctrl_sessions_bkp3 = wx.dataview.TreeListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.TL_DEFAULT_STYLE|wx.dataview.TL_SINGLE ) - self.tree_ctrl_sessions_bkp3.Hide() - - self.tree_ctrl_sessions_bkp3.AppendColumn( _(u"Name"), wx.COL_WIDTH_DEFAULT, wx.ALIGN_LEFT, wx.COL_RESIZABLE ) - self.tree_ctrl_sessions_bkp3.AppendColumn( _(u"Usage"), wx.COL_WIDTH_DEFAULT, wx.ALIGN_LEFT, wx.COL_RESIZABLE ) - - bSizer90.Add( self.tree_ctrl_sessions_bkp3, 1, wx.EXPAND | wx.ALL, 5 ) - - self.tree_ctrl_sessions_bkp = wx.dataview.DataViewCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.DV_SINGLE ) - self.tree_ctrl_sessions_bkp.Hide() - - self.m_dataViewColumn1 = self.tree_ctrl_sessions_bkp.AppendIconTextColumn( _(u"Database"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn3 = self.tree_ctrl_sessions_bkp.AppendProgressColumn( _(u"Size"), 1, wx.dataview.DATAVIEW_CELL_INERT, 50, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - bSizer90.Add( self.tree_ctrl_sessions_bkp, 1, wx.ALL|wx.EXPAND, 5 ) - - self.rows_database_table = wx.StaticText( self, wx.ID_ANY, _(u"%(total_rows)s"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.rows_database_table.Wrap( -1 ) - - bSizer90.Add( self.rows_database_table, 0, wx.ALL, 5 ) - - self.m_staticText44 = wx.StaticText( self, wx.ID_ANY, _(u"rows total"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText44.Wrap( -1 ) - - bSizer90.Add( self.m_staticText44, 0, wx.ALL, 5 ) - - self.____list_ctrl_database_tables = wx.dataview.DataViewCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_dataViewColumn5 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn6 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn7 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn8 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn9 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn10 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn11 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn20 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewColumn21 = self.____list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - bSizer90.Add( self.____list_ctrl_database_tables, 0, wx.ALL, 5 ) - - self.___list_ctrl_database_tables = wx.dataview.DataViewListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_dataViewListColumn6 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Name"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewListColumn7 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Lines"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewListColumn8 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Size"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewListColumn9 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Created at"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewListColumn10 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Updated at"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewListColumn11 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Engine"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - self.m_dataViewListColumn12 = self.___list_ctrl_database_tables.AppendTextColumn( _(u"Comments"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE ) - bSizer90.Add( self.___list_ctrl_database_tables, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_gauge1 = wx.Gauge( self, wx.ID_ANY, 100, wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_gauge1.SetValue( 0 ) - bSizer90.Add( self.m_gauge1, 0, wx.ALL|wx.EXPAND, 5 ) - - - bSizer90.Add( ( 150, 0), 0, wx.EXPAND, 5 ) - - - bSizer90.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.tree_ctrl_explorer__ = wx.dataview.DataViewCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - self.tree_ctrl_explorer__.Hide() - - bSizer90.Add( self.tree_ctrl_explorer__, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_vlistBox1 = wx.VListBox( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.m_vlistBox1, 0, wx.ALL, 5 ) - - m_listBox1Choices = [] - self.m_listBox1 = wx.ListBox( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_listBox1Choices, 0 ) - bSizer90.Add( self.m_listBox1, 0, wx.ALL, 5 ) - - bSizer871 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText401 = wx.StaticText( self, wx.ID_ANY, _(u"Temporary"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText401.Wrap( -1 ) - - bSizer871.Add( self.m_staticText401, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.m_checkBox5 = wx.CheckBox( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer871.Add( self.m_checkBox5, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - - bSizer90.Add( bSizer871, 1, wx.EXPAND, 5 ) - - self.m_collapsiblePane3 = wx.CollapsiblePane( self, wx.ID_ANY, _(u"Engine options"), wx.DefaultPosition, wx.DefaultSize, wx.CP_DEFAULT_STYLE ) - self.m_collapsiblePane3.Collapse( False ) - - bSizer115 = wx.BoxSizer( wx.VERTICAL ) - - self.m_panel41 = wx.Panel( self.m_collapsiblePane3.GetPane(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer115.Add( self.m_panel41, 1, wx.EXPAND | wx.ALL, 5 ) - - self.m_panel42 = wx.Panel( self.m_collapsiblePane3.GetPane(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer115.Add( self.m_panel42, 1, wx.EXPAND | wx.ALL, 5 ) - - self.m_panel43 = wx.Panel( self.m_collapsiblePane3.GetPane(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer115.Add( self.m_panel43, 1, wx.EXPAND | wx.ALL, 5 ) - - - self.m_collapsiblePane3.GetPane().SetSizer( bSizer115 ) - self.m_collapsiblePane3.GetPane().Layout() - bSizer115.Fit( self.m_collapsiblePane3.GetPane() ) - bSizer90.Add( self.m_collapsiblePane3, 1, wx.EXPAND | wx.ALL, 5 ) - - self.m_textCtrl2211 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.m_textCtrl2211, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_textCtrl2212 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.m_textCtrl2212, 1, wx.ALL|wx.EXPAND, 5 ) - - m_comboBox11Choices = [] - self.m_comboBox11 = wx.ComboBox( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, m_comboBox11Choices, 0 ) - bSizer90.Add( self.m_comboBox11, 1, wx.ALL|wx.EXPAND, 5 ) - - gSizer3 = wx.GridSizer( 0, 2, 0, 0 ) - - bSizer8712 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText4012 = wx.StaticText( self, wx.ID_ANY, _(u"Algorithm"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText4012.Wrap( -1 ) - - bSizer8712.Add( self.m_staticText4012, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.m_radioBtn1 = wx.RadioButton( self, wx.ID_ANY, _(u"UNDEFINED"), wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP ) - bSizer8712.Add( self.m_radioBtn1, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_radioBtn2 = wx.RadioButton( self, wx.ID_ANY, _(u"MERGE"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer8712.Add( self.m_radioBtn2, 1, wx.ALL|wx.EXPAND, 5 ) - - self.m_radioBtn3 = wx.RadioButton( self, wx.ID_ANY, _(u"TEMPTABLE"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer8712.Add( self.m_radioBtn3, 1, wx.ALL|wx.EXPAND, 5 ) - - - gSizer3.Add( bSizer8712, 1, wx.EXPAND, 5 ) - - bSizer12211 = wx.BoxSizer( wx.HORIZONTAL ) - - - gSizer3.Add( bSizer12211, 0, wx.EXPAND, 5 ) - - - bSizer90.Add( gSizer3, 1, wx.EXPAND, 5 ) - - self.m_radioBtn10 = wx.RadioButton( self, wx.ID_ANY, _(u"RadioBtn"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.m_radioBtn10, 0, wx.ALL, 5 ) - - bSizer86 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_panel44 = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer86.Add( self.m_panel44, 1, wx.EXPAND | wx.ALL, 5 ) - - - bSizer90.Add( bSizer86, 0, wx.EXPAND, 5 ) - - self.filename1 = wx.FilePickerCtrl( self, wx.ID_ANY, wx.EmptyString, _(u"Select a file"), _(u"*.*"), wx.DefaultPosition, wx.DefaultSize, wx.FLP_CHANGE_DIR|wx.FLP_DEFAULT_STYLE|wx.FLP_FILE_MUST_EXIST ) - bSizer90.Add( self.filename1, 0, wx.ALL, 5 ) - - self.m_textCtrl351 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer90.Add( self.m_textCtrl351, 0, wx.ALL, 5 ) - - self.m_staticText701 = wx.StaticText( self, wx.ID_ANY, _(u"Encryption"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText701.Wrap( -1 ) - - self.m_staticText701.SetMinSize( wx.Size( 150,-1 ) ) - - bSizer90.Add( self.m_staticText701, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - - self.SetSizer( bSizer90 ) - self.Layout() - self.m_menu11 = wx.Menu() - self.Bind( wx.EVT_RIGHT_DOWN, self.TrashOnContextMenu ) - - - # Connect Events - self.cancel_query_execution.Bind( wx.EVT_BUTTON, self.on_cancel_query_execution ) - self.Bind( wx.EVT_MENU, self.on_import, id = self.m_menuItem10.GetId() ) - self.tree_ctrl_sessions.Bind( wx.EVT_TREE_ITEM_RIGHT_CLICK, self.show_tree_ctrl_menu ) - - def __del__( self ): - pass - - - # Virtual event handlers, override them in your derived class - def on_cancel_query_execution( self, event ): - event.Skip() - - def on_import( self, event ): - event.Skip() - - def show_tree_ctrl_menu( self, event ): - event.Skip() - - def m_radioBtn21OnContextMenu( self, event ): - self.m_radioBtn21.PopupMenu( self.m_menu13, event.GetPosition() ) - - def rad_view_constraintOnContextMenu( self, event ): - self.rad_view_constraint.PopupMenu( self.m_menu15, event.GetPosition() ) - - def tree_ctrl_sessionsOnContextMenu( self, event ): - self.tree_ctrl_sessions.PopupMenu( self.m_menu12, event.GetPosition() ) - - def panel_credentialsOnContextMenu( self, event ): - self.panel_credentials.PopupMenu( self.m_menu3, event.GetPosition() ) - - def TrashOnContextMenu( self, event ): - self.PopupMenu( self.m_menu11, event.GetPosition() ) - - -########################################################################### -## Class MyPanel1 -########################################################################### - -class MyPanel1 ( wx.Panel ): - - def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.Size( 500,300 ), style = wx.TAB_TRAVERSAL, name = wx.EmptyString ): - wx.Panel.__init__ ( self, parent, id = id, pos = pos, size = size, style = style, name = name ) - - - def __del__( self ): - pass - - -########################################################################### -## Class EditColumnView -########################################################################### - -class EditColumnView ( wx.Dialog ): - - def __init__( self, parent ): - wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Edit Column"), pos = wx.DefaultPosition, size = wx.Size( 600,600 ), style = wx.DEFAULT_DIALOG_STYLE|wx.STAY_ON_TOP ) - - self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) - - bSizer98 = wx.BoxSizer( wx.VERTICAL ) - - bSizer52 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText26 = wx.StaticText( self, wx.ID_ANY, _(u"Name"), wx.DefaultPosition, wx.Size( 100,-1 ), wx.ST_NO_AUTORESIZE ) - self.m_staticText26.Wrap( -1 ) - - bSizer52.Add( self.m_staticText26, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - self.column_name = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer52.Add( self.column_name, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - self.m_staticText261 = wx.StaticText( self, wx.ID_ANY, _(u"Datatype"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText261.Wrap( -1 ) - - bSizer52.Add( self.m_staticText261, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - column_datatypeChoices = [] - self.column_datatype = wx.Choice( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, column_datatypeChoices, 0 ) - self.column_datatype.SetSelection( 0 ) - bSizer52.Add( self.column_datatype, 1, wx.ALL, 5 ) - - - bSizer98.Add( bSizer52, 0, wx.EXPAND, 5 ) - - bSizer5211 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText2611 = wx.StaticText( self, wx.ID_ANY, _(u"Length/Set"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText2611.Wrap( -1 ) - - bSizer5211.Add( self.m_staticText2611, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - bSizer60 = wx.BoxSizer( wx.HORIZONTAL ) - - self.column_set = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer60.Add( self.column_set, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.column_length = wx.SpinCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_ARROW_KEYS, 0, 65536, 0 ) - bSizer60.Add( self.column_length, 1, wx.ALL, 5 ) - - self.column_scale = wx.SpinCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_WRAP, 0, 65536, 0 ) - self.column_scale.Enable( False ) - - bSizer60.Add( self.column_scale, 1, wx.ALL, 5 ) - - - bSizer5211.Add( bSizer60, 1, wx.EXPAND, 5 ) - - self.m_staticText261111112 = wx.StaticText( self, wx.ID_ANY, _(u"Collation"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText261111112.Wrap( -1 ) - - bSizer5211.Add( self.m_staticText261111112, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - column_collationChoices = [] - self.column_collation = wx.Choice( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, column_collationChoices, 0 ) - self.column_collation.SetSelection( 0 ) - bSizer5211.Add( self.column_collation, 1, wx.ALL, 5 ) - - - bSizer98.Add( bSizer5211, 0, wx.EXPAND, 5 ) - - bSizer52111 = wx.BoxSizer( wx.HORIZONTAL ) - - - bSizer52111.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.column_unsigned = wx.CheckBox( self, wx.ID_ANY, _(u"Unsigned"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer52111.Add( self.column_unsigned, 1, wx.ALL, 5 ) - - - bSizer52111.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.column_allow_null = wx.CheckBox( self, wx.ID_ANY, _(u"Allow NULL"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer52111.Add( self.column_allow_null, 1, wx.ALL, 5 ) - - - bSizer52111.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.column_zero_fill = wx.CheckBox( self, wx.ID_ANY, _(u"Zero Fill"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer52111.Add( self.column_zero_fill, 1, wx.ALL, 5 ) - - - bSizer52111.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - - bSizer98.Add( bSizer52111, 0, wx.EXPAND, 5 ) - - bSizer53 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText26111111 = wx.StaticText( self, wx.ID_ANY, _(u"Default"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText26111111.Wrap( -1 ) - - bSizer53.Add( self.m_staticText26111111, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - self.column_default = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer53.Add( self.column_default, 1, wx.ALL, 5 ) - - - bSizer98.Add( bSizer53, 0, wx.EXPAND, 5 ) - - bSizer531 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText261111111 = wx.StaticText( self, wx.ID_ANY, _(u"Comments"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText261111111.Wrap( -1 ) - - bSizer531.Add( self.m_staticText261111111, 0, wx.ALL, 5 ) - - self.column_comments = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size( -1,100 ), wx.TE_MULTILINE ) - bSizer531.Add( self.column_comments, 1, wx.ALL, 5 ) - - - bSizer98.Add( bSizer531, 0, wx.EXPAND, 5 ) - - bSizer532 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText261111113 = wx.StaticText( self, wx.ID_ANY, _(u"Virtuality"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText261111113.Wrap( -1 ) - - bSizer532.Add( self.m_staticText261111113, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - column_virtualityChoices = [] - self.column_virtuality = wx.Choice( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, column_virtualityChoices, 0 ) - self.column_virtuality.SetSelection( 0 ) - bSizer532.Add( self.column_virtuality, 1, wx.ALL, 5 ) - - - bSizer98.Add( bSizer532, 0, wx.EXPAND, 5 ) - - bSizer5311 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText2611111111 = wx.StaticText( self, wx.ID_ANY, _(u"Expression"), wx.DefaultPosition, wx.Size( 100,-1 ), 0 ) - self.m_staticText2611111111.Wrap( -1 ) - - bSizer5311.Add( self.m_staticText2611111111, 0, wx.ALL, 5 ) - - self.column_expression = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size( -1,100 ), wx.TE_MULTILINE ) - bSizer5311.Add( self.column_expression, 1, wx.ALL, 5 ) - - - bSizer98.Add( bSizer5311, 0, wx.EXPAND, 5 ) - - - bSizer98.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.m_staticline2 = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) - bSizer98.Add( self.m_staticline2, 0, wx.EXPAND | wx.ALL, 5 ) - - bSizer64 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_button16 = wx.Button( self, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) - - self.m_button16.SetDefault() - - self.m_button16.SetBitmap( wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ) ) - bSizer64.Add( self.m_button16, 0, wx.ALL, 5 ) - - - bSizer64.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.m_button15 = wx.Button( self, wx.ID_ANY, _(u"Save"), wx.DefaultPosition, wx.DefaultSize, 0 ) - - self.m_button15.SetBitmap( wx.Bitmap( u"icons/16x16/disk.png", wx.BITMAP_TYPE_ANY ) ) - bSizer64.Add( self.m_button15, 0, wx.ALL, 5 ) - - - bSizer98.Add( bSizer64, 0, wx.EXPAND, 5 ) - - - self.SetSizer( bSizer98 ) - self.Layout() - - self.Centre( wx.BOTH ) def __del__( self ): pass From 161b795034425de079013792859b9dd8b30fa27a Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 17 Mar 2026 09:26:35 +0100 Subject: [PATCH 18/93] refactor(autocomplete): simplify settings lookups in controller AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- .../stc/autocomplete/auto_complete.py | 58 +++++++------------ 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/windows/components/stc/autocomplete/auto_complete.py b/windows/components/stc/autocomplete/auto_complete.py index 45f787e..3d01a05 100644 --- a/windows/components/stc/autocomplete/auto_complete.py +++ b/windows/components/stc/autocomplete/auto_complete.py @@ -25,11 +25,11 @@ class SQLCompletionProvider: def __init__( - self, - get_database: Callable[[], Optional[SQLDatabase]], - get_current_table: Optional[Callable[[], Optional[SQLTable]]] = None, - *, - is_filter_editor: bool = False, + self, + get_database: Callable[[], Optional[SQLDatabase]], + get_current_table: Optional[Callable[[], Optional[SQLTable]]] = None, + *, + is_filter_editor: bool = False, ) -> None: self._get_database = get_database self._get_current_table = get_current_table or (lambda: None) @@ -109,35 +109,24 @@ def _update_cache(self, *, database: SQLDatabase) -> None: class SQLAutoCompleteController: def __init__( - self, - editor: wx.stc.StyledTextCtrl, - provider: SQLCompletionProvider, - *, - settings: Optional[object] = None, - theme_loader: Optional[object] = None, - debounce_ms: int = 80, - is_enabled: bool = True, - min_prefix_length: int = 1, + self, + editor: wx.stc.StyledTextCtrl, + provider: SQLCompletionProvider, + *, + settings: Optional[object] = None, + theme_loader: Optional[object] = None, + debounce_ms: int = 80, + is_enabled: bool = True, + min_prefix_length: int = 1, ) -> None: self._editor = editor self._provider = provider self._settings = settings self._theme_loader = theme_loader - if settings: - self._debounce_ms = ( - settings.get_value("editor", "autocomplete", "debounce_ms", default=debounce_ms) - ) - self._min_prefix_length = ( - settings.get_value("editor", "autocomplete", "min_prefix_length", default=min_prefix_length) - ) - self._add_space_after_completion = settings.get_value( - "editor", "autocomplete", "add_space_after_completion", default=True - ) - else: - self._debounce_ms = debounce_ms - self._min_prefix_length = min_prefix_length - self._add_space_after_completion = True + self._debounce_ms = settings.get_value("editor", "autocomplete", "debounce_ms", default=debounce_ms) + self._min_prefix_length = settings.get_value("editor", "autocomplete", "min_prefix_length", default=min_prefix_length) + self._add_space_after_completion = settings.get_value("editor", "autocomplete", "add_space_after_completion", default=True) self._is_enabled = is_enabled @@ -156,16 +145,11 @@ def set_enabled(self, is_enabled: bool) -> None: self._hide_popup() def get_effective_separator(self) -> str: - if self._settings: - separator = self._settings.get_value("editor", "statement_separator", default=";") - if separator: - return separator - session = CURRENT_SESSION.get_value() if session and hasattr(session, "context"): return session.context.DEFAULT_STATEMENT_SEPARATOR - return ";" + return self._settings.get_value("editor", "statement_separator", default=";") def show(self, *, force: bool) -> None: if not self._is_enabled: @@ -226,8 +210,8 @@ def _on_item_completed(self, item: CompletionItem) -> None: self._editor.SetSelection(start_pos, current_pos) should_add_space = ( - self._add_space_after_completion - and item.item_type == CompletionItemType.KEYWORD + self._add_space_after_completion + and item.item_type == CompletionItemType.KEYWORD ) completion_text = item.name + " " if should_add_space else item.name self._editor.ReplaceSelection(completion_text) @@ -322,7 +306,7 @@ def _cancel_pending(self) -> None: @staticmethod def _unique_sorted_items( - *, items: tuple[CompletionItem, ...] + *, items: tuple[CompletionItem, ...] ) -> list[CompletionItem]: seen_names: set[str] = set() unique_items: list[CompletionItem] = [] From 550a790d4877a33a286b5c4ea8de88b89a4fd194 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 17 Mar 2026 11:50:52 +0100 Subject: [PATCH 19/93] Preserve expanded tree state after failed connection attempt AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- windows/dialogs/connections/view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/windows/dialogs/connections/view.py b/windows/dialogs/connections/view.py index d0a570a..4534ecb 100644 --- a/windows/dialogs/connections/view.py +++ b/windows/dialogs/connections/view.py @@ -130,7 +130,9 @@ def _record_connection_attempt( ) if not connection.is_new: + expanded_paths = self._capture_expanded_directory_paths() self._repository.save_connection(connection) + wx.CallAfter(self._restore_expanded_directory_paths, expanded_paths) def _sync_statistics_to_model(self, connection: Connection) -> None: self.connections_model.created_at(connection.created_at or "") From 060204c19f38875514837b02e68dd62ff8f1a638 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 17 Mar 2026 11:51:12 +0100 Subject: [PATCH 20/93] Remove database character set option and refresh apply state on collation changes AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- windows/main/controller.py | 1 - windows/main/tabs/database_options.py | 30 +++------------------------ 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/windows/main/controller.py b/windows/main/controller.py index 59f7252..ce2042a 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -116,7 +116,6 @@ def _setup_database_action_buttons_bindings(self) -> None: database_observables = [ model.database_name, - model.database_character_set, model.database_collation, model.database_encryption, model.database_read_only, diff --git a/windows/main/tabs/database_options.py b/windows/main/tabs/database_options.py index 034161e..99da9d2 100644 --- a/windows/main/tabs/database_options.py +++ b/windows/main/tabs/database_options.py @@ -3,7 +3,7 @@ import wx from helpers.bindings import AbstractModel -from helpers.observables import Observable, debounce +from helpers.observables import CallbackEvent, Observable, debounce from structures.connection import ConnectionEngine @@ -13,7 +13,6 @@ class EditDatabaseOptionsModel(AbstractModel): def __init__(self): self.database_name = Observable() - self.database_character_set = Observable() self.database_collation = Observable() self.database_encryption = Observable(False) self.database_read_only = Observable(False) @@ -30,7 +29,6 @@ def __init__(self): debounce( self.database_name, - self.database_character_set, self.database_collation, self.database_encryption, self.database_read_only, @@ -78,15 +76,6 @@ def _load_database(self, database) -> None: self._first_attr(database, ["default_collation", "collation", "collation_name"], "") ) - context = database.context if database else None - charset = None - if context and self.database_collation.get_value() and getattr(context, "COLLATIONS", None): - charset = context.COLLATIONS.get(self.database_collation.get_value()) - - self.database_character_set.set_initial( - charset or self._first_attr(database, ["character_set", "charset"], "") - ) - self.database_encryption.set_initial( self._encryption_to_bool(self._first_attr(database, ["encryption"], None)) ) @@ -121,8 +110,6 @@ def _update_database(self, *args) -> None: database.name = self.database_name.get_value() mapping = { - "character_set": self.database_character_set.get_value(), - "charset": self.database_character_set.get_value(), "default_collation": self.database_collation.get_value(), "collation": self.database_collation.get_value(), "collation_name": self.database_collation.get_value(), @@ -145,6 +132,8 @@ def _update_database(self, *args) -> None: if hasattr(database, attr): setattr(database, attr, value) + CURRENT_DATABASE.execute_callback(CallbackEvent.AFTER_CHANGE) + class DatabaseOptionsController: def __init__(self, parent): @@ -213,7 +202,6 @@ def _batch_show_hide(self, show: list[wx.Window], hide: list[wx.Window]) -> None def _bind_controls(self) -> None: self.model.bind_controls( database_name=self.parent.database_name, - database_character_set=self.parent.database_character_set, database_collation=self.parent.database_collation, database_encryption=self.parent.database_encryption, database_read_only=self.parent.database_read_only, @@ -231,7 +219,6 @@ def _bind_controls(self) -> None: def _build_controls_all(self) -> list[wx.Window]: return [ - self.parent.database_character_set, self.parent.database_collation, self.parent.database_encryption, self.parent.database_read_only, @@ -249,7 +236,6 @@ def _build_controls_all(self) -> list[wx.Window]: def _build_panel_by_name(self) -> dict[str, wx.Window]: return { - "database_character_set_panel": self.parent.database_character_set_panel, "database_collation_panel": self.parent.database_collation_panel, "database_encryption_panel": self.parent.database_encryption_panel, "database_read_only_panel": self.parent.database_read_only_panel, @@ -268,7 +254,6 @@ def _build_panel_by_name(self) -> dict[str, wx.Window]: def _get_panel_names_for_engine(self, engine: Optional[ConnectionEngine]) -> list[str]: if engine in [ConnectionEngine.MYSQL, ConnectionEngine.MARIADB]: return [ - "database_character_set_panel", "database_collation_panel", "database_encryption_panel", ] @@ -312,15 +297,6 @@ def _populate_choices(self, database) -> None: if context and getattr(context, "COLLATIONS", None): collations = sorted(context.COLLATIONS.keys()) - charsets = [] - if context and getattr(context, "COLLATIONS", None): - charsets = sorted(set(context.COLLATIONS.values())) - - self._apply_choice( - self.parent.database_character_set, - charsets, - self.model.database_character_set.get_value(), - ) self._apply_choice( self.parent.database_collation, collations, From e73a534ba14baad3a7ef3a69e76d9a053c3940b8 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 17 Mar 2026 11:51:28 +0100 Subject: [PATCH 21/93] Fix DataView has-value detection for record-backed rows AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- helpers/dataview.py | 2 +- windows/main/tabs/records.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/helpers/dataview.py b/helpers/dataview.py index f5d0a19..9f0c164 100644 --- a/helpers/dataview.py +++ b/helpers/dataview.py @@ -128,7 +128,7 @@ def HasValue(self, item, col): if fields := self._get_column_fields(): return fields[col].has_value(self.get_data_by_item(item)) - return self.get_data_by_item(item)[col] is not None + return getattr(self.get_data_by_item(item), col, None) is not None class BaseDataViewListModel(_DataViewListValueMixin, BaseDataModel, wx.dataview.DataViewIndexListModel): diff --git a/windows/main/tabs/records.py b/windows/main/tabs/records.py index 1c21b16..d0c0e9c 100644 --- a/windows/main/tabs/records.py +++ b/windows/main/tabs/records.py @@ -90,6 +90,17 @@ def GetAttr(self, item, col, attr): return super().GetAttr(item, col, attr) + def HasValue(self, item, col): + if not self.data: + return False + + column = self.table.columns[col] + + record: SQLRecord = self.get_data_by_item(item) + + return record.values.get(column.name, None) is not None + + def add_row(self, data: SQLRecord) -> wx.dataview.DataViewItem: self.data.append(data) self.RowAppended() From 8efe25b5951bf2eea5026629826df6054abde459 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 17 Mar 2026 13:38:11 +0100 Subject: [PATCH 22/93] fix: address code review findings (typing, imports, lazy import comment, style limit) AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- CODE_STYLE.md | 6 +++--- helpers/dataview.py | 2 +- windows/components/dataview.py | 2 +- windows/components/stc/autocomplete/auto_complete.py | 2 +- windows/main/controller.py | 4 ++-- windows/main/tabs/query.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CODE_STYLE.md b/CODE_STYLE.md index c074b74..03024e2 100644 --- a/CODE_STYLE.md +++ b/CODE_STYLE.md @@ -23,7 +23,7 @@ The following rules are strict and MUST NOT be violated. When generating or modi 2. Python typing rules MUST be respected (PEP 585 generics; `Optional[T]`, not `T | None`). 3. `from __future__ import annotations` MUST NOT be used. 4. Import ordering and grouping rules MUST be followed exactly. -5. Functions and methods MUST NOT exceed 50 lines. +5. Functions and methods MUST NOT exceed 80 lines. 6. Code changes MUST avoid modifying unrelated code. 7. Naming MUST remain explicit and descriptive (no aggressive abbreviations). 8. Code MUST remain mypy-friendly whenever possible. @@ -620,8 +620,8 @@ class Example: ## 9. Function and Method Size -- A function/method MUST be at most 50 lines. -- If it exceeds 50 lines, it MUST be split into smaller functions/methods with clear names. +- A function/method MUST be at most 80 lines. +- If it exceeds 80 lines, it MUST be split into smaller functions/methods with clear names. --- diff --git a/helpers/dataview.py b/helpers/dataview.py index 9f0c164..3cf5e22 100644 --- a/helpers/dataview.py +++ b/helpers/dataview.py @@ -185,7 +185,7 @@ def move(self, data: Any, current: int, future: int) -> bool: class BaseObservableDataModel(BaseDataModel): def __init__(self, column_count: Optional[int] = None): super().__init__(column_count) - self._observable: Union[ObservableList, ObservableLazyList] = None + self._observable: Union[ObservableList, ObservableLazyList] def load(self, data: list[Any]): super().load(data.copy()) diff --git a/windows/components/dataview.py b/windows/components/dataview.py index e0b1d43..c96ba88 100644 --- a/windows/components/dataview.py +++ b/windows/components/dataview.py @@ -330,7 +330,7 @@ def __init__(self, *args, **kwargs): CURRENT_TABLE.subscribe(self._load_table) def make_advanced_dialog(self, parent, value: str, read_only : bool = False): - from windows.dialogs.column_content import ColumnContentDialogController + from windows.dialogs.column_content import ColumnContentDialogController # Lazy import: unavoidable circular dependency return ColumnContentDialogController(parent, value, read_only) def _get_column_renderer(self, column: SQLColumn) -> wx.dataview.DataViewRenderer: diff --git a/windows/components/stc/autocomplete/auto_complete.py b/windows/components/stc/autocomplete/auto_complete.py index 3d01a05..149dddf 100644 --- a/windows/components/stc/autocomplete/auto_complete.py +++ b/windows/components/stc/autocomplete/auto_complete.py @@ -113,7 +113,7 @@ def __init__( editor: wx.stc.StyledTextCtrl, provider: SQLCompletionProvider, *, - settings: Optional[object] = None, + settings: object, theme_loader: Optional[object] = None, debounce_ms: int = 80, is_enabled: bool = True, diff --git a/windows/main/controller.py b/windows/main/controller.py index ce2042a..8315d6e 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -1,9 +1,9 @@ +import contextlib +import dataclasses import math import os import threading import time -import contextlib -import dataclasses from collections import defaultdict from gettext import gettext as _ diff --git a/windows/main/tabs/query.py b/windows/main/tabs/query.py index e68e5d1..f71625e 100644 --- a/windows/main/tabs/query.py +++ b/windows/main/tabs/query.py @@ -19,10 +19,10 @@ from structures.connection import Connection, ConnectionEngine from structures.engines.datatype import DataTypeCategory, SQLDataType -from windows.components.stc.autocomplete.statement_extractor import StatementExtractor from windows.components.popup import PopupCalendar, PopupCalendarTime from windows.components.renders import AdvancedTextRenderer, FloatRenderer, IntegerRenderer, PopupRenderer, TextRenderer, TimeRenderer from windows.components.dataview import QueryEditorResultsDataViewCtrl +from windows.components.stc.autocomplete.statement_extractor import StatementExtractor class _ReadOnlyPopupRenderer(PopupRenderer): From dadc2772d099b2b4dbd6fb6b4cff8dbe7ac47dd2 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 17 Mar 2026 16:53:01 +0100 Subject: [PATCH 23/93] feat(table-options): add row_format and convert_data for MySQL/MariaDB - Add ROW_FORMATS constant to AbstractContext (empty), MySQLContext and MariaDBContext - Add row_format field to MySQLTable/MariaDBTable; fetched from information_schema - Add convert_data transient flag (compare=False) to control CONVERT TO CHARACTER SET - Fix MySQLTable.alter_collation() signature to accept collation_name (was buggy) - Add alter_row_format() to MySQLTable and MariaDBTable - Wire convert_data and row_format into EditTableModel and MainFrameController - Fix server_version stored as instance attribute in MySQL/MariaDB after_connect AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- structures/engines/context.py | 2 ++ structures/engines/mariadb/context.py | 9 ++++++--- structures/engines/mariadb/database.py | 13 ++++++++++++- structures/engines/mysql/context.py | 9 ++++++--- structures/engines/mysql/database.py | 19 +++++++++++++++---- windows/main/controller.py | 12 +++++++++++- windows/main/tabs/table.py | 11 ++++++++++- 7 files changed, 62 insertions(+), 13 deletions(-) diff --git a/structures/engines/context.py b/structures/engines/context.py index 15ce547..922345d 100755 --- a/structures/engines/context.py +++ b/structures/engines/context.py @@ -45,6 +45,8 @@ class AbstractContext(abc.ABC): DATATYPE: StandardDataType INDEXTYPE: StandardIndexType COLLATIONS: dict[str, str] = {} + ROW_FORMATS: list[str] = [] + server_version: str = "" IDENTIFIER_QUOTE_CHAR: str = '"' DEFAULT_STATEMENT_SEPARATOR: str = ";" diff --git a/structures/engines/mariadb/context.py b/structures/engines/mariadb/context.py index 979f04e..48d6240 100755 --- a/structures/engines/mariadb/context.py +++ b/structures/engines/mariadb/context.py @@ -46,6 +46,8 @@ class MariaDBContext(AbstractContext): IDENTIFIER_QUOTE_CHAR = "`" DEFAULT_STATEMENT_SEPARATOR = ";" + ROW_FORMATS: list[str] = ["DEFAULT", "DYNAMIC", "FIXED", "COMPRESSED", "REDUNDANT", "COMPACT"] + def __init__(self, connection: Connection): super().__init__(connection) @@ -68,9 +70,9 @@ def after_connect(self, *args, **kwargs): self.execute("""SHOW ENGINES;""") self.ENGINES = [dict(row).get("Engine") for row in self.fetchall()] - server_version = self.get_server_version() + self.server_version = self.get_server_version() self.KEYWORDS, builtin_functions = self.get_engine_vocabulary( - "mariadb", server_version + "mariadb", self.server_version ) self.execute(""" @@ -348,7 +350,7 @@ def get_tables(self, database: SQLDatabase) -> list[SQLTable]: QUERY_LOGS.append(f"/* get_tables for database={database.name} */") self.execute(f""" - SELECT TABLE_NAME, ENGINE, TABLE_COLLATION, TABLE_ROWS, AUTO_INCREMENT, + SELECT TABLE_NAME, ENGINE, TABLE_COLLATION, TABLE_ROWS, AUTO_INCREMENT, ROW_FORMAT, CREATE_TIME, UPDATE_TIME, ROUND(DATA_LENGTH + INDEX_LENGTH, 2) as total_bytes FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{database.name}' @@ -365,6 +367,7 @@ def get_tables(self, database: SQLDatabase) -> list[SQLTable]: database=database, engine=row["ENGINE"], collation_name=row["TABLE_COLLATION"], + row_format=row["ROW_FORMAT"], auto_increment=int(row["AUTO_INCREMENT"] or 0), total_bytes=row["total_bytes"], total_rows=row["TABLE_ROWS"], diff --git a/structures/engines/mariadb/database.py b/structures/engines/mariadb/database.py index d383b8a..5a52634 100644 --- a/structures/engines/mariadb/database.py +++ b/structures/engines/mariadb/database.py @@ -57,6 +57,9 @@ def drop(self) -> bool: @dataclasses.dataclass(eq=False) class MariaDBTable(SQLTable): + row_format: Optional[str] = None + convert_data: bool = dataclasses.field(default=False, compare=False) + def raw_create(self) -> str: columns = [str(MariaDBColumnBuilder(column)) for column in self.columns] @@ -90,6 +93,12 @@ def alter_engine(self, engine: str): return True + def alter_row_format(self, row_format: str): + statement = f"ALTER TABLE {self.fully_qualified_name} ROW_FORMAT={row_format};" + self.database.context.execute(statement) + + return True + def rename(self, table: Self, new_name: str) -> bool: statement = f"ALTER TABLE {table.fully_qualified_name} RENAME TO `{new_name}`;" self.database.context.execute(statement) @@ -140,9 +149,11 @@ def alter(self) -> bool: if self.auto_increment != original_table.auto_increment: original_table.alter_auto_increment(self.auto_increment) if self.collation_name != original_table.collation_name: - original_table.alter_collation(self.collation_name) + original_table.alter_collation(self.collation_name, convert=self.convert_data) if self.engine != original_table.engine: original_table.alter_engine(self.engine) + if self.row_format != original_table.row_format and self.row_format: + original_table.alter_row_format(self.row_format) for i, (original, current) in enumerate(map_columns): if original is None: diff --git a/structures/engines/mysql/context.py b/structures/engines/mysql/context.py index f1260e7..4733d20 100644 --- a/structures/engines/mysql/context.py +++ b/structures/engines/mysql/context.py @@ -46,6 +46,8 @@ class MySQLContext(AbstractContext): IDENTIFIER_QUOTE_CHAR = "`" DEFAULT_STATEMENT_SEPARATOR = ";" + ROW_FORMATS: list[str] = ["DEFAULT", "DYNAMIC", "FIXED", "COMPRESSED", "REDUNDANT", "COMPACT"] + def __init__(self, connection: Connection): super().__init__(connection) @@ -68,9 +70,9 @@ def after_connect(self, *args, **kwargs): self.execute("""SHOW ENGINES;""") self.ENGINES = [dict(row).get("Engine") for row in self.fetchall()] - server_version = self.get_server_version() + self.server_version = self.get_server_version() self.KEYWORDS, builtin_functions = self.get_engine_vocabulary( - "mysql", server_version + "mysql", self.server_version ) self.execute(""" @@ -359,7 +361,7 @@ def get_tables(self, database: SQLDatabase) -> list[SQLTable]: QUERY_LOGS.append(f"/* get_tables for database={database.name} */") self.execute(f""" - SELECT TABLE_NAME, ENGINE, TABLE_COLLATION, TABLE_ROWS, AUTO_INCREMENT, + SELECT TABLE_NAME, ENGINE, TABLE_COLLATION, TABLE_ROWS, AUTO_INCREMENT, ROW_FORMAT, ROUND((DATA_LENGTH + INDEX_LENGTH), 2) as total_bytes FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{database.name}' @@ -376,6 +378,7 @@ def get_tables(self, database: SQLDatabase) -> list[SQLTable]: database=database, engine=row["ENGINE"], collation_name=row["TABLE_COLLATION"], + row_format=row["ROW_FORMAT"], auto_increment=int(row["AUTO_INCREMENT"] or 0), total_bytes=row["total_bytes"], total_rows=row["TABLE_ROWS"], diff --git a/structures/engines/mysql/database.py b/structures/engines/mysql/database.py index fd2694d..399f4e3 100644 --- a/structures/engines/mysql/database.py +++ b/structures/engines/mysql/database.py @@ -69,6 +69,9 @@ def drop(self) -> bool: @dataclasses.dataclass(eq=False) class MySQLTable(SQLTable): + row_format: Optional[str] = None + convert_data: bool = dataclasses.field(default=False, compare=False) + def raw_create(self) -> str: columns = [str(MySQLColumnBuilder(column)) for column in self.columns] @@ -90,11 +93,11 @@ def alter_auto_increment(self, auto_increment: int): return True - def alter_collation(self, convert: bool = True): + def alter_collation(self, collation_name: str, convert: bool = True): charset = "" if convert: - charset = f"CONVERT TO CHARACTER SET {self.database.context.COLLATIONS[self.collation_name]}" - return self.database.context.execute(f"""ALTER TABLE `{self.database.name}`.`{self.name}` {charset} COLLATE {self.collation_name};""") + charset = f"CONVERT TO CHARACTER SET {self.database.context.COLLATIONS[collation_name]} " + return self.database.context.execute(f"ALTER TABLE `{self.database.name}`.`{self.name}` {charset}COLLATE {collation_name};") def alter_engine(self, engine: str): statement = f"ALTER TABLE `{self.database.name}`.`{self.name}` ENGINE {engine};" @@ -102,6 +105,12 @@ def alter_engine(self, engine: str): return True + def alter_row_format(self, row_format: str): + statement = f"ALTER TABLE `{self.database.name}`.`{self.name}` ROW_FORMAT={row_format};" + self.database.context.execute(statement) + + return True + def rename(self, table: Self, new_name: str) -> bool: statement = f"ALTER TABLE `{self.database.name}`.`{table.name}` RENAME TO `{new_name}`;" self.database.context.execute(statement) @@ -152,9 +161,11 @@ def alter(self) -> bool: if self.auto_increment != original_table.auto_increment: original_table.alter_auto_increment(self.auto_increment) if self.collation_name != original_table.collation_name: - original_table.alter_collation(self.collation_name) + original_table.alter_collation(self.collation_name, convert=self.convert_data) if self.engine != original_table.engine: original_table.alter_engine(self.engine) + if self.row_format != original_table.row_format and self.row_format: + original_table.alter_row_format(self.row_format) for original, current in map_columns: if original is None: diff --git a/windows/main/controller.py b/windows/main/controller.py index 8315d6e..1ee4ee3 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -65,7 +65,9 @@ def __init__(self): comments=self.table_comment, auto_increment=self.table_auto_increment, collation=self.table_collation, + convert_data=self.convert_data_collation, engine=self.table_engine, + row_format=self.table_row_format, ) self.list_database_tables = ListDatabaseTable(self.list_ctrl_database_tables) @@ -1082,7 +1084,7 @@ def _on_current_session(self, session: Session): if session: wx.CallAfter(self.status_bar.SetStatusText, f"{_('Connection')}: {session.name}", 0) - wx.CallAfter(self.status_bar.SetStatusText, f"{_('Version')}: {session.context.get_server_version()}", 1) + wx.CallAfter(self.status_bar.SetStatusText, f"{_('Version')}: {session.context.server_version}", 1) wx.CallAfter(self.status_bar.SetStatusText, f"{_('Uptime')}: {self._format_server_uptime(session.context.get_server_uptime())}", 2) @@ -1118,8 +1120,16 @@ def _on_current_database(self, database: SQLDatabase): self.table_collation.Enable(len(database.context.COLLATIONS.keys()) > 1) self.table_collation.SetItems(list(database.context.COLLATIONS.keys())) + row_formats = database.context.ROW_FORMATS + self.table_row_format.Enable(bool(row_formats)) + self.table_row_format.SetItems(row_formats) + + self.convert_data_collation.Enable(bool(database.context.COLLATIONS)) + if (session := CURRENT_SESSION.get_value()) and session.engine in [ConnectionEngine.SQLITE]: self.table_collation.Enable(False) + self.convert_data_collation.Enable(False) + self.table_row_format.Enable(False) def on_apply_database(self, event: wx.Event): database = CURRENT_DATABASE.get_value() diff --git a/windows/main/tabs/table.py b/windows/main/tabs/table.py index e53116f..626843b 100644 --- a/windows/main/tabs/table.py +++ b/windows/main/tabs/table.py @@ -14,10 +14,13 @@ def __init__(self): self.auto_increment = Observable() self.collation = Observable() + self.convert_data = Observable() self.engine = Observable() + self.row_format = Observable() debounce( - self.name, self.comment, self.auto_increment, self.collation, self.engine, + self.name, self.comment, self.auto_increment, self.collation, self.convert_data, + self.engine, self.row_format, callback=self.update_table ) @@ -31,7 +34,9 @@ def _load_table(self, table: SQLTable): self.comment.set_initial(table.comment) self.auto_increment.set_initial(table.auto_increment) self.collation.set_initial(table.collation_name) + self.convert_data.set_initial(False) self.engine.set_initial(table.engine) + self.row_format.set_initial(getattr(table, "row_format", None)) def update_table(self, *args): if not any(args): @@ -44,6 +49,10 @@ def update_table(self, *args): table.auto_increment = int(self.auto_increment.get_value() or 0) table.collation_name = self.collation.get_value() table.engine = self.engine.get_value() + if hasattr(table, "convert_data"): + table.convert_data = bool(self.convert_data.get_value()) + if hasattr(table, "row_format"): + table.row_format = self.row_format.get_value() or None if not table.is_new: original_table = next((t for t in CURRENT_DATABASE.get_value().tables if t.id == table.id), None) From d59afd83593ce6c4226cedcb739ec515ef6e58f7 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 17 Mar 2026 17:34:27 +0100 Subject: [PATCH 24/93] docs(engines): add docstrings to type parsing and mapping methods Clarify the distinct roles of _parse_type (DDL column string parsing), _get_field_type_name (pymysql type code resolution), and get_result_column_datatypes (query result metadata mapping) in both MySQL and MariaDB contexts. AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- structures/engines/mariadb/context.py | 18 ++++++++++++++++++ structures/engines/mysql/context.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/structures/engines/mariadb/context.py b/structures/engines/mariadb/context.py index 48d6240..b459c06 100755 --- a/structures/engines/mariadb/context.py +++ b/structures/engines/mariadb/context.py @@ -85,6 +85,12 @@ def after_connect(self, *args, **kwargs): self.FUNCTIONS = tuple(dict.fromkeys(builtin_functions + user_functions)) def _parse_type(self, column_type: str): + """Parse a raw COLUMN_TYPE string from information_schema into structured field attributes. + + Used in get_columns() to extract length, precision, scale, set values, and flags + from DDL-style strings such as 'varchar(255)', 'decimal(10,2)', or 'enum('a','b')'. + Returns an empty dict when no pattern matches. + """ types = MariaDBDataType.get_all() type_set = [ x.lower() @@ -121,6 +127,12 @@ def _parse_type(self, column_type: str): return dict() def _get_field_type_name(self, type_code: Optional[int]) -> Optional[str]: + """Resolve a pymysql FIELD_TYPE integer code to its constant name. + + Used in get_result_column_datatypes() to bridge the driver's numeric type + representation to a named string (e.g. 253 -> 'VAR_STRING'). + Returns None when the code is absent or unrecognised. + """ if type_code is None: return None @@ -136,6 +148,12 @@ def _get_field_type_name(self, type_code: Optional[int]) -> Optional[str]: def get_result_column_datatypes( self, cursor: pymysql.cursors.Cursor ) -> list[Optional[SQLDataType]]: + """Map each result column to its SQLDataType using the pymysql cursor description. + + Resolves the driver's numeric type code via _get_field_type_name(), then looks up + the matching SQLDataType by name. Returns None for columns whose type cannot be resolved. + Unlike _parse_type(), this operates on query result metadata, not on DDL column strings. + """ datatypes: list[Optional[SQLDataType]] = [] for description in cursor.description or []: diff --git a/structures/engines/mysql/context.py b/structures/engines/mysql/context.py index 4733d20..baed054 100644 --- a/structures/engines/mysql/context.py +++ b/structures/engines/mysql/context.py @@ -85,6 +85,12 @@ def after_connect(self, *args, **kwargs): self.FUNCTIONS = tuple(dict.fromkeys(builtin_functions + user_functions)) def _parse_type(self, column_type: str): + """Parse a raw COLUMN_TYPE string from information_schema into structured field attributes. + + Used in get_columns() to extract length, precision, scale, set values, and flags + from DDL-style strings such as 'varchar(255)', 'decimal(10,2)', or 'enum('a','b')'. + Returns an empty dict when no pattern matches. + """ types = MySQLDataType.get_all() type_set = [ x.lower() @@ -121,6 +127,12 @@ def _parse_type(self, column_type: str): return dict() def _get_field_type_name(self, type_code: Optional[int]) -> Optional[str]: + """Resolve a pymysql FIELD_TYPE integer code to its constant name. + + Used in get_result_column_datatypes() to bridge the driver's numeric type + representation to a named string (e.g. 253 -> 'VAR_STRING'). + Returns None when the code is absent or unrecognised. + """ if type_code is None: return None @@ -136,6 +148,12 @@ def _get_field_type_name(self, type_code: Optional[int]) -> Optional[str]: def get_result_column_datatypes( self, cursor: pymysql.cursors.Cursor ) -> list[Optional[SQLDataType]]: + """Map each result column to its SQLDataType using the pymysql cursor description. + + Resolves the driver's numeric type code via _get_field_type_name(), then looks up + the matching SQLDataType by name. Returns None for columns whose type cannot be resolved. + Unlike _parse_type(), this operates on query result metadata, not on DDL column strings. + """ datatypes: list[Optional[SQLDataType]] = [] for description in cursor.description or []: From bc43516eca511ca2a4f60353238f2026c278b093 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 17 Mar 2026 17:35:12 +0100 Subject: [PATCH 25/93] chore: checkpoint before windows/main reorganization Includes pending UI changes (views, fbp layout, database_options controller, settings, dataview, PostgreSQL/SQLite context updates). AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- PeterSQL.fbp | 762 +++++++++++++++-------- helpers/dataview.py | 21 +- settings.yml | 4 +- structures/engines/postgresql/context.py | 4 +- structures/engines/sqlite/context.py | 4 +- windows/main/tabs/database.py | 2 - windows/main/tabs/database_options.py | 153 +++-- windows/main/tabs/table.py | 1 + windows/views.py | 94 ++- 9 files changed, 686 insertions(+), 359 deletions(-) diff --git a/PeterSQL.fbp b/PeterSQL.fbp index c9e09d0..e331341 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -6401,7 +6401,7 @@ wxTAB_TRAVERSAL 1 do_close - + 1 @@ -6423,7 +6423,7 @@ - + File m_menu2 protected @@ -6442,7 +6442,7 @@ on_settings - + Help m_menu4 protected @@ -6531,6 +6531,9 @@ do_open_connection_manager + + protected + Load From File; icons/16x16/disconnect.png 0 @@ -6541,10 +6544,7 @@ protected - do_disconnect - - - protected + on_database_disconnect Load From File; icons/16x16/database_refresh.png @@ -6556,6 +6556,7 @@ protected Refresh Refresh + on_database_refresh protected @@ -7330,7 +7331,7 @@ Load From File; icons/16x16/database.png Database - 1 + 0 1 1 @@ -7776,211 +7777,20 @@ none - + 5 wxEXPAND 0 - + bSizer142 wxHORIZONTAL none - + 5 wxALIGN_CENTER 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_character_set_panel - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer139 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Character set - 0 - - 0 - - - 0 - 150,-1 - 1 - m_staticText70 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - database_character_set - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - 5 - wxALIGN_CENTER - 1 - + 1 1 1 @@ -8167,13 +7977,23 @@ + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + - + 5 wxEXPAND 0 - + bSizer13911 wxHORIZONTAL @@ -11298,7 +11118,7 @@ Load From File; icons/16x16/table.png Table - 0 + 1 1 1 @@ -11418,8 +11238,8 @@ - - + + 1 1 1 @@ -11471,16 +11291,16 @@ wxTAB_TRAVERSAL - + bSizer55 wxVERTICAL none - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -11537,7 +11357,7 @@ Load From Embedded File; icons/16x16/table.png Base - 1 + 0 1 1 @@ -11874,11 +11694,11 @@ - + Load From File; icons/16x16/wrench.png Options - 0 - + 1 + 1 1 1 @@ -11930,16 +11750,16 @@ wxTAB_TRAVERSAL - + bSizer261 wxVERTICAL none - + 5 wxEXPAND 0 - + 2 0 @@ -12015,14 +11835,228 @@ - -1 + -1 + + + + 5 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 0 + + 0 + + 1 + table_auto_increment + 1 + + + protected + 1 + + Resizable + 1 + + + + 0 + + + wxFILTER_EXCLUDE_CHAR_LIST|wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer2712 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Engine + 0 + + 0 + + + 0 + + 1 + m_staticText812 + 1 + + + protected + 1 + + Resizable + 1 + 150,-1 + + + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + table_engine + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxEXPAND + 0 + + + bSizer2721 + wxHORIZONTAL + none 5 - wxALL|wxEXPAND - 1 - + wxALIGN_CENTER|wxALL + 0 + 1 1 1 @@ -12051,15 +12085,16 @@ 0 0 wxID_ANY + Default Collation + 0 0 - 0 0 1 - table_auto_increment + m_staticText821 1 @@ -12068,37 +12103,22 @@ Resizable 1 - + 150,-1 0 - - wxFILTER_EXCLUDE_CHAR_LIST|wxFILTER_NONE - wxDefaultValidator - - + -1 - - - - 5 - wxEXPAND - 0 - - - bSizer2712 - wxHORIZONTAL - none 5 - wxALIGN_CENTER|wxALL - 0 - + wxALL + 1 + 1 1 1 @@ -12112,6 +12132,7 @@ 1 0 + 1 1 @@ -12127,8 +12148,6 @@ 0 0 wxID_ANY - Engine - 0 0 @@ -12136,7 +12155,7 @@ 0 1 - m_staticText812 + table_collation 1 @@ -12144,23 +12163,27 @@ 1 Resizable + 0 1 - 150,-1 + - + ; ; forward_declare 0 + + wxFILTER_NONE + wxDefaultValidator + - -1 5 - wxALL|wxEXPAND - 1 - + wxALIGN_CENTER|wxALL + 0 + 1 1 1 @@ -12174,7 +12197,7 @@ 1 0 - "" + 0 1 1 @@ -12190,6 +12213,7 @@ 0 0 wxID_ANY + Convert data 0 @@ -12197,7 +12221,7 @@ 0 1 - table_engine + convert_data_collation 1 @@ -12205,11 +12229,10 @@ 1 Resizable - 1 1 - + ; ; forward_declare 0 @@ -12223,20 +12246,20 @@ - + 5 wxEXPAND - 0 - + 1 + - bSizer2721 + bSizer145 wxHORIZONTAL none - + 5 wxALIGN_CENTER|wxALL 0 - + 1 1 1 @@ -12265,16 +12288,16 @@ 0 0 wxID_ANY - Default Collation + Row format 0 0 0 - + 150,-1 1 - m_staticText821 + m_staticText71 1 @@ -12283,9 +12306,9 @@ Resizable 1 - 150,-1 + - + ; ; forward_declare 0 @@ -12294,11 +12317,11 @@ -1 - + 5 wxALL 1 - + 1 1 1 @@ -12335,7 +12358,7 @@ 0 1 - table_collation + table_row_format 1 @@ -18789,6 +18812,203 @@ 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 + + + + + + + + + + 0 diff --git a/helpers/dataview.py b/helpers/dataview.py index 3cf5e22..2205906 100644 --- a/helpers/dataview.py +++ b/helpers/dataview.py @@ -114,12 +114,25 @@ def GetColumnCount(self) -> int: def GetValueByRow(self, row, col): if not self.data or row >= len(self.data): - return "" + return None - if fields := self._get_column_fields(): - return fields[col].get_value(self.get_data_by_row(row)) + field = self.get_data_by_row(row) + + # if fields := self._get_column_fields(): + # return fields[col].get_value(self.get_data_by_row(row)) + + # return self.get_data_by_row(row)[col] + + if self._get_column_fields(): + return self._get_column_fields()[col].get_value(field) - return self.get_data_by_row(row)[col] + # def GetValueByRow(self, row, col): + # if not len(self.data): + # return None + # + # table: SQLTable = self.get_data_by_row(row) + # + # return self.MAP_COLUMN_FIELDS[col].get_value(table) def HasValue(self, item, col): if not self.data: diff --git a/settings.yml b/settings.yml index 4bda072..adaeed6 100755 --- a/settings.yml +++ b/settings.yml @@ -2,8 +2,8 @@ language: en_US ui: window: size: - - 1920 - - 1048 + - 2256 + - 1472 position: - 0 - 0 diff --git a/structures/engines/postgresql/context.py b/structures/engines/postgresql/context.py index 1aa246d..964fd4f 100644 --- a/structures/engines/postgresql/context.py +++ b/structures/engines/postgresql/context.py @@ -59,9 +59,9 @@ def after_connect(self, *args, **kwargs): self.execute("SELECT collname FROM pg_collation;") self.COLLATIONS = {row["collname"]: row["collname"] for row in self.fetchall()} - server_version = self.get_server_version() + self.server_version = self.get_server_version() self.KEYWORDS, builtin_functions = self.get_engine_vocabulary( - "postgresql", server_version + "postgresql", self.server_version ) self.execute(""" diff --git a/structures/engines/sqlite/context.py b/structures/engines/sqlite/context.py index a68e05c..015d55b 100755 --- a/structures/engines/sqlite/context.py +++ b/structures/engines/sqlite/context.py @@ -69,9 +69,9 @@ def __init__(self, connection: Connection): def after_connect(self, *args, **kwargs): super().after_connect(*args, **kwargs) - server_version = self.get_server_version() + self.server_version = self.get_server_version() spec_keywords, spec_functions = self.get_engine_vocabulary( - "sqlite", server_version + "sqlite", self.server_version ) self.KEYWORDS = tuple( dict.fromkeys( diff --git a/windows/main/tabs/database.py b/windows/main/tabs/database.py index e80bb46..53fabd5 100644 --- a/windows/main/tabs/database.py +++ b/windows/main/tabs/database.py @@ -13,8 +13,6 @@ from windows.main import CURRENT_DATABASE, CURRENT_TABLE, CURRENT_SESSION -# SELECTED_TABLE: Observable[SQLTable] = Observable() - class ModelDatabaseTable(BaseObservableDataViewListModel): MAP_COLUMN_FIELDS = { 0: ColumnField("name", str), diff --git a/windows/main/tabs/database_options.py b/windows/main/tabs/database_options.py index 99da9d2..e3197c5 100644 --- a/windows/main/tabs/database_options.py +++ b/windows/main/tabs/database_options.py @@ -138,6 +138,9 @@ def _update_database(self, *args) -> None: class DatabaseOptionsController: def __init__(self, parent): self.parent = parent + self._is_updating_choices = False + self._is_apply_scheduled = False + self._last_applied_state: Optional[tuple[Optional[ConnectionEngine], Optional[int], Optional[str]]] = None self.model = EditDatabaseOptionsModel() self._panel_by_name = self._build_panel_by_name() self._panels_all = list(self._panel_by_name.values()) @@ -148,7 +151,19 @@ def __init__(self, parent): CURRENT_SESSION.subscribe(self._on_current_session) CURRENT_DATABASE.subscribe(self._on_current_database) - self.apply_for_current_state() + self._schedule_apply_for_current_state() + + def _schedule_apply_for_current_state(self) -> None: + if self._is_apply_scheduled: + return + + self._is_apply_scheduled = True + + def _run(): + self._is_apply_scheduled = False + self.apply_for_current_state() + + wx.CallAfter(_run) @staticmethod def _first_attr(source, names: list[str], default=None): @@ -163,20 +178,34 @@ def _first_attr(source, names: list[str], default=None): return default - def _apply_choice(self, choice: wx.Choice, items: list[str], selected: Optional[str]) -> None: - normalized = [str(item) for item in items if item is not None and str(item)] + @staticmethod + def _safe_text(value) -> str: + if value is None: + return "" - if selected is not None and str(selected) and str(selected) not in normalized: - normalized.append(str(selected)) + if isinstance(value, bytes): + return value.decode("utf-8", errors="replace") - choice.SetItems(normalized) + text = str(value) + return text.encode("utf-8", errors="replace").decode("utf-8", errors="replace") + + def _apply_choice(self, choice: wx.Choice, items: list[str], selected: Optional[str]) -> None: + normalized = [self._safe_text(item) for item in items if item is not None and self._safe_text(item)] + selected_text = self._safe_text(selected) + + if selected_text and selected_text not in normalized: + normalized.append(selected_text) if not normalized: - return + if choice.GetCount() > 0: + choice.Clear() - if selected and choice.SetStringSelection(str(selected)): return + choice.SetItems(normalized) + + if selected_text and choice.SetStringSelection(selected_text): + return choice.SetSelection(0) def _apply_engine(self, engine: Optional[ConnectionEngine]) -> None: @@ -285,58 +314,92 @@ def _layout_database_options(self) -> None: parent.Layout() def _on_current_database(self, database) -> None: - self.apply_for_current_state() + self._schedule_apply_for_current_state() def _on_current_session(self, session) -> None: - self.apply_for_current_state() + self._schedule_apply_for_current_state() + + def _populate_choices(self, database, engine: Optional[ConnectionEngine]) -> None: + if database is None: + return - def _populate_choices(self, database) -> None: context = database.context if database else None collations = [] - if context and getattr(context, "COLLATIONS", None): - collations = sorted(context.COLLATIONS.keys()) + collations_source = getattr(context, "COLLATIONS", None) if context else None + + if isinstance(collations_source, dict): + collations = sorted( + str(key) + for key in collations_source.keys() + if key is not None and str(key) + ) + + if engine in [ConnectionEngine.MYSQL, ConnectionEngine.MARIADB, ConnectionEngine.POSTGRESQL]: + self._apply_choice( + self.parent.database_collation, + collations, + self.model.database_collation.get_value(), + ) - self._apply_choice( - self.parent.database_collation, - collations, - self.model.database_collation.get_value(), - ) + if engine == ConnectionEngine.POSTGRESQL: + self._apply_choice( + self.parent.database_tablespace, + [self._first_attr(database, ["tablespace", "default_tablespace"])], + self.model.database_tablespace.get_value(), + ) - self._apply_choice( - self.parent.database_tablespace, - [self._first_attr(database, ["tablespace", "default_tablespace"])], - self.model.database_tablespace.get_value(), - ) - self._apply_choice( - self.parent.database_profile, - [self._first_attr(database, ["profile"])], - self.model.database_profile.get_value(), - ) - self._apply_choice( - self.parent.database_default_tablespace, - [self._first_attr(database, ["default_tablespace"])], - self.model.database_default_tablespace.get_value(), - ) - self._apply_choice( - self.parent.database_temporary_tablespace, - [self._first_attr(database, ["temporary_tablespace"])], - self.model.database_temporary_tablespace.get_value(), - ) - self._apply_choice( - self.parent.database_account_status, - [self._first_attr(database, ["account_status"])], - self.model.database_account_status.get_value(), - ) + if engine == ConnectionEngine.ORACLE: + self._apply_choice( + self.parent.database_profile, + [self._first_attr(database, ["profile"])], + self.model.database_profile.get_value(), + ) + self._apply_choice( + self.parent.database_default_tablespace, + [self._first_attr(database, ["default_tablespace"])], + self.model.database_default_tablespace.get_value(), + ) + self._apply_choice( + self.parent.database_temporary_tablespace, + [self._first_attr(database, ["temporary_tablespace"])], + self.model.database_temporary_tablespace.get_value(), + ) + self._apply_choice( + self.parent.database_account_status, + [self._first_attr(database, ["account_status"])], + self.model.database_account_status.get_value(), + ) def _set_controls_enabled(self, enabled: bool) -> None: for control in self._controls_all: control.Enable(enabled) def apply_for_current_state(self) -> None: + if self._is_updating_choices: + return + session = CURRENT_SESSION.get_value() database = CURRENT_DATABASE.get_value() engine = session.engine if session else None - self._populate_choices(database) - self._apply_engine(engine) + if database is None: + self._last_applied_state = None + return + + current_state = ( + engine, + getattr(database, "id", None), + getattr(database, "name", None), + ) + + if current_state == self._last_applied_state: + return + + self._is_updating_choices = True + try: + self._populate_choices(database, engine) + self._apply_engine(engine) + self._last_applied_state = current_state + finally: + self._is_updating_choices = False diff --git a/windows/main/tabs/table.py b/windows/main/tabs/table.py index 626843b..a948aa9 100644 --- a/windows/main/tabs/table.py +++ b/windows/main/tabs/table.py @@ -49,6 +49,7 @@ def update_table(self, *args): table.auto_increment = int(self.auto_increment.get_value() or 0) table.collation_name = self.collation.get_value() table.engine = self.engine.get_value() + if hasattr(table, "convert_data"): table.convert_data = bool(self.convert_data.get_value()) if hasattr(table, "row_format"): diff --git a/windows/views.py b/windows/views.py index 72adb78..d9e181d 100755 --- a/windows/views.py +++ b/windows/views.py @@ -897,10 +897,10 @@ def __init__( self, parent ): self.m_toolBar1 = self.CreateToolBar( wx.TB_HORIZONTAL, wx.ID_ANY ) self.m_tool5 = self.m_toolBar1.AddTool( wx.ID_ANY, _(u"Open connection manager"), wx.Bitmap( u"icons/16x16/server_connect.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - self.m_tool4 = self.m_toolBar1.AddTool( wx.ID_ANY, _(u"Disconnect from server"), wx.Bitmap( u"icons/16x16/disconnect.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - self.m_toolBar1.AddSeparator() + self.m_tool4 = self.m_toolBar1.AddTool( wx.ID_ANY, _(u"Disconnect from server"), wx.Bitmap( u"icons/16x16/disconnect.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + self.database_refresh = self.m_toolBar1.AddTool( wx.ID_ANY, _(u"tool"), wx.Bitmap( u"icons/16x16/database_refresh.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Refresh"), _(u"Refresh"), None ) self.m_toolBar1.AddSeparator() @@ -1023,27 +1023,6 @@ def __init__( self, parent ): bSizer142 = wx.BoxSizer( wx.HORIZONTAL ) - self.database_character_set_panel = wx.Panel( self.m_panel54, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - bSizer139 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText70 = wx.StaticText( self.database_character_set_panel, wx.ID_ANY, _(u"Character set"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText70.Wrap( -1 ) - - self.m_staticText70.SetMinSize( wx.Size( 150,-1 ) ) - - bSizer139.Add( self.m_staticText70, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - database_character_setChoices = [] - self.database_character_set = wx.Choice( self.database_character_set_panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, database_character_setChoices, 0 ) - self.database_character_set.SetSelection( 0 ) - bSizer139.Add( self.database_character_set, 1, wx.ALL, 5 ) - - - self.database_character_set_panel.SetSizer( bSizer139 ) - self.database_character_set_panel.Layout() - bSizer139.Fit( self.database_character_set_panel ) - bSizer142.Add( self.database_character_set_panel, 1, wx.ALIGN_CENTER, 5 ) - self.database_collation_panel = wx.Panel( self.m_panel54, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer1392 = wx.BoxSizer( wx.HORIZONTAL ) @@ -1066,6 +1045,9 @@ def __init__( self, parent ): bSizer142.Add( self.database_collation_panel, 1, wx.ALIGN_CENTER, 5 ) + bSizer142.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + + bSizer158.Add( bSizer142, 0, wx.EXPAND, 5 ) bSizer13911 = wx.BoxSizer( wx.HORIZONTAL ) @@ -1439,7 +1421,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"), True ) + self.MainFrameNotebook.AddPage( self.panel_database, _(u"Database"), False ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/database.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -1494,7 +1476,7 @@ def __init__( self, parent ): self.PanelTableBase.SetSizer( bSizer262 ) self.PanelTableBase.Layout() bSizer262.Fit( self.PanelTableBase ) - self.m_notebook3.AddPage( self.PanelTableBase, _(u"Base"), True ) + self.m_notebook3.AddPage( self.PanelTableBase, _(u"Base"), False ) m_notebook3Bitmap = wx.Bitmap( u"icons/16x16/table.png", wx.BITMAP_TYPE_ANY ) if ( m_notebook3Bitmap.IsOk() ): m_notebook3Images.Add( m_notebook3Bitmap ) @@ -1526,9 +1508,9 @@ def __init__( self, parent ): bSizer2712.Add( self.m_staticText812, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - table_engineChoices = [ wx.EmptyString ] + table_engineChoices = [] self.table_engine = wx.Choice( self.PanelTableOptions, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, table_engineChoices, 0 ) - self.table_engine.SetSelection( 1 ) + self.table_engine.SetSelection( 0 ) bSizer2712.Add( self.table_engine, 1, wx.ALL|wx.EXPAND, 5 ) @@ -1546,9 +1528,29 @@ def __init__( self, parent ): self.table_collation.SetSelection( 0 ) bSizer2721.Add( self.table_collation, 1, wx.ALL, 5 ) + self.convert_data_collation = wx.CheckBox( self.PanelTableOptions, wx.ID_ANY, _(u"Convert data"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer2721.Add( self.convert_data_collation, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + gSizer11.Add( bSizer2721, 0, wx.EXPAND, 5 ) + bSizer145 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText71 = wx.StaticText( self.PanelTableOptions, wx.ID_ANY, _(u"Row format"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText71.Wrap( -1 ) + + self.m_staticText71.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer145.Add( self.m_staticText71, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + table_row_formatChoices = [] + self.table_row_format = wx.Choice( self.PanelTableOptions, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, table_row_formatChoices, 0 ) + self.table_row_format.SetSelection( 0 ) + bSizer145.Add( self.table_row_format, 1, wx.ALL, 5 ) + + + gSizer11.Add( bSizer145, 1, wx.EXPAND, 5 ) + bSizer261.Add( gSizer11, 0, wx.EXPAND, 5 ) @@ -1556,7 +1558,7 @@ def __init__( self, parent ): self.PanelTableOptions.SetSizer( bSizer261 ) self.PanelTableOptions.Layout() bSizer261.Fit( self.PanelTableOptions ) - self.m_notebook3.AddPage( self.PanelTableOptions, _(u"Options"), False ) + self.m_notebook3.AddPage( self.PanelTableOptions, _(u"Options"), True ) m_notebook3Bitmap = wx.Bitmap( u"icons/16x16/wrench.png", wx.BITMAP_TYPE_ANY ) if ( m_notebook3Bitmap.IsOk() ): m_notebook3Images.Add( m_notebook3Bitmap ) @@ -1833,7 +1835,7 @@ def __init__( self, parent ): self.panel_table.SetSizer( bSizer251 ) self.panel_table.Layout() bSizer251.Fit( self.panel_table ) - self.MainFrameNotebook.AddPage( self.panel_table, _(u"Table"), False ) + self.MainFrameNotebook.AddPage( self.panel_table, _(u"Table"), True ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/table.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2454,7 +2456,8 @@ def __init__( self, parent ): self.Bind( wx.EVT_MENU, self.on_settings, id = self.m_menuItem22.GetId() ) self.Bind( wx.EVT_MENU, self.on_menu_about, id = self.m_menuItem15.GetId() ) self.Bind( wx.EVT_TOOL, self.do_open_connection_manager, id = self.m_tool5.GetId() ) - self.Bind( wx.EVT_TOOL, self.do_disconnect, id = self.m_tool4.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_database_disconnect, id = self.m_tool4.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_database_refresh, id = self.database_refresh.GetId() ) self.MainFrameNotebook.Bind( wx.EVT_NOTEBOOK_PAGE_CHANGED, self.on_page_chaged ) self.btn_insert_table.Bind( wx.EVT_BUTTON, self.on_insert_table ) self.btn_clone_table.Bind( wx.EVT_BUTTON, self.on_clone_table ) @@ -2511,7 +2514,10 @@ def on_menu_about( self, event ): def do_open_connection_manager( self, event ): event.Skip() - def do_disconnect( self, event ): + def on_database_disconnect( self, event ): + event.Skip() + + def on_database_refresh( self, event ): event.Skip() def on_page_chaged( self, event ): @@ -2662,6 +2668,32 @@ 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.SetSizer( bSizer144 ) + self.Layout() def __del__( self ): pass From 93a82b099ccf72ef4c9a9a812453e4e2c831ad25 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Wed, 18 Mar 2026 19:37:12 +0100 Subject: [PATCH 26/93] Refactor windows/main modules and update UI controllers/tests imports AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- PeterSQL.fbp | 154 ++-- helpers/loader.py | 4 +- structures/engines/database.py | 6 + tests/core/test_view_editor.py | 40 +- tests/test_column_controller.py | 34 +- tests/ui/test_column_controller.py | 34 +- tests/ui/test_index_controller.py | 14 +- windows/components/__init__.py | 3 + windows/main/controller.py | 87 +- windows/main/{tabs => database}/__init__.py | 0 .../{tabs/database.py => database/list.py} | 0 .../options.py} | 0 windows/main/{tabs => database}/view.py | 0 windows/main/{tabs => }/explorer.py | 0 windows/main/query/__init__.py | 0 windows/main/query/controller.py | 274 ++++++ windows/main/query/executor.py | 259 ++++++ windows/main/query/parser.py | 83 ++ windows/main/query/renderer.py | 304 ++++++ windows/main/table/__init__.py | 0 windows/main/{tabs => table}/check.py | 2 +- windows/main/{tabs => table}/column.py | 0 windows/main/{tabs => table}/foreign_key.py | 0 windows/main/{tabs => table}/index.py | 6 +- .../main/{tabs/table.py => table/options.py} | 0 windows/main/{tabs => table}/records.py | 52 +- windows/main/tabs/query.py | 872 ------------------ windows/views.py | 10 +- 28 files changed, 1146 insertions(+), 1092 deletions(-) rename windows/main/{tabs => database}/__init__.py (100%) rename windows/main/{tabs/database.py => database/list.py} (100%) rename windows/main/{tabs/database_options.py => database/options.py} (100%) rename windows/main/{tabs => database}/view.py (100%) rename windows/main/{tabs => }/explorer.py (100%) create mode 100644 windows/main/query/__init__.py create mode 100644 windows/main/query/controller.py create mode 100644 windows/main/query/executor.py create mode 100644 windows/main/query/parser.py create mode 100644 windows/main/query/renderer.py create mode 100644 windows/main/table/__init__.py rename windows/main/{tabs => table}/check.py (98%) rename windows/main/{tabs => table}/column.py (100%) rename windows/main/{tabs => table}/foreign_key.py (100%) rename windows/main/{tabs => table}/index.py (98%) rename windows/main/{tabs/table.py => table/options.py} (100%) rename windows/main/{tabs => table}/records.py (91%) delete mode 100644 windows/main/tabs/query.py diff --git a/PeterSQL.fbp b/PeterSQL.fbp index e331341..2589fd0 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -11118,7 +11118,7 @@ Load From File; icons/16x16/table.png Table - 1 + 0 1 1 @@ -17730,7 +17730,7 @@ Load From File; icons/16x16/arrow_right.png Query - 0 + 1 1 1 @@ -18117,80 +18117,6 @@ - - 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 - - - - - - @@ -18788,7 +18714,7 @@ - + 0 wxAUI_MGR_DEFAULT @@ -19008,6 +18934,80 @@ + + 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 + + + + + + diff --git a/helpers/loader.py b/helpers/loader.py index b25d725..b3e25cf 100644 --- a/helpers/loader.py +++ b/helpers/loader.py @@ -16,9 +16,8 @@ def _update_loading_state(cls): @contextmanager def cursor_wait(cls): """Context manager to show wait cursor during operations""" - token = object() # Unique token for this operation + token = object() - # Add token to queue current_queue = cls._queue() current_queue.append(token) cls._queue(current_queue) @@ -27,7 +26,6 @@ def cursor_wait(cls): try: yield finally: - # Remove token from queue current_queue = cls._queue() if token in current_queue: current_queue.remove(token) diff --git a/structures/engines/database.py b/structures/engines/database.py index 70802dc..4238062 100755 --- a/structures/engines/database.py +++ b/structures/engines/database.py @@ -578,6 +578,12 @@ def __eq__(self, other: object) -> bool: return self.values == other.values + def copy(self): + cls = self.__class__ + field_values = {f.name: getattr(self, f.name) for f in dataclasses.fields(cls)} + field_values["values"] = dict(field_values["values"]) + return cls(**field_values) + def __str__(self) -> str: return f"{self.__class__.__name__}(id={self.id}, table={self.table.name}, values={self.values})" diff --git a/tests/core/test_view_editor.py b/tests/core/test_view_editor.py index 6cc3b3d..b98397a 100644 --- a/tests/core/test_view_editor.py +++ b/tests/core/test_view_editor.py @@ -8,7 +8,7 @@ class TestEditViewModel: def test_init_creates_observables(self): """Test that EditViewModel initializes all required observables.""" - from windows.main.tabs.view import EditViewModel + from windows.main.database.view import EditViewModel model = EditViewModel() @@ -24,7 +24,7 @@ def test_init_creates_observables(self): def test_load_view_sets_name_observable(self): """Test that _load_view sets name observable from view.""" - from windows.main.tabs.view import EditViewModel + from windows.main.database.view import EditViewModel model = EditViewModel() @@ -32,7 +32,7 @@ def test_load_view_sets_name_observable(self): mock_view.name = "test_view" mock_view.statement = "SELECT * FROM test" - with patch('windows.main.tabs.view.CURRENT_SESSION') as mock_session: + with patch('windows.main.database.view.CURRENT_SESSION') as mock_session: mock_session.get_value.return_value = None model._load_view(mock_view) @@ -41,7 +41,7 @@ def test_load_view_sets_name_observable(self): def test_update_view_sets_name_and_statement(self): """Test that update_view sets view name and statement from observables.""" - from windows.main.tabs.view import EditViewModel + from windows.main.database.view import EditViewModel model = EditViewModel() @@ -49,7 +49,7 @@ def test_update_view_sets_name_and_statement(self): mock_view.name = "" mock_view.statement = "" - with patch('windows.main.tabs.view.CURRENT_VIEW') as mock_current_view: + with patch('windows.main.database.view.CURRENT_VIEW') as mock_current_view: mock_current_view.get_value.return_value = mock_view model.name.set_value("updated_view") @@ -104,10 +104,10 @@ def mock_parent(self): def test_init_binds_controls(self, mock_parent): """Test that controller initializes and binds controls.""" - from windows.main.tabs.view import ViewEditorController + from windows.main.database.view import ViewEditorController - with patch('windows.main.tabs.view.CURRENT_VIEW') as mock_current_view: - with patch('windows.main.tabs.view.wx_call_after_debounce'): + with patch('windows.main.database.view.CURRENT_VIEW') as mock_current_view: + with patch('windows.main.database.view.wx_call_after_debounce'): controller = ViewEditorController(mock_parent) assert controller.parent == mock_parent @@ -116,10 +116,10 @@ def test_init_binds_controls(self, mock_parent): def test_get_original_view_returns_none_for_new_view(self, mock_parent): """Test that _get_original_view returns None for new views.""" - from windows.main.tabs.view import ViewEditorController + from windows.main.database.view import ViewEditorController - with patch('windows.main.tabs.view.CURRENT_VIEW'): - with patch('windows.main.tabs.view.wx_call_after_debounce'): + with patch('windows.main.database.view.CURRENT_VIEW'): + with patch('windows.main.database.view.wx_call_after_debounce'): controller = ViewEditorController(mock_parent) mock_view = Mock() @@ -130,10 +130,10 @@ def test_get_original_view_returns_none_for_new_view(self, mock_parent): def test_has_changes_returns_true_for_new_view(self, mock_parent): """Test that _has_changes returns True for new views.""" - from windows.main.tabs.view import ViewEditorController + from windows.main.database.view import ViewEditorController - with patch('windows.main.tabs.view.CURRENT_VIEW'): - with patch('windows.main.tabs.view.wx_call_after_debounce'): + with patch('windows.main.database.view.CURRENT_VIEW'): + with patch('windows.main.database.view.wx_call_after_debounce'): controller = ViewEditorController(mock_parent) mock_view = Mock() @@ -144,10 +144,10 @@ def test_has_changes_returns_true_for_new_view(self, mock_parent): def test_update_button_states_disables_all_when_no_view(self, mock_parent): """Test that update_button_states disables all buttons when no view.""" - from windows.main.tabs.view import ViewEditorController + from windows.main.database.view import ViewEditorController - with patch('windows.main.tabs.view.CURRENT_VIEW') as mock_current_view: - with patch('windows.main.tabs.view.wx_call_after_debounce'): + with patch('windows.main.database.view.CURRENT_VIEW') as mock_current_view: + with patch('windows.main.database.view.wx_call_after_debounce'): mock_current_view.get_value.return_value = None controller = ViewEditorController(mock_parent) @@ -159,10 +159,10 @@ def test_update_button_states_disables_all_when_no_view(self, mock_parent): def test_update_button_states_enables_save_cancel_for_new_view(self, mock_parent): """Test that update_button_states enables save/cancel for new views.""" - from windows.main.tabs.view import ViewEditorController + from windows.main.database.view import ViewEditorController - with patch('windows.main.tabs.view.CURRENT_VIEW') as mock_current_view: - with patch('windows.main.tabs.view.wx_call_after_debounce'): + with patch('windows.main.database.view.CURRENT_VIEW') as mock_current_view: + with patch('windows.main.database.view.wx_call_after_debounce'): mock_view = Mock() type(mock_view).is_new = PropertyMock(return_value=True) mock_current_view.get_value.return_value = mock_view diff --git a/tests/test_column_controller.py b/tests/test_column_controller.py index bc475cf..7bfd050 100644 --- a/tests/test_column_controller.py +++ b/tests/test_column_controller.py @@ -3,7 +3,7 @@ from structures.engines.sqlite.context import SQLiteContext from structures.engines.sqlite.database import SQLiteDatabase, SQLiteTable, SQLiteIndex, SQLiteColumn -from windows.main.tabs.column import TableColumnsController +from windows.main.table.column import TableColumnsController @pytest.fixture @@ -37,9 +37,9 @@ def mock_table(mock_session): @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_append_column_index(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): # Setup mocks mock_get_app.return_value = Mock() @@ -72,9 +72,9 @@ def test_append_column_index(mock_new_table, mock_current_table, mock_current_se @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_on_column_insert(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): # Setup mocks mock_get_app.return_value = Mock() @@ -115,9 +115,9 @@ def test_on_column_insert(mock_new_table, mock_current_table, mock_current_sessi @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_on_column_delete(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): # Setup mocks mock_get_app.return_value = Mock() @@ -163,10 +163,10 @@ def test_on_column_delete(mock_new_table, mock_current_table, mock_current_sessi @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.CURRENT_COLUMN') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.CURRENT_COLUMN') +@patch('windows.main.table.column.NEW_TABLE') def test_on_column_move_up(mock_new_table, mock_current_column, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): # Setup mocks mock_get_app.return_value = Mock() @@ -205,9 +205,9 @@ def test_on_column_move_up(mock_new_table, mock_current_column, mock_current_tab @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_insert_column_index(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): # Setup mocks mock_get_app.return_value = Mock() diff --git a/tests/ui/test_column_controller.py b/tests/ui/test_column_controller.py index 209f234..07a341c 100644 --- a/tests/ui/test_column_controller.py +++ b/tests/ui/test_column_controller.py @@ -2,7 +2,7 @@ from unittest.mock import Mock, patch, call from structures.engines.sqlite.database import SQLiteDatabase, SQLiteTable, SQLiteIndex, SQLiteColumn -from windows.main.tabs.column import TableColumnsController +from windows.main.table.column import TableColumnsController @pytest.fixture @@ -27,9 +27,9 @@ def mock_table(sqlite_session): @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_append_column_index(mock_new_table, mock_current_table, mock_current_session, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_session.get_value.return_value = sqlite_session @@ -61,9 +61,9 @@ def test_append_column_index(mock_new_table, mock_current_table, mock_current_se @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_on_column_insert(mock_new_table, mock_current_table, mock_current_session, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_session.get_value.return_value = sqlite_session @@ -96,9 +96,9 @@ def test_on_column_insert(mock_new_table, mock_current_table, mock_current_sessi @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_on_column_delete(mock_new_table, mock_current_table, mock_current_session, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_session.get_value.return_value = sqlite_session @@ -136,10 +136,10 @@ def test_on_column_delete(mock_new_table, mock_current_table, mock_current_sessi @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.CURRENT_COLUMN') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.CURRENT_COLUMN') +@patch('windows.main.table.column.NEW_TABLE') def test_on_column_move_up(mock_new_table, mock_current_column, mock_current_table, mock_current_session, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_session.get_value.return_value = sqlite_session @@ -170,9 +170,9 @@ def test_on_column_move_up(mock_new_table, mock_current_column, mock_current_tab @patch('wx.GetApp') -@patch('windows.main.tabs.column.CURRENT_SESSION') -@patch('windows.main.tabs.column.CURRENT_TABLE') -@patch('windows.main.tabs.column.NEW_TABLE') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') def test_insert_column_index(mock_new_table, mock_current_table, mock_current_session, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_session.get_value.return_value = sqlite_session diff --git a/tests/ui/test_index_controller.py b/tests/ui/test_index_controller.py index d241349..c3727b3 100644 --- a/tests/ui/test_index_controller.py +++ b/tests/ui/test_index_controller.py @@ -2,7 +2,7 @@ from unittest.mock import Mock, patch, call from structures.engines.sqlite.database import SQLiteDatabase, SQLiteTable, SQLiteIndex -from windows.main.tabs.index import TableIndexController +from windows.main.table.index import TableIndexController @pytest.fixture @@ -24,9 +24,9 @@ def mock_table(sqlite_session): @patch('wx.GetApp') -@patch('windows.main.tabs.index.CURRENT_TABLE') -@patch('windows.main.tabs.index.CURRENT_INDEX') -@patch('windows.main.tabs.index.NEW_TABLE') +@patch('windows.main.table.index.CURRENT_TABLE') +@patch('windows.main.table.index.CURRENT_INDEX') +@patch('windows.main.table.index.NEW_TABLE') def test_on_index_delete(mock_new_table, mock_current_index, mock_current_table, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_table.get_value.return_value = mock_table @@ -51,9 +51,9 @@ def test_on_index_delete(mock_new_table, mock_current_index, mock_current_table, @patch('wx.GetApp') -@patch('windows.main.tabs.index.CURRENT_TABLE') -@patch('windows.main.tabs.index.CURRENT_INDEX') -@patch('windows.main.tabs.index.NEW_TABLE') +@patch('windows.main.table.index.CURRENT_TABLE') +@patch('windows.main.table.index.CURRENT_INDEX') +@patch('windows.main.table.index.NEW_TABLE') def test_on_index_clear(mock_new_table, mock_current_index, mock_current_table, mock_get_app, sqlite_session, mock_table): mock_get_app.return_value = Mock() mock_current_table.get_value.return_value = mock_table diff --git a/windows/components/__init__.py b/windows/components/__init__.py index 12c6c5f..60bcf7d 100644 --- a/windows/components/__init__.py +++ b/windows/components/__init__.py @@ -84,6 +84,9 @@ def RenderText(self, text, x, rect, dc, state): return True def StartEditing(self, item, labelRect): + from windows.main.table.records import NULL_DISPLAY + if self._value == NULL_DISPLAY: + self._value = "" logger.debug("StartEditing") return super().StartEditing(item, labelRect) diff --git a/windows/main/controller.py b/windows/main/controller.py index 1ee4ee3..05ea299 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -34,17 +34,21 @@ from windows.components.stc.template_menu import SQLTemplateMenuController from windows.main import CURRENT_CONNECTION, CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE, CURRENT_COLUMN, CURRENT_INDEX, CURRENT_FOREIGN_KEY, CURRENT_RECORDS, AUTO_APPLY, CURRENT_VIEW, CURRENT_TRIGGER -from windows.main.tabs.query import QueryResultsController -from windows.main.tabs.table import EditTableModel, NEW_TABLE -from windows.main.tabs.index import TableIndexController -from windows.main.tabs.check import TableCheckController -from windows.main.tabs.column import TableColumnsController -from windows.main.tabs.records import TableRecordsController -from windows.main.tabs.database import ListDatabaseTable -from windows.main.tabs.database_options import DatabaseOptionsController -from windows.main.tabs.explorer import TreeExplorerController -from windows.main.tabs.foreign_key import TableForeignKeyController -from windows.main.tabs.view import ViewEditorController + +from windows.main.explorer import TreeExplorerController + +from windows.main.database.list import ListDatabaseTable +from windows.main.database.view import ViewEditorController +from windows.main.database.options import DatabaseOptionsController + +from windows.main.table.check import TableCheckController +from windows.main.table.index import TableIndexController +from windows.main.table.column import TableColumnsController +from windows.main.table.records import TableRecordsController +from windows.main.table.options import EditTableModel, NEW_TABLE +from windows.main.table.foreign_key import TableForeignKeyController + +from windows.main.query.controller import QueryResultsController class MainFrameController(MainFrameView): @@ -370,6 +374,7 @@ def _register_query_page( on_save_query=self.on_save, on_save_as_query=self.on_save_as_query, on_stop_state_changed=lambda enabled: self._set_query_stop_enabled(panel, enabled), + on_before_execute=lambda: self._autosave_query_page_before_execute(panel), ) self._query_pages.append(panel) self._query_page_meta[panel] = { @@ -593,7 +598,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) - QUERY_LOGS.append(_("Saved query to {file_path}").format(file_path=file_path)) + QUERY_LOGS.append(_("-- Saved query to {file_path}").format(file_path=file_path)) return True def _autosave_query_page_before_execute(self, page: wx.Panel) -> bool: @@ -618,7 +623,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) - QUERY_LOGS.append(_("Autosaved query to {file_path}").format(file_path=file_path)) + QUERY_LOGS.append(_("-- Autosaved query to {file_path}").format(file_path=file_path)) return True def _setup_subscribers(self): @@ -788,10 +793,10 @@ def _build_records_total_key(self, table: SQLTable, filters: str) -> tuple[str, return table.database.name, schema, table.name, filters def _count_table_records( - self, - table: SQLTable, - filters: str, - context: Optional[Any] = None, + self, + table: SQLTable, + filters: str, + context: Optional[Any] = None, ) -> int: if context is None: context = table.database.context @@ -815,12 +820,12 @@ def _count_table_records( return int(total_rows or 0) def _count_table_records_worker( - self, - session: Session, - table: SQLTable, - filters: str, - total_key: tuple[str, str, str, str], - request_id: int, + self, + session: Session, + table: SQLTable, + filters: str, + total_key: tuple[str, str, str, str], + request_id: int, ) -> None: total_rows = 0 error = None @@ -828,7 +833,7 @@ def _count_table_records_worker( try: context = session._get_context_class()(self._build_records_count_connection(session)) - context.connect() + context.connect(skip_before_connect=True, skip_after_connect=True) context.set_database(table.database) total_rows = self._count_table_records(table, filters, context) except Exception as ex: @@ -962,11 +967,11 @@ def _refresh_records_total_rows(self, table: SQLTable, filters: str) -> None: worker.start() def _on_records_count_complete( - self, - total_key: tuple[str, str, str, str], - request_id: int, - total_rows: int, - error: Optional[str], + self, + total_key: tuple[str, str, str, str], + request_id: int, + total_rows: int, + error: Optional[str], ) -> None: if request_id != self._records_total_request_id: return @@ -1163,11 +1168,11 @@ def on_cancel_database(self, event: wx.Event): return if wx.MessageDialog( - None, - message=_("Do you want discard the change to {database_name}?").format( - database_name=database.name - ), - style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, + None, + message=_("Do you want discard the change to {database_name}?").format( + database_name=database.name + ), + style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, ).ShowModal() != wx.ID_YES: return @@ -1572,26 +1577,12 @@ def on_save_as_query(self, event): def on_execute_statement(self, event): controller = self._get_active_query_controller() if controller is not None: - page = self.MainFrameNotebook.GetCurrentPage() - if page is None: - return - - if not self._autosave_query_page_before_execute(page): - return - self.controller_query_records = controller controller.execute_current(event) def on_execute_statements(self, event): controller = self._get_active_query_controller() if controller is not None: - page = self.MainFrameNotebook.GetCurrentPage() - if page is None: - return - - if not self._autosave_query_page_before_execute(page): - return - self.controller_query_records = controller controller.execute_all(event) diff --git a/windows/main/tabs/__init__.py b/windows/main/database/__init__.py similarity index 100% rename from windows/main/tabs/__init__.py rename to windows/main/database/__init__.py diff --git a/windows/main/tabs/database.py b/windows/main/database/list.py similarity index 100% rename from windows/main/tabs/database.py rename to windows/main/database/list.py diff --git a/windows/main/tabs/database_options.py b/windows/main/database/options.py similarity index 100% rename from windows/main/tabs/database_options.py rename to windows/main/database/options.py diff --git a/windows/main/tabs/view.py b/windows/main/database/view.py similarity index 100% rename from windows/main/tabs/view.py rename to windows/main/database/view.py diff --git a/windows/main/tabs/explorer.py b/windows/main/explorer.py similarity index 100% rename from windows/main/tabs/explorer.py rename to windows/main/explorer.py diff --git a/windows/main/query/__init__.py b/windows/main/query/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/windows/main/query/controller.py b/windows/main/query/controller.py new file mode 100644 index 0000000..81376dc --- /dev/null +++ b/windows/main/query/controller.py @@ -0,0 +1,274 @@ +from typing import Any, Callable, Optional +from gettext import gettext as _ + +import wx +import wx.stc + +from helpers.logger import logger + +from structures.session import Session + +from windows.main.query.parser import ExecutionMode, SQLStatementParser, StatementSelector +from windows.main.query.executor import ExecutionResult, ExecutionSummary, QueryExecutor +from windows.main.query.renderer import QueryResultsRenderer + + +class QueryEditorController: + def __init__( + self, + stc_editor: wx.stc.StyledTextCtrl, + results_notebook: wx.Notebook, + session_provider: Callable[[], Optional[Session]], + database_provider: Optional[Callable[[], Optional[Any]]] = None, + cancel_button: Optional[wx.Button] = None, + on_new_query: Optional[Callable[[wx.Event], None]] = None, + on_close_query: Optional[Callable[[wx.Event], None]] = None, + on_save_query: Optional[Callable[[wx.Event], None]] = None, + on_save_as_query: Optional[Callable[[wx.Event], None]] = None, + on_stop_state_changed: Optional[Callable[[bool], None]] = None, + on_before_execute: Optional[Callable[[], bool]] = None, + ): + self.editor = stc_editor + self.notebook = results_notebook + self.get_session = session_provider + self.get_database = database_provider or (lambda: None) + self.cancel_button = cancel_button + self.on_new_query = on_new_query + self.on_close_query = on_close_query + self.on_save_query = on_save_query + self.on_save_as_query = on_save_as_query + self.on_stop_state_changed = on_stop_state_changed + self.on_before_execute = on_before_execute + + self.parser: Optional[SQLStatementParser] = None + self.selector = StatementSelector(stc_editor) + self.executor: Optional[QueryExecutor] = None + self.renderer: Optional[QueryResultsRenderer] = None + self._cancel_feedback_pending = False + self._shortcuts = self._load_shortcuts() + + self._bind_shortcuts() + self._set_cancel_button_enabled(False) + + def _load_shortcuts(self) -> dict[str, str]: + settings = wx.GetApp().settings + return { + "execute_current": settings.get_value("ui", "shortcuts", "query", "execute_current", default="Ctrl+Enter"), + "execute_all": settings.get_value("ui", "shortcuts", "query", "execute_all", default="Ctrl+Shift+Enter"), + "stop": settings.get_value("ui", "shortcuts", "query", "stop", default="Esc"), + "new_query": settings.get_value("ui", "shortcuts", "query", "new_query", default="Ctrl+T"), + "close_query": settings.get_value("ui", "shortcuts", "query", "close_query", default="Ctrl+W"), + "save": settings.get_value("ui", "shortcuts", "query", "save", default="Ctrl+S"), + "save_as": settings.get_value("ui", "shortcuts", "query", "save_as", default="Ctrl+Shift+S"), + } + + @staticmethod + def _matches_shortcut_key(key_name: str, key_code: int) -> bool: + if key_name == "enter": + return key_code in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER] + + if key_name == "esc": + return key_code == wx.WXK_ESCAPE + + if len(key_name) == 1: + return key_code == ord(key_name.upper()) + + return False + + def _matches_shortcut(self, event: wx.KeyEvent, shortcut: str) -> bool: + parts = [part.strip().lower() for part in shortcut.split("+") if part.strip()] + if not parts: + return False + + key_name = parts[-1] + modifiers = set(parts[:-1]) + + if event.ControlDown() != ("ctrl" in modifiers): + return False + + if event.ShiftDown() != ("shift" in modifiers): + return False + + if event.AltDown() != ("alt" in modifiers): + return False + + key_code = event.GetKeyCode() + return self._matches_shortcut_key(key_name, key_code) + + def _bind_shortcuts(self) -> None: + self.editor.Bind(wx.EVT_KEY_DOWN, self._on_key_down) + + def _set_cancel_button_enabled(self, enabled: bool) -> None: + if self.cancel_button is not None: + self.cancel_button.Enable(enabled) + + if self.on_stop_state_changed is not None: + self.on_stop_state_changed(enabled) + + def _format_elapsed(self, elapsed_ms: float) -> str: + if elapsed_ms < 1000: + return _("{elapsed_ms:.0f} ms").format(elapsed_ms=elapsed_ms) + + return _("{elapsed_s:.2f} s").format(elapsed_s=elapsed_ms / 1000) + + def _show_cancel_message(self, summary: ExecutionSummary) -> None: + last_statement_label = _("none") + if summary.last_statement is not None: + last_statement_label = str(summary.last_statement.statement_index + 1) + + wx.MessageBox( + _( + "Query execution stopped after {elapsed}.\n" + "Completed statements: {completed}/{total}.\n" + "Successful: {success}.\n" + "Failed: {failed}.\n" + "Last statement: #{last}." + ).format( + elapsed=self._format_elapsed(summary.elapsed_ms), + completed=summary.completed_statements, + total=summary.total_statements, + success=summary.successful_statements, + failed=summary.failed_statements, + last=last_statement_label, + ), + _("Query execution cancelled"), + wx.OK | wx.ICON_INFORMATION, + ) + + def _on_key_down(self, event: wx.KeyEvent) -> None: + if self._matches_shortcut(event, self._shortcuts["execute_current"]): + self.execute_current(event) + return + + if self._matches_shortcut(event, self._shortcuts["execute_all"]): + self.execute_all(event) + return + + if self._matches_shortcut(event, self._shortcuts["stop"]): + self.cancel_execution(event) + return + + if self._matches_shortcut(event, self._shortcuts["new_query"]) and self.on_new_query is not None: + self.on_new_query(event) + return + + if self._matches_shortcut(event, self._shortcuts["close_query"]) and self.on_close_query is not None: + self.on_close_query(event) + return + + if self._matches_shortcut(event, self._shortcuts["save_as"]) and self.on_save_as_query is not None: + self.on_save_as_query(event) + return + + if self._matches_shortcut(event, self._shortcuts["save"]) and self.on_save_query is not None: + self.on_save_query(event) + return + + event.Skip() + + def _execute(self, mode: ExecutionMode) -> None: + if self.on_before_execute is not None and not self.on_before_execute(): + return + + session = self.get_session() + if not session or not session.is_connected: + wx.MessageBox( + _("No active database connection"), + _("Error"), + wx.OK | wx.ICON_ERROR + ) + return + + if not self.parser or self.parser.engine != session.engine: + self.parser = SQLStatementParser(session.engine) + self.executor = QueryExecutor(session) + self.renderer = QueryResultsRenderer(self.notebook, session) + + sql_text = self.editor.GetText() + if not sql_text.strip(): + return + + statements = self.parser.parse(sql_text) + if not statements: + return + + if mode == ExecutionMode.CURRENT or mode == ExecutionMode.SELECTION: + _, statements_to_execute = self.selector.get_execution_scope(statements) + else: + statements_to_execute = statements + + if not statements_to_execute: + return + + self.renderer.clear_all_tabs() + self._cancel_feedback_pending = False + self._set_cancel_button_enabled(True) + + self.executor.execute_statements( + statements=statements_to_execute, + on_statement_complete=self._on_statement_complete, + on_all_complete=self._on_all_complete, + current_database=self.get_database(), + stop_on_error=True + ) + + def _on_statement_complete(self, result: ExecutionResult) -> None: + if result.cancelled: + return + + if self.renderer: + self.renderer.create_result_tab(result) + + def _on_all_complete(self, summary: ExecutionSummary) -> None: + self._set_cancel_button_enabled(False) + + if summary.cancelled and self._cancel_feedback_pending: + self._show_cancel_message(summary) + + self._cancel_feedback_pending = False + logger.info("Query execution completed") + + def get_shortcuts(self) -> dict[str, str]: + return dict(self._shortcuts) + + def execute_all(self, event: wx.Event) -> None: + self._execute(ExecutionMode.ALL) + + def execute_current(self, event: wx.Event) -> None: + self._execute(ExecutionMode.CURRENT) + + def cancel_execution(self, event: wx.Event) -> None: + if self.executor and self.executor.is_running(): + self._cancel_feedback_pending = True + self.executor.cancel() + logger.info("Query execution cancelled") + + +class QueryResultsController(QueryEditorController): + def __init__( + self, + stc_sql_query: wx.stc.StyledTextCtrl, + notebook_sql_results: wx.Notebook, + cancel_button: Optional[wx.Button] = None, + on_new_query: Optional[Callable[[wx.Event], None]] = None, + on_close_query: Optional[Callable[[wx.Event], None]] = None, + on_save_query: Optional[Callable[[wx.Event], None]] = None, + on_save_as_query: Optional[Callable[[wx.Event], None]] = None, + on_stop_state_changed: Optional[Callable[[bool], None]] = None, + on_before_execute: Optional[Callable[[], bool]] = None, + ): + from windows.main import CURRENT_DATABASE, CURRENT_SESSION # Lazy import: unavoidable circular dependency. + + super().__init__( + stc_editor=stc_sql_query, + results_notebook=notebook_sql_results, + session_provider=lambda: CURRENT_SESSION.get_value(), + database_provider=lambda: CURRENT_DATABASE.get_value(), + cancel_button=cancel_button, + on_new_query=on_new_query, + on_close_query=on_close_query, + on_save_query=on_save_query, + on_save_as_query=on_save_as_query, + on_stop_state_changed=on_stop_state_changed, + on_before_execute=on_before_execute, + ) diff --git a/windows/main/query/executor.py b/windows/main/query/executor.py new file mode 100644 index 0000000..dea2698 --- /dev/null +++ b/windows/main/query/executor.py @@ -0,0 +1,259 @@ +import contextlib +import dataclasses +import threading +import time + +from typing import Any, Callable, Optional + +import wx + +from helpers.loader import Loader +from helpers.logger import logger + +from structures.session import Session +from structures.connection import Connection, ConnectionEngine +from structures.engines.datatype import SQLDataType + +from windows.main.query.parser import ParsedStatement + + +@dataclasses.dataclass +class ExecutionResult: + statement: ParsedStatement + success: bool + columns: Optional[list[str]] = None + rows: Optional[list[tuple]] = None + column_datatypes: Optional[list[Optional[SQLDataType]]] = None + affected_rows: Optional[int] = None + elapsed_ms: float = 0.0 + error: Optional[str] = None + cancelled: bool = False + warnings: list[str] = dataclasses.field(default_factory=list) + + +@dataclasses.dataclass +class ExecutionSummary: + total_statements: int = 0 + completed_statements: int = 0 + successful_statements: int = 0 + failed_statements: int = 0 + elapsed_ms: float = 0.0 + cancelled: bool = False + last_statement: Optional[ParsedStatement] = None + + +class QueryExecutor: + def __init__(self, session: Session): + self.session = session + self._cancel_requested = False + self._current_thread: Optional[threading.Thread] = None + self._worker_context: Optional[Any] = None + self._loader_context: Optional[Any] = None + self._lock = threading.Lock() + + def execute_statements( + self, + statements: list[ParsedStatement], + on_statement_complete: Callable[[ExecutionResult], None], + on_all_complete: Callable[[ExecutionSummary], None], + current_database: Optional[Any] = None, + stop_on_error: bool = True + ) -> None: + self._cancel_requested = False + self._loader_context = Loader.cursor_wait() + self._loader_context.__enter__() + + self._current_thread = threading.Thread( + target=self._execute_worker, + args=( + statements, + on_statement_complete, + on_all_complete, + current_database, + stop_on_error, + ), + daemon=True + ) + self._current_thread.start() + + def _dispatch_statement_result( + self, + on_statement_complete: Callable[[ExecutionResult], None], + result: ExecutionResult, + ) -> None: + ui_done_event = threading.Event() + + def _on_ui_thread() -> None: + try: + on_statement_complete(result) + finally: + ui_done_event.set() + + wx.CallAfter(_on_ui_thread) + while not ui_done_event.wait(0.05): + continue + + def _execute_worker( + self, + statements: list[ParsedStatement], + on_statement_complete: Callable[[ExecutionResult], None], + on_all_complete: Callable[[ExecutionSummary], None], + current_database: Optional[Any], + stop_on_error: bool + ) -> None: + time_start = time.perf_counter() + summary = ExecutionSummary(total_statements=len(statements)) + + try: + context = self._create_worker_context(current_database) + self._set_worker_context(context) + + for stmt in statements: + if self._cancel_requested: + summary.cancelled = True + break + + summary.last_statement = stmt + result = self._execute_single(context, stmt) + + if result.success: + summary.completed_statements += 1 + summary.successful_statements += 1 + elif not result.cancelled: + summary.completed_statements += 1 + summary.failed_statements += 1 + + self._dispatch_statement_result(on_statement_complete, result) + + if not result.success and stop_on_error: + break + + except Exception as ex: + logger.error(f"Execution worker error: {ex}", exc_info=True) + finally: + summary.cancelled = summary.cancelled or self._cancel_requested + summary.elapsed_ms = (time.perf_counter() - time_start) * 1000 + + self._clear_worker_context() + + wx.CallAfter(self._stop_loader) + wx.CallAfter(on_all_complete, summary) + + def _execute_single(self, context: Any, statement: ParsedStatement) -> ExecutionResult: + start_time = time.time() + + try: + context.execute(statement.text) + + elapsed_ms = (time.time() - start_time) * 1000 + + cursor = context.cursor + if cursor.description: + columns = [desc[0] for desc in cursor.description] + column_datatypes = context.get_result_column_datatypes(cursor) + rows = context.fetchall() + + return ExecutionResult( + statement=statement, + success=True, + columns=columns, + rows=rows, + column_datatypes=column_datatypes, + affected_rows=len(rows), + elapsed_ms=elapsed_ms + ) + else: + affected = cursor.rowcount if cursor.rowcount >= 0 else 0 + + return ExecutionResult( + statement=statement, + success=True, + affected_rows=affected, + elapsed_ms=elapsed_ms + ) + + except Exception as ex: + elapsed_ms = (time.time() - start_time) * 1000 + is_cancelled = self._cancel_requested + + return ExecutionResult( + statement=statement, + success=False, + error=str(ex), + cancelled=is_cancelled, + elapsed_ms=elapsed_ms + ) + + def _build_worker_connection(self) -> Connection: + connection = self.session.connection.copy() + + if not connection.has_enabled_tunnel(): + return connection + + context = getattr(self.session, "context", None) + configuration = getattr(connection, "configuration", None) + + if context is not None and configuration is not None and hasattr(configuration, "_replace"): + replace_kwargs = {} + + if hasattr(configuration, "hostname") and getattr(context, "host", None): + replace_kwargs["hostname"] = context.host + + if hasattr(configuration, "port") and getattr(context, "port", None) is not None: + replace_kwargs["port"] = int(context.port) + + if replace_kwargs: + connection.configuration = configuration._replace(**replace_kwargs) + + connection.ssh_tunnel = None + return connection + + def _create_worker_context(self, current_database: Optional[Any]) -> Any: + context = self.session._get_context_class()(self._build_worker_connection()) + + if self.session.engine == ConnectionEngine.POSTGRESQL: + connect_kwargs = { + "skip_before_connect": True, + "skip_after_connect": True, + } + + if current_database is not None and hasattr(current_database, "name"): + connect_kwargs["database"] = current_database.name + + context.connect(**connect_kwargs) + return context + + context.connect(skip_before_connect=True, skip_after_connect=True, database=current_database.name if current_database is not None else None) + + # if current_database is not None: + # with contextlib.suppress(Exception): + # context.set_database(current_database) + + return context + + def _set_worker_context(self, context: Any) -> None: + with self._lock: + self._worker_context = context + + def _clear_worker_context(self) -> None: + context = None + + with self._lock: + context = self._worker_context + self._worker_context = None + + if context is not None: + with contextlib.suppress(Exception): + context.disconnect() + + def _stop_loader(self) -> None: + if self._loader_context is not None: + self._loader_context.__exit__(None, None, None) + self._loader_context = None + + def cancel(self) -> None: + self._cancel_requested = True + self._clear_worker_context() + + def is_running(self) -> bool: + return self._current_thread is not None and self._current_thread.is_alive() \ No newline at end of file diff --git a/windows/main/query/parser.py b/windows/main/query/parser.py new file mode 100644 index 0000000..e6c3d50 --- /dev/null +++ b/windows/main/query/parser.py @@ -0,0 +1,83 @@ +import dataclasses +import enum + +from typing import Optional + +import wx.stc + +from structures.connection import ConnectionEngine + +from windows.components.stc.autocomplete.statement_extractor import StatementExtractor + + +@dataclasses.dataclass +class ParsedStatement: + text: str + start_pos: int + end_pos: int + statement_index: int + + +class ExecutionMode(enum.Enum): + ALL = "all" + SELECTION = "selection" + CURRENT = "current" + + +class SQLStatementParser: + def __init__(self, engine: ConnectionEngine): + self.engine = engine + + def parse(self, sql_text: str) -> list[ParsedStatement]: + return [ + ParsedStatement(text=text, start_pos=start, end_pos=end, statement_index=i) + for i, (text, start, end) in enumerate(StatementExtractor.extract_all_statements(sql_text)) + ] + + +class StatementSelector: + def __init__(self, stc_editor: wx.stc.StyledTextCtrl): + self.editor = stc_editor + + def get_execution_scope( + self, + statements: list[ParsedStatement] + ) -> tuple[ExecutionMode, list[ParsedStatement]]: + selection_start = self.editor.GetSelectionStart() + selection_end = self.editor.GetSelectionEnd() + + if selection_start != selection_end: + if selected_text := self.editor.GetSelectedText().strip(): + return (ExecutionMode.SELECTION, [ParsedStatement( + text=selected_text, + start_pos=selection_start, + end_pos=selection_end, + statement_index=0 + )]) + + caret_pos = self.editor.GetCurrentPos() + + if current_stmt := self._find_statement_at_caret(caret_pos, statements): + return (ExecutionMode.CURRENT, [current_stmt]) + + return (ExecutionMode.ALL, statements) + + def _find_statement_at_caret( + self, + caret_pos: int, + statements: list[ParsedStatement] + ) -> Optional[ParsedStatement]: + for stmt in statements: + if stmt.start_pos <= caret_pos <= stmt.end_pos: + return stmt + + # Caret is in whitespace: execute next statement + for stmt in statements: + if caret_pos < stmt.start_pos: + return stmt + + # Caret after all statements: execute last + if statements: + return statements[-1] + + return None diff --git a/windows/main/query/renderer.py b/windows/main/query/renderer.py new file mode 100644 index 0000000..01f41de --- /dev/null +++ b/windows/main/query/renderer.py @@ -0,0 +1,304 @@ +import datetime + +from typing import Any, Optional +from gettext import gettext as _ + +import wx +import wx.dataview + +from helpers.dataview import BaseDataViewListModel + +from structures.session import Session +from structures.engines.datatype import DataTypeCategory, SQLDataType + +from windows.components.popup import PopupCalendar, PopupCalendarTime +from windows.components.renders import AdvancedTextRenderer, FloatRenderer, IntegerRenderer, PopupRenderer, TextRenderer, TimeRenderer +from windows.components.dataview import QueryEditorResultsDataViewCtrl + +from windows.main.query.executor import ExecutionResult + + +class _ReadOnlyPopupRenderer(PopupRenderer): + def ActivateCell(self, rect, model, item, col, mouseEvent): + return False + + +class _ReadOnlyTimeRenderer(TimeRenderer): + def HasEditorCtrl(self): + return False + + +class QueryResultsRenderer: + def __init__(self, notebook: wx.Notebook, session: Session): + self.notebook = notebook + self.session = session + self._models: list[Any] = [] + self._tab_counter = 0 + + def create_result_tab(self, result: ExecutionResult) -> wx.Panel: + self._tab_counter += 1 + + panel = wx.Panel(self.notebook) + sizer = wx.BoxSizer(wx.VERTICAL) + + if result.success and result.columns: + results_dataview = QueryEditorResultsDataViewCtrl(panel) + self._populate_grid(results_dataview, result) + sizer.Add(results_dataview, 1, wx.EXPAND | wx.ALL, 5) + + tab_name = self._generate_tab_name(result) + elif result.success: + msg = wx.StaticText( + panel, + label=_("{affected_rows} rows affected").format( + affected_rows=result.affected_rows or 0 + ), + ) + msg.SetFont(msg.GetFont().MakeBold()) + sizer.Add(msg, 1, wx.ALIGN_CENTER | wx.ALL, 20) + + tab_name = _("Query {query_number}").format(query_number=self._tab_counter) + else: + error_panel = self._create_error_panel(panel, result) + sizer.Add(error_panel, 1, wx.EXPAND | wx.ALL, 5) + + tab_name = _("Query {query_number} (Error)").format( + query_number=self._tab_counter + ) + + footer = self._create_footer(panel, result) + sizer.Add(footer, 0, wx.EXPAND | wx.ALL, 5) + + panel.SetSizer(sizer) + self.notebook.AddPage(panel, tab_name, select=True) + + return panel + + def _generate_tab_name(self, result: ExecutionResult) -> str: + if result.columns and result.rows is not None: + return _("Query {query_number} ({rows_count} rows × {columns_count} cols)").format( + query_number=self._tab_counter, + rows_count=len(result.rows), + columns_count=len(result.columns), + ) + return _("Query {query_number}").format(query_number=self._tab_counter) + + def _get_column_datatype(self, result: ExecutionResult, column_index: int) -> Optional[SQLDataType]: + if not result.column_datatypes: + return None + + if column_index >= len(result.column_datatypes): + return None + + return result.column_datatypes[column_index] + + def _get_column_renderer( + self, + results_dataview: QueryEditorResultsDataViewCtrl, + datatype: Optional[SQLDataType] + ) -> wx.dataview.DataViewRenderer: + if datatype is None: + return TextRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) + + if datatype.name == "BOOLEAN": + return wx.dataview.DataViewToggleRenderer( + mode=wx.dataview.DATAVIEW_CELL_INERT, + align=wx.ALIGN_CENTER, + ) + + if datatype.name == "DATE": + return _ReadOnlyPopupRenderer(PopupCalendar) + + if datatype.name == "TIME": + return _ReadOnlyTimeRenderer() + + if datatype.name in ["DATETIME", "TIMESTAMP"]: + return _ReadOnlyPopupRenderer(PopupCalendarTime) + + if datatype.category == DataTypeCategory.INTEGER: + return IntegerRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) + + if datatype.category == DataTypeCategory.REAL: + return FloatRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) + + if datatype.category == DataTypeCategory.TEXT: + return AdvancedTextRenderer( + mode=wx.dataview.DATAVIEW_CELL_INERT, + dialog_factory=results_dataview.make_advanced_dialog, + ) + + return TextRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) + + def _populate_grid( + self, + results_dataview: QueryEditorResultsDataViewCtrl, + result: ExecutionResult + ) -> None: + if not result.columns: + return + + for i, col_name in enumerate(result.columns): + datatype = self._get_column_datatype(result, i) + renderer = self._get_column_renderer(results_dataview, datatype) + align = wx.ALIGN_CENTER if datatype and datatype.name == "BOOLEAN" else wx.ALIGN_LEFT + + column = wx.dataview.DataViewColumn( + col_name, + renderer, + i, + width=results_dataview.measure_text(col_name), + align=align, + flags=wx.dataview.DATAVIEW_COL_RESIZABLE, + ) + results_dataview.AppendColumn(column) + + model = QueryResultsModel(column_count=len(result.columns)) + model.load(result.rows, result.columns, result.column_datatypes) + self._models.append(model) + results_dataview.AssociateModel(model) + wx.CallAfter(results_dataview.autosize_columns_from_content) + + def _create_footer(self, parent: wx.Panel, result: ExecutionResult) -> wx.StaticText: + parts = [] + + if result.affected_rows is not None: + parts.append(_("{rows_count} rows").format(rows_count=result.affected_rows)) + + parts.append(_("{elapsed_ms:.1f} ms").format(elapsed_ms=result.elapsed_ms)) + + if result.warnings: + parts.append( + _("{warnings_count} warnings").format( + warnings_count=len(result.warnings) + ) + ) + + footer_text = " | ".join(parts) + footer = wx.StaticText(parent, label=footer_text) + footer.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)) + + return footer + + def _create_error_panel(self, parent: wx.Panel, result: ExecutionResult) -> wx.Panel: + error_panel = wx.Panel(parent) + error_sizer = wx.BoxSizer(wx.VERTICAL) + + error_label = wx.StaticText(error_panel, label=_("Error:")) + error_label.SetFont(error_label.GetFont().MakeBold()) + error_sizer.Add(error_label, 0, wx.ALL, 5) + + error_text = wx.TextCtrl( + error_panel, + value=result.error or _("Unknown error"), + style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_WORDWRAP + ) + error_text.SetBackgroundColour(wx.Colour(255, 240, 240)) + error_sizer.Add(error_text, 1, wx.EXPAND | wx.ALL, 5) + + error_panel.SetSizer(error_sizer) + return error_panel + + def clear_all_tabs(self) -> None: + while self.notebook.GetPageCount() > 0: + self.notebook.DeletePage(0) + self._models = [] + self._tab_counter = 0 + + +class QueryResultsModel(BaseDataViewListModel): + def __init__(self, column_count: int): + super().__init__(column_count) + self._columns: list[str] = [] + self._column_datatypes: list[Optional[SQLDataType]] = [] + + def load( + self, + data: list[Any], + columns: list[str], + column_datatypes: Optional[list[Optional[SQLDataType]]] = None, + ): + self._columns = columns + self._column_datatypes = column_datatypes or [None for _ in columns] + BaseDataViewListModel.load(self, data) + + def GetValueByRow(self, row, col): + if row < 0 or row >= len(self.data): + return "" + + if col < 0 or col >= len(self._columns): + return "" + + value = self._get_cell_value(self.data[row], col) + if value is None: + return "" + + datatype = self._get_column_datatype(col) + if datatype is None: + return str(value) + + if datatype.name == "BOOLEAN": + return bool(value) + + if datatype.category == DataTypeCategory.TEMPORAL: + return self._format_temporal_value(value, datatype.name) + + return str(value) + + def SetValueByRow(self, value, row, col): + return False + + def HasValue(self, item, col): + if col < 0 or col >= len(self._columns): + return False + + row = self.GetRow(item) + if row < 0 or row >= len(self.data): + return False + + return self._get_cell_value(self.data[row], col) is not None + + def GetAttr(self, item, col, attr): + datatype = self._get_column_datatype(col) + if datatype is None: + return super().GetAttr(item, col, attr) + + color = datatype.category.value.color + attr.SetColour(wx.Colour(color)) + return super().GetAttr(item, col, attr) + + def _get_cell_value(self, row_data: Any, col: int) -> Any: + if isinstance(row_data, dict): + return row_data.get(self._columns[col]) + + if col < len(row_data): + return row_data[col] + + return None + + def _get_column_datatype(self, col: int) -> Optional[SQLDataType]: + if col < 0 or col >= len(self._column_datatypes): + return None + + return self._column_datatypes[col] + + def _format_temporal_value(self, value: Any, datatype_name: str) -> str: + if isinstance(value, datetime.datetime): + if datatype_name == "DATE": + return value.strftime("%Y-%m-%d") + + if datatype_name == "TIME": + return value.strftime("%H:%M:%S") + + if datatype_name in ["DATETIME", "TIMESTAMP"]: + return value.strftime("%Y-%m-%d %H:%M:%S") + + if datatype_name == "YEAR": + return value.strftime("%Y") + + if isinstance(value, datetime.date) and datatype_name == "DATE": + return value.strftime("%Y-%m-%d") + + if isinstance(value, datetime.time) and datatype_name == "TIME": + return value.strftime("%H:%M:%S") + + return str(value) diff --git a/windows/main/table/__init__.py b/windows/main/table/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/windows/main/tabs/check.py b/windows/main/table/check.py similarity index 98% rename from windows/main/tabs/check.py rename to windows/main/table/check.py index 99c3c94..0c6256a 100755 --- a/windows/main/tabs/check.py +++ b/windows/main/table/check.py @@ -8,8 +8,8 @@ from structures.helpers import merge_original_current from structures.engines.database import SQLCheck, SQLTable +from windows.state import NEW_TABLE from windows.main import CURRENT_INDEX, CURRENT_TABLE -from windows.main.tabs.column import NEW_TABLE class TableCheckModel(BaseObservableDataViewListModel): diff --git a/windows/main/tabs/column.py b/windows/main/table/column.py similarity index 100% rename from windows/main/tabs/column.py rename to windows/main/table/column.py diff --git a/windows/main/tabs/foreign_key.py b/windows/main/table/foreign_key.py similarity index 100% rename from windows/main/tabs/foreign_key.py rename to windows/main/table/foreign_key.py diff --git a/windows/main/tabs/index.py b/windows/main/table/index.py similarity index 98% rename from windows/main/tabs/index.py rename to windows/main/table/index.py index 8a469f3..175e8e5 100755 --- a/windows/main/tabs/index.py +++ b/windows/main/table/index.py @@ -5,11 +5,11 @@ from structures.helpers import merge_original_current -from windows.main import CURRENT_TABLE, CURRENT_INDEX -from windows.main.tabs.column import NEW_TABLE - from structures.engines.database import SQLTable, SQLIndex +from windows.state import NEW_TABLE +from windows.main import CURRENT_TABLE, CURRENT_INDEX + class TableIndexModel(BaseObservableDataViewListModel): MAP_COLUMN_FIELDS = { diff --git a/windows/main/tabs/table.py b/windows/main/table/options.py similarity index 100% rename from windows/main/tabs/table.py rename to windows/main/table/options.py diff --git a/windows/main/tabs/records.py b/windows/main/table/records.py similarity index 91% rename from windows/main/tabs/records.py rename to windows/main/table/records.py index d0c0e9c..4f1ac34 100644 --- a/windows/main/tabs/records.py +++ b/windows/main/table/records.py @@ -21,6 +21,8 @@ NEW_RECORDS: ObservableList[SQLRecord] = ObservableList() +NULL_DISPLAY = "NULL" + class RecordsModel(BaseObservableDataViewListModel): def __init__(self, table: SQLTable, column_count: Optional[int] = None): @@ -28,6 +30,14 @@ 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]) + + def _is_null(self, row, col): + column = self.table.columns[col] + record: SQLRecord = self.data[row] + return record.values.get(column.name) is None + def GetValueByRow(self, row, col): if not len(self.data): return None @@ -36,10 +46,12 @@ def GetValueByRow(self, row, col): record: SQLRecord = self.data[row] - value = record.values.get(column.name, "") + value = record.values.get(column.name) if value is None: - return '' + if column.datatype.name == "BOOLEAN": + return False + return NULL_DISPLAY if not str(value).strip(): return '' @@ -65,40 +77,36 @@ def GetValueByRow(self, row, col): return str(value) def SetValueByRow(self, value, row, col): - item = self.GetItem(row) - column: SQLColumn = self.table.columns[col] - self.data[row].values[column.name] = value + if value == NULL_DISPLAY or (isinstance(value, str) and not value.strip()): + value = None - self.ValueChanged(item, col) + self.data[row].values[column.name] = value return True def GetAttr(self, item, col, attr): - try : + try: column: SQLColumn = self.table.columns[col] - except Exception as ex : - logger.error(exc_info=True) - - color = column.datatype.category.value.color + except Exception: + return False - attr.SetColour(wx.Colour(color)) + row = self.GetRow(item) + if 0 <= row < len(self.data) and self._is_null(row, col): + attr.SetItalic(True) + attr.SetColour(wx.Colour(180, 180, 120)) + else: + color = column.datatype.category.value.color + attr.SetColour(wx.Colour(color)) - if self.table.columns[col].is_primary_key: + if column.is_primary_key: attr.SetBold(True) - return super().GetAttr(item, col, attr) + return True def HasValue(self, item, col): - if not self.data: - return False - - column = self.table.columns[col] - - record: SQLRecord = self.get_data_by_item(item) - - return record.values.get(column.name, None) is not None + return bool(self.data) def add_row(self, data: SQLRecord) -> wx.dataview.DataViewItem: diff --git a/windows/main/tabs/query.py b/windows/main/tabs/query.py deleted file mode 100644 index f71625e..0000000 --- a/windows/main/tabs/query.py +++ /dev/null @@ -1,872 +0,0 @@ -import contextlib -import dataclasses -import datetime -import enum -import threading -import time - -from typing import Any, Callable, Optional -from gettext import gettext as _ - -import wx -import wx.dataview - -from helpers.loader import Loader -from helpers.logger import logger -from helpers.dataview import BaseDataViewListModel - -from structures.session import Session -from structures.connection import Connection, ConnectionEngine -from structures.engines.datatype import DataTypeCategory, SQLDataType - -from windows.components.popup import PopupCalendar, PopupCalendarTime -from windows.components.renders import AdvancedTextRenderer, FloatRenderer, IntegerRenderer, PopupRenderer, TextRenderer, TimeRenderer -from windows.components.dataview import QueryEditorResultsDataViewCtrl -from windows.components.stc.autocomplete.statement_extractor import StatementExtractor - - -class _ReadOnlyPopupRenderer(PopupRenderer): - def ActivateCell(self, rect, model, item, col, mouseEvent): - return False - - -class _ReadOnlyTimeRenderer(TimeRenderer): - def HasEditorCtrl(self): - return False - - -@dataclasses.dataclass -class ParsedStatement: - text: str - start_pos: int - end_pos: int - statement_index: int - - -@dataclasses.dataclass -class ExecutionResult: - statement: ParsedStatement - success: bool - columns: Optional[list[str]] = None - rows: Optional[list[tuple]] = None - column_datatypes: Optional[list[Optional[SQLDataType]]] = None - affected_rows: Optional[int] = None - elapsed_ms: float = 0.0 - error: Optional[str] = None - cancelled: bool = False - warnings: list[str] = dataclasses.field(default_factory=list) - - -@dataclasses.dataclass -class ExecutionSummary: - total_statements: int = 0 - completed_statements: int = 0 - successful_statements: int = 0 - failed_statements: int = 0 - elapsed_ms: float = 0.0 - cancelled: bool = False - last_statement: Optional[ParsedStatement] = None - - -class ExecutionMode(enum.Enum): - ALL = "all" - SELECTION = "selection" - CURRENT = "current" - - -class SQLStatementParser: - def __init__(self, engine: ConnectionEngine): - self.engine = engine - - def parse(self, sql_text: str) -> list[ParsedStatement]: - return [ - ParsedStatement(text=text, start_pos=start, end_pos=end, statement_index=i) - for i, (text, start, end) in enumerate(StatementExtractor.extract_all_statements(sql_text)) - ] - - -class StatementSelector: - def __init__(self, stc_editor: wx.stc.StyledTextCtrl): - self.editor = stc_editor - - def get_execution_scope( - self, - statements: list[ParsedStatement] - ) -> tuple[ExecutionMode, list[ParsedStatement]]: - selection_start = self.editor.GetSelectionStart() - selection_end = self.editor.GetSelectionEnd() - - if selection_start != selection_end: - selected_text = self.editor.GetSelectedText().strip() - if selected_text: - return (ExecutionMode.SELECTION, [ParsedStatement( - text=selected_text, - start_pos=selection_start, - end_pos=selection_end, - statement_index=0 - )]) - - caret_pos = self.editor.GetCurrentPos() - current_stmt = self._find_statement_at_caret(caret_pos, statements) - - if current_stmt: - return (ExecutionMode.CURRENT, [current_stmt]) - - return (ExecutionMode.ALL, statements) - - def _find_statement_at_caret( - self, - caret_pos: int, - statements: list[ParsedStatement] - ) -> Optional[ParsedStatement]: - for stmt in statements: - if stmt.start_pos <= caret_pos <= stmt.end_pos: - return stmt - - # Caret is in whitespace: execute next statement - for stmt in statements: - if caret_pos < stmt.start_pos: - return stmt - - # Caret after all statements: execute last - if statements: - return statements[-1] - - return None - - -class QueryExecutor: - def __init__(self, session: Session): - self.session = session - self._cancel_requested = False - self._current_thread: Optional[threading.Thread] = None - self._worker_context: Optional[Any] = None - self._lock = threading.Lock() - - def execute_statements( - self, - statements: list[ParsedStatement], - on_statement_complete: Callable[[ExecutionResult], None], - on_all_complete: Callable[[ExecutionSummary], None], - current_database: Optional[Any] = None, - stop_on_error: bool = True - ) -> None: - self._cancel_requested = False - - self._current_thread = threading.Thread( - target=self._execute_worker, - args=( - statements, - on_statement_complete, - on_all_complete, - current_database, - stop_on_error, - ), - daemon=True - ) - self._current_thread.start() - - def _execute_worker( - self, - statements: list[ParsedStatement], - on_statement_complete: Callable[[ExecutionResult], None], - on_all_complete: Callable[[ExecutionSummary], None], - current_database: Optional[Any], - stop_on_error: bool - ) -> None: - time_start = time.perf_counter() - summary = ExecutionSummary(total_statements=len(statements)) - - try: - with Loader.cursor_wait(): - context = self._create_worker_context(current_database) - self._set_worker_context(context) - - for stmt in statements: - if self._cancel_requested: - summary.cancelled = True - break - - summary.last_statement = stmt - result = self._execute_single(context, stmt) - - if result.success: - summary.completed_statements += 1 - summary.successful_statements += 1 - elif not result.cancelled: - summary.completed_statements += 1 - summary.failed_statements += 1 - - self._dispatch_statement_result(on_statement_complete, result) - - if not result.success and stop_on_error: - break - - except Exception as ex: - logger.error(f"Execution worker error: {ex}", exc_info=True) - finally: - summary.cancelled = summary.cancelled or self._cancel_requested - summary.elapsed_ms = (time.perf_counter() - time_start) * 1000 - - self._clear_worker_context() - - wx.CallAfter(on_all_complete, summary) - - def _dispatch_statement_result( - self, - on_statement_complete: Callable[[ExecutionResult], None], - result: ExecutionResult, - ) -> None: - ui_done_event = threading.Event() - - def _on_ui_thread() -> None: - on_statement_complete(result) - ui_done_event.set() - - wx.CallAfter(_on_ui_thread) - while not ui_done_event.wait(0.05): - continue - - def _execute_single(self, context: Any, statement: ParsedStatement) -> ExecutionResult: - start_time = time.time() - - try: - context.execute(statement.text) - - elapsed_ms = (time.time() - start_time) * 1000 - - cursor = context.cursor - if cursor.description: - columns = [desc[0] for desc in cursor.description] - column_datatypes = context.get_result_column_datatypes(cursor) - rows = context.fetchall() - - return ExecutionResult( - statement=statement, - success=True, - columns=columns, - rows=rows, - column_datatypes=column_datatypes, - affected_rows=len(rows), - elapsed_ms=elapsed_ms - ) - else: - affected = cursor.rowcount if cursor.rowcount >= 0 else 0 - - return ExecutionResult( - statement=statement, - success=True, - affected_rows=affected, - elapsed_ms=elapsed_ms - ) - - except Exception as ex: - elapsed_ms = (time.time() - start_time) * 1000 - is_cancelled = self._cancel_requested - - return ExecutionResult( - statement=statement, - success=False, - error=str(ex), - cancelled=is_cancelled, - elapsed_ms=elapsed_ms - ) - - def _build_worker_connection(self) -> Connection: - connection = self.session.connection.copy() - - if not connection.has_enabled_tunnel(): - return connection - - context = getattr(self.session, "context", None) - configuration = getattr(connection, "configuration", None) - - if context is not None and configuration is not None and hasattr(configuration, "_replace"): - replace_kwargs = {} - - if hasattr(configuration, "hostname") and getattr(context, "host", None): - replace_kwargs["hostname"] = context.host - - if hasattr(configuration, "port") and getattr(context, "port", None) is not None: - replace_kwargs["port"] = int(context.port) - - if replace_kwargs: - connection.configuration = configuration._replace(**replace_kwargs) - - connection.ssh_tunnel = None - return connection - - def _create_worker_context(self, current_database: Optional[Any]) -> Any: - context = self.session._get_context_class()(self._build_worker_connection()) - - if self.session.engine == ConnectionEngine.POSTGRESQL: - connect_kwargs = { - "skip_before_connect": True, - "skip_after_connect": True, - } - - if current_database is not None and hasattr(current_database, "name"): - connect_kwargs["database"] = current_database.name - - context.connect(**connect_kwargs) - return context - - context.connect(skip_before_connect=True, skip_after_connect=True) - - if current_database is not None: - with contextlib.suppress(Exception): - context.set_database(current_database) - - return context - - def _set_worker_context(self, context: Any) -> None: - with self._lock: - self._worker_context = context - - def _clear_worker_context(self) -> None: - context = None - - with self._lock: - context = self._worker_context - self._worker_context = None - - if context is not None: - with contextlib.suppress(Exception): - context.disconnect() - - def cancel(self) -> None: - self._cancel_requested = True - self._clear_worker_context() - - def is_running(self) -> bool: - return self._current_thread is not None and self._current_thread.is_alive() - - -class QueryResultsRenderer: - def __init__(self, notebook: wx.Notebook, session: Session): - self.notebook = notebook - self.session = session - self._models: list[Any] = [] - self._tab_counter = 0 - - def create_result_tab(self, result: ExecutionResult) -> wx.Panel: - self._tab_counter += 1 - - panel = wx.Panel(self.notebook) - sizer = wx.BoxSizer(wx.VERTICAL) - - if result.success and result.columns: - results_dataview = QueryEditorResultsDataViewCtrl(panel) - self._populate_grid(results_dataview, result) - sizer.Add(results_dataview, 1, wx.EXPAND | wx.ALL, 5) - - tab_name = self._generate_tab_name(result) - elif result.success: - msg = wx.StaticText( - panel, - label=_("{affected_rows} rows affected").format( - affected_rows=result.affected_rows or 0 - ), - ) - msg.SetFont(msg.GetFont().MakeBold()) - sizer.Add(msg, 1, wx.ALIGN_CENTER | wx.ALL, 20) - - tab_name = _("Query {query_number}").format(query_number=self._tab_counter) - else: - error_panel = self._create_error_panel(panel, result) - sizer.Add(error_panel, 1, wx.EXPAND | wx.ALL, 5) - - tab_name = _("Query {query_number} (Error)").format( - query_number=self._tab_counter - ) - - footer = self._create_footer(panel, result) - sizer.Add(footer, 0, wx.EXPAND | wx.ALL, 5) - - panel.SetSizer(sizer) - self.notebook.AddPage(panel, tab_name, select=True) - - return panel - - def _generate_tab_name(self, result: ExecutionResult) -> str: - if result.columns and result.rows is not None: - return _("Query {query_number} ({rows_count} rows × {columns_count} cols)").format( - query_number=self._tab_counter, - rows_count=len(result.rows), - columns_count=len(result.columns), - ) - return _("Query {query_number}").format(query_number=self._tab_counter) - - def _populate_grid( - self, - results_dataview: QueryEditorResultsDataViewCtrl, - result: ExecutionResult - ) -> None: - if not result.columns: - return - - for i, col_name in enumerate(result.columns): - datatype = self._get_column_datatype(result, i) - renderer = self._get_column_renderer(results_dataview, datatype) - align = wx.ALIGN_CENTER if datatype and datatype.name == "BOOLEAN" else wx.ALIGN_LEFT - - column = wx.dataview.DataViewColumn( - col_name, - renderer, - i, - width=results_dataview.measure_text(col_name), - align=align, - flags=wx.dataview.DATAVIEW_COL_RESIZABLE, - ) - results_dataview.AppendColumn(column) - - model = QueryResultsModel(column_count=len(result.columns)) - model.load(result.rows, result.columns, result.column_datatypes) - self._models.append(model) - results_dataview.AssociateModel(model) - wx.CallAfter(results_dataview.autosize_columns_from_content) - - def _get_column_datatype(self, result: ExecutionResult, column_index: int) -> Optional[SQLDataType]: - if not result.column_datatypes: - return None - - if column_index >= len(result.column_datatypes): - return None - - return result.column_datatypes[column_index] - - def _get_column_renderer( - self, - results_dataview: QueryEditorResultsDataViewCtrl, - datatype: Optional[SQLDataType] - ) -> wx.dataview.DataViewRenderer: - if datatype is None: - return TextRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) - - if datatype.name == "BOOLEAN": - return wx.dataview.DataViewToggleRenderer( - mode=wx.dataview.DATAVIEW_CELL_INERT, - align=wx.ALIGN_CENTER, - ) - - if datatype.name == "DATE": - return _ReadOnlyPopupRenderer(PopupCalendar) - - if datatype.name == "TIME": - return _ReadOnlyTimeRenderer() - - if datatype.name in ["DATETIME", "TIMESTAMP"]: - return _ReadOnlyPopupRenderer(PopupCalendarTime) - - if datatype.category == DataTypeCategory.INTEGER: - return IntegerRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) - - if datatype.category == DataTypeCategory.REAL: - return FloatRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) - - if datatype.category == DataTypeCategory.TEXT: - return AdvancedTextRenderer( - mode=wx.dataview.DATAVIEW_CELL_INERT, - dialog_factory=results_dataview.make_advanced_dialog, - ) - - return TextRenderer(mode=wx.dataview.DATAVIEW_CELL_INERT) - - def _create_footer(self, parent: wx.Panel, result: ExecutionResult) -> wx.StaticText: - parts = [] - - if result.affected_rows is not None: - parts.append(_("{rows_count} rows").format(rows_count=result.affected_rows)) - - parts.append(_("{elapsed_ms:.1f} ms").format(elapsed_ms=result.elapsed_ms)) - - if result.warnings: - parts.append( - _("{warnings_count} warnings").format( - warnings_count=len(result.warnings) - ) - ) - - footer_text = " | ".join(parts) - footer = wx.StaticText(parent, label=footer_text) - footer.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)) - - return footer - - def _create_error_panel(self, parent: wx.Panel, result: ExecutionResult) -> wx.Panel: - error_panel = wx.Panel(parent) - error_sizer = wx.BoxSizer(wx.VERTICAL) - - error_label = wx.StaticText(error_panel, label=_("Error:")) - error_label.SetFont(error_label.GetFont().MakeBold()) - error_sizer.Add(error_label, 0, wx.ALL, 5) - - error_text = wx.TextCtrl( - error_panel, - value=result.error or _("Unknown error"), - style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_WORDWRAP - ) - error_text.SetBackgroundColour(wx.Colour(255, 240, 240)) - error_sizer.Add(error_text, 1, wx.EXPAND | wx.ALL, 5) - - error_panel.SetSizer(error_sizer) - return error_panel - - def clear_all_tabs(self) -> None: - while self.notebook.GetPageCount() > 0: - self.notebook.DeletePage(0) - self._models = [] - self._tab_counter = 0 - - -class QueryResultsModel(BaseDataViewListModel): - def __init__(self, column_count: int): - super().__init__(column_count) - self._columns: list[str] = [] - self._column_datatypes: list[Optional[SQLDataType]] = [] - - def load( - self, - data: list[Any], - columns: list[str], - column_datatypes: Optional[list[Optional[SQLDataType]]] = None, - ): - self._columns = columns - self._column_datatypes = column_datatypes or [None for _ in columns] - BaseDataViewListModel.load(self, data) - - def GetValueByRow(self, row, col): - if row < 0 or row >= len(self.data): - return "" - - if col < 0 or col >= len(self._columns): - return "" - - value = self._get_cell_value(self.data[row], col) - if value is None: - return "" - - datatype = self._get_column_datatype(col) - if datatype is None: - return str(value) - - if datatype.name == "BOOLEAN": - return bool(value) - - if datatype.category == DataTypeCategory.TEMPORAL: - return self._format_temporal_value(value, datatype.name) - - return str(value) - - def SetValueByRow(self, value, row, col): - return False - - def HasValue(self, item, col): - if col < 0 or col >= len(self._columns): - return False - - row = self.GetRow(item) - if row < 0 or row >= len(self.data): - return False - - return self._get_cell_value(self.data[row], col) is not None - - def GetAttr(self, item, col, attr): - datatype = self._get_column_datatype(col) - if datatype is None: - return super().GetAttr(item, col, attr) - - color = datatype.category.value.color - attr.SetColour(wx.Colour(color)) - return super().GetAttr(item, col, attr) - - def _format_temporal_value(self, value: Any, datatype_name: str) -> str: - if isinstance(value, datetime.datetime): - if datatype_name == "DATE": - return value.strftime("%Y-%m-%d") - - if datatype_name == "TIME": - return value.strftime("%H:%M:%S") - - if datatype_name in ["DATETIME", "TIMESTAMP"]: - return value.strftime("%Y-%m-%d %H:%M:%S") - - if datatype_name == "YEAR": - return value.strftime("%Y") - - if isinstance(value, datetime.date) and datatype_name == "DATE": - return value.strftime("%Y-%m-%d") - - if isinstance(value, datetime.time) and datatype_name == "TIME": - return value.strftime("%H:%M:%S") - - return str(value) - - def _get_cell_value(self, row_data: Any, col: int) -> Any: - if isinstance(row_data, dict): - return row_data.get(self._columns[col]) - - if col < len(row_data): - return row_data[col] - - return None - - def _get_column_datatype(self, col: int) -> Optional[SQLDataType]: - if col < 0 or col >= len(self._column_datatypes): - return None - - return self._column_datatypes[col] - - -class QueryEditorController: - def __init__( - self, - stc_editor: wx.stc.StyledTextCtrl, - results_notebook: wx.Notebook, - session_provider: Callable[[], Optional[Session]], - database_provider: Optional[Callable[[], Optional[Any]]] = None, - cancel_button: Optional[wx.Button] = None, - on_new_query: Optional[Callable[[wx.Event], None]] = None, - on_close_query: Optional[Callable[[wx.Event], None]] = None, - on_save_query: Optional[Callable[[wx.Event], None]] = None, - on_save_as_query: Optional[Callable[[wx.Event], None]] = None, - on_stop_state_changed: Optional[Callable[[bool], None]] = None, - ): - self.editor = stc_editor - self.notebook = results_notebook - self.get_session = session_provider - self.get_database = database_provider or (lambda: None) - self.cancel_button = cancel_button - self.on_new_query = on_new_query - self.on_close_query = on_close_query - self.on_save_query = on_save_query - self.on_save_as_query = on_save_as_query - self.on_stop_state_changed = on_stop_state_changed - - self.parser: Optional[SQLStatementParser] = None - self.selector = StatementSelector(stc_editor) - self.executor: Optional[QueryExecutor] = None - self.renderer: Optional[QueryResultsRenderer] = None - self._cancel_feedback_pending = False - self._shortcuts = self._load_shortcuts() - - self._bind_shortcuts() - self._set_cancel_button_enabled(False) - - def _load_shortcuts(self) -> dict[str, str]: - settings = wx.GetApp().settings - return { - "execute_current": settings.get_value("ui", "shortcuts", "query", "execute_current", default="Ctrl+Enter"), - "execute_all": settings.get_value("ui", "shortcuts", "query", "execute_all", default="Ctrl+Shift+Enter"), - "stop": settings.get_value("ui", "shortcuts", "query", "stop", default="Esc"), - "new_query": settings.get_value("ui", "shortcuts", "query", "new_query", default="Ctrl+T"), - "close_query": settings.get_value("ui", "shortcuts", "query", "close_query", default="Ctrl+W"), - "save": settings.get_value("ui", "shortcuts", "query", "save", default="Ctrl+S"), - "save_as": settings.get_value("ui", "shortcuts", "query", "save_as", default="Ctrl+Shift+S"), - } - - def get_shortcuts(self) -> dict[str, str]: - return dict(self._shortcuts) - - def _matches_shortcut(self, event: wx.KeyEvent, shortcut: str) -> bool: - parts = [part.strip().lower() for part in shortcut.split("+") if part.strip()] - if not parts: - return False - - key_name = parts[-1] - modifiers = set(parts[:-1]) - - if event.ControlDown() != ("ctrl" in modifiers): - return False - - if event.ShiftDown() != ("shift" in modifiers): - return False - - if event.AltDown() != ("alt" in modifiers): - return False - - key_code = event.GetKeyCode() - return self._matches_shortcut_key(key_name, key_code) - - @staticmethod - def _matches_shortcut_key(key_name: str, key_code: int) -> bool: - if key_name == "enter": - return key_code in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER] - - if key_name == "esc": - return key_code == wx.WXK_ESCAPE - - if len(key_name) == 1: - return key_code == ord(key_name.upper()) - - return False - - def _bind_shortcuts(self) -> None: - self.editor.Bind(wx.EVT_KEY_DOWN, self._on_key_down) - - def _on_key_down(self, event: wx.KeyEvent) -> None: - if self._matches_shortcut(event, self._shortcuts["execute_current"]): - self.execute_current(event) - return - - if self._matches_shortcut(event, self._shortcuts["execute_all"]): - self.execute_all(event) - return - - if self._matches_shortcut(event, self._shortcuts["stop"]): - self.cancel_execution(event) - return - - if self._matches_shortcut(event, self._shortcuts["new_query"]) and self.on_new_query is not None: - self.on_new_query(event) - return - - if self._matches_shortcut(event, self._shortcuts["close_query"]) and self.on_close_query is not None: - self.on_close_query(event) - return - - if self._matches_shortcut(event, self._shortcuts["save_as"]) and self.on_save_as_query is not None: - self.on_save_as_query(event) - return - - if self._matches_shortcut(event, self._shortcuts["save"]) and self.on_save_query is not None: - self.on_save_query(event) - return - - event.Skip() - - def execute_all(self, event: wx.Event) -> None: - self._execute(ExecutionMode.ALL) - - def execute_current(self, event: wx.Event) -> None: - self._execute(ExecutionMode.CURRENT) - - def cancel_execution(self, event: wx.Event) -> None: - if self.executor and self.executor.is_running(): - self._cancel_feedback_pending = True - self.executor.cancel() - logger.info("Query execution cancelled") - - def _execute(self, mode: ExecutionMode) -> None: - session = self.get_session() - if not session or not session.is_connected: - wx.MessageBox( - _("No active database connection"), - _("Error"), - wx.OK | wx.ICON_ERROR - ) - return - - if not self.parser or self.parser.engine != session.engine: - self.parser = SQLStatementParser(session.engine) - self.executor = QueryExecutor(session) - self.renderer = QueryResultsRenderer(self.notebook, session) - - sql_text = self.editor.GetText() - if not sql_text.strip(): - return - - statements = self.parser.parse(sql_text) - if not statements: - return - - if mode == ExecutionMode.CURRENT or mode == ExecutionMode.SELECTION: - _, statements_to_execute = self.selector.get_execution_scope(statements) - else: - statements_to_execute = statements - - if not statements_to_execute: - return - - self.renderer.clear_all_tabs() - self._cancel_feedback_pending = False - self._set_cancel_button_enabled(True) - - self.executor.execute_statements( - statements=statements_to_execute, - on_statement_complete=self._on_statement_complete, - on_all_complete=self._on_all_complete, - current_database=self.get_database(), - stop_on_error=True - ) - - def _on_statement_complete(self, result: ExecutionResult) -> None: - if result.cancelled: - return - - if self.renderer: - self.renderer.create_result_tab(result) - - def _set_cancel_button_enabled(self, enabled: bool) -> None: - if self.cancel_button is not None: - self.cancel_button.Enable(enabled) - - if self.on_stop_state_changed is not None: - self.on_stop_state_changed(enabled) - - def _format_elapsed(self, elapsed_ms: float) -> str: - if elapsed_ms < 1000: - return _("{elapsed_ms:.0f} ms").format(elapsed_ms=elapsed_ms) - - return _("{elapsed_s:.2f} s").format(elapsed_s=elapsed_ms / 1000) - - def _show_cancel_message(self, summary: ExecutionSummary) -> None: - last_statement_label = _("none") - if summary.last_statement is not None: - last_statement_label = str(summary.last_statement.statement_index + 1) - - wx.MessageBox( - _( - "Query execution stopped after {elapsed}.\n" - "Completed statements: {completed}/{total}.\n" - "Successful: {success}.\n" - "Failed: {failed}.\n" - "Last statement: #{last}." - ).format( - elapsed=self._format_elapsed(summary.elapsed_ms), - completed=summary.completed_statements, - total=summary.total_statements, - success=summary.successful_statements, - failed=summary.failed_statements, - last=last_statement_label, - ), - _("Query execution cancelled"), - wx.OK | wx.ICON_INFORMATION, - ) - - def _on_all_complete(self, summary: ExecutionSummary) -> None: - self._set_cancel_button_enabled(False) - - if summary.cancelled and self._cancel_feedback_pending: - self._show_cancel_message(summary) - - self._cancel_feedback_pending = False - logger.info("Query execution completed") - - -class QueryResultsController(QueryEditorController): - def __init__( - self, - stc_sql_query: wx.stc.StyledTextCtrl, - notebook_sql_results: wx.Notebook, - cancel_button: Optional[wx.Button] = None, - on_new_query: Optional[Callable[[wx.Event], None]] = None, - on_close_query: Optional[Callable[[wx.Event], None]] = None, - on_save_query: Optional[Callable[[wx.Event], None]] = None, - on_save_as_query: Optional[Callable[[wx.Event], None]] = None, - on_stop_state_changed: Optional[Callable[[bool], None]] = None, - ): - from windows.main import CURRENT_DATABASE, CURRENT_SESSION # Lazy import: unavoidable circular dependency. - - super().__init__( - stc_editor=stc_sql_query, - results_notebook=notebook_sql_results, - session_provider=lambda: CURRENT_SESSION.get_value(), - database_provider=lambda: CURRENT_DATABASE.get_value(), - cancel_button=cancel_button, - on_new_query=on_new_query, - on_close_query=on_close_query, - on_save_query=on_save_query, - on_save_as_query=on_save_as_query, - on_stop_state_changed=on_stop_state_changed, - ) diff --git a/windows/views.py b/windows/views.py index d9e181d..7f64e1b 100755 --- a/windows/views.py +++ b/windows/views.py @@ -1835,7 +1835,7 @@ def __init__( self, parent ): self.panel_table.SetSizer( bSizer251 ) self.panel_table.Layout() bSizer251.Fit( self.panel_table ) - self.MainFrameNotebook.AddPage( self.panel_table, _(u"Table"), True ) + self.MainFrameNotebook.AddPage( self.panel_table, _(u"Table"), False ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/table.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2320,9 +2320,6 @@ def __init__( self, parent ): self.sql_query_editor.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) ) bSizer125.Add( self.sql_query_editor, 1, wx.EXPAND | wx.ALL, 5 ) - self.m_button12 = wx.Button( self.m_panel52, wx.ID_ANY, _(u"New"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer125.Add( self.m_button12, 0, wx.ALIGN_RIGHT|wx.ALL, 5 ) - self.m_panel52.SetSizer( bSizer125 ) self.m_panel52.Layout() @@ -2347,7 +2344,7 @@ def __init__( self, parent ): self.panel_query.SetSizer( bSizer26 ) self.panel_query.Layout() bSizer26.Fit( self.panel_query ) - self.MainFrameNotebook.AddPage( self.panel_query, _(u"Query"), False ) + self.MainFrameNotebook.AddPage( self.panel_query, _(u"Query"), True ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/arrow_right.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2691,6 +2688,9 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. 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.SetSizer( bSizer144 ) self.Layout() From 1c876dc6dfc10aa4f4954924217de953abdcd3b6 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Wed, 18 Mar 2026 19:37:32 +0100 Subject: [PATCH 27/93] Extend SQL autocomplete context detection for INSERT/UPDATE/DELETE and string literals AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- tests/autocomplete/README.md | 20 +- tests/autocomplete/cases/delete.json | 438 ++++++++++++++++++ tests/autocomplete/cases/insert.json | 340 ++++++++++++++ tests/autocomplete/cases/update.json | 413 +++++++++++++++++ tests/autocomplete/cases/where.json | 60 +++ .../stc/autocomplete/context_detector.py | 315 +++++++++++++ .../stc/autocomplete/sql_context.py | 36 ++ .../stc/autocomplete/suggestion_builder.py | 351 +++++++++++++- 8 files changed, 1968 insertions(+), 5 deletions(-) create mode 100644 tests/autocomplete/cases/delete.json create mode 100644 tests/autocomplete/cases/insert.json create mode 100644 tests/autocomplete/cases/update.json diff --git a/tests/autocomplete/README.md b/tests/autocomplete/README.md index 846956d..7dd6e58 100644 --- a/tests/autocomplete/README.md +++ b/tests/autocomplete/README.md @@ -81,7 +81,7 @@ The system detects which table is on the left of the operator and filters out AL ## Test Coverage Matrix -Golden tests organized by SQL query writing flow (180 base tests, executed across 11 engine/version targets): +Golden tests organized by SQL query writing flow (211 base tests, executed across 11 engine/version targets): - mysql: `8`, `9` - mariadb: `5`, `10`, `11`, `12` @@ -153,7 +153,19 @@ Golden tests organized by SQL query writing flow (180 base tests, executed acros | WINDOW_FUNCTIONS_OVER ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/window_functions_over.json` | 1 | 1 | 0 | 0 | OVER-clause bootstrap suggestions (`PARTITION BY`, `ORDER BY`). | | CURSOR_IN_TOKEN ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/cursor_in_token.json` | 1 | 1 | 0 | 0 | Correct prefix/context when cursor is inside an existing token. | -### 11. Multi-Query & Special Cases +### 11. INSERT, UPDATE, DELETE Operations +| Test Group | File | Total | ✅ | ❌ | ⚠️ | Description | +|------------|------|-------|---|---|---|-------------| +| INSERT ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/insert.json` | 14 | 14 | 0 | 0 | INSERT INTO statements with table selection, column specification, VALUES clauses, and string literal handling. | +| UPDATE ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/update.json` | 16 | 16 | 0 | 0 | UPDATE statements with table selection, SET clauses, WHERE conditions, JOIN operations, and string literal handling. | +| DELETE ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/delete.json` | 15 | 15 | 0 | 0 | DELETE FROM statements with table selection, WHERE conditions, JOIN/USING clauses, subqueries, and string literal handling. | + +### 12. String Literal Handling +| Test Group | File | Total | ✅ | ❌ | ⚠️ | Description | +|------------|------|-------|---|---|---|-------------| +| STRING_LITERALS ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/insert.json`, `cases/update.json`, `cases/delete.json`, `cases/where.json` | 11 | 11 | 0 | 0 | No suggestions when cursor is inside string literals across INSERT, UPDATE, DELETE, and SELECT statements. | + +### 13. Multi-Query & Special Cases | Test Group | File | Total | ✅ | ❌ | ⚠️ | Description | |------------|------|-------|---|---|---|-------------| | DERIVED_TABLES_CTE ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/derived_tables_cte.json` | 9 | 9 | 0 | 0 | Minimal CTE/derived-table scope extraction for FROM/JOIN/WHERE and dot completion. | @@ -165,8 +177,8 @@ Golden tests organized by SQL query writing flow (180 base tests, executed acros | LARGE_SCHEMA_GUARDRAILS ![status](https://img.shields.io/badge/status-pass-brightgreen) | `cases/perf.json` | 2 | 2 | 0 | 0 | Large-schema guardrails for prefix filtering and noise control. | ### Summary Statistics -- **Total Tests**: 1936 (176 base × 11 engine/version targets) -- **✅ Passing**: 1936 (176 base × 11 targets, 100%) +- **Total Tests**: 2321 (211 base × 11 engine/version targets) +- **✅ Passing**: 2321 (211 base × 11 targets, 100%) - **❌ Failing**: 0 (remaining tests, 0%) - **⚠️ Expected Failures (xfail)**: 0 (0 base × 11 targets, 0%) - **⚪ Not Implemented**: 0 (0%) diff --git a/tests/autocomplete/cases/delete.json b/tests/autocomplete/cases/delete.json new file mode 100644 index 0000000..6725212 --- /dev/null +++ b/tests/autocomplete/cases/delete.json @@ -0,0 +1,438 @@ +{ + "group": "DELETE", + "cases": [ + { + "case_id": "DELETE_001", + "title": "DELETE FROM without prefix suggests tables", + "sql": "DELETE FROM |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_FROM", + "prefix": null, + "suggestions": [ + "carts", + "customers", + "inventory", + "items", + "orders", + "orders_archive", + "payments", + "products", + "users", + "user_sessions" + ] + } + }, + { + "case_id": "DELETE_002", + "title": "DELETE FROM with prefix filters tables", + "sql": "DELETE FROM u|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "DELETE_FROM", + "prefix": "u", + "suggestions": [ + "users", + "user_sessions" + ] + } + }, + { + "case_id": "DELETE_003", + "title": "DELETE FROM table suggests WHERE", + "sql": "DELETE FROM users |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_CLAUSE", + "prefix": null, + "suggestions": [ + "WHERE", + "JOIN", + "INNER JOIN", + "LEFT JOIN", + "RIGHT JOIN", + "CROSS JOIN", + "USING", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "DELETE_004", + "title": "DELETE FROM table WHERE suggests columns", + "sql": "DELETE FROM users WHERE |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_CONDITIONS", + "prefix": null, + "suggestions": [ + "id", + "name", + "email", + "status", + "created_at", + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "DELETE_005", + "title": "DELETE FROM table WHERE with column prefix filters columns", + "sql": "DELETE FROM users WHERE e|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "DELETE_WHERE_CONDITIONS", + "prefix": "e", + "suggestions": [ + "email" + ] + } + }, + { + "case_id": "DELETE_006", + "title": "DELETE FROM table WHERE column suggests operators", + "sql": "DELETE FROM users WHERE id |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_OPERATORS", + "prefix": null, + "suggestions": [ + "=", + "!=", + "<>", + ">", + "<", + ">=", + "<=", + "IS", + "IS NOT", + "IN", + "NOT IN", + "LIKE", + "NOT LIKE", + "BETWEEN", + "NOT BETWEEN", + "AND", + "OR" + ] + } + }, + { + "case_id": "DELETE_007", + "title": "DELETE FROM table WHERE column = suggests expressions", + "sql": "DELETE FROM users WHERE id = |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_EXPRESSIONS", + "prefix": null, + "suggestions": [ + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "DELETE_008", + "title": "DELETE FROM table WHERE column = value suggests AND/OR", + "sql": "DELETE FROM users WHERE id = 1 |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_CONDITIONS", + "prefix": null, + "suggestions": [ + "AND", + "OR", + "LIMIT", + "ORDER BY", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "DELETE_009", + "title": "DELETE with JOIN suggests ON", + "sql": "DELETE users FROM users u JOIN orders o ON |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_JOIN_ON", + "prefix": null, + "suggestions": [ + "u.id", + "u.name", + "u.email", + "u.status", + "u.created_at", + "o.id", + "o.user_id", + "o.total", + "o.status", + "o.created_at", + "NULL", + "TRUE", + "FALSE", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "DELETE_010", + "title": "DELETE with JOIN ON condition suggests WHERE", + "sql": "DELETE users FROM users u JOIN orders o ON u.id = o.user_id |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_CLAUSE", + "prefix": null, + "suggestions": [ + "WHERE", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "DELETE_011", + "title": "DELETE FROM table USING suggests tables", + "sql": "DELETE FROM users USING |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_USING", + "prefix": null, + "suggestions": [ + "carts", + "customers", + "inventory", + "items", + "orders", + "orders_archive", + "payments", + "products", + "user_sessions" + ] + } + }, + { + "case_id": "DELETE_012", + "title": "DELETE FROM table USING table suggests WHERE", + "sql": "DELETE FROM users USING orders |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_CLAUSE", + "prefix": null, + "suggestions": [ + "WHERE", + "JOIN", + "INNER JOIN", + "LEFT JOIN", + "RIGHT JOIN", + "CROSS JOIN", + "USING", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "DELETE_013", + "title": "DELETE FROM table WHERE IN subquery", + "sql": "DELETE FROM users WHERE id IN (|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_SUBQUERY", + "prefix": null, + "suggestions": [ + "SELECT", + "WITH", + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "DELETE_014", + "title": "DELETE WHERE inside string literal no suggestions", + "sql": "DELETE FROM users WHERE name = '|'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "DELETE_WHERE_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside WHERE string literal should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "DELETE_015", + "title": "DELETE WHERE inside string literal with content no suggestions", + "sql": "DELETE FROM users WHERE name = 'test|'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "DELETE_WHERE_STRING_LITERAL", + "prefix": "test", + "comment": "Cursor inside WHERE string literal with content should not suggest anything", + "suggestions": [] + } + } + ] +} diff --git a/tests/autocomplete/cases/insert.json b/tests/autocomplete/cases/insert.json new file mode 100644 index 0000000..a9621bb --- /dev/null +++ b/tests/autocomplete/cases/insert.json @@ -0,0 +1,340 @@ +{ + "group": "INSERT", + "cases": [ + { + "case_id": "INSERT_001", + "title": "INSERT INTO without prefix suggests tables", + "sql": "INSERT INTO |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_INTO", + "prefix": null, + "suggestions": [ + "carts", + "customers", + "inventory", + "items", + "orders", + "orders_archive", + "payments", + "products", + "users", + "user_sessions" + ] + } + }, + { + "case_id": "INSERT_002", + "title": "INSERT INTO with prefix filters tables", + "sql": "INSERT INTO u|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "INSERT_INTO", + "prefix": "u", + "suggestions": [ + "users", + "user_sessions" + ] + } + }, + { + "case_id": "INSERT_003", + "title": "INSERT INTO table suggests columns", + "sql": "INSERT INTO users (|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_COLUMNS", + "prefix": null, + "suggestions": [ + "id", + "name", + "email", + "status", + "created_at" + ] + } + }, + { + "case_id": "INSERT_004", + "title": "INSERT INTO table with column prefix filters columns", + "sql": "INSERT INTO users (e|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "INSERT_COLUMNS", + "prefix": "e", + "suggestions": [ + "email" + ] + } + }, + { + "case_id": "INSERT_005", + "title": "INSERT INTO table after comma suggests more columns", + "sql": "INSERT INTO users (name, |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_COLUMNS", + "prefix": null, + "suggestions": [ + "id", + "email", + "status", + "created_at" + ] + } + }, + { + "case_id": "INSERT_006", + "title": "INSERT INTO table VALUES suggests keywords", + "sql": "INSERT INTO users (name, email) |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_VALUES", + "prefix": null, + "suggestions": [ + "VALUES", + "SELECT", + "DEFAULT" + ] + } + }, + { + "case_id": "INSERT_007", + "title": "INSERT INTO table VALUES prefix filters keywords", + "sql": "INSERT INTO users (name, email) V|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "INSERT_VALUES", + "prefix": "V", + "suggestions": [ + "VALUES" + ] + } + }, + { + "case_id": "INSERT_008", + "title": "INSERT INTO table VALUES ( suggests literals", + "sql": "INSERT INTO users (name, email) VALUES (|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_VALUE_EXPRESSIONS", + "prefix": null, + "suggestions": [ + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "INSERT_009", + "title": "INSERT INTO table VALUES (value, suggests next value", + "sql": "INSERT INTO users (name, email) VALUES ('test', |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_VALUE_EXPRESSIONS", + "prefix": null, + "suggestions": [ + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "INSERT_010", + "title": "INSERT INTO table VALUES () suggests closing", + "sql": "INSERT INTO users (name, email) VALUES ('test', 'test@example.com')|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "INSERT_COMPLETE", + "prefix": ")", + "suggestions": [] + } + }, + { + "case_id": "INSERT_011", + "title": "INSERT INTO table VALUES () , suggests more inserts", + "sql": "INSERT INTO users (name, email) VALUES ('test', 'test@example.com'), |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_VALUE_EXPRESSIONS", + "prefix": null, + "suggestions": [ + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "INSERT_012", + "title": "INSERT complete suggests ON DUPLICATE KEY etc", + "sql": "INSERT INTO users (name, email) VALUES ('test', 'test@example.com') |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_POST_VALUES", + "prefix": null, + "suggestions": [ + "ON DUPLICATE KEY UPDATE", + "ON CONFLICT", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "INSERT_013", + "title": "INSERT inside string literal no suggestions", + "sql": "INSERT INTO users (name, email) VALUES ('|', 'test@example.com')", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "INSERT_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside string literal should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "INSERT_014", + "title": "INSERT inside string literal with content no suggestions", + "sql": "INSERT INTO users (name, email) VALUES ('test|', 'test@example.com')", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "INSERT_STRING_LITERAL", + "prefix": "test", + "comment": "Cursor inside string literal with content should not suggest anything", + "suggestions": [] + } + } + ] +} diff --git a/tests/autocomplete/cases/update.json b/tests/autocomplete/cases/update.json new file mode 100644 index 0000000..d2c7bf1 --- /dev/null +++ b/tests/autocomplete/cases/update.json @@ -0,0 +1,413 @@ +{ + "group": "UPDATE", + "cases": [ + { + "case_id": "UPDATE_001", + "title": "UPDATE without prefix suggests tables", + "sql": "UPDATE |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_TABLE", + "prefix": null, + "suggestions": [ + "carts", + "customers", + "inventory", + "items", + "orders", + "orders_archive", + "payments", + "products", + "users", + "user_sessions" + ] + } + }, + { + "case_id": "UPDATE_002", + "title": "UPDATE with prefix filters tables", + "sql": "UPDATE u|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "UPDATE_TABLE", + "prefix": "u", + "suggestions": [ + "users", + "user_sessions" + ] + } + }, + { + "case_id": "UPDATE_003", + "title": "UPDATE table suggests SET", + "sql": "UPDATE users |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_SET_CLAUSE", + "prefix": null, + "suggestions": [ + "SET", + "JOIN", + "INNER JOIN", + "LEFT JOIN", + "RIGHT JOIN", + "CROSS JOIN" + ] + } + }, + { + "case_id": "UPDATE_004", + "title": "UPDATE table SET suggests columns", + "sql": "UPDATE users SET |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_SET_COLUMNS", + "prefix": null, + "suggestions": [ + "id", + "name", + "email", + "status", + "created_at" + ] + } + }, + { + "case_id": "UPDATE_005", + "title": "UPDATE table SET with column prefix filters columns", + "sql": "UPDATE users SET e|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "UPDATE_SET_COLUMNS", + "prefix": "e", + "suggestions": [ + "email" + ] + } + }, + { + "case_id": "UPDATE_006", + "title": "UPDATE table SET column = suggests expressions", + "sql": "UPDATE users SET email = |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_SET_EXPRESSIONS", + "prefix": null, + "suggestions": [ + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "UPDATE_007", + "title": "UPDATE table SET column = value, suggests more columns", + "sql": "UPDATE users SET email = 'test@example.com', |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_SET_COLUMNS", + "prefix": null, + "suggestions": [ + "id", + "name", + "status", + "created_at" + ] + } + }, + { + "case_id": "UPDATE_008", + "title": "UPDATE table SET column = value WHERE suggests WHERE", + "sql": "UPDATE users SET email = 'test@example.com' |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_WHERE_CLAUSE", + "prefix": null, + "suggestions": [ + "WHERE", + "JOIN", + "INNER JOIN", + "LEFT JOIN", + "RIGHT JOIN", + "CROSS JOIN", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "UPDATE_009", + "title": "UPDATE table SET WHERE suggests columns", + "sql": "UPDATE users SET email = 'test@example.com' WHERE |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_WHERE_CONDITIONS", + "prefix": null, + "suggestions": [ + "id", + "name", + "email", + "status", + "created_at", + "NULL", + "TRUE", + "FALSE", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "UPDATE_010", + "title": "UPDATE table SET WHERE column = suggests operators", + "sql": "UPDATE users SET email = 'test@example.com' WHERE id |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_WHERE_OPERATORS", + "prefix": null, + "suggestions": [ + "=", + "!=", + "<>", + ">", + "<", + ">=", + "<=", + "IS", + "IS NOT", + "IN", + "NOT IN", + "LIKE", + "NOT LIKE", + "BETWEEN", + "NOT BETWEEN", + "AND", + "OR" + ] + } + }, + { + "case_id": "UPDATE_011", + "title": "UPDATE table SET WHERE column = value suggests AND/OR", + "sql": "UPDATE users SET email = 'test@example.com' WHERE id = 1 |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_WHERE_CONDITIONS", + "prefix": null, + "suggestions": [ + "AND", + "OR", + "LIMIT", + "ORDER BY", + "RETURNING", + ";" + ] + } + }, + { + "case_id": "UPDATE_012", + "title": "UPDATE with JOIN suggests ON", + "sql": "UPDATE users u JOIN orders o ON |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_JOIN_ON", + "prefix": null, + "suggestions": [ + "u.id", + "u.name", + "u.email", + "u.status", + "u.created_at", + "o.id", + "o.user_id", + "o.total", + "o.status", + "o.created_at", + "NULL", + "TRUE", + "FALSE", + "AVG", + "COALESCE", + "CONCAT", + "COUNT", + "DATE", + "GROUP_CONCAT", + "IF", + "IFNULL", + "LENGTH", + "LOWER", + "MAX", + "MIN", + "MONTH", + "NOW", + "NULLIF", + "PI", + "POW", + "POWER", + "ROW_NUMBER", + "SUBSTR", + "SUM", + "TRIM", + "UNIX_TIMESTAMP", + "UPPER", + "UUID", + "YEAR" + ] + } + }, + { + "case_id": "UPDATE_013", + "title": "UPDATE with JOIN ON condition suggests SET", + "sql": "UPDATE users u JOIN orders o ON u.id = o.user_id |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_SET_CLAUSE", + "prefix": null, + "suggestions": [ + "SET" + ] + } + }, + { + "case_id": "UPDATE_014", + "title": "UPDATE inside string literal no suggestions", + "sql": "UPDATE users SET email = '|', name = 'test'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside string literal should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "UPDATE_015", + "title": "UPDATE inside string literal with content no suggestions", + "sql": "UPDATE users SET email = 'test|', name = 'test2'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "UPDATE_STRING_LITERAL", + "prefix": "test", + "comment": "Cursor inside string literal with content should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "UPDATE_016", + "title": "UPDATE WHERE inside string literal no suggestions", + "sql": "UPDATE users SET email = 'test@example.com' WHERE name = '|'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "UPDATE_WHERE_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside WHERE string literal should not suggest anything", + "suggestions": [] + } + } + ] +} diff --git a/tests/autocomplete/cases/where.json b/tests/autocomplete/cases/where.json index bc72adb..c7f204a 100644 --- a/tests/autocomplete/cases/where.json +++ b/tests/autocomplete/cases/where.json @@ -395,6 +395,66 @@ "email" ] } + }, + { + "case_id": "WHERE_014", + "title": "WHERE inside string literal no suggestions", + "sql": "SELECT * FROM users WHERE name = '|'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "WHERE_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside WHERE string literal should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "WHERE_015", + "title": "WHERE inside string literal with content no suggestions", + "sql": "SELECT * FROM users WHERE name = 'test|'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "WHERE_STRING_LITERAL", + "prefix": "test", + "comment": "Cursor inside WHERE string literal with content should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "WHERE_016", + "title": "WHERE IN inside string literal no suggestions", + "sql": "SELECT * FROM users WHERE name IN ('|')", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "WHERE_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside WHERE IN string literal should not suggest anything", + "suggestions": [] + } + }, + { + "case_id": "WHERE_017", + "title": "WHERE LIKE inside string literal no suggestions", + "sql": "SELECT * FROM users WHERE name LIKE '|'", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "WHERE_STRING_LITERAL", + "prefix": null, + "comment": "Cursor inside WHERE LIKE string literal should not suggest anything", + "suggestions": [] + } } ] } diff --git a/windows/components/stc/autocomplete/context_detector.py b/windows/components/stc/autocomplete/context_detector.py index 487d10a..72f6d60 100644 --- a/windows/components/stc/autocomplete/context_detector.py +++ b/windows/components/stc/autocomplete/context_detector.py @@ -69,6 +69,8 @@ def detect( try: context = self._detect_context_with_regex(left_text, prefix) + if context == SQLContext.INSERT_COMPLETE and left_text.rstrip().endswith(")"): + prefix = ")" return context, scope, prefix except Exception as ex: logger.debug(f"context detection error: {ex}") @@ -98,6 +100,19 @@ def _check_dot_completion(self, left_text: str, prefix: str) -> Optional[re.Matc def _detect_context_with_regex(self, left_text: str, prefix: str) -> SQLContext: left_upper = left_text.upper() + insert_pos = left_upper.rfind("INSERT") + update_pos = left_upper.rfind("UPDATE") + delete_pos = left_upper.rfind("DELETE") + + if insert_pos != -1 and insert_pos >= max(update_pos, delete_pos, -1): + return self._detect_insert_context(left_text, left_upper, prefix, insert_pos) + + if update_pos != -1 and update_pos >= max(insert_pos, delete_pos, -1): + return self._detect_update_context(left_text, left_upper, prefix, update_pos) + + if delete_pos != -1 and delete_pos >= max(insert_pos, update_pos, -1): + return self._detect_delete_context(left_text, left_upper, prefix, delete_pos) + select_pos = left_upper.rfind("SELECT") from_pos = left_upper.rfind("FROM") where_pos = left_upper.rfind("WHERE") @@ -155,6 +170,8 @@ def _detect_context_with_regex(self, left_text: str, prefix: str) -> SQLContext: if where_pos > select_pos and where_pos != -1: if where_pos > max(from_pos, order_by_pos, group_by_pos, -1): + if self._is_inside_string_literal(left_text[where_pos:]): + return SQLContext.WHERE_STRING_LITERAL if self._is_after_where_operator(left_text, where_pos, prefix): return SQLContext.WHERE_AFTER_OPERATOR if self._is_after_where_is(left_text, where_pos, prefix): @@ -169,6 +186,280 @@ def _detect_context_with_regex(self, left_text: str, prefix: str) -> SQLContext: return SQLContext.SELECT_LIST + # ── INSERT ────────────────────────────────────────────────────── + + @staticmethod + def _is_inside_string_literal(text: str) -> bool: + count = 0 + i = 0 + while i < len(text): + if text[i] == "'" and (i == 0 or text[i - 1] != "\\"): + count += 1 + i += 1 + return count % 2 == 1 + + def _detect_insert_context( + self, left_text: str, left_upper: str, prefix: str, insert_pos: int + ) -> SQLContext: + after_insert = left_text[insert_pos:] + + if self._is_inside_string_literal(after_insert): + return SQLContext.INSERT_STRING_LITERAL + + into_pos = left_upper.rfind("INTO", insert_pos) + values_pos = left_upper.rfind("VALUES", insert_pos) + + # INSERT INTO table (...) VALUES (...) | + if values_pos != -1: + after_values = left_text[values_pos + 6:].strip() + # Inside VALUES parentheses or after comma between value groups + if after_values: + # Check if we have a complete VALUES(...) group + open_count = after_values.count("(") + close_count = after_values.count(")") + if open_count > close_count: + # Inside VALUES(...) + return SQLContext.INSERT_VALUE_EXPRESSIONS + if close_count > 0 and after_values.rstrip().endswith(")"): + # Cursor right after ) — no space + if left_text.endswith(")"): + return SQLContext.INSERT_COMPLETE + # After complete VALUES(...) with space + return SQLContext.INSERT_POST_VALUES + if after_values.rstrip().endswith(","): + # After VALUES(...), + return SQLContext.INSERT_VALUE_EXPRESSIONS + # Just after VALUES keyword + return SQLContext.INSERT_VALUE_EXPRESSIONS + + if into_pos == -1: + return SQLContext.INSERT_INTO + + after_into = left_text[into_pos + 4:].strip() + if not after_into: + return SQLContext.INSERT_INTO + + # Check if we're inside column list parentheses + paren_open = after_into.rfind("(") + paren_close = after_into.rfind(")") + if paren_open != -1 and paren_close < paren_open: + # Inside parentheses — column list + return SQLContext.INSERT_COLUMNS + + if paren_close != -1 and paren_close > paren_open: + # After closed parentheses — expect VALUES/SELECT + return SQLContext.INSERT_VALUES + + # After INSERT INTO, check if table name is present + tokens = after_into.split() + if not tokens: + return SQLContext.INSERT_INTO + + # We have at least a table name + if len(tokens) == 1 and prefix: + return SQLContext.INSERT_INTO + + return SQLContext.INSERT_INTO + + # ── UPDATE ────────────────────────────────────────────────────── + + def _detect_update_context( + self, left_text: str, left_upper: str, prefix: str, update_pos: int + ) -> SQLContext: + after_update = left_text[update_pos:] + + if self._is_inside_string_literal(after_update): + where_pos = left_upper.rfind("WHERE", update_pos) + if where_pos != -1 and self._is_inside_string_literal(left_text[where_pos:]): + return SQLContext.UPDATE_WHERE_STRING_LITERAL + return SQLContext.UPDATE_STRING_LITERAL + + set_pos = left_upper.rfind(" SET ", update_pos) + where_pos = left_upper.rfind("WHERE", update_pos) + join_pos = left_upper.rfind("JOIN", update_pos) + on_pos = left_upper.rfind(" ON ", update_pos) + + # WHERE clause + if where_pos != -1 and where_pos > max(set_pos, on_pos, -1): + after_where = left_text[where_pos + 5:] + after_where_stripped = after_where.strip() + if not after_where_stripped: + return SQLContext.UPDATE_WHERE_CONDITIONS + # After value (column op value) — suggest AND/OR + if self._is_after_complete_condition(after_where, prefix): + return SQLContext.UPDATE_WHERE_CONDITIONS + # After column — suggest operators + if self._is_after_column_name(after_where, prefix): + return SQLContext.UPDATE_WHERE_OPERATORS + return SQLContext.UPDATE_WHERE_CONDITIONS + + # ON clause (JOIN) + if on_pos != -1 and on_pos > max(join_pos, -1) and set_pos == -1: + after_on = left_text[on_pos + 4:].strip() + if self._is_after_complete_join_condition(after_on, prefix): + return SQLContext.UPDATE_SET_CLAUSE + return SQLContext.UPDATE_JOIN_ON + + # SET clause + if set_pos != -1: + after_set = left_text[set_pos + 5:].strip() + if not after_set: + return SQLContext.UPDATE_SET_COLUMNS + # After = operator + if re.search(r"=\s*$", after_set): + return SQLContext.UPDATE_SET_EXPRESSIONS + # After complete assignment (col = value) with trailing space + if self._is_after_complete_assignment(after_set, prefix): + if after_set.rstrip().endswith(","): + return SQLContext.UPDATE_SET_COLUMNS + return SQLContext.UPDATE_WHERE_CLAUSE + # Column prefix + return SQLContext.UPDATE_SET_COLUMNS + + # After UPDATE keyword — table name + after_update_keyword = left_text[update_pos + 6:].strip() + if not after_update_keyword: + return SQLContext.UPDATE_TABLE + + # After table name — suggest SET or JOINs + tokens = after_update_keyword.split() + if len(tokens) >= 1 and not prefix: + if join_pos != -1 and join_pos > update_pos: + # After JOIN table — suggest ON or SET + after_join = left_text[join_pos + 4:].strip() + join_tokens = after_join.split() + if len(join_tokens) >= 1: + return SQLContext.UPDATE_SET_CLAUSE + return SQLContext.UPDATE_TABLE + return SQLContext.UPDATE_SET_CLAUSE + + return SQLContext.UPDATE_TABLE + + # ── DELETE ────────────────────────────────────────────────────── + + def _detect_delete_context( + self, left_text: str, left_upper: str, prefix: str, delete_pos: int + ) -> SQLContext: + after_delete = left_text[delete_pos:] + + if self._is_inside_string_literal(after_delete): + return SQLContext.DELETE_WHERE_STRING_LITERAL + + from_pos = left_upper.rfind("FROM", delete_pos) + where_pos = left_upper.rfind("WHERE", delete_pos) + using_pos = left_upper.rfind("USING", delete_pos) + join_pos = left_upper.rfind("JOIN", delete_pos) + on_pos = left_upper.rfind(" ON ", delete_pos) + in_pos = left_upper.rfind(" IN ", delete_pos) + + # WHERE clause + if where_pos != -1 and where_pos > max(from_pos, using_pos, on_pos, -1): + after_where = left_text[where_pos + 5:] + after_where_stripped = after_where.strip() + if not after_where_stripped: + return SQLContext.DELETE_WHERE_CONDITIONS + # Check for IN ( subquery + if in_pos != -1 and in_pos > where_pos: + after_in = left_text[in_pos + 4:].strip() + if after_in.startswith("(") and after_in.count("(") > after_in.count(")"): + return SQLContext.DELETE_SUBQUERY + # After complete condition + if self._is_after_complete_condition(after_where, prefix): + return SQLContext.DELETE_WHERE_CONDITIONS + # After = operator + if re.search(r"(?:=|!=|<>|<=|>=|<|>)\s*$", after_where): + return SQLContext.DELETE_WHERE_EXPRESSIONS + # After column name + if self._is_after_column_name(after_where, prefix): + return SQLContext.DELETE_WHERE_OPERATORS + return SQLContext.DELETE_WHERE_CONDITIONS + + # ON clause (JOIN) + if on_pos != -1 and on_pos > max(join_pos, from_pos, -1): + after_on = left_text[on_pos + 4:].strip() + if self._is_after_complete_join_condition(after_on, prefix): + return SQLContext.DELETE_WHERE_CLAUSE + return SQLContext.DELETE_JOIN_ON + + # USING clause + if using_pos != -1 and using_pos > max(from_pos, -1): + after_using = left_text[using_pos + 5:].strip() + if not after_using: + return SQLContext.DELETE_USING + # After USING table — suggest WHERE + tokens = after_using.split() + if len(tokens) >= 1 and not prefix: + return SQLContext.DELETE_WHERE_CLAUSE + return SQLContext.DELETE_USING + + # FROM clause + if from_pos != -1 and from_pos > delete_pos: + after_from = left_text[from_pos + 4:].strip() + if not after_from: + return SQLContext.DELETE_FROM + + # After table name + tokens = after_from.split() + if len(tokens) >= 1 and not prefix: + if join_pos != -1 and join_pos > from_pos: + after_join = left_text[join_pos + 4:].strip() + join_tokens = after_join.split() + if len(join_tokens) >= 1: + return SQLContext.DELETE_WHERE_CLAUSE + return SQLContext.DELETE_WHERE_CLAUSE + return SQLContext.DELETE_FROM + + return SQLContext.DELETE_FROM + + # ── Helpers for INSERT/UPDATE/DELETE ───────────────────────────── + + @staticmethod + def _is_after_complete_condition(clause: str, prefix: str) -> bool: + if prefix: + return False + # Match: [word] operator value at end + return bool(re.search( + r"(?:[A-Za-z_][A-Za-z0-9_]*\.)?[A-Za-z_][A-Za-z0-9_]*\s*" + r"(?:=|!=|<>|<=|>=|<|>|LIKE|IN|BETWEEN)\s*" + r"(?:[A-Za-z_][A-Za-z0-9_]*|\d+|'[^']*'|\"[^\"]*\"|NULL|TRUE|FALSE|\w+\([^)]*\))\s*$", + clause, + re.IGNORECASE, + )) + + @staticmethod + def _is_after_column_name(clause: str, prefix: str) -> bool: + if prefix: + return False + # Match: identifier at end (with optional qualifier), followed by space + return bool(re.search( + r"(?:(?:AND|OR)\s+)?(?:[A-Za-z_][A-Za-z0-9_]*\.)?[A-Za-z_][A-Za-z0-9_]*\s+$", + clause, + re.IGNORECASE, + )) + + @staticmethod + def _is_after_complete_assignment(clause: str, prefix: str) -> bool: + if prefix: + return False + return bool(re.search( + r"[A-Za-z_][A-Za-z0-9_]*\s*=\s*" + r"(?:[A-Za-z_][A-Za-z0-9_.]*|\d+|'[^']*'|\"[^\"]*\"|NULL|TRUE|FALSE|\w+\([^)]*\))\s*$", + clause, + re.IGNORECASE, + )) + + @staticmethod + def _is_after_complete_join_condition(clause: str, prefix: str) -> bool: + if prefix: + return False + return bool(re.search( + r"(?:[A-Za-z_][A-Za-z0-9_]*\.)?[A-Za-z_][A-Za-z0-9_]*\s*" + r"(?:=|!=|<>|<=|>=|<|>)\s*" + r"(?:[A-Za-z_][A-Za-z0-9_]*\.)?[A-Za-z_][A-Za-z0-9_]*\s*$", + clause, + re.IGNORECASE, + )) + def _is_after_join_table(self, left_text: str, prefix: str) -> bool: if prefix: return False @@ -396,6 +687,30 @@ def _extract_scope_from_text( join_tables = [] aliases = {} + # Extract UPDATE target table into scope (only for UPDATE statements) + update_match = re.match( + r"\s*UPDATE\s+([A-Za-z_][A-Za-z0-9_]*)" + r"(?:\s+(?:AS\s+)?([A-Za-z_][A-Za-z0-9_]*))?", + main_text, + re.IGNORECASE, + ) + if update_match: + table_name = update_match.group(1) + alias = update_match.group(2) if update_match.group(2) else None + if alias and alias.upper() in sql_keywords: + alias = None + if table_name.upper() not in sql_keywords: + table_obj = ( + self._find_table_in_database(table_name, database) + if database + else None + ) + ref = TableReference(name=table_name, alias=alias, table=table_obj) + from_tables.append(ref) + if alias: + aliases[alias.lower()] = ref + aliases[table_name.lower()] = ref + for table_name, alias, projected_columns in self._extract_from_table_tokens( main_text ): diff --git a/windows/components/stc/autocomplete/sql_context.py b/windows/components/stc/autocomplete/sql_context.py index 80e4286..bd1015e 100644 --- a/windows/components/stc/autocomplete/sql_context.py +++ b/windows/components/stc/autocomplete/sql_context.py @@ -24,4 +24,40 @@ class SQLContext(Enum): WINDOW_OVER = "WINDOW_OVER" LIMIT_OFFSET_CLAUSE = "LIMIT_OFFSET" AFTER_LIMIT_NUMBER = "AFTER_LIMIT_NUMBER" + + # INSERT contexts + INSERT_INTO = "INSERT_INTO" + INSERT_COLUMNS = "INSERT_COLUMNS" + INSERT_VALUES = "INSERT_VALUES" + INSERT_VALUE_EXPRESSIONS = "INSERT_VALUE_EXPRESSIONS" + INSERT_COMPLETE = "INSERT_COMPLETE" + INSERT_POST_VALUES = "INSERT_POST_VALUES" + INSERT_STRING_LITERAL = "INSERT_STRING_LITERAL" + + # UPDATE contexts + UPDATE_TABLE = "UPDATE_TABLE" + UPDATE_SET_CLAUSE = "UPDATE_SET_CLAUSE" + UPDATE_SET_COLUMNS = "UPDATE_SET_COLUMNS" + UPDATE_SET_EXPRESSIONS = "UPDATE_SET_EXPRESSIONS" + UPDATE_WHERE_CLAUSE = "UPDATE_WHERE_CLAUSE" + UPDATE_WHERE_CONDITIONS = "UPDATE_WHERE_CONDITIONS" + UPDATE_WHERE_OPERATORS = "UPDATE_WHERE_OPERATORS" + UPDATE_JOIN_ON = "UPDATE_JOIN_ON" + UPDATE_STRING_LITERAL = "UPDATE_STRING_LITERAL" + UPDATE_WHERE_STRING_LITERAL = "UPDATE_WHERE_STRING_LITERAL" + + # DELETE contexts + DELETE_FROM = "DELETE_FROM" + DELETE_WHERE_CLAUSE = "DELETE_WHERE_CLAUSE" + DELETE_WHERE_CONDITIONS = "DELETE_WHERE_CONDITIONS" + DELETE_WHERE_OPERATORS = "DELETE_WHERE_OPERATORS" + DELETE_WHERE_EXPRESSIONS = "DELETE_WHERE_EXPRESSIONS" + DELETE_JOIN_ON = "DELETE_JOIN_ON" + DELETE_USING = "DELETE_USING" + DELETE_SUBQUERY = "DELETE_SUBQUERY" + DELETE_WHERE_STRING_LITERAL = "DELETE_WHERE_STRING_LITERAL" + + # SELECT WHERE string literal + WHERE_STRING_LITERAL = "WHERE_STRING_LITERAL" + UNKNOWN = "UNKNOWN" diff --git a/windows/components/stc/autocomplete/suggestion_builder.py b/windows/components/stc/autocomplete/suggestion_builder.py index 784f607..8fdbc13 100644 --- a/windows/components/stc/autocomplete/suggestion_builder.py +++ b/windows/components/stc/autocomplete/suggestion_builder.py @@ -65,6 +65,12 @@ class SuggestionBuilder: "POWER", } + _literal_functions = { + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + } + _max_database_columns = 400 _scope_restricted_contexts = { @@ -212,6 +218,64 @@ def build( if context == SQLContext.AFTER_LIMIT_NUMBER: return self._build_after_limit_number(prefix) + # INSERT contexts + if context == SQLContext.INSERT_INTO: + return self._build_insert_into(prefix) + if context == SQLContext.INSERT_COLUMNS: + return self._build_insert_columns(scope, prefix, statement) + if context == SQLContext.INSERT_VALUES: + return self._build_insert_values(prefix) + if context == SQLContext.INSERT_VALUE_EXPRESSIONS: + return self._build_insert_value_expressions(prefix) + if context == SQLContext.INSERT_COMPLETE: + return [] + if context == SQLContext.INSERT_POST_VALUES: + return self._build_insert_post_values(prefix) + if context == SQLContext.INSERT_STRING_LITERAL: + return [] + + # UPDATE contexts + if context == SQLContext.UPDATE_TABLE: + return self._build_update_table(prefix) + if context == SQLContext.UPDATE_SET_CLAUSE: + return self._build_update_set_clause(prefix, scope) + if context == SQLContext.UPDATE_SET_COLUMNS: + return self._build_update_set_columns(scope, prefix, statement) + if context == SQLContext.UPDATE_SET_EXPRESSIONS: + return self._build_insert_value_expressions(prefix) + if context == SQLContext.UPDATE_WHERE_CLAUSE: + return self._build_update_where_clause(prefix, scope) + if context == SQLContext.UPDATE_WHERE_CONDITIONS: + return self._build_update_where_conditions(scope, prefix, statement) + if context == SQLContext.UPDATE_WHERE_OPERATORS: + return self._build_update_where_operators(prefix) + if context == SQLContext.UPDATE_JOIN_ON: + return self._build_update_join_on(scope, prefix) + if context in (SQLContext.UPDATE_STRING_LITERAL, SQLContext.UPDATE_WHERE_STRING_LITERAL): + return [] + + # DELETE contexts + if context == SQLContext.DELETE_FROM: + return self._build_delete_from(prefix) + if context == SQLContext.DELETE_WHERE_CLAUSE: + return self._build_delete_where_clause(prefix, scope, statement) + if context == SQLContext.DELETE_WHERE_CONDITIONS: + return self._build_delete_where_conditions(scope, prefix, statement) + if context == SQLContext.DELETE_WHERE_OPERATORS: + return self._build_update_where_operators(prefix) + if context == SQLContext.DELETE_WHERE_EXPRESSIONS: + return self._build_insert_value_expressions(prefix) + if context == SQLContext.DELETE_JOIN_ON: + return self._build_update_join_on(scope, prefix) + if context == SQLContext.DELETE_USING: + return self._build_delete_using(scope, prefix, statement) + if context == SQLContext.DELETE_SUBQUERY: + return self._build_delete_subquery(prefix) + if context == SQLContext.DELETE_WHERE_STRING_LITERAL: + return [] + if context == SQLContext.WHERE_STRING_LITERAL: + return [] + return self._build_keywords(prefix) def _build_empty(self, prefix: str) -> list[CompletionItem]: @@ -1387,6 +1451,20 @@ def _build_where_literals(prefix: str) -> list[CompletionItem]: for literal in literals ] + @staticmethod + def _build_join_literals(prefix: str) -> list[CompletionItem]: + literals = ["NULL", "TRUE", "FALSE"] + if prefix: + prefix_upper = prefix.upper() + literals = [ + literal for literal in literals if literal.startswith(prefix_upper) + ] + + return [ + CompletionItem(name=literal, item_type=CompletionItemType.KEYWORD) + for literal in literals + ] + @staticmethod def _build_after_is_keywords(prefix: str) -> list[CompletionItem]: keywords = ["NULL", "NOT NULL", "TRUE", "FALSE"] @@ -1802,7 +1880,9 @@ def _build_select_list_functions(self, prefix: str) -> list[CompletionItem]: if item.name not in self._select_list_excluded_functions ] - def _build_functions(self, prefix: str) -> list[CompletionItem]: + def _build_functions( + self, prefix: str, exclude: Optional[set[str]] = None + ) -> list[CompletionItem]: if not self._database: return [] @@ -1813,6 +1893,7 @@ def _build_functions(self, prefix: str) -> list[CompletionItem]: name=str(func).upper(), item_type=CompletionItemType.FUNCTION ) for func in functions + if not exclude or str(func).upper() not in exclude ] except (AttributeError, TypeError): return [] @@ -2393,3 +2474,271 @@ def _filter_columns_by_prefix( filtered.append(col) return filtered + + # ── INSERT builders ────────────────────────────────────────────── + + def _build_insert_into(self, prefix: str) -> list[CompletionItem]: + return self._build_all_tables(prefix) + + def _build_insert_columns( + self, scope: QueryScope, prefix: str, statement: str + ) -> list[CompletionItem]: + table = self._find_insert_target_table(statement) + if not table: + return [] + + already_listed = self._extract_already_listed_columns(statement) + columns = [] + try: + for col in table.columns: + if col.name.lower() not in already_listed: + if not prefix or col.name.lower().startswith(prefix.lower()): + columns.append( + CompletionItem(name=col.name, item_type=CompletionItemType.COLUMN) + ) + except (AttributeError, TypeError): + pass + return columns + + def _build_insert_values(self, prefix: str) -> list[CompletionItem]: + keywords = ["VALUES", "SELECT", "DEFAULT"] + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + def _build_insert_value_expressions(self, prefix: str) -> list[CompletionItem]: + items = self._build_where_literals(prefix) + items.extend(self._build_functions(prefix, exclude=self._literal_functions)) + return items + + def _build_insert_post_values(self, prefix: str) -> list[CompletionItem]: + keywords = ["ON DUPLICATE KEY UPDATE", "ON CONFLICT", "RETURNING", ";"] + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.upper().startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + # ── UPDATE builders ────────────────────────────────────────────── + + def _build_update_table(self, prefix: str) -> list[CompletionItem]: + return self._build_all_tables(prefix) + + def _build_update_set_clause(self, prefix: str, scope: QueryScope) -> list[CompletionItem]: + keywords = ["SET"] + if not scope.join_tables: + keywords.extend(["JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "CROSS JOIN"]) + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.upper().startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + def _build_update_set_columns( + self, scope: QueryScope, prefix: str, statement: str + ) -> list[CompletionItem]: + table = self._find_update_target_table(statement) + if not table: + return [] + + already_set = self._extract_already_set_columns(statement) + columns = [] + try: + for col in table.columns: + if col.name.lower() not in already_set: + if not prefix or col.name.lower().startswith(prefix.lower()): + columns.append( + CompletionItem(name=col.name, item_type=CompletionItemType.COLUMN) + ) + except (AttributeError, TypeError): + pass + return columns + + def _build_update_where_clause(self, prefix: str, scope: QueryScope) -> list[CompletionItem]: + keywords = ["WHERE"] + if not scope.join_tables: + keywords.extend(["JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "CROSS JOIN"]) + keywords.extend(["RETURNING", ";"]) + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.upper().startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + def _build_update_where_conditions( + self, scope: QueryScope, prefix: str, statement: str + ) -> list[CompletionItem]: + table = self._find_update_target_table(statement) + + # After a complete condition: suggest AND/OR/LIMIT/etc + if self._is_after_where_condition_value(statement): + keywords = ["AND", "OR", "LIMIT", "ORDER BY", "RETURNING", ";"] + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.upper().startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + # Suggest columns + functions + columns = self._get_table_columns_as_items(table, prefix) + items = list(columns) + items.extend(self._build_where_literals(prefix)) + items.extend(self._build_functions(prefix, exclude=self._literal_functions)) + return items + + def _build_update_where_operators(self, prefix: str) -> list[CompletionItem]: + operators = [ + "=", "!=", "<>", ">", "<", ">=", "<=", + "IS", "IS NOT", "IN", "NOT IN", "LIKE", "NOT LIKE", + "BETWEEN", "NOT BETWEEN", "AND", "OR", + ] + if prefix: + prefix_upper = prefix.upper() + operators = [op for op in operators if op.upper().startswith(prefix_upper)] + return [CompletionItem(name=op, item_type=CompletionItemType.KEYWORD) for op in operators] + + def _build_update_join_on(self, scope: QueryScope, prefix: str) -> list[CompletionItem]: + items = self._resolve_columns_in_scope(scope, prefix, SQLContext.JOIN_ON) + items.extend(self._build_join_literals(prefix)) + items.extend(self._build_functions(prefix, exclude=self._literal_functions)) + return items + + # ── DELETE builders ────────────────────────────────────────────── + + def _build_delete_from(self, prefix: str) -> list[CompletionItem]: + return self._build_all_tables(prefix) + + def _build_delete_where_clause( + self, prefix: str, scope: QueryScope, statement: str + ) -> list[CompletionItem]: + keywords = ["WHERE"] + if not scope.join_tables: + keywords.extend(["JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "CROSS JOIN"]) + keywords.append("USING") + keywords.extend(["RETURNING", ";"]) + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.upper().startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + def _build_delete_where_conditions( + self, scope: QueryScope, prefix: str, statement: str + ) -> list[CompletionItem]: + table = self._find_delete_target_table(statement) + + if self._is_after_where_condition_value(statement): + keywords = ["AND", "OR", "LIMIT", "ORDER BY", "RETURNING", ";"] + if prefix: + prefix_upper = prefix.upper() + keywords = [k for k in keywords if k.upper().startswith(prefix_upper)] + return [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + + columns = self._get_table_columns_as_items(table, prefix) + items = list(columns) + items.extend(self._build_where_literals(prefix)) + items.extend(self._build_functions(prefix, exclude=self._literal_functions)) + return items + + def _build_delete_using( + self, scope: QueryScope, prefix: str, statement: str + ) -> list[CompletionItem]: + delete_table = self._find_delete_target_table(statement) + exclude = {delete_table.name.lower()} if delete_table else set() + return self._build_all_tables(prefix, exclude=exclude) + + def _build_delete_subquery(self, prefix: str) -> list[CompletionItem]: + keywords = ["SELECT", "WITH"] + items = [CompletionItem(name=k, item_type=CompletionItemType.KEYWORD) for k in keywords] + items.extend(self._build_where_literals(prefix)) + items.extend(self._build_functions(prefix, exclude=self._literal_functions)) + if prefix: + prefix_upper = prefix.upper() + items = [item for item in items if item.name.upper().startswith(prefix_upper)] + return items + + # ── Shared helpers ─────────────────────────────────────────────── + + def _build_all_tables( + self, prefix: str, exclude: Optional[set[str]] = None + ) -> list[CompletionItem]: + if not self._database: + return [] + try: + tables = [] + for table in self._database.tables: + if exclude and table.name.lower() in exclude: + continue + if not prefix or table.name.lower().startswith(prefix.lower()): + tables.append( + CompletionItem(name=table.name, item_type=CompletionItemType.TABLE) + ) + return sorted(tables, key=lambda x: self._table_name_sort_key(x.name)) + except (AttributeError, TypeError): + return [] + + def _find_insert_target_table(self, statement: str) -> Optional[SQLTable]: + match = re.search(r"\bINSERT\s+INTO\s+(\w+)", statement, re.IGNORECASE) + if match: + return self._get_table_by_name(match.group(1)) + return None + + def _find_update_target_table(self, statement: str) -> Optional[SQLTable]: + match = re.search(r"\bUPDATE\s+(\w+)", statement, re.IGNORECASE) + if match: + return self._get_table_by_name(match.group(1)) + return None + + def _find_delete_target_table(self, statement: str) -> Optional[SQLTable]: + match = re.search(r"\bFROM\s+(\w+)", statement, re.IGNORECASE) + if match: + return self._get_table_by_name(match.group(1)) + return None + + def _get_table_columns_as_items( + self, table: Optional[SQLTable], prefix: str + ) -> list[CompletionItem]: + if not table: + return [] + try: + columns = [] + for col in table.columns: + if not prefix or col.name.lower().startswith(prefix.lower()): + columns.append( + CompletionItem(name=col.name, item_type=CompletionItemType.COLUMN) + ) + return columns + except (AttributeError, TypeError): + return [] + + @staticmethod + def _extract_already_listed_columns(statement: str) -> set[str]: + match = re.search(r"\(\s*(.+?)(?:\)|$)", statement, re.IGNORECASE) + if not match: + return set() + column_list = match.group(1) + return {col.strip().lower() for col in column_list.split(",") if col.strip()} + + @staticmethod + def _extract_already_set_columns(statement: str) -> set[str]: + set_match = re.search(r"\bSET\s+(.*)", statement, re.IGNORECASE | re.DOTALL) + if not set_match: + return set() + set_clause = set_match.group(1) + columns = set() + for assignment in set_clause.split(","): + eq_match = re.match(r"\s*(\w+)\s*=", assignment) + if eq_match: + columns.add(eq_match.group(1).lower()) + return columns + + def _is_after_where_condition_value(self, statement: str) -> bool: + where_match = re.search(r"\bWHERE\s+(.*)", statement, re.IGNORECASE | re.DOTALL) + if not where_match: + return False + clause = where_match.group(1).strip() + if not clause: + return False + return bool(re.search( + r"(?:[A-Za-z_][A-Za-z0-9_]*\.)?[A-Za-z_][A-Za-z0-9_]*\s*" + r"(?:=|!=|<>|<=|>=|<|>|LIKE|IN|BETWEEN)\s*" + r"(?:[A-Za-z_][A-Za-z0-9_]*|\d+|'[^']*'|\"[^\"]*\"|NULL|TRUE|FALSE|\w+\([^)]*\))\s*$", + clause, + re.IGNORECASE, + )) From 4e13a5eb2d6d284c3b72d2e8f69ca84e213ab6c8 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 23 Mar 2026 10:11:32 +0100 Subject: [PATCH 28/93] feat(autocomplete): improve parsing, provider behavior and coverage AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- structures/engines/mysql/context.py | 3 +- tests/autocomplete/RULES.md | 425 ++++-------------- tests/autocomplete/cases/from.json | 37 ++ tests/autocomplete/cases/provider_edges.json | 45 ++ tests/autocomplete/cases/sel.json | 2 +- .../cases/select_column_behavior.json | 4 +- tests/autocomplete/test_autocomplete_basic.py | 235 +++++++++- .../stc/autocomplete/auto_complete.py | 93 +++- .../stc/autocomplete/autocomplete_popup.py | 82 ++-- .../stc/autocomplete/context_detector.py | 74 ++- .../autocomplete/dot_completion_handler.py | 35 +- .../stc/autocomplete/statement_extractor.py | 133 ++++-- .../stc/autocomplete/suggestion_builder.py | 381 +++++++++------- windows/main/controller.py | 136 ++++-- 14 files changed, 1024 insertions(+), 661 deletions(-) create mode 100644 tests/autocomplete/cases/provider_edges.json diff --git a/structures/engines/mysql/context.py b/structures/engines/mysql/context.py index baed054..c8eaf8a 100644 --- a/structures/engines/mysql/context.py +++ b/structures/engines/mysql/context.py @@ -32,6 +32,7 @@ MySQLTable, MySQLTrigger, MySQLView, + MySQLCheck, ) from structures.engines.mysql.datatype import MySQLDataType from structures.engines.mysql.indextype import MySQLIndexType @@ -682,8 +683,6 @@ def build_empty_check( expression: Optional[str] = None, **default_values, ) -> "MySQLCheck": - from structures.engines.mysql.database import MySQLCheck - id = MySQLContext.get_temporary_id(table.checks) if name is None: diff --git a/tests/autocomplete/RULES.md b/tests/autocomplete/RULES.md index 229403e..6a488fc 100644 --- a/tests/autocomplete/RULES.md +++ b/tests/autocomplete/RULES.md @@ -39,18 +39,25 @@ The scope classification determines which columns are suggested in expression co - **SCOPED**: Explicit scope exists via FROM/JOIN clauses in the current statement - Example: `SELECT id, | FROM users` → scope = `users` table - Example: `SELECT * FROM users u JOIN orders o ON u.id = o.user_id; SELECT u.id, |` → scope = `u`, `o` tables - - Behavior: Suggest only columns from scope tables (qualified if multiple tables, unqualified if single table) + - Behavior: Suggest only columns from scope tables + - normally **unqualified** if there is a single explicit table + - **qualified** if there are multiple explicit tables + - if the statement already uses qualified style (for example `users.id` or `u.id`), suggestions must continue in **qualified** form - **VIRTUAL_SCOPED**: Implicit scope inferred from context without explicit FROM/JOIN - - Via qualified columns: `SELECT users.id, |` → virtual scope = `users` (inferred from qualified column) - - Via CURRENT_TABLE: `SELECT id, |` with CURRENT_TABLE=users → virtual scope = `users` - - **CRITICAL:** VIRTUAL_SCOPED requires CURRENT_TABLE to be set in UI (or qualified column present) - - When VIRTUAL_SCOPED via CURRENT_TABLE (no FROM/JOIN), columns MUST be qualified (e.g., `users.id`, not `id`) - - Behavior: Suggest columns from the inferred table(s), but allow DB-wide suggestions with prefix + - Via qualified references already present in the statement: + - `SELECT users.id, |` → virtual scope = `users` + - `SELECT users.*, |` → virtual scope = `users` + - Via CURRENT_TABLE: + - `SELECT |` with CURRENT_TABLE=users → virtual scope = `users` + - **CRITICAL:** VIRTUAL_SCOPED exists when there is no explicit `FROM/JOIN` scope, but there is still a strong table reference intent + - In VIRTUAL_SCOPED, columns MUST be **always qualified** + - `users.id`, not `id` + - Behavior: Suggest columns from the inferred table(s); DB-wide suggestions are allowed only when there is no explicit scope and either a prefix exists or autocomplete is forced - **NO_SCOPED**: No scope information available - No FROM/JOIN in current statement - - No qualified columns to infer scope from + - No qualified references to infer virtual scope from - No CURRENT_TABLE set - Example: `SELECT id, |` with CURRENT_TABLE=null and no qualified columns - Behavior: Suggest only functions (no columns without prefix) @@ -99,8 +106,8 @@ For clarity, examples use the following assumed schema order: ## Context Detection -The autocomplete system uses `sqlglot` to parse the SQL query and determine the current context. -Contexts are defined in the `SQLContext` enum. +The autocomplete system determines the current SQL context (for example `SELECT_LIST`, `WHERE_CLAUSE`, `JOIN_ON`, `ORDER_BY_CLAUSE`). +This document defines the expected behavior for each context, independently of the internal implementation. --- @@ -110,7 +117,7 @@ These examples demonstrate the strict separation between table-selection and exp **Assume:** `CURRENT_TABLE = users` (set in table editor) -### Example 1: SELECT with no FROM → CURRENT_TABLE + DB-wide allowed +### Example 1: SELECT with no FROM → VIRTUAL_SCOPED via CURRENT_TABLE ```sql SELECT u| @@ -119,11 +126,11 @@ SELECT u| **Context:** SELECT_LIST, no scope tables exist **Suggestions:** -- `users.id, users.name, users.email, ...` (CURRENT_TABLE columns first) -- `products.unit_price, ...` (DB-wide columns matching 'u') +- `users.id, users.name, users.email, ...` (CURRENT_TABLE columns first, always qualified in VIRTUAL_SCOPED) +- `products.unit_price, ...` (DB-wide columns matching 'u', only because no explicit scope exists and a prefix is present) - `UPPER, UUID, UNIX_TIMESTAMP` (functions) -**Rationale:** No scope tables exist, so CURRENT_TABLE and DB-wide columns are allowed. +**Rationale:** No explicit scope tables exist, but CURRENT_TABLE creates a VIRTUAL_SCOPED context. Its columns must stay qualified. DB-wide suggestions are allowed here only because there is still no explicit scope and a prefix is present. --- @@ -161,7 +168,7 @@ SELECT * FROM orders WHERE u| - ❌ `users.*` (CURRENT_TABLE not in scope) - ❌ `products.unit_price` (DB-wide column) -**Rationale:** WHERE is an expression context with scope tables. CURRENT_TABLE is not in scope, so it MUST be ignored. DB-wide columns MUST NOT be suggested. +**Rationale:** WHERE is an expression context with explicit scope tables. CURRENT_TABLE is not in scope, so it MUST be ignored. DB-wide columns MUST NOT be suggested when explicit scope exists. ```sql -- Case B: CURRENT_TABLE in scope @@ -191,9 +198,8 @@ The autocomplete resolution follows this strict precedence order: - Show columns of that table/alias (ignore broader context) - Example: `WHERE u.i|` → show columns of `u` starting with `i` -3. **Context Detection** (sqlglot/regex) +3. **Context Detection** - Determine SQL context: SELECT_LIST, WHERE, JOIN ON, ORDER BY, etc. - - Use sqlglot parsing (primary) or regex fallback 4. **Within Context: Prefix Rules** - If prefix exists (token before cursor without `.`) → apply prefix matching @@ -280,7 +286,7 @@ SELECT ui| **Qualification rules:** -1. **Single table in scope (no ambiguity):** +1. **Single explicit table in scope (no ambiguity):** - **No prefix:** Use **unqualified** column names (e.g., `id`, `name`) - **With prefix:** - **Column-name match** (Generic Prefix Matching rule B): Use **unqualified** column names (e.g., `name`) @@ -300,13 +306,15 @@ SELECT ui| - **Implication for autocomplete:** If prefix does not match the alias and does not match any column name, return empty suggestions. Do NOT suggest qualified columns with the original table name. - **Example:** `FROM users u WHERE us|` → NO suggestions (prefix 'us' does not match alias 'u' or any column) -4. **Consistency rule - Qualified context propagation:** If the query already uses at least one qualified column (e.g., `users.id` or `u.id`) in the SELECT list, column suggestions MUST stay qualified for consistency, even for single-table scopes. +4. **Consistency rule - Qualified context propagation:** If the query already uses at least one qualified column (e.g., `users.id` or `u.id`) in the current statement, column suggestions MUST stay qualified for consistency, even for single-table scopes. - This is a style lock: once qualified style is used, autocomplete keeps qualified style. - Applies to all column contexts: SELECT list (after comma), WHERE, JOIN ON, ORDER BY, GROUP BY, HAVING. - For aliased tables, qualification MUST use alias only (never table name). - For non-aliased tables, qualification uses `table.column`. -**Rationale:** When only one table is in scope, qualification usually adds noise. However, table-name prefix expansion and explicit qualified usage both express a clear qualification intent. Once user intent is qualified style, maintaining it across contexts keeps SQL consistent and avoids invalid `table.column` usage when aliases are present. +5. **VIRTUAL_SCOPED rule:** If scope is only virtual (via `CURRENT_TABLE`, `table.column`, or `table.*` without explicit `FROM/JOIN`), suggestions MUST stay **qualified**. + +**Rationale:** When only one explicit table is in scope, qualification usually adds noise. However, table-name prefix expansion, VIRTUAL_SCOPED contexts, and explicit qualified usage all express a clear qualification intent. Once user intent is qualified style, maintaining it across contexts keeps SQL consistent and unambiguous. **Examples:** ```sql @@ -314,6 +322,11 @@ SELECT ui| SELECT * FROM users WHERE | → id, name, email, password, is_enabled, created_at +-- Virtual scoped via CURRENT_TABLE: always qualified +SELECT | +-- CURRENT_TABLE = users +→ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at + -- Single table, prefix matches ONLY column name: unqualified SELECT * FROM users WHERE n| → name (column-name match, unqualified) @@ -344,6 +357,10 @@ SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE | SELECT u.id FROM users u WHERE | → u.id, u.name, u.email, u.status, u.created_at +-- Virtual scoped via qualified reference: always qualified +SELECT users.id, | +→ users.name, users.email, users.password, users.is_enabled, users.created_at + SELECT u.id FROM users u ORDER BY | → u.id, u.name, u.email, ... @@ -505,14 +522,18 @@ These contexts suggest **columns** from scope tables only. #### SELECT_LIST Context (Special Case) **If statement has NO scope tables (no FROM/JOIN yet):** -- Without prefix: `CURRENT_TABLE` columns MUST be included first (if set) +- Without prefix: + - if `CURRENT_TABLE` exists, the context is VIRTUAL_SCOPED and `CURRENT_TABLE` columns MUST be included first in **qualified** form + - if `CURRENT_TABLE` does not exist and there are no qualified references, suggest only functions - With prefix: `CURRENT_TABLE` columns MUST be included ONLY if they match the prefix via: - Column-name match (e.g., `SELECT na|` → `name` from CURRENT_TABLE) - Table-name expansion (e.g., `SELECT u|` and CURRENT_TABLE is `users` → suggest `users.*` columns) -- Database-wide columns MUST be included ONLY if prefix exists (guardrail: avoid noise when no prefix) +- Database-wide columns MUST be included ONLY if: + - no explicit scope exists + - and a prefix exists, **or** autocomplete was explicitly forced - Functions and keywords are included -**If statement HAS scope tables (FROM/JOIN exists):** +**If statement HAS explicit scope tables (FROM/JOIN exists):** - `CURRENT_TABLE` columns MUST be included ONLY if `CURRENT_TABLE` is in scope - If `CURRENT_TABLE` is not in scope, it MUST be ignored - Database-wide columns MUST NOT be suggested @@ -625,7 +646,7 @@ SEL| → SELECT (SINGLE_TOKEN) #### 3a. Without prefix (after SELECT, no FROM/JOIN in query) **Show:** -- `CURRENT_TABLE` columns (if set) +- `CURRENT_TABLE` columns in qualified form (if set) - Functions - Keywords (FROM, WHERE, etc.) - **only if SELECT list already has items** - e.g., `SELECT id |` → show keywords (can continue query) @@ -682,8 +703,8 @@ SELECT * FROM users u JOIN orders o ON u.id = o.user_id; SELECT | **CURRENT_TABLE handling:** - **When NO scope tables exist (no FROM/JOIN):** - - `CURRENT_TABLE` columns MUST be included first (if set) - - Database-wide table-name expansion and column-name matching are included + - `CURRENT_TABLE` columns MUST be included first in qualified form (if set) + - Database-wide table-name expansion and column-name matching are included only if a prefix exists or autocomplete is forced - Functions are included - **When NO scope tables AND NO prefix:** @@ -704,8 +725,8 @@ SELECT * FROM users u JOIN orders o ON u.id = o.user_id; SELECT | ```sql -- Assume CURRENT_TABLE = users SELECT u| -→ users.id, users.name, users.email, ... (CURRENT_TABLE via table-name expansion) -→ user_sessions.* (other tables starting with 'u') +→ users.id, users.name, users.email, ... (CURRENT_TABLE first, VIRTUAL_SCOPED) +→ user_sessions.id, user_sessions.user_id, ... (DB-wide columns allowed because no explicit scope and prefix exists) → orders.user_id, products.unit_price (DB-wide columns starting with 'u') → Functions: UPPER, UUID, UNIX_TIMESTAMP ``` @@ -1550,7 +1571,7 @@ Suggestions are always ordered by priority: **CURRENT_TABLE group inclusion is context-dependent:** - **Expression contexts (JOIN_ON, WHERE, ORDER_BY, GROUP_BY, HAVING):** CURRENT_TABLE group MUST be omitted unless `CURRENT_TABLE` is in scope -- **SELECT_LIST without scope tables:** CURRENT_TABLE group MUST be included (if set) +- **SELECT_LIST without explicit scope tables:** CURRENT_TABLE group MUST be included (if set) - **SELECT_LIST with scope tables:** CURRENT_TABLE group MUST be included ONLY if `CURRENT_TABLE` is in scope - **Table-selection contexts (FROM_CLAUSE, JOIN_CLAUSE):** Not applicable (these suggest tables, not columns) @@ -1563,9 +1584,9 @@ Suggestions are always ordered by priority: **Column ordering reminder:** See "Important Note on Column Ordering in Examples" section at the beginning of this document. All columns preserve schema order (ordinal_position), NOT alphabetical order. -1. **Columns from CURRENT_TABLE** (if set in context, e.g., table editor) - - **Single table in scope:** Unqualified (e.g., `id`, `name`) - - **Multiple tables in scope:** Use `alias.column` format if the table has an alias in the current query, otherwise `table.column` +1. **Columns from CURRENT_TABLE** (if allowed by context rules) + - In **VIRTUAL_SCOPED** they are always **qualified** + - In explicit scope they follow the normal qualification rules of that context - Columns preserve their definition order (ordinal position in the table schema). They must NOT be reordered alphabetically. 2. **Columns from tables in FROM clause** (if any) @@ -1586,17 +1607,16 @@ Suggestions are always ordered by priority: 4. **All table.column from database** (all other tables not in FROM/JOIN) - **CRITICAL: Group 4 eligibility is context-dependent:** - - ✅ **Eligible in SELECT_LIST when NO scope tables exist** (and only with prefix - guardrail against noise) + - ✅ **Eligible in SELECT_LIST when NO explicit scope tables exist** and either a prefix exists or autocomplete is forced - ❌ **NOT eligible in SELECT_LIST when scope tables exist** (scope restriction active) - ❌ **NOT eligible in scope-restricted expression contexts** (WHERE, JOIN_ON, ORDER_BY, GROUP_BY, HAVING) - ❌ **NOT applicable in table-selection contexts** (FROM_CLAUSE, JOIN_CLAUSE suggest tables, not columns) - Always use `table.column` format (no aliases for tables not in query) - Columns preserve their definition order (ordinal position in the table schema). They must NOT be reordered alphabetically. - Database-wide tables follow a deterministic stable order (schema order or internal stable ordering); within each table, preserve column definition order. - - **Performance guardrail (applies ONLY to this group when eligible):** If no prefix and total suggestions exceed threshold (400 items), skip this group to avoid lag in large databases - **No prefix definition:** prefix is `None` OR empty string after trimming whitespace - The cap applies only to group 4 (DB-wide columns). Groups 1-3 (CURRENT_TABLE, FROM, JOIN) are always included in full (already loaded/scoped). - - With prefix: always include this group when eligible (filtered results are manageable) + - With prefix: include this group when eligible (filtered results are manageable) 5. **Functions** - Alphabetically within this group @@ -1712,7 +1732,7 @@ Given a prefix P (token immediately before cursor, without '.'): - **Single table in scope:** Unqualified (e.g., `name`) - **Multiple tables in scope:** Qualified with `alias.column` if table has alias, otherwise `table.column` -**A) Table-name match expansion:** +**Table-name match expansion:* - For EVERY table T whose name startswith(P), return ALL columns of T as column suggestions - **Qualification:** Always qualified with `alias.column` if table has alias, otherwise `table.column` (even for single table) - **Rationale:** Qualified names indicate the match is from table name, helping users discover dot-completion @@ -1739,17 +1759,17 @@ Given a prefix P (token immediately before cursor, without '.'): See **Scope-Restricted Expression Contexts** section for complete rules. -**SELECT_LIST without scope tables:** +**SELECT_LIST without explicit scope tables:** - `CURRENT_TABLE` columns MUST be included first (if set) -- Database-wide table-name expansion and column-name matching are included **ONLY when prefix exists** -- **CRITICAL: When no prefix exists, DB-wide columns MUST NOT be shown (guardrail against noise)** +- Database-wide table-name expansion and column-name matching are included **ONLY when a prefix exists or autocomplete is forced** +- **CRITICAL: When there is no prefix and autocomplete is not forced, DB-wide columns MUST NOT be shown** - **With prefix matching order:** 1. **CURRENT_TABLE table-name expansion** (if CURRENT_TABLE name matches prefix) 2. **Other DB-wide table-name expansions** (tables whose names match prefix) 3. **Column-name matching from all DB tables** (columns whose names match prefix) 4. **Functions** -**SELECT_LIST with scope tables:** +**SELECT_LIST with explicit scope tables:** - `CURRENT_TABLE` columns MUST be included ONLY if `CURRENT_TABLE` is in scope - Database-wide columns MUST NOT be suggested (regardless of prefix) - Scope table columns are included with alias-first qualification @@ -1765,7 +1785,7 @@ See **Scope-Restricted Expression Contexts** section for complete rules. **Examples:** -**SELECT_LIST without scope, with CURRENT_TABLE and prefix:** +**SELECT_LIST without explicit scope, with CURRENT_TABLE and prefix:** ```sql -- Assume CURRENT_TABLE = users, prefix = "u" SELECT u| @@ -1825,57 +1845,6 @@ SELECT * FROM users u JOIN orders o WHERE u| → CURRENT_TABLE priority ignored in this case ``` -**Example in table editor context (CURRENT_TABLE = users, no alias):** -```sql -SELECT u| -→ Context: SELECT_LIST without scope (no FROM/JOIN) -→ Prefix: "u" -→ users.id (CURRENT_TABLE column starting with 'u') -→ users.name (CURRENT_TABLE column - shown for context) -→ orders.user_id (DB-wide column starting with 'u' - allowed because no scope AND prefix exists) -→ products.unit_price (DB-wide column starting with 'u') -→ UPPER (function starting with 'u') -→ UUID (function starting with 'u') -→ UPDATE (keyword starting with 'u') -``` - -**Example in multi-query context (CURRENT_TABLE = users, second query has no scope):** -```sql -SELECT * FROM users u WHERE id = 1; SELECT u| -→ Context: SELECT_LIST without scope (second query has no FROM/JOIN) -→ Prefix: "u" -→ users.id (CURRENT_TABLE column starting with 'u') -→ users.name (CURRENT_TABLE column - shown for context) -→ orders.user_id (DB-wide column starting with 'u' - allowed because no scope AND prefix exists) -→ products.unit_price (DB-wide column starting with 'u') -→ UPPER (function starting with 'u') -→ UUID (function starting with 'u') -→ UPDATE (keyword starting with 'u') -``` - -**Example in query with FROM:** -```sql -SELECT * FROM users WHERE u| -→ Columns: (none - no columns start with 'u') (single table: unqualified) -→ UPPER (function starting with 'u') -→ UPDATE (keyword starting with 'u') -→ (DB-wide columns excluded - WHERE is scope-restricted expression context) -``` - -**Example in query with JOIN (alias-exact-match mode):** -```sql -SELECT * FROM users u JOIN orders o WHERE u| -→ Context: WHERE (scope-restricted) -→ Prefix: "u" -→ Alias-exact-match mode (u == alias 'u') -→ u.id, u.name, u.email, u.password, u.is_enabled, u.created_at (all columns from alias 'u', multiple tables: qualified) -→ UPPER, UUID, UNIX_TIMESTAMP (functions starting with 'u') -→ (DB-wide columns excluded - WHERE is scope-restricted expression context) -→ UPDATE (KEYWORD) -``` - ---- - ### Out-of-Scope Table Hints (SELECT_LIST with Scope) **Applies ONLY in SELECT_LIST when scope tables already exist (FROM/JOIN present).** @@ -1984,7 +1953,7 @@ This table provides a quick reference for implementers to understand the behavio | Context | Scope Required | DB-wide Columns | CURRENT_TABLE | Table Hints | |---------|---------------|-----------------|---------------|-------------| -| **SELECT_LIST (no scope)** | No | Only with prefix | Yes (if set) | No | +| **SELECT_LIST (no scope)** | No | With prefix, or with forced autocomplete | Yes (if set) | No | | **SELECT_LIST (with scope)** | Yes | No | Only if in scope | Yes (if prefix matches) | | **FROM_CLAUSE** | Scope building | N/A | Yes (if set, not present) | N/A | | **JOIN_CLAUSE** | Scope extension | N/A | Yes (if set, not present) | N/A | @@ -2001,125 +1970,37 @@ This table provides a quick reference for implementers to understand the behavio - **Table Hints:** Whether out-of-scope table hints can be suggested **Notes:** -- SELECT_LIST without scope: DB-wide columns included only when prefix exists (guardrail against noise) +- SELECT_LIST without explicit scope: DB-wide columns included only when a prefix exists or autocomplete is forced - SELECT_LIST with scope: DB-wide columns excluded; Out-of-Scope Table Hints shown when prefix matches DB tables but no scope tables/columns - FROM_CLAUSE and JOIN_CLAUSE are table-selection contexts (scope building/extension), not column contexts - All scope-restricted expression contexts (JOIN_ON, WHERE, ORDER_BY, GROUP_BY, HAVING) follow the same rules (see **Scope-Restricted Expression Contexts** section) -- Performance guardrail applies only to DB-wide columns group when no prefix (see Ordering Rules group 4) ---- +## DB-wide policy summary -## Implementation Notes +DB-wide suggestions are allowed only when there is **no explicit scope**. -- Context detection uses `sqlglot.parse_one()` with `ErrorLevel.IGNORE` for incomplete SQL -- Dialect is retrieved from `CURRENT_CONNECTION.get_value().engine.value.dialect` -- `CURRENT_TABLE` is an observable: `CURRENT_TABLE.get_value() -> Optional[SQLTable]` - - Used to prioritize columns from the current table when set - - Can be `None` if no table is currently selected -- Fallback to regex-based context detection if sqlglot parsing fails +They are allowed when at least one of the following is true: +- a prefix exists +- autocomplete was explicitly forced --- -### Architecture Notes - -**Critical:** Centralize resolution logic to avoid duplication, but distinguish between table-selection and expression contexts. - -**Two distinct resolution functions are needed:** - -#### 1. Table Selection (FROM_CLAUSE, JOIN_CLAUSE) - -```python -def resolve_tables_for_table_selection( - context: SQLContext, - scope: QueryScope, - current_table: Optional[SQLTable] = None, - prefix: Optional[str] = None -) -> List[TableSuggestion]: - """ - Resolve table candidates for FROM/JOIN clauses. - - Returns tables in priority order: - 1. CTE names (if available from WITH clause) - 2. Physical tables from database - 3. CURRENT_TABLE (if set and not already present in current statement) - convenience shortcut - - Filtering: - - If prefix provided, filter by startswith(prefix) - - Exclude tables already present in the current statement (query separated by separator) - - Note: This is table-selection, not column resolution. - CURRENT_TABLE can appear even if scope tables already exist. - """ - pass -``` - -#### 2. Expression Contexts (SELECT_LIST, WHERE, JOIN_ON, ORDER_BY, GROUP_BY, HAVING) - -```python -def resolve_columns_for_expression( - context: SQLContext, - scope: QueryScope, - current_table: Optional[SQLTable] = None, - prefix: Optional[str] = None -) -> List[ColumnSuggestion]: - """ - Resolve columns for expression contexts with scope-aware restrictions. - - Behavior depends on context and scope: - - SCOPE-RESTRICTED contexts (WHERE, JOIN_ON, HAVING, ORDER_BY, GROUP_BY): - - See Scope-Restricted Expression Contexts section for complete rules - - Priority: FROM tables > JOIN tables - - SELECT_LIST context: - - If NO scope tables: - * Include CURRENT_TABLE columns (if set) - * Include database-wide columns (only with prefix - guardrail against noise) - - If scope tables exist: - * CURRENT_TABLE included only if in scope; otherwise ignored - * Include scope table columns - * Database-wide columns EXCLUDED (scope restriction active) - * Exception: Out-of-Scope Table Hints if prefix matches DB tables but no scope columns - - All columns use alias.column format when alias exists, otherwise table.column. - """ - pass -``` - -**Benefits:** -- Clear separation between table-selection and expression contexts -- Enforces scope restriction rules consistently -- Single source of truth for each context type -- Easier to test and maintain -- Avoids logic duplication - -**Architectural improvement (optional):** +## Scope and Product Notes -For cleaner architecture, consider using a `QueryScope` object instead of passing multiple parameters: +This file describes the **target autocomplete behavior**. -```python -@dataclass -class QueryScope: - from_tables: List[TableReference] - join_tables: List[TableReference] - derived_tables: List[DerivedTable] - ctes: List[CTE] - current_table: Optional[SQLTable] - aliases: Dict[str, TableReference] # alias -> table mapping +Implementation details such as: +- parser choice +- regex strategy +- internal architecture +- helper functions +- caching strategy -def resolve_columns_in_scope( - scope: QueryScope, - prefix: Optional[str] = None -) -> List[ColumnSuggestion]: - """Pure function - no global context dependency.""" - pass -``` - -This makes the function pure and easier to test. +do **not** belong in `RULES.md`. They belong in technical documentation such as the README or developer docs. --- -**Tables in Scope Definition (with CTEs and Derived Tables):** +## Tables in Scope Definition (with CTEs and Derived Tables) With CTEs and subquery aliases, "tables in scope" is not just physical tables from FROM/JOIN. The priority order is: @@ -2163,9 +2044,9 @@ WHERE | **Important:** Scope handling for subqueries and CTEs. -**v1 subquery scope resolution:** -- **v1 supports inner scope when cursor is inside parentheses of a subquery** (sqlglot typically handles this correctly) -- Fallback to outer scope only when parsing/cursor mapping fails +**Goal:** +- when cursor is inside a subquery, suggestions should use the subquery scope +- when cursor is outside, suggestions should use the outer query scope - When subquery has FROM clause, suggest columns from subquery scope (inner scope) - When cursor is outside subquery parentheses, suggest columns from outer scope @@ -2263,119 +2144,6 @@ SELECT *, ROW_NUMBER() OVER (PARTITION BY status ORDER BY | --- -### Potential Challenges - -**sqlglot Parsing of Incomplete SQL:** - -Test thoroughly with partial queries. You might need a hybrid approach that falls back to regex faster than expected. - -**Examples of challenging cases:** -```sql -SELECT id, name FROM users WHERE | -→ sqlglot may parse successfully - -SELECT id, name FROM users WH| -→ sqlglot may fail, need regex fallback - -SELECT * FROM users WHERE status = '| -→ Incomplete string, sqlglot may fail -``` - -**Recommendation:** -- Use `sqlglot.parse_one()` with `ErrorLevel.IGNORE` as primary approach -- Implement robust regex fallback for common patterns -- Test with many incomplete query variations -- Log parsing failures to identify patterns that need special handling - -**Fallback trigger rule:** -- If sqlglot does not produce a useful AST → fallback to regex -- If cursor position cannot be mapped to an AST node → fallback to regex -- Log: `(dialect, snippet_around_cursor, reason)` for building golden test cases - -**Example logging:** -```python -if not ast or not can_map_cursor_to_node(ast, cursor_pos): - logger.debug( - "sqlglot_fallback", - dialect=dialect, - snippet=text[max(0, cursor_pos-50):cursor_pos+50], - reason="no_useful_ast" if not ast else "cursor_mapping_failed" - ) - return regex_based_context_detection(text, cursor_pos) -``` - -**Benefit:** Build real-world golden tests from production edge cases - -**Cursor Position Context:** - -Make sure context detection knows exactly where the cursor is, not just what's before it. - -**Critical distinction:** -```sql -SELECT | FROM users -→ Context: SELECT_LIST (before FROM) -→ Show: columns, functions - -SELECT id| FROM users -→ Context: After column name (before FROM) -→ Show: FROM, AS, etc. (comma is never suggested) -``` - -**Implementation note:** -- Extract text before cursor: `text[:cursor_pos]` -- Extract text after cursor: `text[cursor_pos:]` (for context validation) -- Check if cursor is immediately after a complete token vs in the middle -- Use both left and right context for accurate detection - ---- - -### Performance Optimization - -**Large Schemas:** - -The 400-item guardrail is good, but additional optimizations are recommended: - -**Debouncing:** -- Delay autocomplete trigger by 150-300ms after last keystroke -- Avoids excessive computation while user is typing rapidly -- Cancel pending autocomplete requests if new input arrives - -**Caching:** -- Cache database schema (tables, columns) in memory -- Refresh only when schema changes (DDL operations detected) -- Cache parsed query structure for current statement -- Invalidate cache when query changes significantly - -**Schema cache invalidation triggers:** -- DDL operations: `CREATE`, `ALTER`, `DROP`, `TRUNCATE` -- Database/schema change (e.g., `USE database`) -- Manual refresh (user-triggered) -- Reconnection to database -- **Best-effort approach:** Some engines (e.g., PostgreSQL) support event listeners for schema changes; if not available, invalidate on DDL keyword detection or periodic refresh - -**Lazy Loading:** -- Load column details only when needed (not all upfront) -- For large tables (>100 columns), load columns on-demand -- Consider pagination for very large suggestion lists - -**Example implementation:** -```python -class AutocompleteCache: - def __init__(self): - self._schema_cache = {} # {database: {table: [columns]}} - self._last_query_hash = None - self._parsed_query_cache = None - - def get_columns(self, table: str) -> List[Column]: - if table not in self._schema_cache: - self._schema_cache[table] = fetch_columns(table) - return self._schema_cache[table] - - def invalidate_schema(self): - self._schema_cache.clear() -``` - ---- ### Statement Separator @@ -2420,19 +2188,7 @@ effective_separator = user_override or engine_default - `"$$"` → invalid (symbols only) - `"GO "` → invalid after trim (contains space before trim) -**Implementation notes:** - -- **Single-character separators:** Simple string split with string/comment awareness -- **Multi-character token separators:** Require word boundary detection using regex `\b{separator}\b` - - Example: `\bGO\b` for SQL Server - - Word boundary `\b` works correctly because multi-char separators are restricted to `[A-Za-z0-9_]+` -- **Both types MUST respect:** - - String literals: `'...'`, `"..."`, `` `...` `` - - Comments: `-- ...`, `/* ... */` - - Dollar-quoted strings (PostgreSQL): `$$...$$`, `$tag$...$tag$` - -All multi-query splitting logic MUST use the effective separator. -Hardcoding `";"` is forbidden. +The separator detection must respect strings, comments, and the active separator rules. --- @@ -2440,10 +2196,10 @@ Hardcoding `";"` is forbidden. **Important:** When multiple queries are present in the editor (separated by the effective statement separator), context detection must operate on the **current query** (where the cursor is), not the entire buffer. -**Implementation approach:** -1. Find statement boundaries by detecting the effective separator -2. Extract the query containing the cursor position -3. Run context detection only on that query +The autocomplete system must: +1. find the current statement using the effective separator +2. extract only the statement containing the cursor +3. resolve context only inside that statement **Edge cases:** - If cursor is on the separator, treat it as "end of previous statement". @@ -2478,11 +2234,6 @@ Context detection should analyze only: `SELECT * FROM orders WHERE |` - Dollar-quoted strings (PostgreSQL: `$$...$$`) - For token separators, word boundaries MUST be respected (e.g., `users_GO` is NOT a separator) -**Recommended approach:** -- Use sqlglot lexer/tokenizer to find statement boundaries (handles strings/comments correctly) -- For token separators: use regex with word boundaries (e.g., `\bGO\b` case-insensitive) -- Or implement robust separator detection with string/comment awareness - ### Multi-Word Keywords Multi-word keywords (e.g., `ORDER BY`, `GROUP BY`, `IS NULL`, `IS NOT NULL`, `NULLS FIRST`) are suggested as a single completion item but inserted verbatim. @@ -2534,7 +2285,7 @@ SELECT * FROM users;;| SELECT * FROM users WHERE name = '| ``` -**Resolution:** sqlglot parsing may fail. Fallback to regex-based context detection. Suggest literal keywords (`NULL`, `TRUE`, `FALSE`) and allow user to complete the string. +**Resolution:** Keep the user in the string-literal context and avoid unrelated suggestions. --- @@ -2546,7 +2297,7 @@ SELECT * FROM users WHERE name = '| SELECT * FROM users WHERE note = 'Price: $10; Discount: 20%' AND | ``` -**Resolution:** The `;` inside the string MUST be ignored. Use sqlglot lexer/tokenizer or implement string/comment-aware separator detection. Context: WHERE clause. +**Resolution:** The `;` inside the string MUST be ignored. Context remains WHERE clause. --- @@ -2627,7 +2378,7 @@ SELECT * FROM active_users WHERE | WITH active_users AS (SELECT * FROM users WHERE status = 'active') ``` -**Resolution:** sqlglot parsing may fail or produce incorrect AST. Fallback to regex-based context detection. CTE `active_users` is not recognized (defined after usage). Treat as physical table or show error hint. +**Resolution:** `active_users` is not yet available in scope. Treat it as unresolved and do not assume CTE scope before its definition. --- diff --git a/tests/autocomplete/cases/from.json b/tests/autocomplete/cases/from.json index 3fb1c65..fa7635e 100644 --- a/tests/autocomplete/cases/from.json +++ b/tests/autocomplete/cases/from.json @@ -231,6 +231,43 @@ "WHERE" ] } + }, + { + "case_id": "FROM_013", + "title": "Schema-qualified table token supports follow-up clause keywords", + "sql": "SELECT * FROM public.users W|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "FROM_CLAUSE", + "prefix": "W", + "suggestions": [ + "WHERE" + ] + } + }, + { + "case_id": "FROM_014", + "title": "Quoted table token supports follow-up clause keywords", + "sql": "SELECT * FROM \"users\" |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "CONTEXT", + "context": "FROM_CLAUSE", + "prefix": null, + "suggestions_contains": [ + "WHERE", + "JOIN" + ], + "suggestions_not_contains": [ + "users", + "orders" + ] + } } ] } diff --git a/tests/autocomplete/cases/provider_edges.json b/tests/autocomplete/cases/provider_edges.json new file mode 100644 index 0000000..fe80b72 --- /dev/null +++ b/tests/autocomplete/cases/provider_edges.json @@ -0,0 +1,45 @@ +{ + "group": "PROVIDER_EDGES", + "cases": [ + { + "case_id": "PROVIDER_EDGES_001", + "title": "Cursor on new whitespace-only statement after separator opens EMPTY context", + "sql": "SELECT * FROM users; |", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "EMPTY", + "context": "EMPTY", + "prefix": null, + "suggestions_contains": [ + "SELECT", + "INSERT" + ], + "suggestions_not_contains": [ + "users", + "id" + ] + } + }, + { + "case_id": "PROVIDER_EDGES_002", + "title": "Second statement scope is isolated from the previous statement in multi-query buffer", + "sql": "SELECT * FROM users;\nSELECT * FROM orders WHERE st|", + "dialect": "generic", + "current_table": null, + "schema_variant": "small", + "expected": { + "mode": "PREFIX", + "context": "WHERE_CLAUSE", + "prefix": "st", + "suggestions_contains": [ + "status" + ], + "suggestions_not_contains": [ + "users.status" + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/autocomplete/cases/sel.json b/tests/autocomplete/cases/sel.json index a41fba6..698b066 100644 --- a/tests/autocomplete/cases/sel.json +++ b/tests/autocomplete/cases/sel.json @@ -3,7 +3,7 @@ "cases": [ { "case_id": "SEL_LIST_001", - "title": "SELECT_LIST without FROM/JOIN shows functions + clause keywords", + "title": "SELECT_LIST without FROM/JOIN and without CURRENT_TABLE shows functions only", "sql": "SELECT |", "dialect": "generic", "current_table": null, diff --git a/tests/autocomplete/cases/select_column_behavior.json b/tests/autocomplete/cases/select_column_behavior.json index 5734c28..4ca425e 100644 --- a/tests/autocomplete/cases/select_column_behavior.json +++ b/tests/autocomplete/cases/select_column_behavior.json @@ -89,7 +89,7 @@ }, { "case_id": "NO_SCOPED_COMMA_001", - "title": "After column + comma suggests next-item (columns + functions)", + "title": "After column + comma with NO_SCOPED suggests next-item functions only", "sql": "SELECT id, |", "dialect": "generic", "current_table": null, @@ -98,7 +98,7 @@ "mode": "CONTEXT", "context": "SELECT_LIST", "prefix": null, - "comment": "Comma = next-item rules. With no scope and no prefix, expect functions.", + "comment": "Comma = next-item rules. With no explicit scope, no qualified references, and no CURRENT_TABLE, this remains NO_SCOPED so only functions are expected.", "suggestions_contains": [ "COUNT", "UUID" diff --git a/tests/autocomplete/test_autocomplete_basic.py b/tests/autocomplete/test_autocomplete_basic.py index 445db85..9da6f0f 100644 --- a/tests/autocomplete/test_autocomplete_basic.py +++ b/tests/autocomplete/test_autocomplete_basic.py @@ -1,8 +1,15 @@ from typing import Optional from unittest.mock import Mock -from windows.components.stc.autocomplete.auto_complete import SQLCompletionProvider +from windows.components.stc.autocomplete.auto_complete import ( + SQLAutoCompleteController, + SQLCompletionProvider, +) +from windows.components.stc.autocomplete.suggestion_builder import SuggestionBuilder from windows.components.stc.autocomplete.completion_types import CompletionItemType +from windows.components.stc.autocomplete.completion_types import CompletionItem +from windows.components.stc.autocomplete.completion_types import CompletionResult +from windows.state import CURRENT_SESSION def create_mock_column(col_id: int, name: str, table): @@ -197,6 +204,62 @@ def test_dot_completion(): assert "users." not in name +def test_dot_completion_with_prefix_in_select_list(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, get_current_table=lambda: None + ) + + result = provider.get(text="SELECT users.na", pos=len("SELECT users.na")) + + assert result is not None + item_names = [item.name for item in result.items] + assert item_names == ["name"] + + +def test_dot_completion_with_prefix_in_where_clause(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, get_current_table=lambda: None + ) + + sql = "SELECT * FROM users u WHERE u.em" + result = provider.get(text=sql, pos=len(sql)) + + assert result is not None + item_names = [item.name for item in result.items] + assert item_names == ["email"] + + +def test_dot_completion_with_prefix_in_order_by_clause(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, get_current_table=lambda: None + ) + + sql = "SELECT * FROM users u ORDER BY u.na" + result = provider.get(text=sql, pos=len(sql)) + + assert result is not None + item_names = [item.name for item in result.items] + assert item_names == ["name"] + + +def test_non_dot_prefix_keeps_context_suggestions(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, get_current_table=lambda: None + ) + + sql = "SELECT * FROM users WHERE na" + result = provider.get(text=sql, pos=len(sql)) + + assert result is not None + item_names = [item.name for item in result.items] + assert "name" in item_names + assert "email" not in item_names + + def test_multi_query(): database = create_mock_database() provider = SQLCompletionProvider( @@ -217,6 +280,176 @@ def test_multi_query(): assert "user_id" in item_names +def test_clamp_position_boundaries(): + assert SQLCompletionProvider._clamp_position(pos=-1, text="SELECT") == 0 + assert SQLCompletionProvider._clamp_position(pos=999, text="SELECT") == 6 + assert SQLCompletionProvider._clamp_position(pos=3, text="SELECT") == 3 + + +def test_rebuilds_context_detector_when_dialect_changes(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, + get_current_table=lambda: None, + ) + + session_mysql = Mock() + session_mysql.engine.value.dialect = "mysql" + + session_postgresql = Mock() + session_postgresql.engine.value.dialect = "postgresql" + + try: + CURRENT_SESSION.set_value(session_mysql) + first_result = provider.get(text="SEL", pos=3) + assert first_result is not None + assert provider._context_detector is not None + first_detector = provider._context_detector + + CURRENT_SESSION.set_value(session_postgresql) + second_result = provider.get(text="SEL", pos=3) + assert second_result is not None + assert provider._context_detector is not None + + assert provider._context_detector is not first_detector + assert provider._context_detector._dialect == "postgresql" + finally: + CURRENT_SESSION.set_value(None) + + +def test_unique_items_keeps_same_name_for_different_types(): + items = ( + CompletionItem(name="COUNT", item_type=CompletionItemType.FUNCTION), + CompletionItem(name="COUNT", item_type=CompletionItemType.KEYWORD), + CompletionItem(name="COUNT", item_type=CompletionItemType.FUNCTION), + ) + + unique = SQLAutoCompleteController._unique_items(items=items) + + assert unique == [ + CompletionItem(name="COUNT", item_type=CompletionItemType.FUNCTION), + CompletionItem(name="COUNT", item_type=CompletionItemType.KEYWORD), + ] + + +def test_show_respects_min_prefix_length_when_not_forced(): + class DummyEditor: + @staticmethod + def GetCurrentPos(): + return 0 + + @staticmethod + def GetText(): + return "a" + + controller = SQLAutoCompleteController.__new__(SQLAutoCompleteController) + controller._is_enabled = True + controller._is_showing = False + controller._editor = DummyEditor() + controller._provider = Mock() + controller._min_prefix_length = 2 + controller._current_result = None + + hidden = {"value": False} + shown_items = [] + + controller._hide_popup = lambda: hidden.__setitem__("value", True) + controller._show_popup = lambda items: shown_items.extend(items) + + controller._provider.get.return_value = CompletionResult( + prefix="a", + prefix_length=1, + items=(CompletionItem(name="alpha", item_type=CompletionItemType.COLUMN),), + ) + + controller.show(force=False) + + assert hidden["value"] is True + assert shown_items == [] + + +def test_show_ignores_min_prefix_length_when_forced(): + class DummyEditor: + @staticmethod + def GetCurrentPos(): + return 0 + + @staticmethod + def GetText(): + return "a" + + controller = SQLAutoCompleteController.__new__(SQLAutoCompleteController) + controller._is_enabled = True + controller._is_showing = False + controller._editor = DummyEditor() + controller._provider = Mock() + controller._min_prefix_length = 2 + controller._current_result = None + + hidden = {"value": False} + shown_items = [] + + controller._hide_popup = lambda: hidden.__setitem__("value", True) + controller._show_popup = lambda items: shown_items.extend(items) + + controller._provider.get.return_value = CompletionResult( + prefix="a", + prefix_length=1, + items=(CompletionItem(name="alpha", item_type=CompletionItemType.COLUMN),), + ) + + controller.show(force=True) + + assert hidden["value"] is False + assert [item.name for item in shown_items] == ["alpha"] + + +def test_schema_qualified_from_followup_keywords(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, get_current_table=lambda: None + ) + + sql = "SELECT * FROM public.users W" + result = provider.get(text=sql, pos=len(sql)) + + assert result is not None + assert "WHERE" in [item.name for item in result.items] + + +def test_quoted_from_followup_keywords(): + database = create_mock_database() + provider = SQLCompletionProvider( + get_database=lambda: database, get_current_table=lambda: None + ) + + sql = 'SELECT * FROM "users" ' + result = provider.get(text=sql, pos=len(sql)) + + assert result is not None + assert "WHERE" in [item.name for item in result.items] + + +def test_schema_qualified_update_target_table_lookup(): + database = create_mock_database() + builder = SuggestionBuilder(database=database, current_table=None) + + table = builder._find_update_target_table("UPDATE public.users SET na") + + assert table is not None + assert table.name == "users" + + +def test_quoted_update_target_table_lookup(): + database = create_mock_database() + builder = SuggestionBuilder(database=database, current_table=None) + + table = builder._find_update_target_table('UPDATE "users" SET na') + + assert table is not None + assert table.name == "users" + + if __name__ == "__main__": test_empty_context() test_single_token() diff --git a/windows/components/stc/autocomplete/auto_complete.py b/windows/components/stc/autocomplete/auto_complete.py index 149dddf..c5fd783 100644 --- a/windows/components/stc/autocomplete/auto_complete.py +++ b/windows/components/stc/autocomplete/auto_complete.py @@ -35,6 +35,7 @@ def __init__( self._get_current_table = get_current_table or (lambda: None) self._is_filter_editor = is_filter_editor self._cached_database_id: Optional[int] = None + self._cached_dialect: Optional[str] = None self._context_detector: Optional[ContextDetector] = None self._dot_handler: Optional[DotCompletionHandler] = None @@ -44,7 +45,12 @@ def _get_current_dialect(self) -> Optional[str]: if session := CURRENT_SESSION.get_value(): return session.engine.value.dialect - def get(self, text: str, pos: int) -> Optional[CompletionResult]: + def get( + self, + text: str, + pos: int, + separator: Optional[str] = None, + ) -> Optional[CompletionResult]: try: database = self._get_database() if database is None: @@ -55,7 +61,11 @@ def get(self, text: str, pos: int) -> Optional[CompletionResult]: safe_pos = self._clamp_position(pos=pos, text=text) statement, relative_pos = ( - self._statement_extractor.extract_current_statement(text, safe_pos) + self._statement_extractor.extract_current_statement( + text, + safe_pos, + separator=separator, + ) ) if not self._context_detector: @@ -99,10 +109,15 @@ def _clamp_position(*, pos: int, text: str) -> int: def _update_cache(self, *, database: SQLDatabase) -> None: database_id = id(database) - if self._cached_database_id != database_id: + dialect = self._get_current_dialect() + + if ( + self._cached_database_id != database_id + or self._cached_dialect != dialect + ): self._cached_database_id = database_id + self._cached_dialect = dialect - dialect = self._get_current_dialect() self._context_detector = ContextDetector(dialect) self._dot_handler = DotCompletionHandler(database, None) @@ -145,11 +160,23 @@ def set_enabled(self, is_enabled: bool) -> None: self._hide_popup() def get_effective_separator(self) -> str: + user_override = ";" + if settings := getattr(self, "_settings", None): + user_override = settings.get_value( + "editor", + "statement_separator", + default=";", + ) + session = CURRENT_SESSION.get_value() + default_separator = ";" if session and hasattr(session, "context"): - return session.context.DEFAULT_STATEMENT_SEPARATOR + default_separator = session.context.DEFAULT_STATEMENT_SEPARATOR - return self._settings.get_value("editor", "statement_separator", default=";") + return StatementExtractor.normalize_separator( + user_override, + default=default_separator, + ) def show(self, *, force: bool) -> None: if not self._is_enabled: @@ -162,7 +189,11 @@ def show(self, *, force: bool) -> None: pos = self._editor.GetCurrentPos() text = self._editor.GetText() - result = self._provider.get(pos=pos, text=text) + result = self._provider.get( + pos=pos, + text=text, + separator=self.get_effective_separator(), + ) if result is None: self._hide_popup() @@ -172,8 +203,12 @@ def show(self, *, force: bool) -> None: self._hide_popup() return + if not force and result.prefix_length < self._min_prefix_length: + self._hide_popup() + return + self._current_result = result - items = self._unique_sorted_items(items=result.items) + items = self._unique_items(items=result.items) self._show_popup(items) except Exception as ex: logger.error(f"Error in show(): {ex}", exc_info=True) @@ -205,9 +240,13 @@ def _on_item_completed(self, item: CompletionItem) -> None: return current_pos = self._editor.GetCurrentPos() - start_pos = current_pos - self._current_result.prefix_length + start_pos, end_pos = self._get_identifier_bounds_around_cursor(current_pos) - self._editor.SetSelection(start_pos, current_pos) + if start_pos == end_pos: + start_pos = max(0, current_pos - self._current_result.prefix_length) + end_pos = current_pos + + self._editor.SetSelection(start_pos, end_pos) should_add_space = ( self._add_space_after_completion @@ -233,6 +272,25 @@ def _on_item_completed(self, item: CompletionItem) -> None: if item.name.upper() in trigger_keywords: wx.CallAfter(lambda: self._schedule_show(force=False)) + @staticmethod + def _is_identifier_char(character: str) -> bool: + return character.isalnum() or character == "_" + + def _get_identifier_bounds_around_cursor(self, cursor_pos: int) -> tuple[int, int]: + text = self._editor.GetText() + text_length = len(text) + + start_pos = min(max(cursor_pos, 0), text_length) + end_pos = start_pos + + while start_pos > 0 and self._is_identifier_char(text[start_pos - 1]): + start_pos -= 1 + + while end_pos < text_length and self._is_identifier_char(text[end_pos]): + end_pos += 1 + + return start_pos, end_pos + def _on_key_down(self, event: wx.KeyEvent) -> None: if not self._is_enabled: event.Skip() @@ -305,15 +363,22 @@ def _cancel_pending(self) -> None: self._pending_call = None @staticmethod - def _unique_sorted_items( + def _unique_items( *, items: tuple[CompletionItem, ...] ) -> list[CompletionItem]: - seen_names: set[str] = set() + seen_keys: set[tuple[str, CompletionItemType]] = set() unique_items: list[CompletionItem] = [] for item in items: - if item.name not in seen_names: - seen_names.add(item.name) + key = (item.name, item.item_type) + if key not in seen_keys: + seen_keys.add(key) unique_items.append(item) return unique_items + + @staticmethod + def _unique_sorted_items( + *, items: tuple[CompletionItem, ...] + ) -> list[CompletionItem]: + return SQLAutoCompleteController._unique_items(items=items) diff --git a/windows/components/stc/autocomplete/autocomplete_popup.py b/windows/components/stc/autocomplete/autocomplete_popup.py index f77396e..671bbb9 100644 --- a/windows/components/stc/autocomplete/autocomplete_popup.py +++ b/windows/components/stc/autocomplete/autocomplete_popup.py @@ -1,7 +1,6 @@ from typing import Callable, Optional import wx -import wx.dataview from windows.components.stc.autocomplete.completion_types import CompletionItem, CompletionItemType from windows.components.stc.theme_loader import ThemeLoader @@ -10,81 +9,79 @@ class AutoCompletePopup(wx.PopupWindow): def __init__(self, parent: wx.Window, settings: object = None, theme_loader: ThemeLoader = None) -> None: super().__init__(parent, wx.BORDER_SIMPLE) - - self._selected_index: int = 0 + self._items: list[CompletionItem] = [] self._on_item_selected: Optional[Callable] = None self._settings = settings self._theme_loader = theme_loader - + if settings: self._popup_width = settings.get_value("editor", "autocomplete", "popup_width", default=300) self._popup_max_height = settings.get_value("editor", "autocomplete", "popup_max_height", default=10) else: self._popup_width = 300 self._popup_max_height = 10 - + self._create_ui() self._bind_events() - + def _create_ui(self) -> None: panel = wx.Panel(self) sizer = wx.BoxSizer(wx.VERTICAL) - + self._list_ctrl = wx.ListCtrl( panel, style=wx.LC_REPORT | wx.LC_NO_HEADER | wx.LC_SINGLE_SEL ) - + self._image_list = wx.ImageList(16, 16) self._list_ctrl.SetImageList(self._image_list, wx.IMAGE_LIST_SMALL) - + self._list_ctrl.InsertColumn(0, "", width=self._popup_width) self._list_ctrl.SetMinSize((self._popup_width, 200)) - + sizer.Add(self._list_ctrl, 1, wx.EXPAND) panel.SetSizer(sizer) - + main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.Add(panel, 1, wx.EXPAND) self.SetSizer(main_sizer) - + def _bind_events(self) -> None: self._list_ctrl.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._on_item_activated) self._list_ctrl.Bind(wx.EVT_KEY_DOWN, self._on_key_down) self.Bind(wx.EVT_KILL_FOCUS, self._on_kill_focus) - + def show_items(self, items: list[CompletionItem], position: wx.Point) -> None: self._items = items - self._selected_index = 0 - + self._list_ctrl.DeleteAllItems() self._image_list.RemoveAll() - + for idx, item in enumerate(items): bitmap = self._get_bitmap_for_type(item.item_type) color = self._get_color_for_type(item.item_type) - + image_idx = self._image_list.Add(bitmap) list_idx = self._list_ctrl.InsertItem(idx, item.name, image_idx) - + if color: self._list_ctrl.SetItemTextColour(list_idx, color) - + if items: self._list_ctrl.Select(0) self._list_ctrl.Focus(0) - + self.SetPosition(position) - + item_count = min(len(items), self._popup_max_height) item_height = 24 height = item_count * item_height + 10 self.SetSize((self._popup_width, height)) - + self.Show() self._list_ctrl.SetFocus() - + def _get_bitmap_for_type(self, item_type: CompletionItemType) -> wx.Bitmap: icon_map = { CompletionItemType.KEYWORD: wx.ART_INFORMATION, @@ -92,17 +89,17 @@ def _get_bitmap_for_type(self, item_type: CompletionItemType) -> wx.Bitmap: CompletionItemType.TABLE: wx.ART_FOLDER, CompletionItemType.COLUMN: wx.ART_NORMAL_FILE, } - + art_id = icon_map.get(item_type, wx.ART_INFORMATION) return wx.ArtProvider.GetBitmap(art_id, wx.ART_MENU, (16, 16)) - + def _get_color_for_type(self, item_type: CompletionItemType) -> wx.Colour: if self._theme_loader: colors = self._theme_loader.get_autocomplete_colors() color_hex = colors.get(item_type.value) if color_hex: return wx.Colour(color_hex) - + color_map = { CompletionItemType.KEYWORD: wx.Colour(0, 0, 255), CompletionItemType.FUNCTION: wx.Colour(128, 0, 128), @@ -110,25 +107,25 @@ def _get_color_for_type(self, item_type: CompletionItemType) -> wx.Colour: CompletionItemType.COLUMN: wx.Colour(0, 0, 0), } return color_map.get(item_type, wx.Colour(0, 0, 0)) - + def _on_item_activated(self, event: wx.Event) -> None: row = self._list_ctrl.GetFirstSelected() if row != wx.NOT_FOUND and row < len(self._items): self._complete_with_item(self._items[row]) - + def _on_key_down(self, event: wx.KeyEvent) -> None: key_code = event.GetKeyCode() - + if key_code == wx.WXK_ESCAPE: self.Hide() return - + if key_code in (wx.WXK_RETURN, wx.WXK_TAB): row = self._list_ctrl.GetFirstSelected() if row != wx.NOT_FOUND and row < len(self._items): self._complete_with_item(self._items[row]) return - + if key_code == wx.WXK_PAGEDOWN: current = self._list_ctrl.GetFirstSelected() if current != wx.NOT_FOUND: @@ -137,7 +134,7 @@ def _on_key_down(self, event: wx.KeyEvent) -> None: self._list_ctrl.Focus(new_index) self._list_ctrl.EnsureVisible(new_index) return - + if key_code == wx.WXK_PAGEUP: current = self._list_ctrl.GetFirstSelected() if current != wx.NOT_FOUND: @@ -146,18 +143,31 @@ def _on_key_down(self, event: wx.KeyEvent) -> None: self._list_ctrl.Focus(new_index) self._list_ctrl.EnsureVisible(new_index) return - + event.Skip() - + + def _owns_window(self, window: Optional[wx.Window]) -> bool: + current = window + while current is not None: + if current is self: + return True + current = current.GetParent() + return False + def _on_kill_focus(self, event: wx.FocusEvent) -> None: + next_focus = event.GetWindow() if hasattr(event, "GetWindow") else None + if self._owns_window(next_focus): + event.Skip() + return + self.Hide() event.Skip() - + def _complete_with_item(self, item: CompletionItem) -> None: if self._on_item_selected: self._on_item_selected(item) self.Hide() - + def set_on_item_selected(self, callback: Callable) -> None: self._on_item_selected = callback diff --git a/windows/components/stc/autocomplete/context_detector.py b/windows/components/stc/autocomplete/context_detector.py index 72f6d60..7c60d1b 100644 --- a/windows/components/stc/autocomplete/context_detector.py +++ b/windows/components/stc/autocomplete/context_detector.py @@ -17,8 +17,12 @@ class ContextDetector: _prefix_pattern = re.compile(r"[A-Za-z_][A-Za-z0-9_]*$") + _identifier_segment_pattern = r"(?:[A-Za-z_][A-Za-z0-9_]*|`[^`]+`|\"[^\"]+\"|\[[^\]]+\])" + _table_name_pattern = ( + _identifier_segment_pattern + r"(?:\." + _identifier_segment_pattern + r")?" + ) _join_after_table_pattern = re.compile( - r"\b(?:(?:INNER|LEFT|RIGHT|FULL|CROSS)(?:\s+OUTER)?\s+)?JOIN\s+([A-Za-z_][A-Za-z0-9_]*)" + r"\b(?:(?:INNER|LEFT|RIGHT|FULL|CROSS)(?:\s+OUTER)?\s+)?JOIN\s+(" + _table_name_pattern + r")" r"\s*(?:(?:AS\s+)?([A-Za-z_][A-Za-z0-9_]*))?\s*$", re.IGNORECASE, ) @@ -90,7 +94,10 @@ def _extract_prefix(self, text: str, cursor_pos: int) -> str: return "" return match.group(0) - _dot_pattern = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)?$") + _dot_pattern = re.compile( + r"((?:[A-Za-z_][A-Za-z0-9_]*|`[^`]+`|\"[^\"]+\"|\[[^\]]+\]))" + r"\.([A-Za-z_][A-Za-z0-9_]*)?$" + ) def _check_dot_completion(self, left_text: str, prefix: str) -> Optional[re.Match]: if "." in left_text: @@ -99,21 +106,37 @@ def _check_dot_completion(self, left_text: str, prefix: str) -> Optional[re.Matc def _detect_context_with_regex(self, left_text: str, prefix: str) -> SQLContext: left_upper = left_text.upper() + statement_type, statement_pos = self._detect_statement_type(left_upper) + + if statement_type == "INSERT": + return self._detect_insert_context(left_text, left_upper, prefix, statement_pos) + if statement_type == "UPDATE": + return self._detect_update_context(left_text, left_upper, prefix, statement_pos) + if statement_type == "DELETE": + return self._detect_delete_context(left_text, left_upper, prefix, statement_pos) + if statement_type != "SELECT": + return SQLContext.UNKNOWN - insert_pos = left_upper.rfind("INSERT") - update_pos = left_upper.rfind("UPDATE") - delete_pos = left_upper.rfind("DELETE") - - if insert_pos != -1 and insert_pos >= max(update_pos, delete_pos, -1): - return self._detect_insert_context(left_text, left_upper, prefix, insert_pos) - - if update_pos != -1 and update_pos >= max(insert_pos, delete_pos, -1): - return self._detect_update_context(left_text, left_upper, prefix, update_pos) + return self._detect_select_context(left_text, left_upper, prefix) - if delete_pos != -1 and delete_pos >= max(insert_pos, update_pos, -1): - return self._detect_delete_context(left_text, left_upper, prefix, delete_pos) + @staticmethod + def _detect_statement_type(left_upper: str) -> tuple[str, int]: + statement_positions = { + "SELECT": left_upper.rfind("SELECT"), + "INSERT": left_upper.rfind("INSERT"), + "UPDATE": left_upper.rfind("UPDATE"), + "DELETE": left_upper.rfind("DELETE"), + } + statement_type = max(statement_positions, key=lambda key: statement_positions[key]) + return statement_type, statement_positions[statement_type] + def _detect_select_context( + self, left_text: str, left_upper: str, prefix: str + ) -> SQLContext: select_pos = left_upper.rfind("SELECT") + if select_pos == -1: + return SQLContext.UNKNOWN + from_pos = left_upper.rfind("FROM") where_pos = left_upper.rfind("WHERE") join_pos = left_upper.rfind("JOIN") @@ -124,9 +147,6 @@ def _detect_context_with_regex(self, left_text: str, prefix: str) -> SQLContext: limit_pos = left_upper.rfind("LIMIT") offset_pos = left_upper.rfind("OFFSET") - if select_pos == -1: - return SQLContext.UNKNOWN - if re.search(r"\bOVER\s*(?:\(\s*)?$", left_text, re.IGNORECASE): return SQLContext.WINDOW_OVER @@ -186,6 +206,15 @@ def _detect_context_with_regex(self, left_text: str, prefix: str) -> SQLContext: return SQLContext.SELECT_LIST + @staticmethod + def _normalize_identifier(identifier: str) -> str: + normalized = identifier.strip() + if not normalized: + return normalized + if normalized[0] in {'`', '"', '['} and normalized[-1] in {'`', '"', ']'}: + return normalized[1:-1] + return normalized + # ── INSERT ────────────────────────────────────────────────────── @staticmethod @@ -679,7 +708,7 @@ def _extract_scope_from_text( } join_pattern = re.compile( - r"\bJOIN\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:(?:AS\s+)?([A-Za-z_][A-Za-z0-9_]*))?\s*(?:\bON\b|\bUSING\b|$)", + r"\bJOIN\s+(" + self._table_name_pattern + r")\s*(?:(?:AS\s+)?([A-Za-z_][A-Za-z0-9_]*))?\s*(?:\bON\b|\bUSING\b|$)", re.IGNORECASE, ) @@ -689,7 +718,7 @@ def _extract_scope_from_text( # Extract UPDATE target table into scope (only for UPDATE statements) update_match = re.match( - r"\s*UPDATE\s+([A-Za-z_][A-Za-z0-9_]*)" + r"\s*UPDATE\s+(" + self._table_name_pattern + r")" r"(?:\s+(?:AS\s+)?([A-Za-z_][A-Za-z0-9_]*))?", main_text, re.IGNORECASE, @@ -804,7 +833,7 @@ def _extract_from_table_tokens( if not ( table_match := re.match( - r"(?P[A-Za-z_][A-Za-z0-9_]*)(?:\s+(?:AS\s+)?(?P[A-Za-z_][A-Za-z0-9_]*))?(?:\s+[A-Za-z_][A-Za-z0-9_]*)?$", + r"(?P
" + ContextDetector._table_name_pattern + r")(?:\s+(?:AS\s+)?(?P[A-Za-z_][A-Za-z0-9_]*))?(?:\s+[A-Za-z_][A-Za-z0-9_]*)?$", part, re.IGNORECASE, ) @@ -939,9 +968,14 @@ def _parse_cte_definitions(self, text: str) -> tuple[list[TableReference], int]: def _find_table_in_database( self, table_name: str, database: SQLDatabase ) -> Optional[SQLTable]: + table_name_candidate = table_name.split(".")[-1] + normalized_candidate = self._normalize_identifier(table_name_candidate) + normalized_full_name = self._normalize_identifier(table_name) try: for table in database.tables: - if table.name.lower() == table_name.lower(): + if table.name.lower() == normalized_full_name.lower(): + return table + if table.name.lower() == normalized_candidate.lower(): return table except Exception: pass diff --git a/windows/components/stc/autocomplete/dot_completion_handler.py b/windows/components/stc/autocomplete/dot_completion_handler.py index 0fe4ed4..bd8b0e0 100644 --- a/windows/components/stc/autocomplete/dot_completion_handler.py +++ b/windows/components/stc/autocomplete/dot_completion_handler.py @@ -11,7 +11,21 @@ class DotCompletionHandler: - _token_pattern = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") + _identifier_segment_pattern = ( + r"(?:[A-Za-z_][A-Za-z0-9_]*|`[^`]+`|\"[^\"]+\"|\[[^\]]+\])" + ) + _token_pattern = re.compile(_identifier_segment_pattern) + + @staticmethod + def _normalize_identifier(identifier: str) -> str: + normalized = identifier.strip() + if not normalized: + return normalized + + if normalized[0] in {'`', '"', '['} and normalized[-1] in {'`', '"', ']'}: + return normalized[1:-1] + + return normalized def __init__(self, database: Optional[SQLDatabase], scope: Optional[object] = None): self._database = database @@ -64,6 +78,8 @@ def get_completions( if not table_or_alias: return None, "" + table_or_alias = self._normalize_identifier(table_or_alias) + table = self._find_table(table_or_alias) if not table: return None, "" @@ -116,9 +132,13 @@ def _build_table_index(self) -> None: try: for ref in self._scope.from_tables + self._scope.join_tables: if ref.table: - self._table_index[ref.name.lower()] = ref.table + ref_name = self._normalize_identifier(ref.name).lower() + self._table_index[ref_name] = ref.table + if "." in ref_name: + self._table_index[ref_name.split(".")[-1]] = ref.table if ref.alias: - self._table_index[ref.alias.lower()] = ref.table + alias_name = self._normalize_identifier(ref.alias).lower() + self._table_index[alias_name] = ref.table except (AttributeError, TypeError): pass @@ -127,8 +147,13 @@ def _build_table_index(self) -> None: try: for table in self._database.tables: - if table.name.lower() not in self._table_index: - self._table_index[table.name.lower()] = table + table_name = self._normalize_identifier(table.name).lower() + if table_name not in self._table_index: + self._table_index[table_name] = table + if "." in table_name: + base_name = table_name.split(".")[-1] + if base_name not in self._table_index: + self._table_index[base_name] = table except (AttributeError, TypeError): pass diff --git a/windows/components/stc/autocomplete/statement_extractor.py b/windows/components/stc/autocomplete/statement_extractor.py index b2f766b..3e4eb12 100644 --- a/windows/components/stc/autocomplete/statement_extractor.py +++ b/windows/components/stc/autocomplete/statement_extractor.py @@ -6,16 +6,86 @@ class StatementExtractor: _string_pattern = re.compile(r"'(?:[^'\\]|\\.)*'|\"(?:[^\"\\]|\\.)*\"") _comment_pattern = re.compile(r"--[^\n]*|/\*.*?\*/", re.DOTALL) + + @staticmethod + def normalize_separator( + separator: Optional[str], *, default: str = ";" + ) -> str: + raw_separator = (separator or "").strip() + if not raw_separator: + return default + + if len(raw_separator) == 1: + return raw_separator + + if re.fullmatch(r"[A-Za-z0-9_]+", raw_separator): + return raw_separator + + return default + + @staticmethod + def _statement_boundaries(text: str, cleaned_text: str, separator: str) -> list[int]: + boundaries = [0] + + if len(separator) == 1: + for idx, char in enumerate(cleaned_text): + if char == separator: + boundaries.append(idx + 1) + else: + pattern = re.compile(rf"\b{re.escape(separator)}\b", re.IGNORECASE) + for match in pattern.finditer(cleaned_text): + boundaries.append(match.end()) + + boundaries.append(len(text)) + return boundaries + + @staticmethod + def _trim_statement( + statement: str, + cursor_offset: int, + separator: str, + ) -> tuple[str, int]: + statement_without_separator = statement + if len(separator) == 1: + if statement_without_separator.endswith(separator): + statement_without_separator = statement_without_separator[:-1] + else: + separator_pattern = re.compile( + rf"\b{re.escape(separator)}\b\s*$", + re.IGNORECASE, + ) + statement_without_separator = separator_pattern.sub( + "", + statement_without_separator, + count=1, + ) + + if statement_without_separator != statement: + cursor_offset = min(cursor_offset, len(statement_without_separator)) + + statement = statement_without_separator + + leading_whitespace = len(statement) - len(statement.lstrip()) + trimmed_statement = statement.lstrip() + relative_pos = max(0, cursor_offset - leading_whitespace) + relative_pos = min(relative_pos, len(trimmed_statement)) + + return trimmed_statement, relative_pos @staticmethod - def extract_current_statement(text: str, cursor_pos: int) -> tuple[str, int]: + def extract_current_statement( + text: str, + cursor_pos: int, + separator: Optional[str] = None, + ) -> tuple[str, int]: + effective_separator = StatementExtractor.normalize_separator(separator) cleaned_text = StatementExtractor._remove_strings_and_comments(text) - - statement_boundaries = [0] - for i, char in enumerate(cleaned_text): - if char == ';': - statement_boundaries.append(i + 1) - statement_boundaries.append(len(text)) + + statement_boundaries = StatementExtractor._statement_boundaries( + text, + cleaned_text, + effective_separator, + ) for i in range(len(statement_boundaries) - 1): start = statement_boundaries[i] @@ -23,41 +93,50 @@ def extract_current_statement(text: str, cursor_pos: int) -> tuple[str, int]: if start <= cursor_pos <= end: statement = text[start:end] - - if statement.endswith(';'): - statement = statement[:-1] - - statement = statement.lstrip() - - relative_pos = cursor_pos - start - if start > 0: - leading_whitespace = len(text[start:]) - len(text[start:].lstrip()) - relative_pos = cursor_pos - start - leading_whitespace - + + cursor_offset = cursor_pos - start + statement, relative_pos = StatementExtractor._trim_statement( + statement, + cursor_offset, + effective_separator, + ) + return statement, relative_pos return text, cursor_pos @staticmethod - def extract_all_statements(text: str) -> list[tuple[str, int, int]]: + def extract_all_statements( + text: str, + separator: Optional[str] = None, + ) -> list[tuple[str, int, int]]: """Return all statements as (text, start_pos, end_pos) tuples.""" if not text.strip(): return [] + effective_separator = StatementExtractor.normalize_separator(separator) cleaned = StatementExtractor._remove_strings_and_comments(text) - - boundaries = [0] - for i, char in enumerate(cleaned): - if char == ';': - boundaries.append(i + 1) - boundaries.append(len(text)) + boundaries = StatementExtractor._statement_boundaries( + text, + cleaned, + effective_separator, + ) results = [] for i in range(len(boundaries) - 1): start, end = boundaries[i], boundaries[i + 1] statement = text[start:end] - if statement.endswith(';'): - statement = statement[:-1] + if len(effective_separator) == 1: + if statement.endswith(effective_separator): + statement = statement[:-1] + else: + statement = re.sub( + rf"\b{re.escape(effective_separator)}\b\s*$", + "", + statement, + count=1, + flags=re.IGNORECASE, + ) statement = statement.strip() if statement: results.append((statement, start, end)) diff --git a/windows/components/stc/autocomplete/suggestion_builder.py b/windows/components/stc/autocomplete/suggestion_builder.py index 8fdbc13..7459edd 100644 --- a/windows/components/stc/autocomplete/suggestion_builder.py +++ b/windows/components/stc/autocomplete/suggestion_builder.py @@ -6,6 +6,9 @@ CompletionItem, CompletionItemType, ) +from windows.components.stc.autocomplete.dot_completion_handler import ( + DotCompletionHandler, +) from windows.components.stc.autocomplete.query_scope import QueryScope, TableReference from windows.components.stc.autocomplete.sql_context import SQLContext @@ -13,6 +16,10 @@ class SuggestionBuilder: + _identifier_segment_pattern = r"(?:[A-Za-z_][A-Za-z0-9_]*|`[^`]+`|\"[^\"]+\"|\[[^\]]+\])" + _table_name_pattern = ( + _identifier_segment_pattern + r"(?:\." + _identifier_segment_pattern + r")?" + ) _primary_keywords = { "SELECT", "INSERT", @@ -111,114 +118,135 @@ def build( statement: str = "", cursor_pos: Optional[int] = None, ) -> list[CompletionItem]: - if context == SQLContext.EMPTY: - return self._build_empty(prefix) - - if context == SQLContext.SINGLE_TOKEN: - return self._build_single_token(prefix) + if context in (SQLContext.EMPTY, SQLContext.SINGLE_TOKEN): + return self._build_empty(prefix) if context == SQLContext.EMPTY else self._build_single_token(prefix) if context == SQLContext.DOT_COMPLETION: return self._build_dot_completion(scope, prefix, statement) - if context == SQLContext.SELECT_LIST: - return self._build_select_list(scope, prefix, statement, cursor_pos) - - if context == SQLContext.FROM_CLAUSE: - import re - - statement_upper = statement.upper() - - if re.search(r"\bAS\s+$", statement_upper): - return [] - - if prefix and re.search(r"\bAS\s+\w+$", statement_upper): - return [] + if context in { + SQLContext.SELECT_LIST, + SQLContext.FROM_CLAUSE, + SQLContext.JOIN_CLAUSE, + SQLContext.JOIN_AFTER_TABLE, + SQLContext.JOIN_ON, + SQLContext.JOIN_ON_AFTER_OPERATOR, + SQLContext.JOIN_ON_AFTER_EXPRESSION, + SQLContext.WHERE_CLAUSE, + SQLContext.WHERE_AFTER_EXPRESSION, + SQLContext.WHERE_AFTER_OPERATOR, + SQLContext.ORDER_BY_CLAUSE, + SQLContext.ORDER_BY_AFTER_COLUMN, + SQLContext.GROUP_BY_CLAUSE, + SQLContext.HAVING_CLAUSE, + SQLContext.HAVING_AFTER_OPERATOR, + SQLContext.HAVING_AFTER_EXPRESSION, + SQLContext.WINDOW_OVER, + SQLContext.LIMIT_OFFSET_CLAUSE, + SQLContext.AFTER_LIMIT_NUMBER, + SQLContext.WHERE_STRING_LITERAL, + }: + return self._build_select_family_context( + context=context, + scope=scope, + prefix=prefix, + statement=statement, + cursor_pos=cursor_pos, + ) - if ( - prefix - and scope.from_tables - and "," not in statement - and self._is_after_completed_from_table_with_prefix( - statement, prefix, scope - ) - ): - return self._build_from_followup_keywords(prefix, scope) - - if not prefix and scope.from_tables: - if "," in statement: - in_scope_table_names = { - ref.name.lower() for ref in scope.from_tables - } - try: - tables = [ - CompletionItem( - name=table.name, item_type=CompletionItemType.TABLE - ) - for table in self._database.tables - if table.name.lower() not in in_scope_table_names - ] - return sorted( - tables, key=lambda x: self._table_name_sort_key(x.name) - ) - except (AttributeError, TypeError): - return [] - else: - return self._build_from_followup_keywords(prefix, scope) + if context in { + SQLContext.INSERT_INTO, + SQLContext.INSERT_COLUMNS, + SQLContext.INSERT_VALUES, + SQLContext.INSERT_VALUE_EXPRESSIONS, + SQLContext.INSERT_COMPLETE, + SQLContext.INSERT_POST_VALUES, + SQLContext.INSERT_STRING_LITERAL, + }: + return self._build_insert_context(context, scope, prefix, statement) + + if context in { + SQLContext.UPDATE_TABLE, + SQLContext.UPDATE_SET_CLAUSE, + SQLContext.UPDATE_SET_COLUMNS, + SQLContext.UPDATE_SET_EXPRESSIONS, + SQLContext.UPDATE_WHERE_CLAUSE, + SQLContext.UPDATE_WHERE_CONDITIONS, + SQLContext.UPDATE_WHERE_OPERATORS, + SQLContext.UPDATE_JOIN_ON, + SQLContext.UPDATE_STRING_LITERAL, + SQLContext.UPDATE_WHERE_STRING_LITERAL, + }: + return self._build_update_context(context, scope, prefix, statement) + + if context in { + SQLContext.DELETE_FROM, + SQLContext.DELETE_WHERE_CLAUSE, + SQLContext.DELETE_WHERE_CONDITIONS, + SQLContext.DELETE_WHERE_OPERATORS, + SQLContext.DELETE_WHERE_EXPRESSIONS, + SQLContext.DELETE_JOIN_ON, + SQLContext.DELETE_USING, + SQLContext.DELETE_SUBQUERY, + SQLContext.DELETE_WHERE_STRING_LITERAL, + }: + return self._build_delete_context(context, scope, prefix, statement) - return self._build_from_clause(prefix, statement, scope) + return self._build_keywords(prefix) + def _build_select_family_context( + self, + context: SQLContext, + scope: QueryScope, + prefix: str, + statement: str, + cursor_pos: Optional[int], + ) -> list[CompletionItem]: + if context == SQLContext.SELECT_LIST: + return self._build_select_list(scope, prefix, statement, cursor_pos) + if context == SQLContext.FROM_CLAUSE: + return self._build_from_context(scope, prefix, statement) if context == SQLContext.JOIN_CLAUSE: return self._build_join_clause(prefix, scope, statement) - if context == SQLContext.JOIN_AFTER_TABLE: return self._build_join_after_table(scope) - if context == SQLContext.JOIN_ON: return self._build_join_on(scope, prefix, statement) - if context == SQLContext.JOIN_ON_AFTER_OPERATOR: return self._build_join_on_after_operator(scope, prefix, statement) - if context == SQLContext.JOIN_ON_AFTER_EXPRESSION: return self._build_join_on_after_expression(prefix) - if context == SQLContext.WHERE_CLAUSE: return self._build_where_clause(scope, prefix, statement) - if context == SQLContext.WHERE_AFTER_EXPRESSION: return self._build_where_after_expression(prefix, statement) - if context == SQLContext.WHERE_AFTER_OPERATOR: return self._build_where_after_operator(scope, prefix, statement) - if context == SQLContext.ORDER_BY_CLAUSE: return self._build_order_by(scope, prefix, statement) - if context == SQLContext.ORDER_BY_AFTER_COLUMN: return self._build_order_by_after_column(prefix) - if context == SQLContext.GROUP_BY_CLAUSE: return self._build_group_by(scope, prefix, statement, cursor_pos) - if context == SQLContext.HAVING_CLAUSE: return self._build_having(scope, prefix, statement) - if context == SQLContext.HAVING_AFTER_OPERATOR: return self._build_having_after_operator(scope, prefix, statement) - if context == SQLContext.HAVING_AFTER_EXPRESSION: return self._build_having_after_expression(prefix) - if context == SQLContext.WINDOW_OVER: return self._build_window_over(prefix) - - if context == SQLContext.LIMIT_OFFSET_CLAUSE: - return [] - if context == SQLContext.AFTER_LIMIT_NUMBER: return self._build_after_limit_number(prefix) + return [] - # INSERT contexts + def _build_insert_context( + self, + context: SQLContext, + scope: QueryScope, + prefix: str, + statement: str, + ) -> list[CompletionItem]: if context == SQLContext.INSERT_INTO: return self._build_insert_into(prefix) if context == SQLContext.INSERT_COLUMNS: @@ -227,14 +255,17 @@ def build( return self._build_insert_values(prefix) if context == SQLContext.INSERT_VALUE_EXPRESSIONS: return self._build_insert_value_expressions(prefix) - if context == SQLContext.INSERT_COMPLETE: - return [] if context == SQLContext.INSERT_POST_VALUES: return self._build_insert_post_values(prefix) - if context == SQLContext.INSERT_STRING_LITERAL: - return [] + return [] - # UPDATE contexts + def _build_update_context( + self, + context: SQLContext, + scope: QueryScope, + prefix: str, + statement: str, + ) -> list[CompletionItem]: if context == SQLContext.UPDATE_TABLE: return self._build_update_table(prefix) if context == SQLContext.UPDATE_SET_CLAUSE: @@ -251,10 +282,15 @@ def build( return self._build_update_where_operators(prefix) if context == SQLContext.UPDATE_JOIN_ON: return self._build_update_join_on(scope, prefix) - if context in (SQLContext.UPDATE_STRING_LITERAL, SQLContext.UPDATE_WHERE_STRING_LITERAL): - return [] + return [] - # DELETE contexts + def _build_delete_context( + self, + context: SQLContext, + scope: QueryScope, + prefix: str, + statement: str, + ) -> list[CompletionItem]: if context == SQLContext.DELETE_FROM: return self._build_delete_from(prefix) if context == SQLContext.DELETE_WHERE_CLAUSE: @@ -271,12 +307,56 @@ def build( return self._build_delete_using(scope, prefix, statement) if context == SQLContext.DELETE_SUBQUERY: return self._build_delete_subquery(prefix) - if context == SQLContext.DELETE_WHERE_STRING_LITERAL: + return [] + + def _build_from_context( + self, + scope: QueryScope, + prefix: str, + statement: str, + ) -> list[CompletionItem]: + statement_upper = statement.upper() + + if re.search(r"\bAS\s+$", statement_upper): return [] - if context == SQLContext.WHERE_STRING_LITERAL: + + if prefix and re.search(r"\bAS\s+\w+$", statement_upper): return [] - return self._build_keywords(prefix) + if ( + prefix + and scope.from_tables + and "," not in statement + and self._is_after_completed_from_table_with_prefix( + statement, prefix, scope + ) + ): + return self._build_from_followup_keywords(prefix, scope) + + if not prefix and scope.from_tables: + if "," in statement: + in_scope_table_names = {ref.name.lower() for ref in scope.from_tables} + try: + tables = [ + CompletionItem(name=table.name, item_type=CompletionItemType.TABLE) + for table in self._database.tables + if table.name.lower() not in in_scope_table_names + ] + return sorted(tables, key=lambda x: self._table_name_sort_key(x.name)) + except (AttributeError, TypeError): + return [] + return self._build_from_followup_keywords(prefix, scope) + + return self._build_from_clause(prefix, statement, scope) + + @staticmethod + def _normalize_identifier(identifier: str) -> str: + normalized = identifier.strip() + if not normalized: + return normalized + if normalized[0] in {'`', '"', '['} and normalized[-1] in {'`', '"', ']'}: + normalized = normalized[1:-1] + return normalized def _build_empty(self, prefix: str) -> list[CompletionItem]: keywords = [ @@ -293,95 +373,25 @@ def _build_empty(self, prefix: str) -> list[CompletionItem]: def _build_dot_completion( self, scope: QueryScope, prefix: str, statement: str ) -> list[CompletionItem]: - import re - + # Moved to DotCompletionHandler - this method now delegates to the centralized handler + handler = DotCompletionHandler(self._database, scope) cursor_pos = len(statement) - text_before_cursor = statement[:cursor_pos] - - dot_match = re.search( - r"([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)?$", - text_before_cursor, - ) - if not dot_match: + if prefix and statement.endswith(prefix): + cursor_pos -= len(prefix) + items, _resolved_prefix = handler.get_completions(statement, cursor_pos) + if items is None: return [] - - table_alias = dot_match.group(1) - column_prefix = dot_match.group(2) if dot_match.group(2) else "" - - resolved_table = self._resolve_table_alias(table_alias, scope, statement) - if not resolved_table: - return [] - - table_name = resolved_table.name - columns = resolved_table.columns - - try: - ordered_columns = self._order_columns_for_dot_completion(columns) - except (AttributeError, TypeError): - ordered_columns = [] - - column_items = [] - for col in ordered_columns: - col_name = col.name - if not column_prefix or col_name.upper().startswith(column_prefix.upper()): - column_items.append( - CompletionItem(name=col_name, item_type=CompletionItemType.COLUMN) - ) - - return column_items - - @staticmethod - def _order_columns_for_dot_completion(columns: object) -> list[object]: - columns_list = list(columns) - - def key(item_with_index: tuple[int, object]) -> tuple[int, int]: - idx, col = item_with_index - raw_id = getattr(col, "id", None) - if isinstance(raw_id, int): - return (0, raw_id) - if isinstance(raw_id, str) and raw_id.isdigit(): - return (0, int(raw_id)) - return (1, idx) - - return [col for _, col in sorted(enumerate(columns_list), key=key)] - - def _resolve_table_alias(self, table_alias: str, scope: QueryScope, statement: str): - for ref in scope.from_tables: - if ref.alias and ref.alias.lower() == table_alias.lower(): - if ref.table is not None: - return ref.table - return self._get_table_by_name(ref.name) - if ref.name.lower() == table_alias.lower(): - if ref.table is not None: - return ref.table - return self._get_table_by_name(ref.name) - - for ref in scope.join_tables: - if ref.alias and ref.alias.lower() == table_alias.lower(): - if ref.table is not None: - return ref.table - return self._get_table_by_name(ref.name) - if ref.name.lower() == table_alias.lower(): - if ref.table is not None: - return ref.table - return self._get_table_by_name(ref.name) - - for ref in scope.cte_tables: - if ref.name.lower() == table_alias.lower() and ref.table is not None: - return ref.table - - if ( - scope.current_table - and scope.current_table.name.lower() == table_alias.lower() - ): - return scope.current_table - - return self._get_table_by_name(table_alias) + if prefix: + prefix_lower = prefix.lower() + items = [item for item in items if item.name.lower().startswith(prefix_lower)] + return items def _get_table_by_name(self, table_name: str): + raw_name = table_name.split(".")[-1] + normalized_name = self._normalize_identifier(raw_name).lower() try: for table in self._database.tables: - if table.name.lower() == table_name.lower(): + if table.name.lower() == normalized_name: return table except (AttributeError, TypeError): pass @@ -823,8 +833,6 @@ def _build_from_clause( def _is_after_completed_from_table_with_prefix( statement: str, prefix: str, scope: QueryScope ) -> bool: - import re - if not scope.from_tables: return False @@ -837,8 +845,12 @@ def _is_after_completed_from_table_with_prefix( return False prefix_lower = prefix.lower() - matches_completed_table = prefix_lower == last_ref.name.lower() or ( - bool(last_ref.alias) and prefix_lower == last_ref.alias.lower() + last_ref_name = SuggestionBuilder._normalize_identifier(last_ref.name).lower() + last_ref_base_name = last_ref_name.split(".")[-1] + matches_completed_table = ( + prefix_lower == last_ref_name + or prefix_lower == last_ref_base_name + or (bool(last_ref.alias) and prefix_lower == last_ref.alias.lower()) ) if matches_completed_table: @@ -846,7 +858,9 @@ def _is_after_completed_from_table_with_prefix( return bool( re.search( - r"\bFROM\s+[A-Za-z_][A-Za-z0-9_]*(?:\s+(?:AS\s+)?[A-Za-z_][A-Za-z0-9_]*)?\s+[A-Za-z_][A-Za-z0-9_]*$", + r"\bFROM\s+" + + SuggestionBuilder._table_name_pattern + + r"(?:\s+(?:AS\s+)?[A-Za-z_][A-Za-z0-9_]*)?\s+[A-Za-z_][A-Za-z0-9_]*$", statement_trimmed, re.IGNORECASE, ) @@ -979,14 +993,21 @@ def _is_after_completed_join_table_with_prefix( prefix_lower = prefix.lower() - if prefix_lower == last_ref.name.lower() or ( - bool(last_ref.alias) and prefix_lower == last_ref.alias.lower() + last_ref_name = SuggestionBuilder._normalize_identifier(last_ref.name).lower() + last_ref_base_name = last_ref_name.split(".")[-1] + + if ( + prefix_lower == last_ref_name + or prefix_lower == last_ref_base_name + or (bool(last_ref.alias) and prefix_lower == last_ref.alias.lower()) ): return True return bool( re.search( - r"\bJOIN\s+[A-Za-z_][A-Za-z0-9_]*(?:\s+(?:AS\s+)?[A-Za-z_][A-Za-z0-9_]*)?\s+[A-Za-z_][A-Za-z0-9_]*$", + r"\bJOIN\s+" + + SuggestionBuilder._table_name_pattern + + r"(?:\s+(?:AS\s+)?[A-Za-z_][A-Za-z0-9_]*)?\s+[A-Za-z_][A-Za-z0-9_]*$", statement_trimmed, re.IGNORECASE, ) @@ -1346,7 +1367,7 @@ def _build_single_table_where_columns( if not table: return [] - if reference.alias or prefer_qualified: + if reference.alias or prefer_qualified or "." in reference.name: return self._build_qualified_columns_for_reference(reference, prefix) if not prefix: @@ -2674,19 +2695,31 @@ def _build_all_tables( return [] def _find_insert_target_table(self, statement: str) -> Optional[SQLTable]: - match = re.search(r"\bINSERT\s+INTO\s+(\w+)", statement, re.IGNORECASE) + match = re.search( + r"\bINSERT\s+INTO\s+(" + self._table_name_pattern + r")", + statement, + re.IGNORECASE, + ) if match: return self._get_table_by_name(match.group(1)) return None def _find_update_target_table(self, statement: str) -> Optional[SQLTable]: - match = re.search(r"\bUPDATE\s+(\w+)", statement, re.IGNORECASE) + match = re.search( + r"\bUPDATE\s+(" + self._table_name_pattern + r")", + statement, + re.IGNORECASE, + ) if match: return self._get_table_by_name(match.group(1)) return None def _find_delete_target_table(self, statement: str) -> Optional[SQLTable]: - match = re.search(r"\bFROM\s+(\w+)", statement, re.IGNORECASE) + match = re.search( + r"\bFROM\s+(" + self._table_name_pattern + r")", + statement, + re.IGNORECASE, + ) if match: return self._get_table_by_name(match.group(1)) return None diff --git a/windows/main/controller.py b/windows/main/controller.py index 05ea299..ee75069 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -346,7 +346,7 @@ def _build_query_page(self) -> tuple[wx.Panel, wx.stc.StyledTextCtrl, wx.Window, return panel_query, editor, results_notebook, toolbar, tool_ids def _get_active_query_controller(self) -> Optional[QueryResultsController]: - page = self.MainFrameNotebook.GetCurrentPage() + page = self.notebook_query_editor.GetCurrentPage() if page is None: return None @@ -434,7 +434,7 @@ def _update_query_page_title(self, page: wx.Panel) -> None: if meta is None: return - page_index = self.MainFrameNotebook.FindPage(page) + page_index = self.notebook_query_editor.FindPage(page) if page_index < 0: return @@ -442,39 +442,51 @@ def _update_query_page_title(self, page: wx.Panel) -> None: if meta["is_dirty"]: title = f"{title} *" - self.MainFrameNotebook.SetPageText(page_index, title) + self.notebook_query_editor.SetPageText(page_index, title) + + def _build_query_editor_panel(self) -> tuple[wx.Panel, wx.stc.StyledTextCtrl]: + panel = wx.Panel(self.notebook_query_editor, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL) + sizer = wx.BoxSizer(wx.VERTICAL) + editor = self._build_query_editor(panel) + sizer.Add(editor, 1, wx.EXPAND | wx.ALL, 5) + panel.SetSizer(sizer) + return panel, editor + + def _on_notebook_query_tab_changed(self, event: wx.BookCtrlEvent) -> None: + controller = self._get_active_query_controller() + if controller is not None: + self.controller_query_records = controller + event.Skip() def _setup_query_pages(self) -> None: - template_index = self.MainFrameNotebook.FindPage(self.QueryPanelTpl) - if template_index >= 0: - self.MainFrameNotebook.DeletePage(template_index) + shared_tool_ids = { + "new": self.new_query.GetId(), + "close": self.close_query.GetId(), + "execute": self.execute_statement.GetId(), + "execute_all": self.execute_all_statements.GetId(), + "stop": self.stop_statements.GetId(), + "save": self.save.GetId(), + } - query_index = self.MainFrameNotebook.FindPage(self.panel_query) - self.MainFrameNotebook.SetPageText(query_index, _("Query (1)")) + self.notebook_query_editor.SetPageText(0, _("Query (1)")) self._register_query_page( - panel=self.panel_query, + panel=self.m_panel63, editor=self.sql_query_editor, - results_notebook=self.notebook_sql_results, + results_notebook=self.notebook_query_results, toolbar=self.m_toolBar2, - tool_ids={ - "new": self.new_query.GetId(), - "close": self.close_query.GetId(), - "execute": self.execute_statement.GetId(), - "execute_all": self.execute_all_statements.GetId(), - "stop": self.stop_statements.GetId(), - "save": self.save.GetId(), - }, + tool_ids=shared_tool_ids, display_name=_("Query (1)"), ) - self._apply_query_toolbar_shortcuts(self.m_toolBar2, self._query_page_meta[self.panel_query]["tool_ids"]) + self._apply_query_toolbar_shortcuts(self.m_toolBar2, shared_tool_ids) - self.controller_query_records = self._query_page_meta[self.panel_query]["controller"] + self.controller_query_records = self._query_page_meta[self.m_panel63]["controller"] + self.notebook_query_editor.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._on_notebook_query_tab_changed) self._update_query_close_tools_state() def _update_query_close_tools_state(self) -> None: - can_close = len(self._query_pages) > 1 + can_close = self.notebook_query_editor.GetPageCount() > 1 for meta in self._query_page_meta.values(): toolbar = meta["toolbar"] close_query_tool_id = meta["tool_ids"]["close"] @@ -484,15 +496,24 @@ 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, results_notebook, toolbar, tool_ids = self._build_query_page() - self.MainFrameNotebook.AddPage(panel, label, select=True) + panel, editor = self._build_query_editor_panel() + self.notebook_query_editor.AddPage(panel, label, select=True) + + shared_tool_ids = { + "new": self.new_query.GetId(), + "close": self.close_query.GetId(), + "execute": self.execute_statement.GetId(), + "execute_all": self.execute_all_statements.GetId(), + "stop": self.stop_statements.GetId(), + "save": self.save.GetId(), + } self._register_query_page( panel=panel, editor=editor, - results_notebook=results_notebook, - toolbar=toolbar, - tool_ids=tool_ids, + results_notebook=self.notebook_query_results, + toolbar=self.m_toolBar2, + tool_ids=shared_tool_ids, display_name=label, ) @@ -520,10 +541,10 @@ def _confirm_close_query_page(self, page: wx.Panel) -> bool: return False def _close_active_query_page(self) -> None: - if len(self._query_pages) <= 1: + if self.notebook_query_editor.GetPageCount() <= 1: return - page = self.MainFrameNotebook.GetCurrentPage() + page = self.notebook_query_editor.GetCurrentPage() if page is None or page not in self._query_page_meta: return @@ -536,9 +557,9 @@ def _close_active_query_page(self) -> None: controller = meta["controller"] controller.cancel_execution(wx.CommandEvent()) - query_page_index = self.MainFrameNotebook.FindPage(page) + query_page_index = self.notebook_query_editor.FindPage(page) if query_page_index >= 0: - self.MainFrameNotebook.DeletePage(query_page_index) + self.notebook_query_editor.DeletePage(query_page_index) self._update_query_close_tools_state() @@ -657,6 +678,12 @@ def _setup_subscribers(self): AUTO_APPLY.subscribe(self._on_auto_apply) + # Initialize record toolbar states + self._initialize_record_toolbar_states() + + # Initialize column toolbar states + self._initialize_column_toolbar_states() + def _write_query_log(self, text: str): self.sql_query_logs.AppendText(f"{text}\n") self.sql_query_logs.GotoLine(self.sql_query_logs.GetLineCount() - 1) @@ -1437,17 +1464,17 @@ def on_clone_table(self, event): def _on_current_column(self, column: SQLColumn): selected = self.controller_list_table_columns.list_ctrl_table_columns.GetSelection() if not selected.IsOk(): - self.btn_delete_column.Enable(False) - self.btn_move_up_column.Enable(False) - self.btn_move_down_column.Enable(False) + self.toolbar_columns.EnableTool(self.tool_remove_column.GetId(), False) + self.toolbar_columns.EnableTool(self.tool_move_up_column.GetId(), False) + self.toolbar_columns.EnableTool(self.tool_move_down_column.GetId(), False) return row = self.controller_list_table_columns.model.GetRow(selected) total_rows = len(self.controller_list_table_columns.model.data) - 1 - self.btn_delete_column.Enable(column is not None) - self.btn_move_up_column.Enable(column is not None and row > 0) - self.btn_move_down_column.Enable(column is not None and row < total_rows) + self.toolbar_columns.EnableTool(self.tool_remove_column.GetId(), column is not None) + self.toolbar_columns.EnableTool(self.tool_move_up_column.GetId(), column is not None and row > 0) + self.toolbar_columns.EnableTool(self.tool_move_down_column.GetId(), column is not None and row < total_rows) def on_insert_column(self, event: wx.Event): self.controller_list_table_columns.on_column_insert(event) @@ -1486,8 +1513,29 @@ def on_clear_foreign_key(self, event: wx.Event): # RECORDS def _on_auto_apply(self, value: bool): - self.btn_cancel_record.Enable(not self.chb_auto_apply.GetValue()) - self.btn_apply_record.Enable(not self.chb_auto_apply.GetValue()) + auto_apply_enabled = self.chb_auto_apply.GetValue() + + # Enable/disable apply and cancel tools based on auto-apply state + self.m_toolBar3.EnableTool(self.tool_apply_record.GetId(), not auto_apply_enabled) + self.m_toolBar3.EnableTool(self.tool_cancel_record.GetId(), not auto_apply_enabled) + + def _initialize_record_toolbar_states(self): + """Initialize toolbar states to ensure proper default behavior.""" + # Initially disable duplicate and delete tools (no selection) + self.m_toolBar3.EnableTool(self.tool_duplicate_record.GetId(), False) + self.m_toolBar3.EnableTool(self.tool_delete_record.GetId(), False) + + # Set apply/cancel tools based on auto-apply checkbox state + auto_apply_enabled = self.chb_auto_apply.GetValue() + self.m_toolBar3.EnableTool(self.tool_apply_record.GetId(), not auto_apply_enabled) + self.m_toolBar3.EnableTool(self.tool_cancel_record.GetId(), not auto_apply_enabled) + + def _initialize_column_toolbar_states(self): + """Initialize column toolbar states to ensure proper default behavior.""" + # Initially disable all column tools (no selection) + self.toolbar_columns.EnableTool(self.tool_remove_column.GetId(), False) + self.toolbar_columns.EnableTool(self.tool_move_up_column.GetId(), False) + self.toolbar_columns.EnableTool(self.tool_move_down_column.GetId(), False) def on_auto_apply(self, event): AUTO_APPLY.set_value(self.chb_auto_apply.GetValue()) @@ -1497,12 +1545,16 @@ def on_collapsible_pane_changed(self, event): event.Skip() def _on_current_records(self, records: list[SQLRecord]): - self.btn_duplicate_record.Enable(len(records) == 1) - self.btn_delete_record.Enable(len(records) > 0) + # Enable/disable duplicate and delete tools based on record selection + self.m_toolBar3.EnableTool(self.tool_duplicate_record.GetId(), len(records) == 1) + self.m_toolBar3.EnableTool(self.tool_delete_record.GetId(), len(records) > 0) def on_insert_record(self, event): self.controller_list_table_records.do_insert_record() + def on_refresh_records(self, event): + self.controller_list_table_records.do_refresh_records() + def on_duplicate_record(self, event): self.controller_list_table_records.do_duplicate_record() @@ -1561,14 +1613,14 @@ def on_close_query(self, event): self._close_active_query_page() def on_save(self, event): - page = self.MainFrameNotebook.GetCurrentPage() + page = self.notebook_query_editor.GetCurrentPage() if page is None: return self._save_query_page(page, force_save_as=False) def on_save_as_query(self, event): - page = self.MainFrameNotebook.GetCurrentPage() + page = self.notebook_query_editor.GetCurrentPage() if page is None: return From a6857541240f950a3bd659e6faed30cf279e21da Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 23 Mar 2026 10:14:46 +0100 Subject: [PATCH 29/93] feat(ui): add table execution flow and update related views AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- tests/ui/test_dataview.py | 6 +- windows/main/query.py | 17 ++ windows/main/table/column.py | 4 +- windows/main/table/executor.py | 240 +++++++++++++++++++++ windows/main/table/records.py | 110 +++++++++- windows/views.py | 369 ++++++++++++++++++++------------- 6 files changed, 593 insertions(+), 153 deletions(-) create mode 100644 windows/main/query.py create mode 100644 windows/main/table/executor.py diff --git a/tests/ui/test_dataview.py b/tests/ui/test_dataview.py index 5326794..8aadca5 100644 --- a/tests/ui/test_dataview.py +++ b/tests/ui/test_dataview.py @@ -1,7 +1,7 @@ import pytest from dataclasses import dataclass -from helpers.dataview import ColumnField, AbstractBaseDataModel +from helpers.dataview import ColumnField, BaseDataModel @dataclass @@ -60,14 +60,14 @@ def test_has_value_false(self): assert field.has_value(item) is False -class ConcreteDataModel(AbstractBaseDataModel): +class ConcreteDataModel(BaseDataModel): """Concrete implementation for testing.""" def set_observable(self, observable): self._observable = observable -class TestAbstractBaseDataModel: +class TestBaseDataModel: """Tests for AbstractBaseDataModel.""" def test_load(self): diff --git a/windows/main/query.py b/windows/main/query.py new file mode 100644 index 0000000..0d06e58 --- /dev/null +++ b/windows/main/query.py @@ -0,0 +1,17 @@ +import wx + + +class QueryMixin: + def on_new_query(self, event): + new_panel = wx.Panel(self.notebook_query_editor) + sizer = wx.BoxSizer(wx.VERTICAL) + stc = wx.stc.StyledTextCtrl(new_panel, style=0) + sizer.Add(stc, 1, wx.EXPAND) + new_panel.SetSizer(sizer) + n = self.notebook_query_editor.GetPageCount() + 1 + self.notebook_query_editor.AddPage(new_panel, f"Query #{n}", select=True) + + def on_close_query(self, event): + n = self.notebook_query_editor.GetSelection() + if n != wx.NOT_FOUND: + self.notebook_query_editor.DeletePage(n) \ No newline at end of file diff --git a/windows/main/table/column.py b/windows/main/table/column.py index dec0d29..20671c0 100644 --- a/windows/main/table/column.py +++ b/windows/main/table/column.py @@ -247,8 +247,8 @@ def on_column_insert(self, event: wx.Event): default_values['set'] = datatype.default_set new_empty_column = session.context.build_empty_column( - table=table, - datatype=datatype, + table, + datatype, **default_values ) diff --git a/windows/main/table/executor.py b/windows/main/table/executor.py new file mode 100644 index 0000000..1fdd2be --- /dev/null +++ b/windows/main/table/executor.py @@ -0,0 +1,240 @@ +import contextlib +import dataclasses +import threading +import time + +from typing import Any, Callable, Optional + +import wx + +from helpers.loader import Loader +from helpers.logger import logger + +from structures.session import Session +from structures.connection import Connection, ConnectionEngine +from structures.engines.database import SQLTable, SQLRecord + +from windows.main.query.executor import QueryExecutor + + +@dataclasses.dataclass +class RecordsOperationResult: + operation: str + success: bool + records: Optional[list[SQLRecord]] = None + affected_records: Optional[int] = None + elapsed_ms: float = 0.0 + error: Optional[str] = None + cancelled: bool = False + warnings: list[str] = dataclasses.field(default_factory=list) + + +@dataclasses.dataclass +class RecordsOperationSummary: + total_operations: int = 0 + completed_operations: int = 0 + successful_operations: int = 0 + failed_operations: int = 0 + elapsed_ms: float = 0.0 + cancelled: bool = False + last_operation: Optional[str] = None + + +class RecordsExecutor: + def __init__(self, session: Session): + self.session = session + self._cancel_requested = False + self._current_thread: Optional[threading.Thread] = None + self._worker_context: Optional[Any] = None + self._loader_context: Optional[Any] = None + self._lock = threading.Lock() + + def load_records( + self, + table: SQLTable, + on_complete: Callable[[RecordsOperationResult], None], + filters: Optional[str] = None, + limit: int = 1000, + offset: int = 0, + orders: Optional[str] = None + ) -> None: + self._execute_operation( + operation="load_records", + on_complete=on_complete, + table=table, + filters=filters, + limit=limit, + offset=offset, + orders=orders + ) + + def _execute_operation(self, **kwargs) -> None: + self._cancel_requested = False + self._loader_context = Loader.cursor_wait() + self._loader_context.__enter__() + + self._current_thread = threading.Thread( + target=self._execute_worker, + args=(kwargs,), + daemon=True + ) + self._current_thread.start() + + def _dispatch_operation_result( + self, + on_complete: Callable[[RecordsOperationResult], None], + result: RecordsOperationResult, + ) -> None: + ui_done_event = threading.Event() + + def _on_ui_thread() -> None: + try: + on_complete(result) + finally: + ui_done_event.set() + + wx.CallAfter(_on_ui_thread) + while not ui_done_event.wait(0.05): + continue + + def _execute_worker(self, operation_kwargs: dict) -> None: + time_start = time.perf_counter() + operation = operation_kwargs.get("operation", "unknown") + on_complete = operation_kwargs.get("on_complete") + + try: + context = self._create_worker_context() + self._set_worker_context(context) + + result = self._execute_single_operation(context, operation_kwargs) + + self._dispatch_operation_result(on_complete, result) + + except Exception as ex: + logger.error(f"Records executor error: {ex}", exc_info=True) + error_result = RecordsOperationResult( + operation=operation, + success=False, + error=str(ex), + elapsed_ms=(time.perf_counter() - time_start) * 1000 + ) + self._dispatch_operation_result(on_complete, error_result) + finally: + self._clear_worker_context() + wx.CallAfter(self._stop_loader) + + def _execute_single_operation(self, context: Any, operation_kwargs: dict) -> RecordsOperationResult: + start_time = time.time() + operation = operation_kwargs.get("operation", "unknown") + + try: + if operation == "load_records": + return self._load_records_operation(context, operation_kwargs) + else: + raise ValueError(f"Unknown operation: {operation}") + + except Exception as ex: + elapsed_ms = (time.time() - start_time) * 1000 + is_cancelled = self._cancel_requested + + return RecordsOperationResult( + operation=operation, + success=False, + error=str(ex), + cancelled=is_cancelled, + elapsed_ms=elapsed_ms + ) + + def _load_records_operation(self, context: Any, operation_kwargs: dict) -> RecordsOperationResult: + start_time = time.time() + table = operation_kwargs.get("table") + filters = operation_kwargs.get("filters") + limit = operation_kwargs.get("limit", 1000) + offset = operation_kwargs.get("offset", 0) + orders = operation_kwargs.get("orders") + + records = context.get_records( + table=table, + filters=filters, + limit=limit, + offset=offset, + orders=orders + ) + + elapsed_ms = (time.time() - start_time) * 1000 + + return RecordsOperationResult( + operation="load_records", + success=True, + records=records, + affected_records=len(records), + elapsed_ms=elapsed_ms + ) + + + def _build_worker_connection(self) -> Connection: + """Build a worker connection similar to QueryExecutor.""" + connection = self.session.connection.copy() + + if not connection.has_enabled_tunnel(): + return connection + + context = getattr(self.session, "context", None) + configuration = getattr(connection, "configuration", None) + + if context is not None and configuration is not None and hasattr(configuration, "_replace"): + replace_kwargs = {} + + if hasattr(configuration, "hostname") and getattr(context, "host", None): + replace_kwargs["hostname"] = context.host + + if hasattr(configuration, "port") and getattr(context, "port", None) is not None: + replace_kwargs["port"] = int(context.port) + + if replace_kwargs: + connection.configuration = configuration._replace(**replace_kwargs) + + connection.ssh_tunnel = None + return connection + + def _create_worker_context(self) -> Any: + """Create a worker context similar to QueryExecutor.""" + context = self.session._get_context_class()(self._build_worker_connection()) + + # if self.session.engine == ConnectionEngine.POSTGRESQL: + # connect_kwargs = { + # "skip_before_connect": True, + # "skip_after_connect": True, + # } + # context.connect(**connect_kwargs) + # return context + + context.connect(skip_before_connect=True, skip_after_connect=True, database=self.session.database) + return context + + def _set_worker_context(self, context: Any) -> None: + with self._lock: + self._worker_context = context + + def _clear_worker_context(self) -> None: + context = None + + with self._lock: + context = self._worker_context + self._worker_context = None + + if context is not None: + with contextlib.suppress(Exception): + context.disconnect() + + def _stop_loader(self) -> None: + if self._loader_context is not None: + self._loader_context.__exit__(None, None, None) + self._loader_context = None + + def cancel(self) -> None: + self._cancel_requested = True + self._clear_worker_context() + + def is_running(self) -> bool: + return self._current_thread is not None and self._current_thread.is_alive() diff --git a/windows/main/table/records.py b/windows/main/table/records.py index 4f1ac34..7712260 100644 --- a/windows/main/table/records.py +++ b/windows/main/table/records.py @@ -18,6 +18,7 @@ from windows.dialogs.column_content import ColumnContentDialogController from windows.main import CURRENT_TABLE, CURRENT_SESSION, CURRENT_DATABASE, AUTO_APPLY, CURRENT_RECORDS +from windows.main.table.executor import RecordsExecutor, RecordsOperationResult NEW_RECORDS: ObservableList[SQLRecord] = ObservableList() @@ -128,9 +129,12 @@ def __init__(self, list_ctrl_records: TableRecordsDataViewCtrl): CURRENT_SESSION.subscribe(self._load_session) CURRENT_DATABASE.subscribe(self._load_database) CURRENT_TABLE.subscribe(self._load_table) + + self.executor: Optional[RecordsExecutor] = None def _load_session(self, session: Session): self.session = session + self.executor = RecordsExecutor(session) if session else None def _load_database(self, database: SQLDatabase): self.database = database @@ -138,8 +142,40 @@ def _load_database(self, database: SQLDatabase): def _load_table(self, table: SQLTable): if table is not None: self.table = table - self.table.load_records() + self.load_records_async() + + def _on_auto_apply_changed(self, auto_apply_enabled: bool): + """Handle auto-apply setting change and update toolbar states.""" + selected_records = self.get_selected_records() + self._update_toolbar_states(selected_records) + + def load_records_async(self, filters: Optional[str] = None, limit: int = 1000, offset: int = 0, orders: Optional[str] = None): + """Load records asynchronously using RecordsExecutor.""" + if not self.executor or not self.table: + return + + self.executor.load_records( + table=self.table, + on_complete=self._on_records_loaded, + filters=filters, + limit=limit, + offset=offset, + orders=orders + ) + + def _on_records_loaded(self, result: RecordsOperationResult): + """Handle completion of records loading.""" + if result.success and result.records is not None: + self.table.records.set_value(result.records) self.load_model() + else: + logger.error(f"Failed to load records: {result.error}") + # Fallback to synchronous loading + try: + self.table.load_records() + self.load_model() + except Exception as ex: + logger.error(f"Fallback loading also failed: {ex}", exc_info=True) def load_model(self): self.model = RecordsModel(self.table, len(self.table.columns)) @@ -169,11 +205,9 @@ def _on_item_value_changed(self, event: wx.dataview.DataViewEvent): current_record.save() except Exception as ex: logger.error(f"Error saving record: {ex}", exc_info=True) - else: - records = list(self.session.context.get_records(table=self.table)) - - self.table.records.set_value(records) + # Refresh records after successful save + self.load_records_async() else: NEW_RECORDS.append(current_record, replace_existing=True) @@ -181,9 +215,52 @@ def _on_item_value_changed(self, event: wx.dataview.DataViewEvent): def _on_selection_changed(self, event: wx.dataview.DataViewEvent): logger.debug(f"{'#' * 10} ON SELECTION CHANGED {'#' * 10}") - CURRENT_RECORDS.set_value(self.get_selected_records()) + selected_records = self.get_selected_records() + CURRENT_RECORDS.set_value(selected_records) + + # Update toolbar states based on selection + self._update_toolbar_states(selected_records) + event.Skip() + + def _update_toolbar_states(self, selected_records: list): + """Update toolbar tool states based on record selection and auto-apply setting.""" + # This method provides the logic for toolbar state management + # The actual toolbar updates will be handled by the main controller + # through the CURRENT_RECORDS observable subscription + + # Calculate toolbar states + has_selection = len(selected_records) > 0 + has_single_selection = len(selected_records) == 1 + auto_apply_enabled = AUTO_APPLY.get_value() + + # Store states for the main controller to use + self._toolbar_states = { + 'duplicate_enabled': has_single_selection, + 'delete_enabled': has_selection, + 'apply_enabled': not auto_apply_enabled, + 'cancel_enabled': not auto_apply_enabled + } + + def get_selection_state(self): + """Return the current selection state for toolbar management.""" + selected_records = self.get_selected_records() + return { + 'has_selection': len(selected_records) > 0, + 'has_single_selection': len(selected_records) == 1, + 'auto_apply_enabled': AUTO_APPLY.get_value() + } + + def get_toolbar_states(self): + """Return the current toolbar states for the main controller.""" + return getattr(self, '_toolbar_states', { + 'duplicate_enabled': False, + 'delete_enabled': False, + 'apply_enabled': not AUTO_APPLY.get_value(), + 'cancel_enabled': not AUTO_APPLY.get_value() + }) + def make_advanced_dialog(self, parent, value: str): dialog = ColumnContentDialogController(parent, value) @@ -240,6 +317,11 @@ def _do_new_empty_record(self, index: int, copy_from_selected: bool = False, use self._do_edit(new_empty_item, 1) + def do_refresh_records(self): + """Refresh records from database.""" + if self.table: + self.load_records_async() + def do_insert_record(self): session = CURRENT_SESSION.get_value() table = CURRENT_TABLE.get_value() @@ -274,9 +356,19 @@ def do_delete_record(self): records = CURRENT_RECORDS.get_value() - SQLRecord.delete_many(table, records) - - CURRENT_RECORDS.set_value([]) + if records: + try: + SQLRecord.delete_many(table, records) + CURRENT_RECORDS.set_value([]) + # Refresh records after successful deletion + self.load_records_async() + except Exception as ex: + logger.error(f"Error deleting records: {ex}", exc_info=True) + wx.MessageBox( + f"Failed to delete records: {ex}", + "Error", + wx.OK | wx.ICON_ERROR + ) # def update_record(self, row, record): # if row < 0 or row >= len(self.list_ctrl_records.GetModel().records): diff --git a/windows/views.py b/windows/views.py index 7f64e1b..acf2939 100755 --- a/windows/views.py +++ b/windows/views.py @@ -1476,7 +1476,7 @@ def __init__( self, parent ): self.PanelTableBase.SetSizer( bSizer262 ) self.PanelTableBase.Layout() bSizer262.Fit( self.PanelTableBase ) - self.m_notebook3.AddPage( self.PanelTableBase, _(u"Base"), False ) + 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 ) @@ -1558,7 +1558,7 @@ def __init__( self, parent ): self.PanelTableOptions.SetSizer( bSizer261 ) self.PanelTableOptions.Layout() bSizer261.Fit( self.PanelTableOptions ) - self.m_notebook3.AddPage( self.PanelTableOptions, _(u"Options"), True ) + 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 ) @@ -1746,47 +1746,24 @@ def __init__( self, parent ): bSizer54 = wx.BoxSizer( wx.VERTICAL ) - bSizer53 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText39 = wx.StaticText( self.panel_table_columns, wx.ID_ANY, _(u"Columns:"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.toolbar_columns = wx.ToolBar( self.panel_table_columns, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL|wx.TB_HORZ_TEXT ) + self.m_staticText39 = wx.StaticText( self.toolbar_columns, wx.ID_ANY, _(u"Columns:"), wx.DefaultPosition, wx.DefaultSize, 0 ) self.m_staticText39.Wrap( -1 ) - bSizer53.Add( self.m_staticText39, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) + self.toolbar_columns.AddControl( self.m_staticText39 ) + self.tool_add_column = self.toolbar_columns.AddTool( wx.ID_ANY, _(u"Add"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + self.tool_remove_column = self.toolbar_columns.AddTool( wx.ID_ANY, _(u"Remove"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - bSizer53.Add( ( 100, 0), 0, wx.EXPAND, 5 ) + self.toolbar_columns.AddSeparator() - self.btn_insert_column = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Insert"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.tool_move_up_column = self.toolbar_columns.AddTool( wx.ID_ANY, _(u"Move Up"), wx.Bitmap( u"icons/16x16/arrow_up.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - 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.panel_table_columns, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_delete_column.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_column.Enable( False ) + self.tool_move_down_column = self.toolbar_columns.AddTool( wx.ID_ANY, _(u"Move Down"), wx.Bitmap( u"icons/16x16/arrow_down.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) - bSizer53.Add( self.btn_delete_column, 0, wx.LEFT|wx.RIGHT, 2 ) + self.toolbar_columns.Realize() - self.btn_move_up_column = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Up"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_move_up_column.SetBitmap( wx.Bitmap( u"icons/16x16/arrow_up.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_move_up_column.Enable( False ) - - bSizer53.Add( self.btn_move_up_column, 0, wx.LEFT|wx.RIGHT, 2 ) - - self.btn_move_down_column = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Down"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_move_down_column.SetBitmap( wx.Bitmap( u"icons/16x16/arrow_down.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_move_down_column.Enable( False ) - - bSizer53.Add( self.btn_move_down_column, 0, wx.LEFT|wx.RIGHT, 2 ) - - - bSizer53.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - - bSizer54.Add( bSizer53, 0, wx.ALL|wx.EXPAND, 5 ) + bSizer54.Add( self.toolbar_columns, 0, wx.EXPAND, 5 ) self.list_ctrl_table_columns = TableColumnsDataViewCtrl( self.panel_table_columns, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) bSizer54.Add( self.list_ctrl_table_columns, 1, wx.ALL|wx.EXPAND, 5 ) @@ -2092,6 +2069,33 @@ def __init__( self, parent ): self.panel_records = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer61 = wx.BoxSizer( wx.VERTICAL ) + self.m_toolBar3 = wx.ToolBar( self.panel_records, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL|wx.TB_HORZ_TEXT ) + self.tool_refresh_records = self.m_toolBar3.AddTool( wx.ID_ANY, _(u"Refrsh"), wx.Bitmap( u"icons/16x16/arrow_refresh.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + + self.m_toolBar3.AddSeparator() + + 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 ) @@ -2129,56 +2133,8 @@ def __init__( self, parent ): bSizer61.Add( bSizer94, 0, wx.EXPAND, 5 ) - bSizer83 = wx.BoxSizer( wx.HORIZONTAL ) - - self.btn_insert_record = wx.Button( self.panel_records, 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 ) ) - bSizer83.Add( self.btn_insert_record, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.btn_duplicate_record = wx.Button( self.panel_records, 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 ) - - bSizer83.Add( self.btn_duplicate_record, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.btn_delete_record = wx.Button( self.panel_records, 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 ) - - bSizer83.Add( self.btn_delete_record, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.m_staticline3 = wx.StaticLine( self.panel_records, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_VERTICAL ) - bSizer83.Add( self.m_staticline3, 0, wx.EXPAND | wx.ALL, 5 ) - - self.chb_auto_apply = wx.CheckBox( self.panel_records, wx.ID_ANY, _(u"Apply changes automatically"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.chb_auto_apply.SetValue(True) - self.chb_auto_apply.SetToolTip( _(u"If enabled, table edits are applied immediately without pressing Apply or Cancel") ) - self.chb_auto_apply.SetHelpText( _(u"If enabled, table edits are applied immediately without pressing Apply or Cancel") ) - - bSizer83.Add( self.chb_auto_apply, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.btn_cancel_record = wx.Button( self.panel_records, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_cancel_record.SetBitmap( wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_cancel_record.Enable( False ) - - bSizer83.Add( self.btn_cancel_record, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.btn_apply_record = wx.Button( self.panel_records, 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 ) - - bSizer83.Add( self.btn_apply_record, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - - bSizer61.Add( bSizer83, 0, wx.EXPAND, 5 ) - self.m_collapsiblePane1 = wx.CollapsiblePane( self.panel_records, wx.ID_ANY, _(u"Filters"), wx.DefaultPosition, wx.DefaultSize, wx.CP_DEFAULT_STYLE|wx.CP_NO_TLW_RESIZE|wx.FULL_REPAINT_ON_RESIZE ) - self.m_collapsiblePane1.Collapse( False ) + self.m_collapsiblePane1.Collapse( True ) bSizer831 = wx.BoxSizer( wx.VERTICAL ) @@ -2245,7 +2201,7 @@ def __init__( self, parent ): self.panel_records.Bind( wx.EVT_RIGHT_DOWN, self.panel_recordsOnContextMenu ) - self.MainFrameNotebook.AddPage( self.panel_records, _(u"Data"), False ) + self.MainFrameNotebook.AddPage( self.panel_records, _(u"Data"), True ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/text_columns.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2261,28 +2217,32 @@ def __init__( self, parent ): self.m_panel52 = wx.Panel( self.m_splitter6, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer125 = wx.BoxSizer( wx.VERTICAL ) - self.m_toolBar2 = wx.ToolBar( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL ) - self.new_query = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"New query"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"New query"), wx.EmptyString, None ) + self.m_toolBar2 = wx.ToolBar( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL|wx.TB_HORZ_TEXT ) + self.new_query = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Add"), wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"New query"), wx.EmptyString, None ) - self.close_query = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Close query"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Close query"), wx.EmptyString, None ) + self.close_query = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Close"), wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Close query"), wx.EmptyString, None ) self.m_toolBar2.AddSeparator() - self.execute_statement = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Execute"), wx.Bitmap( u"icons/16x16/arrow_right.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Execute"), wx.EmptyString, None ) + self.execute_statement = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Run"), wx.Bitmap( u"icons/16x16/arrow_right.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Execute"), wx.EmptyString, None ) - self.execute_all_statements = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Execute all"), wx.Bitmap( u"icons/16x16/arrows_lefttoright.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Execute all statements"), wx.EmptyString, None ) + self.execute_all_statements = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Run all"), wx.Bitmap( u"icons/16x16/arrows_lefttoright.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Execute all statements"), wx.EmptyString, None ) self.stop_statements = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Stop"), wx.Bitmap( u"icons/16x16/cancel.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, _(u"Stop"), wx.EmptyString, None ) self.m_toolBar2.AddSeparator() - self.save = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"tool"), wx.Bitmap( u"icons/16x16/disk.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + self.save = self.m_toolBar2.AddTool( wx.ID_ANY, _(u"Save"), wx.Bitmap( u"icons/16x16/disk.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) self.m_toolBar2.Realize() bSizer125.Add( self.m_toolBar2, 0, wx.EXPAND, 5 ) - self.sql_query_editor = wx.stc.StyledTextCtrl( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0) + self.notebook_query_editor = wx.Notebook( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_panel63 = wx.Panel( self.notebook_query_editor, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer146 = wx.BoxSizer( wx.VERTICAL ) + + self.sql_query_editor = wx.stc.StyledTextCtrl( self.m_panel63, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0) self.sql_query_editor.SetUseTabs ( True ) self.sql_query_editor.SetTabWidth ( 4 ) self.sql_query_editor.SetIndent ( 4 ) @@ -2318,7 +2278,15 @@ def __init__( self, parent ): self.sql_query_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY ) self.sql_query_editor.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) ) self.sql_query_editor.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) ) - bSizer125.Add( self.sql_query_editor, 1, wx.EXPAND | wx.ALL, 5 ) + bSizer146.Add( self.sql_query_editor, 1, wx.EXPAND | wx.ALL, 5 ) + + + self.m_panel63.SetSizer( bSizer146 ) + self.m_panel63.Layout() + bSizer146.Fit( self.m_panel63 ) + self.notebook_query_editor.AddPage( self.m_panel63, _(u"a page"), False ) + + bSizer125.Add( self.notebook_query_editor, 1, wx.EXPAND | wx.ALL, 5 ) self.m_panel52.SetSizer( bSizer125 ) @@ -2329,9 +2297,9 @@ def __init__( self, parent ): bSizer1261 = wx.BoxSizer( wx.VERTICAL ) - self.notebook_sql_results = FlatNotebook( self.m_panel53, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.notebook_query_results = FlatNotebook( self.m_panel53, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer1261.Add( self.notebook_sql_results, 1, wx.EXPAND | wx.ALL, 5 ) + bSizer1261.Add( self.notebook_query_results, 1, wx.EXPAND | wx.ALL, 5 ) self.m_panel53.SetSizer( bSizer1261 ) @@ -2344,40 +2312,13 @@ def __init__( self, parent ): self.panel_query.SetSizer( bSizer26 ) self.panel_query.Layout() bSizer26.Fit( self.panel_query ) - self.MainFrameNotebook.AddPage( self.panel_query, _(u"Query"), True ) + self.MainFrameNotebook.AddPage( self.panel_query, _(u"Query"), False ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/arrow_right.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex ) MainFrameNotebookIndex += 1 - self.QueryPanelTpl = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) - self.QueryPanelTpl.Hide() - - bSizer263 = wx.BoxSizer( wx.VERTICAL ) - - self.m_textCtrl101 = wx.TextCtrl( self.QueryPanelTpl, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE|wx.TE_RICH|wx.TE_RICH2 ) - bSizer263.Add( self.m_textCtrl101, 1, wx.ALL|wx.EXPAND, 5 ) - - bSizer49 = wx.BoxSizer( wx.HORIZONTAL ) - - - bSizer49.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.m_button17 = wx.Button( self.QueryPanelTpl, wx.ID_ANY, _(u"Close"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer49.Add( self.m_button17, 0, wx.ALL, 5 ) - - self.m_button121 = wx.Button( self.QueryPanelTpl, wx.ID_ANY, _(u"New"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer49.Add( self.m_button121, 0, wx.ALL, 5 ) - - - bSizer263.Add( bSizer49, 0, wx.EXPAND, 5 ) - - - self.QueryPanelTpl.SetSizer( bSizer263 ) - self.QueryPanelTpl.Layout() - bSizer263.Fit( self.QueryPanelTpl ) - self.MainFrameNotebook.AddPage( self.QueryPanelTpl, _(u"Query #2"), False ) bSizer25.Add( self.MainFrameNotebook, 1, wx.ALL|wx.EXPAND, 5 ) @@ -2470,21 +2411,24 @@ def __init__( self, parent ): self.btn_insert_check.Bind( wx.EVT_BUTTON, self.on_insert_foreign_key ) self.btn_delete_check.Bind( wx.EVT_BUTTON, self.on_delete_foreign_key ) self.btn_clear_check.Bind( wx.EVT_BUTTON, self.on_clear_foreign_key ) - self.btn_insert_column.Bind( wx.EVT_BUTTON, self.on_insert_column ) - self.btn_delete_column.Bind( wx.EVT_BUTTON, self.on_delete_column ) - self.btn_move_up_column.Bind( wx.EVT_BUTTON, self.on_move_up_column ) - self.btn_move_down_column.Bind( wx.EVT_BUTTON, self.on_move_down_column ) + self.Bind( wx.EVT_TOOL, self.on_insert_column, id = self.tool_add_column.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_column, id = self.tool_remove_column.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_move_up_column, id = self.tool_move_up_column.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_move_down_column, id = self.tool_move_down_column.GetId() ) self.btn_delete_table.Bind( wx.EVT_BUTTON, self.on_delete_table ) self.btn_cancel_table.Bind( wx.EVT_BUTTON, self.on_cancel_table ) self.btn_apply_table.Bind( wx.EVT_BUTTON, self.do_apply_table ) + self.Bind( wx.EVT_TOOL, self.on_refresh_records, id = self.tool_refresh_records.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_insert_record, id = self.tool_insert_record.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_duplicate_record, id = self.tool_duplicate_record.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_record, id = self.tool_delete_record.GetId() ) + self.chb_auto_apply.Bind( wx.EVT_CHECKBOX, self.on_auto_apply ) + self.Bind( wx.EVT_TOOL, self.on_apply_record, id = self.tool_apply_record.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_cancel_record, id = self.tool_cancel_record.GetId() ) self.btn_first_records.Bind( wx.EVT_BUTTON, self.on_first_records ) self.btn_prev_records.Bind( wx.EVT_BUTTON, self.on_prev_records ) self.btn_next_records.Bind( wx.EVT_BUTTON, self.on_next_records ) self.btn_last_records.Bind( wx.EVT_BUTTON, self.on_last_records ) - self.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.chb_auto_apply.Bind( wx.EVT_CHECKBOX, self.on_auto_apply ) self.m_collapsiblePane1.Bind( wx.EVT_COLLAPSIBLEPANE_CHANGED, self.on_collapsible_pane_changed ) self.m_button41.Bind( wx.EVT_BUTTON, self.on_apply_filters ) self.Bind( wx.EVT_TOOL, self.on_new_query, id = self.new_query.GetId() ) @@ -2575,28 +2519,37 @@ def on_cancel_table( self, event ): def do_apply_table( self, event ): event.Skip() - def on_first_records( self, event ): + def on_refresh_records( self, event ): event.Skip() - def on_prev_records( self, event ): + def on_insert_record( self, event ): event.Skip() - def on_next_records( self, event ): + def on_duplicate_record( self, event ): event.Skip() - def on_last_records( self, event ): + def on_delete_record( self, event ): event.Skip() - def on_insert_record( self, event ): + def on_auto_apply( self, event ): event.Skip() - def on_duplicate_record( self, event ): + def on_apply_record( self, event ): event.Skip() - def on_delete_record( self, event ): + def on_cancel_record( self, event ): event.Skip() - def on_auto_apply( self, event ): + def on_first_records( self, event ): + event.Skip() + + def on_prev_records( self, event ): + event.Skip() + + def on_next_records( self, event ): + event.Skip() + + def on_last_records( self, event ): event.Skip() def on_collapsible_pane_changed( self, event ): @@ -2691,14 +2644,152 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. 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 ) + + + 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 ########################################################################### From 5197794cc2436f54e76b85c18f427601734a844b Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 23 Mar 2026 10:15:55 +0100 Subject: [PATCH 30/93] chore(i18n): refresh translation catalogs AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- locale/de_DE/LC_MESSAGES/petersql.mo | Bin 8756 -> 7894 bytes locale/de_DE/LC_MESSAGES/petersql.po | 798 ++++++++++++++++----------- locale/en_US/LC_MESSAGES/petersql.po | 734 ++++++++++++++---------- locale/es_ES/LC_MESSAGES/petersql.mo | Bin 8746 -> 7897 bytes locale/es_ES/LC_MESSAGES/petersql.po | 798 ++++++++++++++++----------- locale/fr_FR/LC_MESSAGES/petersql.mo | Bin 8539 -> 7621 bytes locale/fr_FR/LC_MESSAGES/petersql.po | 798 ++++++++++++++++----------- locale/it_IT/LC_MESSAGES/petersql.mo | Bin 8639 -> 7957 bytes locale/it_IT/LC_MESSAGES/petersql.po | 798 ++++++++++++++++----------- locale/petersql.pot | 653 ++++++++++++---------- 10 files changed, 2631 insertions(+), 1948 deletions(-) diff --git a/locale/de_DE/LC_MESSAGES/petersql.mo b/locale/de_DE/LC_MESSAGES/petersql.mo index 72d1446b221636195e8d0c59ccda113074da239f..a5c6beb378f983413a045b19681b22c4aee34bc8 100644 GIT binary patch delta 3462 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�J>r>MrnIVOhkAbYK#?@4iL2c0M;4{L7 zenuFPjw$&pQ^Mi7IX(sR3^kRl9BcOIMl|CqFEAKw2b3Nrrp7X(aOH@Km|Em!HsvY@ z65^9vFQqNR-Mo=?$ui5P9iEqaUg4PxnFD2x7-XRt;S-LLC9Tzo@ua+1Ia)+obgdIc zXp<$!wc-tPN{?~$vZVL>MT}xT<{0}e_Aon37D85o|AsLM3ZKX>8(8i#VZ*oj=lD2D z7e+*qq&taRS(pow1((|We`k?lqN&!-PdHB+d?pmvU7@n(Kvs{;j2$>5>wG(qA$rvZRTK5U3%QX85Cu!r}@?=*{O%p_J znrKbRsU&l)qfrl%WwwJXn$$Cjn_(~aR2tCBJy~Pka0A}RUf5uvhz_H1vDq$$8P;a~ zMkYN?OCQWrzTX%O4(U9U{^@)|p4x5Np3~YCm$j>oXP3LSf$!CIz-4M$ZVl$hmzzX8 zN__n3Z(+FFc5_IsWcziOt%iwPU9FP^cnk?@u}mE5`v4B6K(d9zkuRwmf;2W=2yhka zwxO)ZZOV1f-t)izDS_P=GBC??J~PSWR);T-T@3NzUMlOJqy)F)A1JA1B|fdCgnlc> zT$?L-Q#dZa?Kl+LafY zO?iV-U=ohX9-rzmcwt*%lWaFkdigH>rpgShcw53dJlD*Ww|FY)y^&Pkd7NAY1F{NM z{HiC+(v#rmlilUK7g$$YiqenG@PbQg^3r6Kx)xz&HK`?O;m06}7yn)$aPb4rWs;Sf zBfeAvxsM%u!B&n$EmF+>o20|lq~MF{;(qxZV}!DypA^5%x%i>hd&e73s&b_qMH->%gp+8k7PIh2;h zzN)4Ek+Wtj+V`h~yEWG0Jai;1vM8L(@sz?awR~OQ&;F@tMi&84cD%NxcHF-5s>`*9 zN$A}dqn~dRNX=w7jtOCLtg(-)EO+xj|07x;{qD>KY>Zr_MozCUIuj@E*xfflF?1^q zNA|a@pLdGIQPyxB+(%p5DN-^{=xl0LU4BiwR6Oyqj(-y63>35rel#tVlPM!-o`vPG R+u}d4~^WZ)#<{{igF8h-!) diff --git a/locale/de_DE/LC_MESSAGES/petersql.po b/locale/de_DE/LC_MESSAGES/petersql.po index b138456..22cec8b 100644 --- a/locale/de_DE/LC_MESSAGES/petersql.po +++ b/locale/de_DE/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-12 19:04+0100\n" +"POT-Creation-Date: 2026-03-23 10:07+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: de_DE\n" @@ -47,66 +47,62 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "OpenSSH-Client nicht gefunden." -#: structures/engines/mariadb/context.py:592 -#: structures/engines/mysql/context.py:563 -#: structures/engines/postgresql/context.py:579 -#: structures/engines/sqlite/context.py:518 +#: structures/engines/mariadb/context.py:611 +#: structures/engines/mysql/context.py:622 +#: structures/engines/postgresql/context.py:645 +#: structures/engines/sqlite/context.py:524 #, python-brace-format msgid "Table{table_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:620 -#: structures/engines/mysql/context.py:591 -#: structures/engines/postgresql/context.py:604 -#: structures/engines/sqlite/context.py:542 +#: structures/engines/mariadb/context.py:639 +#: structures/engines/mysql/context.py:650 +#: structures/engines/postgresql/context.py:670 +#: structures/engines/sqlite/context.py:548 #, python-brace-format msgid "Column{column_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:638 -#: structures/engines/mysql/context.py:609 -#: structures/engines/postgresql/context.py:622 -#: structures/engines/sqlite/context.py:560 +#: structures/engines/mariadb/context.py:657 +#: structures/engines/mysql/context.py:668 +#: structures/engines/postgresql/context.py:688 +#: structures/engines/sqlite/context.py:566 #, python-brace-format msgid "Index{index_number:03}" msgstr "" -#: structures/engines/mariadb/context.py:678 -#: structures/engines/mysql/context.py:649 -#: structures/engines/postgresql/context.py:662 -#: structures/engines/sqlite/context.py:602 +#: structures/engines/mariadb/context.py:697 +#: structures/engines/mysql/context.py:706 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:608 #, python-brace-format msgid "ForeignKey{foreign_key_number:03}" msgstr "" -#: structures/engines/mariadb/context.py:711 -#: structures/engines/mysql/context.py:680 -#: structures/engines/postgresql/context.py:692 -#: structures/engines/sqlite/context.py:630 +#: structures/engines/mariadb/context.py:730 +#: structures/engines/mysql/context.py:737 +#: structures/engines/postgresql/context.py:758 +#: structures/engines/sqlite/context.py:636 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:762 +#: structures/engines/mariadb/context.py:781 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "" -#: windows/dialogs/connections/view.py:402 -#: windows/dialogs/connections/view.py:739 windows/main/controller.py:606 +#: windows/dialogs/connections/view.py:415 +#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 #: windows/views.py:33 msgid "Connection" msgstr "Verbindung" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:1003 -#: windows/views.py:1353 windows/views.py:1460 windows/views.py:1843 -#: windows/views.py:2788 windows/views.py:2811 windows/views.py:2812 -#: windows/views.py:2813 windows/views.py:2814 windows/views.py:2815 -#: windows/views.py:2816 windows/views.py:2817 windows/views.py:2818 -#: windows/views.py:2819 windows/views.py:2823 windows/views.py:3017 -#: windows/views.py:3218 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1006 +#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 +#: windows/views.py:2821 msgid "Name" msgstr "Name" @@ -114,12 +110,12 @@ msgstr "Name" msgid "Last connection" msgstr "Letzte Verbindung" -#: windows/dialogs/connections/view.py:640 windows/views.py:61 +#: windows/dialogs/connections/view.py:653 windows/views.py:61 msgid "New directory" msgstr "Neues Verzeichnis" #: windows/dialogs/connections/model.py:206 -#: windows/dialogs/connections/view.py:600 windows/views.py:65 +#: windows/dialogs/connections/view.py:613 windows/views.py:65 msgid "New connection" msgstr "Neue Verbindung" @@ -133,14 +129,14 @@ msgstr "Name" msgid "Clone connection" msgstr "Neue Verbindung" -#: windows/views.py:81 windows/views.py:603 windows/views.py:1337 -#: windows/views.py:1378 windows/views.py:1753 windows/views.py:1785 -#: windows/views.py:2044 windows/views.py:3354 windows/views.py:3386 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1322 +#: windows/views.py:1365 windows/views.py:1773 windows/views.py:2032 +#: windows/views.py:2726 windows/views.py:2957 windows/views.py:2989 msgid "Delete" msgstr "Löschen" -#: windows/views.py:111 windows/views.py:1358 windows/views.py:1515 -#: windows/views.py:2828 windows/views.py:3273 +#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 +#: windows/views.py:2876 msgid "Engine" msgstr "Engine" @@ -152,7 +148,7 @@ msgstr "Host + Port" msgid "Username" msgstr "Benutzername" -#: windows/views.py:161 windows/views.py:1147 +#: windows/views.py:161 windows/views.py:1132 msgid "Password" msgstr "Passwort" @@ -173,27 +169,26 @@ msgstr "SSH-Tunnel verwenden" msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 windows/views.py:2749 +#: windows/views.py:233 msgid "Filename" msgstr "Dateiname" -#: windows/views.py:238 windows/views.py:358 windows/views.py:2754 -#: windows/views.py:2937 +#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 msgid "Select a file" msgstr "Datei auswählen" -#: windows/views.py:238 windows/views.py:358 windows/views.py:2937 +#: windows/views.py:238 windows/views.py:358 #, fuzzy msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:255 windows/views.py:1360 windows/views.py:1473 -#: windows/views.py:2829 windows/views.py:3115 windows/views.py:3231 +#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 +#: windows/views.py:2834 msgid "Comments" msgstr "Kommentare" -#: windows/main/controller.py:236 windows/views.py:269 windows/views.py:730 +#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 #: windows/views.py:884 msgid "Settings" msgstr "Einstellungen" @@ -251,7 +246,7 @@ msgstr "" msgid "SSH Tunnel" msgstr "SSH-Tunnel" -#: windows/views.py:411 windows/views.py:1356 windows/views.py:2826 +#: windows/views.py:411 windows/views.py:1341 msgid "Created at" msgstr "Erstellt am" @@ -291,7 +286,7 @@ msgstr "Verbindungsmanager öffnen" msgid "Statistics" msgstr "Statistiken" -#: windows/views.py:584 windows/views.py:1719 +#: windows/views.py:584 windows/views.py:1730 msgid "Create" msgstr "Erstellen" @@ -305,14 +300,15 @@ msgstr "Letzte Verbindung" msgid "Create directory" msgstr "Neues Verzeichnis" -#: windows/views.py:620 windows/views.py:844 windows/views.py:1375 -#: windows/views.py:1788 windows/views.py:2049 windows/views.py:2152 -#: windows/views.py:3162 windows/views.py:3389 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1360 +#: windows/views.py:1776 windows/views.py:2037 windows/views.py:2093 +#: windows/views.py:2702 windows/views.py:2992 windows/views.py:3129 msgid "Cancel" msgstr "Abbrechen" -#: windows/views.py:625 windows/views.py:2054 windows/views.py:3172 -#: windows/views.py:3394 +#: windows/main/controller.py:283 windows/main/controller.py:300 +#: windows/main/controller.py:301 windows/views.py:625 windows/views.py:2042 +#: windows/views.py:2235 windows/views.py:2997 windows/views.py:3135 msgid "Save" msgstr "Speichern" @@ -345,8 +341,9 @@ msgid "Locale" msgstr "Lokale" #: windows/views.py:780 -msgid "Edit Value" -msgstr "Wert bearbeiten" +#, fuzzy +msgid "Column content" +msgstr "Neue Verbindung" #: windows/views.py:790 msgid "Syntax" @@ -376,7 +373,7 @@ msgstr "Hilfe" msgid "Open connection manager" msgstr "Verbindungsmanager öffnen" -#: windows/views.py:900 +#: windows/views.py:902 msgid "Disconnect from server" msgstr "Vom Server trennen" @@ -388,21 +385,21 @@ msgstr "Werkzeug" msgid "Refresh" msgstr "Aktualisieren" -#: windows/views.py:908 windows/views.py:910 +#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 +#: windows/views.py:2077 windows/views.py:2221 msgid "Add" msgstr "Hinzufügen" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2232 -#: windows/views.py:2735 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 msgid "MyMenuItem" msgstr "MeinMenüElement" -#: windows/views.py:951 windows/views.py:1816 windows/views.py:3417 +#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 msgid "MyMenu" msgstr "MeinMenü" -#: windows/views.py:966 windows/views.py:1397 windows/views.py:1404 -#: windows/views.py:1411 +#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 +#: windows/views.py:1402 msgid "MyLabel" msgstr "MeinLabel" @@ -410,8 +407,7 @@ msgstr "MeinLabel" msgid "Databases" msgstr "Datenbanken" -#: windows/views.py:973 windows/views.py:1355 windows/views.py:2797 -#: windows/views.py:2825 +#: windows/views.py:973 windows/views.py:1340 msgid "Size" msgstr "Größe" @@ -431,283 +427,269 @@ msgstr "Tabellen" msgid "System" msgstr "System" -#: windows/views.py:1026 -#, fuzzy -msgid "Character set" -msgstr "Erstellt am" - #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1047 -#: windows/views.py:1359 windows/views.py:3061 +#: windows/components/dataview.py:89 windows/views.py:1029 +#: windows/views.py:1344 msgid "Collation" msgstr "Sortierung" -#: windows/views.py:1073 windows/views.py:2943 +#: windows/views.py:1058 msgid "Encryption" msgstr "" -#: windows/views.py:1085 +#: windows/views.py:1070 msgid "Read Only" msgstr "" -#: windows/views.py:1102 +#: windows/views.py:1087 #, fuzzy msgid "Tablespace" msgstr "Tabellen" -#: windows/views.py:1123 +#: windows/views.py:1108 #, fuzzy msgid "Connection limit" msgstr "Verbindung verloren" -#: windows/views.py:1166 +#: windows/views.py:1151 #, fuzzy msgid "Profile" msgstr "Datei" -#: windows/views.py:1192 +#: windows/views.py:1177 #, fuzzy msgid "Default tablespace" msgstr "Tabelle löschen" -#: windows/views.py:1213 +#: windows/views.py:1198 #, fuzzy msgid "Temporary tablespace" msgstr "Temporär" -#: windows/views.py:1239 +#: windows/views.py:1224 msgid "Quota" msgstr "" -#: windows/views.py:1258 +#: windows/views.py:1243 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1275 +#: windows/views.py:1260 msgid "Account status" msgstr "" -#: windows/views.py:1296 +#: windows/views.py:1281 #, fuzzy msgid "Password expire" msgstr "Passwort" -#: windows/views.py:1317 +#: windows/views.py:1302 msgid "Table:" msgstr "Tabelle:" -#: windows/views.py:1325 windows/views.py:1598 windows/views.py:1642 -#: windows/views.py:1748 windows/views.py:3349 +#: 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:1330 +#: windows/views.py:1315 msgid "Clone" msgstr "Klonen" -#: windows/views.py:1354 +#: windows/views.py:1339 msgid "Rows" msgstr "Zeilen" -#: windows/views.py:1357 windows/views.py:2827 +#: windows/views.py:1342 msgid "Updated at" msgstr "Aktualisiert am" -#: windows/views.py:1381 windows/views.py:1793 windows/views.py:2159 -#: windows/views.py:2206 +#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 +#: windows/views.py:2173 windows/views.py:2709 msgid "Apply" msgstr "Anwenden" -#: windows/views.py:1391 windows/views.py:1550 windows/views.py:2003 -#: windows/views.py:3306 +#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 +#: windows/views.py:2909 msgid "Options" msgstr "Optionen" -#: windows/views.py:1422 +#: windows/views.py:1413 msgid "Diagram" msgstr "Diagramm" -#: windows/views.py:1433 windows/views.py:2796 +#: windows/views.py:1424 msgid "Database" msgstr "Datenbank" -#: windows/views.py:1488 windows/views.py:3246 +#: windows/views.py:1479 windows/views.py:2849 msgid "Base" msgstr "Basis" -#: windows/views.py:1502 windows/views.py:3260 +#: windows/views.py:1493 windows/views.py:2863 msgid "Auto Increment" msgstr "Auto Inkrement" -#: windows/views.py:1530 windows/views.py:3288 +#: windows/views.py:1521 windows/views.py:2891 msgid "Default Collation" msgstr "Standard-Sortierung" -#: windows/views.py:1562 windows/views.py:1603 windows/views.py:1647 +#: windows/views.py:1531 +msgid "Convert data" +msgstr "" + +#: windows/views.py:1539 +msgid "Row format" +msgstr "" + +#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 +#: windows/views.py:1756 windows/views.py:2081 msgid "Remove" msgstr "Entfernen" -#: windows/views.py:1569 windows/views.py:1610 windows/views.py:1654 +#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 msgid "Clear" msgstr "Löschen" -#: windows/views.py:1584 windows/views.py:3320 +#: windows/views.py:1595 windows/views.py:2923 msgid "Indexes" msgstr "Indizes" -#: windows/views.py:1628 +#: windows/views.py:1639 msgid "Foreign Keys" msgstr "Fremdschlüssel" -#: windows/views.py:1672 +#: windows/views.py:1683 msgid "Checks" msgstr "Prüfungen" -#: windows/views.py:1740 windows/views.py:3341 +#: windows/views.py:1750 windows/views.py:2944 msgid "Columns:" msgstr "Spalten:" -#: windows/views.py:1760 windows/views.py:3361 -msgid "Up" -msgstr "Hoch" +#: windows/views.py:1760 +#, fuzzy +msgid "Move Up" +msgstr "Nach oben bewegen\tCTRL+UP" -#: windows/views.py:1767 windows/views.py:3368 -msgid "Down" -msgstr "Runter" +#: windows/views.py:1762 +#, fuzzy +msgid "Move Down" +msgstr "Nach unten bewegen\tCTRL+D" -#: windows/views.py:1806 windows/views.py:1813 windows/views.py:3407 -#: windows/views.py:3414 +#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 +#: windows/views.py:3017 msgid "Add Index" msgstr "Index hinzufügen" -#: windows/views.py:1810 windows/views.py:3411 +#: windows/views.py:1798 windows/views.py:3014 msgid "Add PrimaryKey" msgstr "Primärschlüssel hinzufügen" -#: windows/views.py:1827 +#: windows/views.py:1815 msgid "Table" msgstr "Tabelle" -#: windows/views.py:1863 +#: windows/views.py:1851 #, fuzzy msgid "Definer" msgstr "Einfügen" -#: windows/views.py:1883 +#: windows/views.py:1871 msgid "Schema" msgstr "" -#: windows/views.py:1909 +#: windows/views.py:1897 msgid "SQL security" msgstr "" -#: windows/views.py:1916 +#: windows/views.py:1904 #, fuzzy msgid "DEFINER" msgstr "Einfügen" -#: windows/views.py:1916 +#: windows/views.py:1904 #, fuzzy msgid "INVOKER" msgstr "Einfügen" -#: windows/views.py:1928 windows/views.py:2639 windows/views.py:2658 -#: windows/views.py:2901 +#: windows/views.py:1916 msgid "Algorithm" msgstr "" -#: windows/views.py:1930 windows/views.py:2624 windows/views.py:2657 -#: windows/views.py:2906 +#: windows/views.py:1918 #, fuzzy msgid "UNDEFINED" msgstr "Ohne Vorzeichen" -#: windows/views.py:1933 windows/views.py:2627 windows/views.py:2657 -#: windows/views.py:2909 +#: windows/views.py:1921 msgid "MERGE" msgstr "" -#: windows/views.py:1936 windows/views.py:2636 windows/views.py:2657 -#: windows/views.py:2912 +#: windows/views.py:1924 #, fuzzy msgid "TEMPTABLE" msgstr "Tabelle" -#: windows/views.py:1946 windows/views.py:2663 +#: windows/views.py:1934 msgid "View constraint" msgstr "" -#: windows/views.py:1948 windows/views.py:2662 +#: windows/views.py:1936 #, fuzzy msgid "None" msgstr "Klonen" -#: windows/views.py:1951 windows/views.py:2662 +#: windows/views.py:1939 #, fuzzy msgid "LOCAL" msgstr "Lokale" -#: windows/views.py:1954 +#: windows/views.py:1942 #, fuzzy msgid "CASCADE" msgstr "Abbrechen" -#: windows/views.py:1957 +#: windows/views.py:1945 #, fuzzy msgid "CHECK ONLY" msgstr "Prüfen" -#: windows/views.py:1960 windows/views.py:2662 +#: windows/views.py:1948 msgid "READ ONLY" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:1960 msgid "Force" msgstr "" -#: windows/views.py:1984 +#: windows/views.py:1972 msgid "Security barrier" msgstr "" -#: windows/views.py:2066 +#: windows/views.py:2054 msgid "Views" msgstr "Ansichten" -#: windows/views.py:2074 +#: windows/views.py:2062 msgid "Triggers" msgstr "Trigger" -#: windows/views.py:2086 -#, python-brace-format -msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" - -#: windows/views.py:2094 +#: windows/views.py:2073 #, fuzzy -msgid "First" -msgstr "Filter" - -#: windows/views.py:2112 -msgid "Last" -msgstr "" - -#: windows/views.py:2123 -msgid "Insert record" -msgstr "Datensatz einfügen" +msgid "Refrsh" +msgstr "Aktualisieren" -#: windows/views.py:2128 -msgid "Duplicate record" +#: windows/views.py:2079 +#, fuzzy +msgid "Duplicate" msgstr "Datensatz duplizieren" -#: windows/views.py:2135 -msgid "Delete record" -msgstr "Datensatz löschen" - -#: windows/views.py:2145 +#: windows/views.py:2085 msgid "Apply changes automatically" msgstr "Änderungen automatisch anwenden" -#: windows/views.py:2147 windows/views.py:2148 +#: windows/views.py:2087 windows/views.py:2088 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -715,157 +697,152 @@ msgstr "" "Wenn aktiviert, werden Tabellenbearbeitungen sofort angewendet, ohne auf " "Anwenden oder Abbrechen zu drücken" -#: windows/views.py:2169 +#: windows/views.py:2101 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" + +#: windows/views.py:2109 +#, fuzzy +msgid "First" +msgstr "Filter" + +#: windows/views.py:2127 +msgid "Last" +msgstr "" + +#: windows/views.py:2136 msgid "Filters" msgstr "Filter" -#: windows/views.py:2209 +#: windows/views.py:2176 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2229 +#: windows/views.py:2196 msgid "Insert row" msgstr "Zeile einfügen" -#: windows/views.py:2237 +#: windows/views.py:2204 msgid "Data" msgstr "Daten" -#: windows/views.py:2291 windows/views.py:2341 -msgid "New" -msgstr "Neu" - -#: windows/views.py:2318 -msgid "Query" +#: windows/main/controller.py:278 windows/main/controller.py:287 +#: windows/main/controller.py:288 windows/views.py:2221 +#, fuzzy +msgid "New query" msgstr "Abfrage" -#: windows/views.py:2338 +#: windows/views.py:2223 windows/views.py:2660 msgid "Close" msgstr "Schließen" -#: windows/views.py:2351 -msgid "Query #2" -msgstr "Abfrage #2" +#: windows/main/controller.py:279 windows/main/controller.py:289 +#: windows/main/controller.py:290 windows/views.py:2223 +#, fuzzy +msgid "Close query" +msgstr "Abfrage" -#: windows/views.py:2618 -msgid "Column5" -msgstr "Spalte5" +#: windows/views.py:2227 +msgid "Run" +msgstr "" -#: windows/views.py:2629 -msgid "Import" -msgstr "Importieren" +#: windows/main/controller.py:280 windows/main/controller.py:292 +#: windows/main/controller.py:293 windows/views.py:2227 +#, fuzzy +msgid "Execute" +msgstr "SSH-Executable" -#: windows/views.py:2654 -msgid "Read only" +#: windows/views.py:2229 +msgid "Run all" msgstr "" -#: windows/views.py:2662 -msgid "CASCADED" +#: windows/main/controller.py:295 windows/views.py:2229 +msgid "Execute all statements" msgstr "" -#: windows/views.py:2662 -#, fuzzy -msgid "CHECK OPTION" -msgstr "Verbindung" +#: windows/main/controller.py:282 windows/main/controller.py:297 +#: windows/main/controller.py:298 windows/views.py:2231 +msgid "Stop" +msgstr "" -#: windows/views.py:2694 -msgid "collapsible" -msgstr "zusammenklappbar" +#: windows/views.py:2287 +msgid "a page" +msgstr "" -#: windows/views.py:2710 -msgid "Column3" -msgstr "Spalte3" +#: windows/views.py:2315 +msgid "Query" +msgstr "Abfrage" -#: windows/views.py:2711 -msgid "Column4" -msgstr "Spalte4" +#: windows/views.py:2626 +#, fuzzy +msgid "Character set" +msgstr "Erstellt am" -#: windows/views.py:2754 -msgid "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" -msgstr "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#: windows/views.py:2644 windows/views.py:2663 +msgid "New" +msgstr "Neu" -#: windows/views.py:2766 -msgid "Port" -msgstr "Port" +#: windows/views.py:2683 +msgid "Insert record" +msgstr "Datensatz einfügen" -#: windows/views.py:2789 -msgid "Usage" -msgstr "Verwendung" +#: windows/views.py:2688 +msgid "Duplicate record" +msgstr "Datensatz duplizieren" -#: windows/views.py:2800 -#, python-format -msgid "%(total_rows)s" -msgstr "%(total_rows)s" +#: windows/views.py:2695 +msgid "Delete record" +msgstr "Datensatz löschen" -#: windows/views.py:2805 -msgid "rows total" -msgstr "Zeilen insgesamt" +#: windows/views.py:2733 windows/views.py:2964 +msgid "Up" +msgstr "Hoch" -#: windows/views.py:2824 -msgid "Lines" -msgstr "Zeilen" +#: windows/views.py:2740 windows/views.py:2971 +msgid "Down" +msgstr "Runter" -#: windows/views.py:2856 -msgid "Temporary" -msgstr "Temporär" +#: windows/views.py:3100 +msgid "Save Starments" +msgstr "" -#: windows/views.py:2867 +#: windows/views.py:3108 #, fuzzy -msgid "Engine options" -msgstr "Optionen" +msgid "Location" +msgstr "Sortierung" -#: windows/views.py:2926 -msgid "RadioBtn" +#: windows/views.py:3115 +msgid "*.sql" msgstr "" -#: windows/views.py:3009 -msgid "Edit Column" -msgstr "Spalte bearbeiten" - -#: windows/views.py:3025 -msgid "Datatype" -msgstr "Datentyp" - -#: windows/components/dataview.py:121 windows/views.py:3040 -msgid "Length/Set" -msgstr "Länge/Menge" - -#: windows/components/dataview.py:51 windows/views.py:3079 -msgid "Unsigned" -msgstr "Ohne Vorzeichen" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3085 +#: windows/components/dataview.py:75 msgid "Allow NULL" msgstr "NULL erlauben" -#: windows/views.py:3091 -msgid "Zero Fill" -msgstr "Nullfüllung" +#: windows/components/dataview.py:28 +msgid "Check" +msgstr "Prüfen" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3102 +#: windows/components/dataview.py:78 msgid "Default" msgstr "Standard" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3128 +#: windows/components/dataview.py:82 msgid "Virtuality" msgstr "Virtualität" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3143 msgid "Expression" msgstr "Ausdruck" -#: windows/components/dataview.py:28 -msgid "Check" -msgstr "Prüfen" +#: windows/components/dataview.py:51 +msgid "Unsigned" +msgstr "Ohne Vorzeichen" #: windows/components/dataview.py:53 msgid "Zerofill" @@ -879,6 +856,10 @@ msgstr "#" msgid "Data type" msgstr "Datentyp" +#: windows/components/dataview.py:121 +msgid "Length/Set" +msgstr "Länge/Menge" + #: windows/components/dataview.py:155 msgid "Add column\tCTRL+INS" msgstr "Spalte hinzufügen\tCTRL+INS" @@ -955,111 +936,164 @@ msgstr "AUTO INCREMENT" msgid "Text/Expression" msgstr "Text/Ausdruck" -#: windows/dialogs/connections/view.py:122 windows/main/tabs/query.py:387 +#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:401 +#: windows/dialogs/connections/view.py:414 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:413 +#: windows/dialogs/connections/view.py:426 #, python-brace-format msgid "Do you want save the connection {connection_name}?" msgstr "" -#: windows/dialogs/connections/view.py:416 +#: windows/dialogs/connections/view.py:429 msgid "Confirm save" msgstr "Speichern bestätigen" -#: windows/dialogs/connections/view.py:468 +#: windows/dialogs/connections/view.py:481 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:470 +#: windows/dialogs/connections/view.py:483 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:737 +#: windows/dialogs/connections/view.py:750 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:775 #, fuzzy, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "Verbindungsfehler" -#: windows/dialogs/connections/view.py:763 +#: windows/dialogs/connections/view.py:776 msgid "Connection error" msgstr "Verbindungsfehler" -#: windows/dialogs/connections/view.py:789 +#: windows/dialogs/connections/view.py:802 #, fuzzy, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" msgstr "Möchten Sie die Datensätze löschen?" -#: windows/dialogs/connections/view.py:792 -#: windows/dialogs/connections/view.py:809 +#: windows/dialogs/connections/view.py:805 +#: windows/dialogs/connections/view.py:822 msgid "Confirm delete" msgstr "Löschen bestätigen" -#: windows/dialogs/connections/view.py:806 +#: windows/dialogs/connections/view.py:819 #, fuzzy, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "Möchten Sie die Datensätze löschen?" -#: windows/main/controller.py:189 +#: windows/main/controller.py:275 +#, python-brace-format +msgid "{text} ({shortcut})" +msgstr "" + +#: windows/main/controller.py:281 windows/main/controller.py:294 +#, fuzzy +msgid "Execute all" +msgstr "SSH-Executable" + +#: windows/main/controller.py:471 windows/main/controller.py:479 +#, fuzzy +msgid "Query (1)" +msgstr "Abfrage" + +#: windows/main/controller.py:497 +#, python-brace-format +msgid "Query ({query_number})" +msgstr "" + +#: windows/main/controller.py:530 +msgid "You have unsaved changes. Save before closing?" +msgstr "" + +#: windows/main/controller.py:531 +msgid "Unsaved query" +msgstr "" + +#: windows/main/controller.py:576 +#, fuzzy +msgid "Save query" +msgstr "Abfrage" + +#: windows/main/controller.py:579 +msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" +msgstr "" + +#: windows/main/controller.py:616 windows/main/controller.py:642 +#: windows/main/database/list.py:84 windows/main/database/view.py:256 +#: windows/main/database/view.py:282 windows/main/query/controller.py:177 +msgid "Error" +msgstr "Fehler" + +#: windows/main/controller.py:622 +#, python-brace-format +msgid "-- Saved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:647 +#, python-brace-format +msgid "-- Autosaved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:704 msgid "days" msgstr "Tage" -#: windows/main/controller.py:190 +#: windows/main/controller.py:705 msgid "hours" msgstr "Stunden" -#: windows/main/controller.py:191 +#: windows/main/controller.py:706 msgid "minutes" msgstr "Minuten" -#: windows/main/controller.py:192 +#: windows/main/controller.py:707 msgid "seconds" msgstr "Sekunden" -#: windows/main/controller.py:200 +#: windows/main/controller.py:715 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Verwendeter Speicher: {used} ({percentage:.2%})" -#: windows/main/controller.py:236 +#: windows/main/controller.py:751 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:441 +#: windows/main/controller.py:952 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:443 +#: windows/main/controller.py:954 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:608 +#: windows/main/controller.py:1119 msgid "Version" msgstr "Version" -#: windows/main/controller.py:610 +#: windows/main/controller.py:1121 msgid "Uptime" msgstr "Betriebszeit" -#: windows/main/controller.py:678 +#: windows/main/controller.py:1199 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:711 +#: windows/main/controller.py:1232 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1069,153 +1103,177 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:716 windows/main/controller.py:737 +#: windows/main/controller.py:1237 windows/main/controller.py:1258 #, fuzzy msgid "Delete database" msgstr "Tabelle löschen" -#: windows/main/controller.py:722 +#: windows/main/controller.py:1243 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:723 +#: windows/main/controller.py:1244 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:736 +#: windows/main/controller.py:1257 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:751 +#: windows/main/controller.py:1272 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:752 windows/main/tabs/view.py:253 -#: windows/main/tabs/view.py:279 +#: windows/main/controller.py:1273 windows/main/database/view.py:253 +#: windows/main/database/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:871 +#: windows/main/controller.py:1392 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:897 +#: windows/main/controller.py:1418 #, fuzzy, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "Möchten Sie die Datensätze löschen?" -#: windows/main/controller.py:900 +#: windows/main/controller.py:1421 msgid "Delete table" msgstr "Tabelle löschen" -#: windows/main/controller.py:919 +#: windows/main/controller.py:1440 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1017 +#: windows/main/controller.py:1563 msgid "Do you want delete the records?" msgstr "Möchten Sie die Datensätze löschen?" -#: windows/main/tabs/database.py:71 +#: windows/main/database/list.py:69 msgid "The connection to the database was lost." msgstr "Die Verbindung zur Datenbank wurde verloren." -#: windows/main/tabs/database.py:73 +#: windows/main/database/list.py:71 msgid "Do you want to reconnect?" msgstr "Möchten Sie erneut verbinden?" -#: windows/main/tabs/database.py:75 +#: windows/main/database/list.py:73 msgid "Connection lost" msgstr "Verbindung verloren" -#: windows/main/tabs/database.py:85 +#: windows/main/database/list.py:83 msgid "Reconnection failed:" msgstr "Wiederverbindung fehlgeschlagen:" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:489 -#: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 -msgid "Error" -msgstr "Fehler" +#: windows/main/database/view.py:252 +msgid "View created successfully" +msgstr "" -#: windows/main/tabs/query.py:308 -#, python-brace-format -msgid "{affected_rows} rows affected" +#: windows/main/database/view.py:252 +msgid "View updated successfully" msgstr "" -#: windows/main/tabs/query.py:315 windows/main/tabs/query.py:339 +#: windows/main/database/view.py:256 #, python-brace-format -msgid "Query {query_number}" +msgid "Error saving view: {}" msgstr "" -#: windows/main/tabs/query.py:320 +#: windows/main/database/view.py:269 #, python-brace-format -msgid "Query {query_number} (Error)" +msgid "Are you sure you want to delete view '{}'?" msgstr "" -#: windows/main/tabs/query.py:334 -#, python-brace-format -msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" +#: windows/main/database/view.py:270 +#, fuzzy +msgid "Confirm Delete" +msgstr "Löschen bestätigen" + +#: windows/main/database/view.py:279 +msgid "View deleted successfully" msgstr "" -#: windows/main/tabs/query.py:360 +#: windows/main/database/view.py:282 #, python-brace-format -msgid "{rows_count} rows" +msgid "Error deleting view: {}" msgstr "" -#: windows/main/tabs/query.py:362 +#: windows/main/query/controller.py:110 #, python-brace-format -msgid "{elapsed_ms:.1f} ms" +msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/tabs/query.py:366 +#: windows/main/query/controller.py:112 #, python-brace-format -msgid "{warnings_count} warnings" +msgid "{elapsed_s:.2f} s" msgstr "" -#: windows/main/tabs/query.py:381 +#: windows/main/query/controller.py:115 #, fuzzy -msgid "Error:" -msgstr "Fehler" +msgid "none" +msgstr "Klonen" -#: windows/main/tabs/query.py:488 +#: windows/main/query/controller.py:121 +#, python-brace-format +msgid "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." +msgstr "" + +#: windows/main/query/controller.py:134 +msgid "Query execution cancelled" +msgstr "" + +#: windows/main/query/controller.py:176 #, fuzzy msgid "No active database connection" msgstr "Neue Verbindung" -#: windows/main/tabs/view.py:252 -msgid "View created successfully" +#: windows/main/query/renderer.py:53 +#, python-brace-format +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View updated successfully" +#: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 +#, python-brace-format +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/view.py:256 +#: windows/main/query/renderer.py:65 #, python-brace-format -msgid "Error saving view: {}" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/view.py:269 +#: windows/main/query/renderer.py:79 #, python-brace-format -msgid "Are you sure you want to delete view '{}'?" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/view.py:270 -#, fuzzy -msgid "Confirm Delete" -msgstr "Löschen bestätigen" +#: windows/main/query/renderer.py:165 +#, python-brace-format +msgid "{rows_count} rows" +msgstr "" -#: windows/main/tabs/view.py:279 -msgid "View deleted successfully" +#: windows/main/query/renderer.py:167 +#, python-brace-format +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/view.py:282 +#: windows/main/query/renderer.py:171 #, python-brace-format -msgid "Error deleting view: {}" +msgid "{warnings_count} warnings" msgstr "" +#: windows/main/query/renderer.py:186 +#, fuzzy +msgid "Error:" +msgstr "Fehler" + #~ msgid "Created at:" #~ msgstr "" @@ -1281,3 +1339,73 @@ msgstr "" #~ msgid "{} warnings" #~ msgstr "" +#~ msgid "Edit Value" +#~ msgstr "Wert bearbeiten" + +#~ msgid "Query #2" +#~ msgstr "Abfrage #2" + +#~ msgid "Column5" +#~ msgstr "Spalte5" + +#~ msgid "Import" +#~ msgstr "Importieren" + +#~ msgid "Read only" +#~ msgstr "" + +#~ msgid "CASCADED" +#~ msgstr "" + +#~ msgid "CHECK OPTION" +#~ msgstr "Verbindung" + +#~ msgid "collapsible" +#~ msgstr "zusammenklappbar" + +#~ msgid "Column3" +#~ msgstr "Spalte3" + +#~ msgid "Column4" +#~ msgstr "Spalte4" + +#~ msgid "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#~ msgstr "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" + +#~ msgid "Port" +#~ msgstr "Port" + +#~ msgid "Usage" +#~ msgstr "Verwendung" + +#~ msgid "%(total_rows)s" +#~ msgstr "%(total_rows)s" + +#~ msgid "rows total" +#~ msgstr "Zeilen insgesamt" + +#~ msgid "Lines" +#~ msgstr "Zeilen" + +#~ msgid "Temporary" +#~ msgstr "Temporär" + +#~ msgid "Engine options" +#~ msgstr "Optionen" + +#~ msgid "RadioBtn" +#~ msgstr "" + +#~ msgid "Edit Column" +#~ msgstr "Spalte bearbeiten" + +#~ msgid "Datatype" +#~ msgstr "Datentyp" + +#~ msgid "Zero Fill" +#~ msgstr "Nullfüllung" + diff --git a/locale/en_US/LC_MESSAGES/petersql.po b/locale/en_US/LC_MESSAGES/petersql.po index 9d65ccf..5e23444 100644 --- a/locale/en_US/LC_MESSAGES/petersql.po +++ b/locale/en_US/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-12 19:04+0100\n" +"POT-Creation-Date: 2026-03-23 10:07+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: en_US\n" @@ -47,66 +47,62 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "OpenSSH client not found." -#: structures/engines/mariadb/context.py:592 -#: structures/engines/mysql/context.py:563 -#: structures/engines/postgresql/context.py:579 -#: structures/engines/sqlite/context.py:518 +#: structures/engines/mariadb/context.py:611 +#: structures/engines/mysql/context.py:622 +#: structures/engines/postgresql/context.py:645 +#: structures/engines/sqlite/context.py:524 #, python-brace-format msgid "Table{table_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:620 -#: structures/engines/mysql/context.py:591 -#: structures/engines/postgresql/context.py:604 -#: structures/engines/sqlite/context.py:542 +#: structures/engines/mariadb/context.py:639 +#: structures/engines/mysql/context.py:650 +#: structures/engines/postgresql/context.py:670 +#: structures/engines/sqlite/context.py:548 #, python-brace-format msgid "Column{column_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:638 -#: structures/engines/mysql/context.py:609 -#: structures/engines/postgresql/context.py:622 -#: structures/engines/sqlite/context.py:560 +#: structures/engines/mariadb/context.py:657 +#: structures/engines/mysql/context.py:668 +#: structures/engines/postgresql/context.py:688 +#: structures/engines/sqlite/context.py:566 #, python-brace-format msgid "Index{index_number:03}" msgstr "" -#: structures/engines/mariadb/context.py:678 -#: structures/engines/mysql/context.py:649 -#: structures/engines/postgresql/context.py:662 -#: structures/engines/sqlite/context.py:602 +#: structures/engines/mariadb/context.py:697 +#: structures/engines/mysql/context.py:706 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:608 #, python-brace-format msgid "ForeignKey{foreign_key_number:03}" msgstr "" -#: structures/engines/mariadb/context.py:711 -#: structures/engines/mysql/context.py:680 -#: structures/engines/postgresql/context.py:692 -#: structures/engines/sqlite/context.py:630 +#: structures/engines/mariadb/context.py:730 +#: structures/engines/mysql/context.py:737 +#: structures/engines/postgresql/context.py:758 +#: structures/engines/sqlite/context.py:636 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:762 +#: structures/engines/mariadb/context.py:781 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "" -#: windows/dialogs/connections/view.py:402 -#: windows/dialogs/connections/view.py:739 windows/main/controller.py:606 +#: windows/dialogs/connections/view.py:415 +#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 #: windows/views.py:33 msgid "Connection" msgstr "Connection" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:1003 -#: windows/views.py:1353 windows/views.py:1460 windows/views.py:1843 -#: windows/views.py:2788 windows/views.py:2811 windows/views.py:2812 -#: windows/views.py:2813 windows/views.py:2814 windows/views.py:2815 -#: windows/views.py:2816 windows/views.py:2817 windows/views.py:2818 -#: windows/views.py:2819 windows/views.py:2823 windows/views.py:3017 -#: windows/views.py:3218 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1006 +#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 +#: windows/views.py:2821 msgid "Name" msgstr "Name" @@ -114,12 +110,12 @@ msgstr "Name" msgid "Last connection" msgstr "Last connection" -#: windows/dialogs/connections/view.py:640 windows/views.py:61 +#: windows/dialogs/connections/view.py:653 windows/views.py:61 msgid "New directory" msgstr "New directory" #: windows/dialogs/connections/model.py:206 -#: windows/dialogs/connections/view.py:600 windows/views.py:65 +#: windows/dialogs/connections/view.py:613 windows/views.py:65 msgid "New connection" msgstr "New connection" @@ -131,14 +127,14 @@ msgstr "Rename" msgid "Clone connection" msgstr "Clone connection" -#: windows/views.py:81 windows/views.py:603 windows/views.py:1337 -#: windows/views.py:1378 windows/views.py:1753 windows/views.py:1785 -#: windows/views.py:2044 windows/views.py:3354 windows/views.py:3386 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1322 +#: windows/views.py:1365 windows/views.py:1773 windows/views.py:2032 +#: windows/views.py:2726 windows/views.py:2957 windows/views.py:2989 msgid "Delete" msgstr "Delete" -#: windows/views.py:111 windows/views.py:1358 windows/views.py:1515 -#: windows/views.py:2828 windows/views.py:3273 +#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 +#: windows/views.py:2876 msgid "Engine" msgstr "Engine" @@ -150,7 +146,7 @@ msgstr "Host + port" msgid "Username" msgstr "Username" -#: windows/views.py:161 windows/views.py:1147 +#: windows/views.py:161 windows/views.py:1132 msgid "Password" msgstr "Password" @@ -171,26 +167,25 @@ msgstr "Use SSH tunnel" msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 windows/views.py:2749 +#: windows/views.py:233 msgid "Filename" msgstr "Filename" -#: windows/views.py:238 windows/views.py:358 windows/views.py:2754 -#: windows/views.py:2937 +#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 msgid "Select a file" msgstr "Select a file" -#: windows/views.py:238 windows/views.py:358 windows/views.py:2937 +#: windows/views.py:238 windows/views.py:358 msgid "*.*" msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:255 windows/views.py:1360 windows/views.py:1473 -#: windows/views.py:2829 windows/views.py:3115 windows/views.py:3231 +#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 +#: windows/views.py:2834 msgid "Comments" msgstr "Comments" -#: windows/main/controller.py:236 windows/views.py:269 windows/views.py:730 +#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 #: windows/views.py:884 msgid "Settings" msgstr "Settings" @@ -247,7 +242,7 @@ msgstr "" msgid "SSH Tunnel" msgstr "SSH Tunnel" -#: windows/views.py:411 windows/views.py:1356 windows/views.py:2826 +#: windows/views.py:411 windows/views.py:1341 msgid "Created at" msgstr "Created at" @@ -284,7 +279,7 @@ msgstr "" msgid "Statistics" msgstr "" -#: windows/views.py:584 windows/views.py:1719 +#: windows/views.py:584 windows/views.py:1730 msgid "Create" msgstr "" @@ -296,14 +291,15 @@ msgstr "" msgid "Create directory" msgstr "" -#: windows/views.py:620 windows/views.py:844 windows/views.py:1375 -#: windows/views.py:1788 windows/views.py:2049 windows/views.py:2152 -#: windows/views.py:3162 windows/views.py:3389 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1360 +#: windows/views.py:1776 windows/views.py:2037 windows/views.py:2093 +#: windows/views.py:2702 windows/views.py:2992 windows/views.py:3129 msgid "Cancel" msgstr "" -#: windows/views.py:625 windows/views.py:2054 windows/views.py:3172 -#: windows/views.py:3394 +#: windows/main/controller.py:283 windows/main/controller.py:300 +#: windows/main/controller.py:301 windows/views.py:625 windows/views.py:2042 +#: windows/views.py:2235 windows/views.py:2997 windows/views.py:3135 msgid "Save" msgstr "" @@ -336,8 +332,9 @@ msgid "Locale" msgstr "" #: windows/views.py:780 -msgid "Edit Value" -msgstr "" +#, fuzzy +msgid "Column content" +msgstr "Clone connection" #: windows/views.py:790 msgid "Syntax" @@ -367,7 +364,7 @@ msgstr "" msgid "Open connection manager" msgstr "" -#: windows/views.py:900 +#: windows/views.py:902 msgid "Disconnect from server" msgstr "" @@ -379,21 +376,21 @@ msgstr "" msgid "Refresh" msgstr "" -#: windows/views.py:908 windows/views.py:910 +#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 +#: windows/views.py:2077 windows/views.py:2221 msgid "Add" msgstr "" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2232 -#: windows/views.py:2735 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 msgid "MyMenuItem" msgstr "" -#: windows/views.py:951 windows/views.py:1816 windows/views.py:3417 +#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 msgid "MyMenu" msgstr "" -#: windows/views.py:966 windows/views.py:1397 windows/views.py:1404 -#: windows/views.py:1411 +#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 +#: windows/views.py:1402 msgid "MyLabel" msgstr "" @@ -401,8 +398,7 @@ msgstr "" msgid "Databases" msgstr "" -#: windows/views.py:973 windows/views.py:1355 windows/views.py:2797 -#: windows/views.py:2825 +#: windows/views.py:973 windows/views.py:1340 msgid "Size" msgstr "" @@ -422,417 +418,396 @@ msgstr "" msgid "System" msgstr "" -#: windows/views.py:1026 -msgid "Character set" -msgstr "" - #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1047 -#: windows/views.py:1359 windows/views.py:3061 +#: windows/components/dataview.py:89 windows/views.py:1029 +#: windows/views.py:1344 msgid "Collation" msgstr "" -#: windows/views.py:1073 windows/views.py:2943 +#: windows/views.py:1058 msgid "Encryption" msgstr "" -#: windows/views.py:1085 +#: windows/views.py:1070 msgid "Read Only" msgstr "" -#: windows/views.py:1102 +#: windows/views.py:1087 msgid "Tablespace" msgstr "" -#: windows/views.py:1123 +#: windows/views.py:1108 msgid "Connection limit" msgstr "" -#: windows/views.py:1166 +#: windows/views.py:1151 msgid "Profile" msgstr "" -#: windows/views.py:1192 +#: windows/views.py:1177 msgid "Default tablespace" msgstr "" -#: windows/views.py:1213 +#: windows/views.py:1198 msgid "Temporary tablespace" msgstr "" -#: windows/views.py:1239 +#: windows/views.py:1224 msgid "Quota" msgstr "" -#: windows/views.py:1258 +#: windows/views.py:1243 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1275 +#: windows/views.py:1260 msgid "Account status" msgstr "" -#: windows/views.py:1296 +#: windows/views.py:1281 msgid "Password expire" msgstr "" -#: windows/views.py:1317 +#: windows/views.py:1302 msgid "Table:" msgstr "" -#: windows/views.py:1325 windows/views.py:1598 windows/views.py:1642 -#: windows/views.py:1748 windows/views.py:3349 +#: 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:1330 +#: windows/views.py:1315 msgid "Clone" msgstr "" -#: windows/views.py:1354 +#: windows/views.py:1339 msgid "Rows" msgstr "" -#: windows/views.py:1357 windows/views.py:2827 +#: windows/views.py:1342 msgid "Updated at" msgstr "" -#: windows/views.py:1381 windows/views.py:1793 windows/views.py:2159 -#: windows/views.py:2206 +#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 +#: windows/views.py:2173 windows/views.py:2709 msgid "Apply" msgstr "" -#: windows/views.py:1391 windows/views.py:1550 windows/views.py:2003 -#: windows/views.py:3306 +#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 +#: windows/views.py:2909 msgid "Options" msgstr "" -#: windows/views.py:1422 +#: windows/views.py:1413 msgid "Diagram" msgstr "" -#: windows/views.py:1433 windows/views.py:2796 +#: windows/views.py:1424 msgid "Database" msgstr "" -#: windows/views.py:1488 windows/views.py:3246 +#: windows/views.py:1479 windows/views.py:2849 msgid "Base" msgstr "" -#: windows/views.py:1502 windows/views.py:3260 +#: windows/views.py:1493 windows/views.py:2863 msgid "Auto Increment" msgstr "" -#: windows/views.py:1530 windows/views.py:3288 +#: windows/views.py:1521 windows/views.py:2891 msgid "Default Collation" msgstr "" -#: windows/views.py:1562 windows/views.py:1603 windows/views.py:1647 +#: windows/views.py:1531 +msgid "Convert data" +msgstr "" + +#: windows/views.py:1539 +msgid "Row format" +msgstr "" + +#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 +#: windows/views.py:1756 windows/views.py:2081 msgid "Remove" msgstr "" -#: windows/views.py:1569 windows/views.py:1610 windows/views.py:1654 +#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 msgid "Clear" msgstr "" -#: windows/views.py:1584 windows/views.py:3320 +#: windows/views.py:1595 windows/views.py:2923 msgid "Indexes" msgstr "" -#: windows/views.py:1628 +#: windows/views.py:1639 msgid "Foreign Keys" msgstr "" -#: windows/views.py:1672 +#: windows/views.py:1683 msgid "Checks" msgstr "" -#: windows/views.py:1740 windows/views.py:3341 +#: windows/views.py:1750 windows/views.py:2944 msgid "Columns:" msgstr "" -#: windows/views.py:1760 windows/views.py:3361 -msgid "Up" +#: windows/views.py:1760 +msgid "Move Up" msgstr "" -#: windows/views.py:1767 windows/views.py:3368 -msgid "Down" +#: windows/views.py:1762 +msgid "Move Down" msgstr "" -#: windows/views.py:1806 windows/views.py:1813 windows/views.py:3407 -#: windows/views.py:3414 +#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 +#: windows/views.py:3017 msgid "Add Index" msgstr "" -#: windows/views.py:1810 windows/views.py:3411 +#: windows/views.py:1798 windows/views.py:3014 msgid "Add PrimaryKey" msgstr "" -#: windows/views.py:1827 +#: windows/views.py:1815 msgid "Table" msgstr "" -#: windows/views.py:1863 +#: windows/views.py:1851 msgid "Definer" msgstr "" -#: windows/views.py:1883 +#: windows/views.py:1871 msgid "Schema" msgstr "" -#: windows/views.py:1909 +#: windows/views.py:1897 msgid "SQL security" msgstr "" -#: windows/views.py:1916 +#: windows/views.py:1904 msgid "DEFINER" msgstr "" -#: windows/views.py:1916 +#: windows/views.py:1904 msgid "INVOKER" msgstr "" -#: windows/views.py:1928 windows/views.py:2639 windows/views.py:2658 -#: windows/views.py:2901 +#: windows/views.py:1916 msgid "Algorithm" msgstr "" -#: windows/views.py:1930 windows/views.py:2624 windows/views.py:2657 -#: windows/views.py:2906 +#: windows/views.py:1918 msgid "UNDEFINED" msgstr "" -#: windows/views.py:1933 windows/views.py:2627 windows/views.py:2657 -#: windows/views.py:2909 +#: windows/views.py:1921 msgid "MERGE" msgstr "" -#: windows/views.py:1936 windows/views.py:2636 windows/views.py:2657 -#: windows/views.py:2912 +#: windows/views.py:1924 msgid "TEMPTABLE" msgstr "" -#: windows/views.py:1946 windows/views.py:2663 +#: windows/views.py:1934 msgid "View constraint" msgstr "" -#: windows/views.py:1948 windows/views.py:2662 +#: windows/views.py:1936 msgid "None" msgstr "" -#: windows/views.py:1951 windows/views.py:2662 +#: windows/views.py:1939 msgid "LOCAL" msgstr "" -#: windows/views.py:1954 +#: windows/views.py:1942 msgid "CASCADE" msgstr "" -#: windows/views.py:1957 +#: windows/views.py:1945 msgid "CHECK ONLY" msgstr "" -#: windows/views.py:1960 windows/views.py:2662 +#: windows/views.py:1948 msgid "READ ONLY" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:1960 msgid "Force" msgstr "" -#: windows/views.py:1984 +#: windows/views.py:1972 msgid "Security barrier" msgstr "" -#: windows/views.py:2066 +#: windows/views.py:2054 msgid "Views" msgstr "" -#: windows/views.py:2074 +#: windows/views.py:2062 msgid "Triggers" msgstr "" -#: windows/views.py:2086 -#, python-brace-format -msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" - -#: windows/views.py:2094 -msgid "First" +#: windows/views.py:2073 +msgid "Refrsh" msgstr "" -#: windows/views.py:2112 -msgid "Last" +#: windows/views.py:2079 +msgid "Duplicate" msgstr "" -#: windows/views.py:2123 -msgid "Insert record" +#: windows/views.py:2085 +msgid "Apply changes automatically" msgstr "" -#: windows/views.py:2128 -msgid "Duplicate record" +#: windows/views.py:2087 windows/views.py:2088 +msgid "" +"If enabled, table edits are applied immediately without pressing Apply or" +" Cancel" msgstr "" -#: windows/views.py:2135 -msgid "Delete record" +#: windows/views.py:2101 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" -#: windows/views.py:2145 -msgid "Apply changes automatically" +#: windows/views.py:2109 +msgid "First" msgstr "" -#: windows/views.py:2147 windows/views.py:2148 -msgid "" -"If enabled, table edits are applied immediately without pressing Apply or" -" Cancel" +#: windows/views.py:2127 +msgid "Last" msgstr "" -#: windows/views.py:2169 +#: windows/views.py:2136 msgid "Filters" msgstr "" -#: windows/views.py:2209 +#: windows/views.py:2176 msgid "CTRL+ENTER" msgstr "" -#: windows/views.py:2229 +#: windows/views.py:2196 msgid "Insert row" msgstr "" -#: windows/views.py:2237 +#: windows/views.py:2204 msgid "Data" msgstr "" -#: windows/views.py:2291 windows/views.py:2341 -msgid "New" -msgstr "" - -#: windows/views.py:2318 -msgid "Query" -msgstr "" +#: windows/main/controller.py:278 windows/main/controller.py:287 +#: windows/main/controller.py:288 windows/views.py:2221 +#, fuzzy +msgid "New query" +msgstr "New directory" -#: windows/views.py:2338 +#: windows/views.py:2223 windows/views.py:2660 msgid "Close" msgstr "" -#: windows/views.py:2351 -msgid "Query #2" -msgstr "" - -#: windows/views.py:2618 -msgid "Column5" +#: windows/main/controller.py:279 windows/main/controller.py:289 +#: windows/main/controller.py:290 windows/views.py:2223 +msgid "Close query" msgstr "" -#: windows/views.py:2629 -msgid "Import" +#: windows/views.py:2227 +msgid "Run" msgstr "" -#: windows/views.py:2654 -msgid "Read only" -msgstr "" - -#: windows/views.py:2662 -msgid "CASCADED" -msgstr "" - -#: windows/views.py:2662 -msgid "CHECK OPTION" -msgstr "" - -#: windows/views.py:2694 -msgid "collapsible" -msgstr "" +#: windows/main/controller.py:280 windows/main/controller.py:292 +#: windows/main/controller.py:293 windows/views.py:2227 +#, fuzzy +msgid "Execute" +msgstr "SSH executable" -#: windows/views.py:2710 -msgid "Column3" +#: windows/views.py:2229 +msgid "Run all" msgstr "" -#: windows/views.py:2711 -msgid "Column4" +#: windows/main/controller.py:295 windows/views.py:2229 +msgid "Execute all statements" msgstr "" -#: windows/views.py:2754 -msgid "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#: windows/main/controller.py:282 windows/main/controller.py:297 +#: windows/main/controller.py:298 windows/views.py:2231 +msgid "Stop" msgstr "" -#: windows/views.py:2766 -msgid "Port" +#: windows/views.py:2287 +msgid "a page" msgstr "" -#: windows/views.py:2789 -msgid "Usage" +#: windows/views.py:2315 +msgid "Query" msgstr "" -#: windows/views.py:2800 -#, python-format -msgid "%(total_rows)s" +#: windows/views.py:2626 +msgid "Character set" msgstr "" -#: windows/views.py:2805 -msgid "rows total" +#: windows/views.py:2644 windows/views.py:2663 +msgid "New" msgstr "" -#: windows/views.py:2824 -msgid "Lines" +#: windows/views.py:2683 +msgid "Insert record" msgstr "" -#: windows/views.py:2856 -msgid "Temporary" +#: windows/views.py:2688 +msgid "Duplicate record" msgstr "" -#: windows/views.py:2867 -msgid "Engine options" +#: windows/views.py:2695 +msgid "Delete record" msgstr "" -#: windows/views.py:2926 -msgid "RadioBtn" +#: windows/views.py:2733 windows/views.py:2964 +msgid "Up" msgstr "" -#: windows/views.py:3009 -msgid "Edit Column" +#: windows/views.py:2740 windows/views.py:2971 +msgid "Down" msgstr "" -#: windows/views.py:3025 -msgid "Datatype" +#: windows/views.py:3100 +msgid "Save Starments" msgstr "" -#: windows/components/dataview.py:121 windows/views.py:3040 -msgid "Length/Set" +#: windows/views.py:3108 +msgid "Location" msgstr "" -#: windows/components/dataview.py:51 windows/views.py:3079 -msgid "Unsigned" +#: windows/views.py:3115 +msgid "*.sql" msgstr "" #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3085 +#: windows/components/dataview.py:75 msgid "Allow NULL" msgstr "" -#: windows/views.py:3091 -msgid "Zero Fill" +#: windows/components/dataview.py:28 +msgid "Check" msgstr "" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3102 +#: windows/components/dataview.py:78 msgid "Default" msgstr "" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3128 +#: windows/components/dataview.py:82 msgid "Virtuality" msgstr "" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3143 msgid "Expression" msgstr "" -#: windows/components/dataview.py:28 -msgid "Check" +#: windows/components/dataview.py:51 +msgid "Unsigned" msgstr "" #: windows/components/dataview.py:53 @@ -847,6 +822,10 @@ msgstr "" msgid "Data type" msgstr "" +#: windows/components/dataview.py:121 +msgid "Length/Set" +msgstr "" + #: windows/components/dataview.py:155 msgid "Add column\tCTRL+INS" msgstr "" @@ -923,111 +902,162 @@ msgstr "" msgid "Text/Expression" msgstr "" -#: windows/dialogs/connections/view.py:122 windows/main/tabs/query.py:387 +#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:401 +#: windows/dialogs/connections/view.py:414 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:413 +#: windows/dialogs/connections/view.py:426 #, python-brace-format msgid "Do you want save the connection {connection_name}?" msgstr "" -#: windows/dialogs/connections/view.py:416 +#: windows/dialogs/connections/view.py:429 msgid "Confirm save" msgstr "" -#: windows/dialogs/connections/view.py:468 +#: windows/dialogs/connections/view.py:481 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:470 +#: windows/dialogs/connections/view.py:483 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:737 +#: windows/dialogs/connections/view.py:750 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:775 #, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "" -#: windows/dialogs/connections/view.py:763 +#: windows/dialogs/connections/view.py:776 msgid "Connection error" msgstr "" -#: windows/dialogs/connections/view.py:789 +#: windows/dialogs/connections/view.py:802 #, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" msgstr "" -#: windows/dialogs/connections/view.py:792 -#: windows/dialogs/connections/view.py:809 +#: windows/dialogs/connections/view.py:805 +#: windows/dialogs/connections/view.py:822 msgid "Confirm delete" msgstr "" -#: windows/dialogs/connections/view.py:806 +#: windows/dialogs/connections/view.py:819 #, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "" -#: windows/main/controller.py:189 +#: windows/main/controller.py:275 +#, python-brace-format +msgid "{text} ({shortcut})" +msgstr "" + +#: windows/main/controller.py:281 windows/main/controller.py:294 +#, fuzzy +msgid "Execute all" +msgstr "SSH executable" + +#: windows/main/controller.py:471 windows/main/controller.py:479 +msgid "Query (1)" +msgstr "" + +#: windows/main/controller.py:497 +#, python-brace-format +msgid "Query ({query_number})" +msgstr "" + +#: windows/main/controller.py:530 +msgid "You have unsaved changes. Save before closing?" +msgstr "" + +#: windows/main/controller.py:531 +msgid "Unsaved query" +msgstr "" + +#: windows/main/controller.py:576 +msgid "Save query" +msgstr "" + +#: windows/main/controller.py:579 +msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" +msgstr "" + +#: windows/main/controller.py:616 windows/main/controller.py:642 +#: windows/main/database/list.py:84 windows/main/database/view.py:256 +#: windows/main/database/view.py:282 windows/main/query/controller.py:177 +msgid "Error" +msgstr "" + +#: windows/main/controller.py:622 +#, python-brace-format +msgid "-- Saved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:647 +#, python-brace-format +msgid "-- Autosaved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:704 msgid "days" msgstr "" -#: windows/main/controller.py:190 +#: windows/main/controller.py:705 msgid "hours" msgstr "" -#: windows/main/controller.py:191 +#: windows/main/controller.py:706 msgid "minutes" msgstr "" -#: windows/main/controller.py:192 +#: windows/main/controller.py:707 msgid "seconds" msgstr "" -#: windows/main/controller.py:200 +#: windows/main/controller.py:715 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "" -#: windows/main/controller.py:236 +#: windows/main/controller.py:751 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:441 +#: windows/main/controller.py:952 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:443 +#: windows/main/controller.py:954 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:608 +#: windows/main/controller.py:1119 msgid "Version" msgstr "" -#: windows/main/controller.py:610 +#: windows/main/controller.py:1121 msgid "Uptime" msgstr "" -#: windows/main/controller.py:678 +#: windows/main/controller.py:1199 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:711 +#: windows/main/controller.py:1232 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1037,147 +1067,171 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:716 windows/main/controller.py:737 +#: windows/main/controller.py:1237 windows/main/controller.py:1258 msgid "Delete database" msgstr "" -#: windows/main/controller.py:722 +#: windows/main/controller.py:1243 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:723 +#: windows/main/controller.py:1244 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:736 +#: windows/main/controller.py:1257 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:751 +#: windows/main/controller.py:1272 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:752 windows/main/tabs/view.py:253 -#: windows/main/tabs/view.py:279 +#: windows/main/controller.py:1273 windows/main/database/view.py:253 +#: windows/main/database/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:871 +#: windows/main/controller.py:1392 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:897 +#: windows/main/controller.py:1418 #, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "" -#: windows/main/controller.py:900 +#: windows/main/controller.py:1421 msgid "Delete table" msgstr "" -#: windows/main/controller.py:919 +#: windows/main/controller.py:1440 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1017 +#: windows/main/controller.py:1563 msgid "Do you want delete the records?" msgstr "" -#: windows/main/tabs/database.py:71 +#: windows/main/database/list.py:69 msgid "The connection to the database was lost." msgstr "" -#: windows/main/tabs/database.py:73 +#: windows/main/database/list.py:71 msgid "Do you want to reconnect?" msgstr "" -#: windows/main/tabs/database.py:75 +#: windows/main/database/list.py:73 msgid "Connection lost" msgstr "" -#: windows/main/tabs/database.py:85 +#: windows/main/database/list.py:83 msgid "Reconnection failed:" msgstr "" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:489 -#: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 -msgid "Error" +#: windows/main/database/view.py:252 +msgid "View created successfully" msgstr "" -#: windows/main/tabs/query.py:308 -#, python-brace-format -msgid "{affected_rows} rows affected" +#: windows/main/database/view.py:252 +msgid "View updated successfully" msgstr "" -#: windows/main/tabs/query.py:315 windows/main/tabs/query.py:339 +#: windows/main/database/view.py:256 #, python-brace-format -msgid "Query {query_number}" +msgid "Error saving view: {}" msgstr "" -#: windows/main/tabs/query.py:320 +#: windows/main/database/view.py:269 #, python-brace-format -msgid "Query {query_number} (Error)" +msgid "Are you sure you want to delete view '{}'?" +msgstr "" + +#: windows/main/database/view.py:270 +msgid "Confirm Delete" +msgstr "" + +#: windows/main/database/view.py:279 +msgid "View deleted successfully" msgstr "" -#: windows/main/tabs/query.py:334 +#: windows/main/database/view.py:282 #, python-brace-format -msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" +msgid "Error deleting view: {}" msgstr "" -#: windows/main/tabs/query.py:360 +#: windows/main/query/controller.py:110 #, python-brace-format -msgid "{rows_count} rows" +msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/tabs/query.py:362 +#: windows/main/query/controller.py:112 #, python-brace-format -msgid "{elapsed_ms:.1f} ms" +msgid "{elapsed_s:.2f} s" msgstr "" -#: windows/main/tabs/query.py:366 +#: windows/main/query/controller.py:115 +#, fuzzy +msgid "none" +msgstr "Engine" + +#: windows/main/query/controller.py:121 #, python-brace-format -msgid "{warnings_count} warnings" +msgid "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." msgstr "" -#: windows/main/tabs/query.py:381 -msgid "Error:" +#: windows/main/query/controller.py:134 +msgid "Query execution cancelled" msgstr "" -#: windows/main/tabs/query.py:488 +#: windows/main/query/controller.py:176 msgid "No active database connection" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View created successfully" +#: windows/main/query/renderer.py:53 +#, python-brace-format +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View updated successfully" +#: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 +#, python-brace-format +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/view.py:256 +#: windows/main/query/renderer.py:65 #, python-brace-format -msgid "Error saving view: {}" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/view.py:269 +#: windows/main/query/renderer.py:79 #, python-brace-format -msgid "Are you sure you want to delete view '{}'?" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/view.py:270 -msgid "Confirm Delete" +#: windows/main/query/renderer.py:165 +#, python-brace-format +msgid "{rows_count} rows" msgstr "" -#: windows/main/tabs/view.py:279 -msgid "View deleted successfully" +#: windows/main/query/renderer.py:167 +#, python-brace-format +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/view.py:282 +#: windows/main/query/renderer.py:171 #, python-brace-format -msgid "Error deleting view: {}" +msgid "{warnings_count} warnings" +msgstr "" + +#: windows/main/query/renderer.py:186 +msgid "Error:" msgstr "" #~ msgid "Created at:" @@ -1252,3 +1306,71 @@ msgstr "" #~ msgid " Most recent connection duration" #~ msgstr "" +#~ msgid "Edit Value" +#~ msgstr "" + +#~ msgid "Query #2" +#~ msgstr "" + +#~ msgid "Column5" +#~ msgstr "" + +#~ msgid "Import" +#~ msgstr "" + +#~ msgid "Read only" +#~ msgstr "" + +#~ msgid "CASCADED" +#~ msgstr "" + +#~ msgid "CHECK OPTION" +#~ msgstr "" + +#~ msgid "collapsible" +#~ msgstr "" + +#~ msgid "Column3" +#~ msgstr "" + +#~ msgid "Column4" +#~ msgstr "" + +#~ msgid "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#~ msgstr "" + +#~ msgid "Port" +#~ msgstr "" + +#~ msgid "Usage" +#~ msgstr "" + +#~ msgid "%(total_rows)s" +#~ msgstr "" + +#~ msgid "rows total" +#~ msgstr "" + +#~ msgid "Lines" +#~ msgstr "" + +#~ msgid "Temporary" +#~ msgstr "" + +#~ msgid "Engine options" +#~ msgstr "" + +#~ msgid "RadioBtn" +#~ msgstr "" + +#~ msgid "Edit Column" +#~ msgstr "" + +#~ msgid "Datatype" +#~ msgstr "" + +#~ msgid "Zero Fill" +#~ msgstr "" + diff --git a/locale/es_ES/LC_MESSAGES/petersql.mo b/locale/es_ES/LC_MESSAGES/petersql.mo index d1a10627380f680fcc7d536ebffa21c21c546d75..3da8b8a92215f161305347ca44870e7dd5ee79a7 100644 GIT binary patch delta 3460 zcmY+{dvwor9LMp`FB@acY{PEGh8VfzmNY9vHkvR~?rdxsW0<)l`%#MAiZ7<4o%})0 zTuy08%Nc(R>1a;pkjpuam7EUB>6~?(9IDgv_4~bb^f`NfAD_?n_IZCU-=8`#{A#-Y zdUVJRN7+TRBd&!wcL6_Y&Vh34N#}fc8(ZS97>W&~9$VACjA8hjdC#u@ zgDvUbh0!~Mak1D8JEIclfvs>LY9v{xgeKbg8L0afq7q+;N?<^9A}Y}a^CoID-bN0vPemDzM7?Mt4#7F7rKvK%L_JuC8rfM?f|u-k1L}o0QG4fi zR02)OQbwQ#(AwGwray&>UYv?bpeLqcI;w+0vjpR4FGo$~R#b=EQ5}4Y8ptQ8nK+7i zzSi1jPzjvJz!CJ)cu*Lj;?&n&wND!TEYT{wyw>2dR0RK}OhUoeUGJyb_AOp`ip zhq|v9>U=u#=SFZy!5OH5tU)DE5y$-N!Aedjfluv14QgsnTKgN+NbBwV71VP-TDuYT zoj*Wz9L~2K*zKr+Bp}Q0Qn3irQTOeSXZ}_Bj1zic4eEh&cEeTF(%i7?x6OyB&od&y zI=~*ZQ>{G#wIs7p9WO-8GO2a>y z?4cRTzonYuNNkUNPy-u_+AEW+U0^OiCFWm7MK9ij+6((onbzQFJcW8uH2-nHpj{%e z>}~)m(b1^;$D>9x9W|5laX9)>1386S`-`Zhy@Tv$ziYG`nzN&HAs)2^DX4^cqY@Zq z*RxR_=c1-^vYBVjLCshRK8>qUOLYh}V>PJv)?rh9|CgyK^XqouE-GUm^QexaP$NpP zb`Mm?15i^t7}Y@*DzORX6wIQXhZ@)c)cZb1{g55UaDD$LspyyK9BOUr&F{?%<|Wj9 zBZ-;BEMh1zolw#As`Mh}66);nvWkY5RLe5OE~x*&Xfv>boy;>|!7)S*;StG1XJQ&J>G}{VONqAwHU4ok=i}mFJGq?ppr}D$58JcI1CWyZsq!6`{W8 z^~9TY?h&J?JVy*7@(Df|HQU96xxhK7o1(k!|Nz zn8EpT?pfL+h#`bF;}T*v@f7i>XxD2)zDMjNdK3N~99|>l5pNMugvuMlCgL4p0nw3o ziBQqk@~FIQl@u%_wh;P!7ZPKMRH7#_jF>~HbRzl^YxMoc>I~&oB8_-o4a!u4AGbi6 z!Epf5gxD516Fh{%^8{aP;D269gQo-k{jkgGVv^NcV;4d{SSl|Bc2KA4kuAe~UQuL~ zuhd)B>adp)Rp*6A=T;X)xA%pPD=S#MqIze{exH}wrrbN%CM~3}sJPfmZkt=}Z@bx7 zofwzn^VY}Lh7QduEhzPB5-xgGiGO-k?Fy@7lk$9F!%7yEmK86_tM=MA`n)a4yS<(% OIbKD|G_Og=uKxo2GcmOQ literal 8746 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 diff --git a/locale/es_ES/LC_MESSAGES/petersql.po b/locale/es_ES/LC_MESSAGES/petersql.po index 595b0fc..e3de2d7 100644 --- a/locale/es_ES/LC_MESSAGES/petersql.po +++ b/locale/es_ES/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-12 19:04+0100\n" +"POT-Creation-Date: 2026-03-23 10:07+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: es_ES\n" @@ -47,66 +47,62 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "Cliente OpenSSH no encontrado." -#: structures/engines/mariadb/context.py:592 -#: structures/engines/mysql/context.py:563 -#: structures/engines/postgresql/context.py:579 -#: structures/engines/sqlite/context.py:518 +#: structures/engines/mariadb/context.py:611 +#: structures/engines/mysql/context.py:622 +#: structures/engines/postgresql/context.py:645 +#: structures/engines/sqlite/context.py:524 #, python-brace-format msgid "Table{table_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:620 -#: structures/engines/mysql/context.py:591 -#: structures/engines/postgresql/context.py:604 -#: structures/engines/sqlite/context.py:542 +#: structures/engines/mariadb/context.py:639 +#: structures/engines/mysql/context.py:650 +#: structures/engines/postgresql/context.py:670 +#: structures/engines/sqlite/context.py:548 #, python-brace-format msgid "Column{column_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:638 -#: structures/engines/mysql/context.py:609 -#: structures/engines/postgresql/context.py:622 -#: structures/engines/sqlite/context.py:560 +#: structures/engines/mariadb/context.py:657 +#: structures/engines/mysql/context.py:668 +#: structures/engines/postgresql/context.py:688 +#: structures/engines/sqlite/context.py:566 #, python-brace-format msgid "Index{index_number:03}" msgstr "" -#: structures/engines/mariadb/context.py:678 -#: structures/engines/mysql/context.py:649 -#: structures/engines/postgresql/context.py:662 -#: structures/engines/sqlite/context.py:602 +#: structures/engines/mariadb/context.py:697 +#: structures/engines/mysql/context.py:706 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:608 #, python-brace-format msgid "ForeignKey{foreign_key_number:03}" msgstr "" -#: structures/engines/mariadb/context.py:711 -#: structures/engines/mysql/context.py:680 -#: structures/engines/postgresql/context.py:692 -#: structures/engines/sqlite/context.py:630 +#: structures/engines/mariadb/context.py:730 +#: structures/engines/mysql/context.py:737 +#: structures/engines/postgresql/context.py:758 +#: structures/engines/sqlite/context.py:636 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:762 +#: structures/engines/mariadb/context.py:781 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "" -#: windows/dialogs/connections/view.py:402 -#: windows/dialogs/connections/view.py:739 windows/main/controller.py:606 +#: windows/dialogs/connections/view.py:415 +#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 #: windows/views.py:33 msgid "Connection" msgstr "Conexión" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:1003 -#: windows/views.py:1353 windows/views.py:1460 windows/views.py:1843 -#: windows/views.py:2788 windows/views.py:2811 windows/views.py:2812 -#: windows/views.py:2813 windows/views.py:2814 windows/views.py:2815 -#: windows/views.py:2816 windows/views.py:2817 windows/views.py:2818 -#: windows/views.py:2819 windows/views.py:2823 windows/views.py:3017 -#: windows/views.py:3218 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1006 +#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 +#: windows/views.py:2821 msgid "Name" msgstr "Nombre" @@ -114,12 +110,12 @@ msgstr "Nombre" msgid "Last connection" msgstr "Última conexión" -#: windows/dialogs/connections/view.py:640 windows/views.py:61 +#: windows/dialogs/connections/view.py:653 windows/views.py:61 msgid "New directory" msgstr "Nuevo directorio" #: windows/dialogs/connections/model.py:206 -#: windows/dialogs/connections/view.py:600 windows/views.py:65 +#: windows/dialogs/connections/view.py:613 windows/views.py:65 msgid "New connection" msgstr "Nueva conexión" @@ -133,14 +129,14 @@ msgstr "Nombre" msgid "Clone connection" msgstr "Nueva conexión" -#: windows/views.py:81 windows/views.py:603 windows/views.py:1337 -#: windows/views.py:1378 windows/views.py:1753 windows/views.py:1785 -#: windows/views.py:2044 windows/views.py:3354 windows/views.py:3386 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1322 +#: windows/views.py:1365 windows/views.py:1773 windows/views.py:2032 +#: windows/views.py:2726 windows/views.py:2957 windows/views.py:2989 msgid "Delete" msgstr "Eliminar" -#: windows/views.py:111 windows/views.py:1358 windows/views.py:1515 -#: windows/views.py:2828 windows/views.py:3273 +#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 +#: windows/views.py:2876 msgid "Engine" msgstr "Motor" @@ -152,7 +148,7 @@ msgstr "Host + puerto" msgid "Username" msgstr "Nombre de usuario" -#: windows/views.py:161 windows/views.py:1147 +#: windows/views.py:161 windows/views.py:1132 msgid "Password" msgstr "Contraseña" @@ -173,27 +169,26 @@ msgstr "Usar túnel SSH" msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 windows/views.py:2749 +#: windows/views.py:233 msgid "Filename" msgstr "Nombre de archivo" -#: windows/views.py:238 windows/views.py:358 windows/views.py:2754 -#: windows/views.py:2937 +#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 msgid "Select a file" msgstr "Seleccionar un archivo" -#: windows/views.py:238 windows/views.py:358 windows/views.py:2937 +#: windows/views.py:238 windows/views.py:358 #, fuzzy msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:255 windows/views.py:1360 windows/views.py:1473 -#: windows/views.py:2829 windows/views.py:3115 windows/views.py:3231 +#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 +#: windows/views.py:2834 msgid "Comments" msgstr "Comentarios" -#: windows/main/controller.py:236 windows/views.py:269 windows/views.py:730 +#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 #: windows/views.py:884 msgid "Settings" msgstr "Configuraciones" @@ -253,7 +248,7 @@ msgstr "" msgid "SSH Tunnel" msgstr "Túnel SSH" -#: windows/views.py:411 windows/views.py:1356 windows/views.py:2826 +#: windows/views.py:411 windows/views.py:1341 msgid "Created at" msgstr "Creado en" @@ -293,7 +288,7 @@ msgstr "Abrir administrador de conexiones" msgid "Statistics" msgstr "Estadísticas" -#: windows/views.py:584 windows/views.py:1719 +#: windows/views.py:584 windows/views.py:1730 msgid "Create" msgstr "Crear" @@ -307,14 +302,15 @@ msgstr "Última conexión" msgid "Create directory" msgstr "Nuevo directorio" -#: windows/views.py:620 windows/views.py:844 windows/views.py:1375 -#: windows/views.py:1788 windows/views.py:2049 windows/views.py:2152 -#: windows/views.py:3162 windows/views.py:3389 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1360 +#: windows/views.py:1776 windows/views.py:2037 windows/views.py:2093 +#: windows/views.py:2702 windows/views.py:2992 windows/views.py:3129 msgid "Cancel" msgstr "Cancelar" -#: windows/views.py:625 windows/views.py:2054 windows/views.py:3172 -#: windows/views.py:3394 +#: windows/main/controller.py:283 windows/main/controller.py:300 +#: windows/main/controller.py:301 windows/views.py:625 windows/views.py:2042 +#: windows/views.py:2235 windows/views.py:2997 windows/views.py:3135 msgid "Save" msgstr "Guardar" @@ -347,8 +343,9 @@ msgid "Locale" msgstr "Localización" #: windows/views.py:780 -msgid "Edit Value" -msgstr "Editar valor" +#, fuzzy +msgid "Column content" +msgstr "Nueva conexión" #: windows/views.py:790 msgid "Syntax" @@ -378,7 +375,7 @@ msgstr "Ayuda" msgid "Open connection manager" msgstr "Abrir administrador de conexiones" -#: windows/views.py:900 +#: windows/views.py:902 msgid "Disconnect from server" msgstr "Desconectar del servidor" @@ -390,21 +387,21 @@ msgstr "herramienta" msgid "Refresh" msgstr "Actualizar" -#: windows/views.py:908 windows/views.py:910 +#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 +#: windows/views.py:2077 windows/views.py:2221 msgid "Add" msgstr "Agregar" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2232 -#: windows/views.py:2735 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 msgid "MyMenuItem" msgstr "MiElementoMenu" -#: windows/views.py:951 windows/views.py:1816 windows/views.py:3417 +#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 msgid "MyMenu" msgstr "MiMenu" -#: windows/views.py:966 windows/views.py:1397 windows/views.py:1404 -#: windows/views.py:1411 +#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 +#: windows/views.py:1402 msgid "MyLabel" msgstr "MiEtiqueta" @@ -412,8 +409,7 @@ msgstr "MiEtiqueta" msgid "Databases" msgstr "Bases de datos" -#: windows/views.py:973 windows/views.py:1355 windows/views.py:2797 -#: windows/views.py:2825 +#: windows/views.py:973 windows/views.py:1340 msgid "Size" msgstr "Tamaño" @@ -433,283 +429,269 @@ msgstr "Tablas" msgid "System" msgstr "Sistema" -#: windows/views.py:1026 -#, fuzzy -msgid "Character set" -msgstr "Creado en" - #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1047 -#: windows/views.py:1359 windows/views.py:3061 +#: windows/components/dataview.py:89 windows/views.py:1029 +#: windows/views.py:1344 msgid "Collation" msgstr "Intercalación" -#: windows/views.py:1073 windows/views.py:2943 +#: windows/views.py:1058 msgid "Encryption" msgstr "" -#: windows/views.py:1085 +#: windows/views.py:1070 msgid "Read Only" msgstr "" -#: windows/views.py:1102 +#: windows/views.py:1087 #, fuzzy msgid "Tablespace" msgstr "Tablas" -#: windows/views.py:1123 +#: windows/views.py:1108 #, fuzzy msgid "Connection limit" msgstr "Conexión perdida" -#: windows/views.py:1166 +#: windows/views.py:1151 #, fuzzy msgid "Profile" msgstr "Archivo" -#: windows/views.py:1192 +#: windows/views.py:1177 #, fuzzy msgid "Default tablespace" msgstr "Eliminar tabla" -#: windows/views.py:1213 +#: windows/views.py:1198 #, fuzzy msgid "Temporary tablespace" msgstr "Temporal" -#: windows/views.py:1239 +#: windows/views.py:1224 msgid "Quota" msgstr "" -#: windows/views.py:1258 +#: windows/views.py:1243 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1275 +#: windows/views.py:1260 msgid "Account status" msgstr "" -#: windows/views.py:1296 +#: windows/views.py:1281 #, fuzzy msgid "Password expire" msgstr "Contraseña" -#: windows/views.py:1317 +#: windows/views.py:1302 msgid "Table:" msgstr "Tabla:" -#: windows/views.py:1325 windows/views.py:1598 windows/views.py:1642 -#: windows/views.py:1748 windows/views.py:3349 +#: 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:1330 +#: windows/views.py:1315 msgid "Clone" msgstr "Clonar" -#: windows/views.py:1354 +#: windows/views.py:1339 msgid "Rows" msgstr "Filas" -#: windows/views.py:1357 windows/views.py:2827 +#: windows/views.py:1342 msgid "Updated at" msgstr "Actualizado en" -#: windows/views.py:1381 windows/views.py:1793 windows/views.py:2159 -#: windows/views.py:2206 +#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 +#: windows/views.py:2173 windows/views.py:2709 msgid "Apply" msgstr "Aplicar" -#: windows/views.py:1391 windows/views.py:1550 windows/views.py:2003 -#: windows/views.py:3306 +#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 +#: windows/views.py:2909 msgid "Options" msgstr "Opciones" -#: windows/views.py:1422 +#: windows/views.py:1413 msgid "Diagram" msgstr "Diagrama" -#: windows/views.py:1433 windows/views.py:2796 +#: windows/views.py:1424 msgid "Database" msgstr "Base de datos" -#: windows/views.py:1488 windows/views.py:3246 +#: windows/views.py:1479 windows/views.py:2849 msgid "Base" msgstr "Base" -#: windows/views.py:1502 windows/views.py:3260 +#: windows/views.py:1493 windows/views.py:2863 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1530 windows/views.py:3288 +#: windows/views.py:1521 windows/views.py:2891 msgid "Default Collation" msgstr "Intercalación predeterminada" -#: windows/views.py:1562 windows/views.py:1603 windows/views.py:1647 +#: windows/views.py:1531 +msgid "Convert data" +msgstr "" + +#: windows/views.py:1539 +msgid "Row format" +msgstr "" + +#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 +#: windows/views.py:1756 windows/views.py:2081 msgid "Remove" msgstr "Eliminar" -#: windows/views.py:1569 windows/views.py:1610 windows/views.py:1654 +#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 msgid "Clear" msgstr "Limpiar" -#: windows/views.py:1584 windows/views.py:3320 +#: windows/views.py:1595 windows/views.py:2923 msgid "Indexes" msgstr "Índices" -#: windows/views.py:1628 +#: windows/views.py:1639 msgid "Foreign Keys" msgstr "Claves foráneas" -#: windows/views.py:1672 +#: windows/views.py:1683 msgid "Checks" msgstr "Comprobaciones" -#: windows/views.py:1740 windows/views.py:3341 +#: windows/views.py:1750 windows/views.py:2944 msgid "Columns:" msgstr "Columnas:" -#: windows/views.py:1760 windows/views.py:3361 -msgid "Up" -msgstr "Arriba" +#: windows/views.py:1760 +#, fuzzy +msgid "Move Up" +msgstr "Mover arriba\tCTRL+UP" -#: windows/views.py:1767 windows/views.py:3368 -msgid "Down" -msgstr "Abajo" +#: windows/views.py:1762 +#, fuzzy +msgid "Move Down" +msgstr "Mover abajo\tCTRL+D" -#: windows/views.py:1806 windows/views.py:1813 windows/views.py:3407 -#: windows/views.py:3414 +#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 +#: windows/views.py:3017 msgid "Add Index" msgstr "Agregar índice" -#: windows/views.py:1810 windows/views.py:3411 +#: windows/views.py:1798 windows/views.py:3014 msgid "Add PrimaryKey" msgstr "Agregar clave primaria" -#: windows/views.py:1827 +#: windows/views.py:1815 msgid "Table" msgstr "Tabla" -#: windows/views.py:1863 +#: windows/views.py:1851 #, fuzzy msgid "Definer" msgstr "Insertar" -#: windows/views.py:1883 +#: windows/views.py:1871 msgid "Schema" msgstr "" -#: windows/views.py:1909 +#: windows/views.py:1897 msgid "SQL security" msgstr "" -#: windows/views.py:1916 +#: windows/views.py:1904 #, fuzzy msgid "DEFINER" msgstr "Insertar" -#: windows/views.py:1916 +#: windows/views.py:1904 #, fuzzy msgid "INVOKER" msgstr "Insertar" -#: windows/views.py:1928 windows/views.py:2639 windows/views.py:2658 -#: windows/views.py:2901 +#: windows/views.py:1916 msgid "Algorithm" msgstr "" -#: windows/views.py:1930 windows/views.py:2624 windows/views.py:2657 -#: windows/views.py:2906 +#: windows/views.py:1918 #, fuzzy msgid "UNDEFINED" msgstr "Sin signo" -#: windows/views.py:1933 windows/views.py:2627 windows/views.py:2657 -#: windows/views.py:2909 +#: windows/views.py:1921 msgid "MERGE" msgstr "" -#: windows/views.py:1936 windows/views.py:2636 windows/views.py:2657 -#: windows/views.py:2912 +#: windows/views.py:1924 #, fuzzy msgid "TEMPTABLE" msgstr "Tabla" -#: windows/views.py:1946 windows/views.py:2663 +#: windows/views.py:1934 msgid "View constraint" msgstr "" -#: windows/views.py:1948 windows/views.py:2662 +#: windows/views.py:1936 #, fuzzy msgid "None" msgstr "Clonar" -#: windows/views.py:1951 windows/views.py:2662 +#: windows/views.py:1939 #, fuzzy msgid "LOCAL" msgstr "Localización" -#: windows/views.py:1954 +#: windows/views.py:1942 #, fuzzy msgid "CASCADE" msgstr "Cancelar" -#: windows/views.py:1957 +#: windows/views.py:1945 #, fuzzy msgid "CHECK ONLY" msgstr "Verificar" -#: windows/views.py:1960 windows/views.py:2662 +#: windows/views.py:1948 msgid "READ ONLY" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:1960 msgid "Force" msgstr "" -#: windows/views.py:1984 +#: windows/views.py:1972 msgid "Security barrier" msgstr "" -#: windows/views.py:2066 +#: windows/views.py:2054 msgid "Views" msgstr "Vistas" -#: windows/views.py:2074 +#: windows/views.py:2062 msgid "Triggers" msgstr "Disparadores" -#: windows/views.py:2086 -#, python-brace-format -msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" - -#: windows/views.py:2094 +#: windows/views.py:2073 #, fuzzy -msgid "First" -msgstr "Filtros" - -#: windows/views.py:2112 -msgid "Last" -msgstr "" - -#: windows/views.py:2123 -msgid "Insert record" -msgstr "Insertar registro" +msgid "Refrsh" +msgstr "Actualizar" -#: windows/views.py:2128 -msgid "Duplicate record" +#: windows/views.py:2079 +#, fuzzy +msgid "Duplicate" msgstr "Duplicar registro" -#: windows/views.py:2135 -msgid "Delete record" -msgstr "Eliminar registro" - -#: windows/views.py:2145 +#: windows/views.py:2085 msgid "Apply changes automatically" msgstr "Aplicar cambios automáticamente" -#: windows/views.py:2147 windows/views.py:2148 +#: windows/views.py:2087 windows/views.py:2088 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -717,157 +699,152 @@ msgstr "" "Si está habilitado, las ediciones de la tabla se aplican inmediatamente " "sin presionar Aplicar o Cancelar" -#: windows/views.py:2169 +#: windows/views.py:2101 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" + +#: windows/views.py:2109 +#, fuzzy +msgid "First" +msgstr "Filtros" + +#: windows/views.py:2127 +msgid "Last" +msgstr "" + +#: windows/views.py:2136 msgid "Filters" msgstr "Filtros" -#: windows/views.py:2209 +#: windows/views.py:2176 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2229 +#: windows/views.py:2196 msgid "Insert row" msgstr "Insertar fila" -#: windows/views.py:2237 +#: windows/views.py:2204 msgid "Data" msgstr "Datos" -#: windows/views.py:2291 windows/views.py:2341 -msgid "New" -msgstr "Nuevo" - -#: windows/views.py:2318 -msgid "Query" +#: windows/main/controller.py:278 windows/main/controller.py:287 +#: windows/main/controller.py:288 windows/views.py:2221 +#, fuzzy +msgid "New query" msgstr "Consulta" -#: windows/views.py:2338 +#: windows/views.py:2223 windows/views.py:2660 msgid "Close" msgstr "Cerrar" -#: windows/views.py:2351 -msgid "Query #2" -msgstr "Consulta #2" +#: windows/main/controller.py:279 windows/main/controller.py:289 +#: windows/main/controller.py:290 windows/views.py:2223 +#, fuzzy +msgid "Close query" +msgstr "Consulta" -#: windows/views.py:2618 -msgid "Column5" -msgstr "Columna5" +#: windows/views.py:2227 +msgid "Run" +msgstr "" -#: windows/views.py:2629 -msgid "Import" -msgstr "Importar" +#: windows/main/controller.py:280 windows/main/controller.py:292 +#: windows/main/controller.py:293 windows/views.py:2227 +#, fuzzy +msgid "Execute" +msgstr "Ejecutable SSH" -#: windows/views.py:2654 -msgid "Read only" +#: windows/views.py:2229 +msgid "Run all" msgstr "" -#: windows/views.py:2662 -msgid "CASCADED" +#: windows/main/controller.py:295 windows/views.py:2229 +msgid "Execute all statements" msgstr "" -#: windows/views.py:2662 -#, fuzzy -msgid "CHECK OPTION" -msgstr "conexión" +#: windows/main/controller.py:282 windows/main/controller.py:297 +#: windows/main/controller.py:298 windows/views.py:2231 +msgid "Stop" +msgstr "" -#: windows/views.py:2694 -msgid "collapsible" -msgstr "colapsable" +#: windows/views.py:2287 +msgid "a page" +msgstr "" -#: windows/views.py:2710 -msgid "Column3" -msgstr "Columna3" +#: windows/views.py:2315 +msgid "Query" +msgstr "Consulta" -#: windows/views.py:2711 -msgid "Column4" -msgstr "Columna4" +#: windows/views.py:2626 +#, fuzzy +msgid "Character set" +msgstr "Creado en" -#: windows/views.py:2754 -msgid "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" -msgstr "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#: windows/views.py:2644 windows/views.py:2663 +msgid "New" +msgstr "Nuevo" -#: windows/views.py:2766 -msgid "Port" -msgstr "Puerto" +#: windows/views.py:2683 +msgid "Insert record" +msgstr "Insertar registro" -#: windows/views.py:2789 -msgid "Usage" -msgstr "Uso" +#: windows/views.py:2688 +msgid "Duplicate record" +msgstr "Duplicar registro" -#: windows/views.py:2800 -#, python-format -msgid "%(total_rows)s" -msgstr "%(total_rows)s" +#: windows/views.py:2695 +msgid "Delete record" +msgstr "Eliminar registro" -#: windows/views.py:2805 -msgid "rows total" -msgstr "filas en total" +#: windows/views.py:2733 windows/views.py:2964 +msgid "Up" +msgstr "Arriba" -#: windows/views.py:2824 -msgid "Lines" -msgstr "Líneas" +#: windows/views.py:2740 windows/views.py:2971 +msgid "Down" +msgstr "Abajo" -#: windows/views.py:2856 -msgid "Temporary" -msgstr "Temporal" +#: windows/views.py:3100 +msgid "Save Starments" +msgstr "" -#: windows/views.py:2867 +#: windows/views.py:3108 #, fuzzy -msgid "Engine options" -msgstr "Opciones" +msgid "Location" +msgstr "Intercalación" -#: windows/views.py:2926 -msgid "RadioBtn" +#: windows/views.py:3115 +msgid "*.sql" msgstr "" -#: windows/views.py:3009 -msgid "Edit Column" -msgstr "Editar columna" - -#: windows/views.py:3025 -msgid "Datatype" -msgstr "Tipo de datos" - -#: windows/components/dataview.py:121 windows/views.py:3040 -msgid "Length/Set" -msgstr "Longitud/Conjunto" - -#: windows/components/dataview.py:51 windows/views.py:3079 -msgid "Unsigned" -msgstr "Sin signo" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3085 +#: windows/components/dataview.py:75 msgid "Allow NULL" msgstr "Permitir NULL" -#: windows/views.py:3091 -msgid "Zero Fill" -msgstr "Relleno cero" +#: windows/components/dataview.py:28 +msgid "Check" +msgstr "Verificar" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3102 +#: windows/components/dataview.py:78 msgid "Default" msgstr "Predeterminado" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3128 +#: windows/components/dataview.py:82 msgid "Virtuality" msgstr "Virtualidad" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3143 msgid "Expression" msgstr "Expresión" -#: windows/components/dataview.py:28 -msgid "Check" -msgstr "Verificar" +#: windows/components/dataview.py:51 +msgid "Unsigned" +msgstr "Sin signo" #: windows/components/dataview.py:53 msgid "Zerofill" @@ -881,6 +858,10 @@ msgstr "#" msgid "Data type" msgstr "Tipo de datos" +#: windows/components/dataview.py:121 +msgid "Length/Set" +msgstr "Longitud/Conjunto" + #: windows/components/dataview.py:155 msgid "Add column\tCTRL+INS" msgstr "Agregar columna\tCTRL+INS" @@ -957,111 +938,164 @@ msgstr "AUTO INCREMENTO" msgid "Text/Expression" msgstr "Texto/Expresión" -#: windows/dialogs/connections/view.py:122 windows/main/tabs/query.py:387 +#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:401 +#: windows/dialogs/connections/view.py:414 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:413 +#: windows/dialogs/connections/view.py:426 #, python-brace-format msgid "Do you want save the connection {connection_name}?" msgstr "" -#: windows/dialogs/connections/view.py:416 +#: windows/dialogs/connections/view.py:429 msgid "Confirm save" msgstr "Confirmar guardar" -#: windows/dialogs/connections/view.py:468 +#: windows/dialogs/connections/view.py:481 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:470 +#: windows/dialogs/connections/view.py:483 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:737 +#: windows/dialogs/connections/view.py:750 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:775 #, fuzzy, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "Error de conexión" -#: windows/dialogs/connections/view.py:763 +#: windows/dialogs/connections/view.py:776 msgid "Connection error" msgstr "Error de conexión" -#: windows/dialogs/connections/view.py:789 +#: windows/dialogs/connections/view.py:802 #, fuzzy, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" msgstr "¿Quieres eliminar los registros?" -#: windows/dialogs/connections/view.py:792 -#: windows/dialogs/connections/view.py:809 +#: windows/dialogs/connections/view.py:805 +#: windows/dialogs/connections/view.py:822 msgid "Confirm delete" msgstr "Confirmar eliminar" -#: windows/dialogs/connections/view.py:806 +#: windows/dialogs/connections/view.py:819 #, fuzzy, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "¿Quieres eliminar los registros?" -#: windows/main/controller.py:189 +#: windows/main/controller.py:275 +#, python-brace-format +msgid "{text} ({shortcut})" +msgstr "" + +#: windows/main/controller.py:281 windows/main/controller.py:294 +#, fuzzy +msgid "Execute all" +msgstr "Ejecutable SSH" + +#: windows/main/controller.py:471 windows/main/controller.py:479 +#, fuzzy +msgid "Query (1)" +msgstr "Consulta" + +#: windows/main/controller.py:497 +#, python-brace-format +msgid "Query ({query_number})" +msgstr "" + +#: windows/main/controller.py:530 +msgid "You have unsaved changes. Save before closing?" +msgstr "" + +#: windows/main/controller.py:531 +msgid "Unsaved query" +msgstr "" + +#: windows/main/controller.py:576 +#, fuzzy +msgid "Save query" +msgstr "Consulta" + +#: windows/main/controller.py:579 +msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" +msgstr "" + +#: windows/main/controller.py:616 windows/main/controller.py:642 +#: windows/main/database/list.py:84 windows/main/database/view.py:256 +#: windows/main/database/view.py:282 windows/main/query/controller.py:177 +msgid "Error" +msgstr "Error" + +#: windows/main/controller.py:622 +#, python-brace-format +msgid "-- Saved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:647 +#, python-brace-format +msgid "-- Autosaved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:704 msgid "days" msgstr "días" -#: windows/main/controller.py:190 +#: windows/main/controller.py:705 msgid "hours" msgstr "horas" -#: windows/main/controller.py:191 +#: windows/main/controller.py:706 msgid "minutes" msgstr "minutos" -#: windows/main/controller.py:192 +#: windows/main/controller.py:707 msgid "seconds" msgstr "segundos" -#: windows/main/controller.py:200 +#: windows/main/controller.py:715 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizada: {used} ({percentage:.2%})" -#: windows/main/controller.py:236 +#: windows/main/controller.py:751 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:441 +#: windows/main/controller.py:952 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:443 +#: windows/main/controller.py:954 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:608 +#: windows/main/controller.py:1119 msgid "Version" msgstr "Versión" -#: windows/main/controller.py:610 +#: windows/main/controller.py:1121 msgid "Uptime" msgstr "Tiempo de actividad" -#: windows/main/controller.py:678 +#: windows/main/controller.py:1199 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:711 +#: windows/main/controller.py:1232 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1071,153 +1105,177 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:716 windows/main/controller.py:737 +#: windows/main/controller.py:1237 windows/main/controller.py:1258 #, fuzzy msgid "Delete database" msgstr "Eliminar tabla" -#: windows/main/controller.py:722 +#: windows/main/controller.py:1243 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:723 +#: windows/main/controller.py:1244 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:736 +#: windows/main/controller.py:1257 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:751 +#: windows/main/controller.py:1272 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:752 windows/main/tabs/view.py:253 -#: windows/main/tabs/view.py:279 +#: windows/main/controller.py:1273 windows/main/database/view.py:253 +#: windows/main/database/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:871 +#: windows/main/controller.py:1392 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:897 +#: windows/main/controller.py:1418 #, fuzzy, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "¿Quieres eliminar los registros?" -#: windows/main/controller.py:900 +#: windows/main/controller.py:1421 msgid "Delete table" msgstr "Eliminar tabla" -#: windows/main/controller.py:919 +#: windows/main/controller.py:1440 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1017 +#: windows/main/controller.py:1563 msgid "Do you want delete the records?" msgstr "¿Quieres eliminar los registros?" -#: windows/main/tabs/database.py:71 +#: windows/main/database/list.py:69 msgid "The connection to the database was lost." msgstr "Se perdió la conexión a la base de datos." -#: windows/main/tabs/database.py:73 +#: windows/main/database/list.py:71 msgid "Do you want to reconnect?" msgstr "¿Quieres reconectar?" -#: windows/main/tabs/database.py:75 +#: windows/main/database/list.py:73 msgid "Connection lost" msgstr "Conexión perdida" -#: windows/main/tabs/database.py:85 +#: windows/main/database/list.py:83 msgid "Reconnection failed:" msgstr "Reconexión fallida:" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:489 -#: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 -msgid "Error" -msgstr "Error" +#: windows/main/database/view.py:252 +msgid "View created successfully" +msgstr "" -#: windows/main/tabs/query.py:308 -#, python-brace-format -msgid "{affected_rows} rows affected" +#: windows/main/database/view.py:252 +msgid "View updated successfully" msgstr "" -#: windows/main/tabs/query.py:315 windows/main/tabs/query.py:339 +#: windows/main/database/view.py:256 #, python-brace-format -msgid "Query {query_number}" +msgid "Error saving view: {}" msgstr "" -#: windows/main/tabs/query.py:320 +#: windows/main/database/view.py:269 #, python-brace-format -msgid "Query {query_number} (Error)" +msgid "Are you sure you want to delete view '{}'?" msgstr "" -#: windows/main/tabs/query.py:334 -#, python-brace-format -msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" +#: windows/main/database/view.py:270 +#, fuzzy +msgid "Confirm Delete" +msgstr "Confirmar eliminar" + +#: windows/main/database/view.py:279 +msgid "View deleted successfully" msgstr "" -#: windows/main/tabs/query.py:360 +#: windows/main/database/view.py:282 #, python-brace-format -msgid "{rows_count} rows" +msgid "Error deleting view: {}" msgstr "" -#: windows/main/tabs/query.py:362 +#: windows/main/query/controller.py:110 #, python-brace-format -msgid "{elapsed_ms:.1f} ms" +msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/tabs/query.py:366 +#: windows/main/query/controller.py:112 #, python-brace-format -msgid "{warnings_count} warnings" +msgid "{elapsed_s:.2f} s" msgstr "" -#: windows/main/tabs/query.py:381 +#: windows/main/query/controller.py:115 #, fuzzy -msgid "Error:" -msgstr "Error" +msgid "none" +msgstr "Clonar" -#: windows/main/tabs/query.py:488 +#: windows/main/query/controller.py:121 +#, python-brace-format +msgid "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." +msgstr "" + +#: windows/main/query/controller.py:134 +msgid "Query execution cancelled" +msgstr "" + +#: windows/main/query/controller.py:176 #, fuzzy msgid "No active database connection" msgstr "Nueva conexión" -#: windows/main/tabs/view.py:252 -msgid "View created successfully" +#: windows/main/query/renderer.py:53 +#, python-brace-format +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View updated successfully" +#: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 +#, python-brace-format +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/view.py:256 +#: windows/main/query/renderer.py:65 #, python-brace-format -msgid "Error saving view: {}" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/view.py:269 +#: windows/main/query/renderer.py:79 #, python-brace-format -msgid "Are you sure you want to delete view '{}'?" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/view.py:270 -#, fuzzy -msgid "Confirm Delete" -msgstr "Confirmar eliminar" +#: windows/main/query/renderer.py:165 +#, python-brace-format +msgid "{rows_count} rows" +msgstr "" -#: windows/main/tabs/view.py:279 -msgid "View deleted successfully" +#: windows/main/query/renderer.py:167 +#, python-brace-format +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/view.py:282 +#: windows/main/query/renderer.py:171 #, python-brace-format -msgid "Error deleting view: {}" +msgid "{warnings_count} warnings" msgstr "" +#: windows/main/query/renderer.py:186 +#, fuzzy +msgid "Error:" +msgstr "Error" + #~ msgid "Created at:" #~ msgstr "" @@ -1283,3 +1341,73 @@ msgstr "" #~ msgid "{} warnings" #~ msgstr "" +#~ msgid "Edit Value" +#~ msgstr "Editar valor" + +#~ msgid "Query #2" +#~ msgstr "Consulta #2" + +#~ msgid "Column5" +#~ msgstr "Columna5" + +#~ msgid "Import" +#~ msgstr "Importar" + +#~ msgid "Read only" +#~ msgstr "" + +#~ msgid "CASCADED" +#~ msgstr "" + +#~ msgid "CHECK OPTION" +#~ msgstr "conexión" + +#~ msgid "collapsible" +#~ msgstr "colapsable" + +#~ msgid "Column3" +#~ msgstr "Columna3" + +#~ msgid "Column4" +#~ msgstr "Columna4" + +#~ msgid "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#~ msgstr "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" + +#~ msgid "Port" +#~ msgstr "Puerto" + +#~ msgid "Usage" +#~ msgstr "Uso" + +#~ msgid "%(total_rows)s" +#~ msgstr "%(total_rows)s" + +#~ msgid "rows total" +#~ msgstr "filas en total" + +#~ msgid "Lines" +#~ msgstr "Líneas" + +#~ msgid "Temporary" +#~ msgstr "Temporal" + +#~ msgid "Engine options" +#~ msgstr "Opciones" + +#~ msgid "RadioBtn" +#~ msgstr "" + +#~ msgid "Edit Column" +#~ msgstr "Editar columna" + +#~ msgid "Datatype" +#~ msgstr "Tipo de datos" + +#~ msgid "Zero Fill" +#~ msgstr "Relleno cero" + diff --git a/locale/fr_FR/LC_MESSAGES/petersql.mo b/locale/fr_FR/LC_MESSAGES/petersql.mo index 18340790dd95918fe3dd85cad7e9c076edab477a..b40cd26c0c3477da3b84ce1a6715949848ff38be 100644 GIT binary patch delta 3324 zcmYk-du)wo0LSs??37-tdYr1N)~W6&W$M)QF%lci$k8mOVKF2NLozd$1WOE?WY}N2i0u12=UL)Sf9LbOx95INuZ})B!nx*; zX)+w!Nd{?+G3F#z#Ph+i@M&W_Sce_49^2t6^x_88qnqsgPcfPEJ(z@tF#&(F=NC}# z-^6rd9P<|iRop(&7}_=|s293nd(1|?(9fO^w&w-bBI{UGM@p>IurueC7>A3kEA0Js z*pc?lRtogNe1WmJAJu_F=)+^Ek(@_$=!U(18}-~nRL8xHLLG=hHR!kJ>8N_Mu@mN^ z>M6oR+Bd}%)PZu;gH_lCYcLZVF%7>&HS|43I)-ZKJgUOWsP}K6X6z2CgMXvmYePN% z81-B%qjJ=65(QO|fqF2A9WV!{U_R=FwW#MdBZD^EQ4Q>}_nT1-v><=xC?C3i)_NKB z{!Q$KcaxZZHRNYF8c~1`?wJ9o28&QHj6+p48P&m=*4d~{Sd9$I%tLi-3G!#w@bMHj zp_b-z>n=>?e7}$R*VLZif*QPr8o@2=J=9b_L~W+WsNJ2+LaL*^QSS{yb+ibzbj9e$ zS*Q*#L=B(;)zL=O`&%3ekUsGxYuUq(p!D0;m^9Ae}Q4P!-I=XK@~C>ReO@ze0_4FRFns>iHw~ z`bm5JJZd19tk>=RTaxz80}A|^NA`xFh1H(uj;gRPYDw}@yLOVj9m6zaKi=%{dm0{h22Kz$v(v_jD&p&TCLJ zv<3rs0JW6oQ1AVMnyGuJj{S?8F;7?KUl-yTo*wX_Zlt4@B52PCqZ%$mb!ZZ531*;X zssa~a4Qdl!Mve43s)Kh>?|WHz)!zlxZXk{M*VJWmK{HT*>PQKy;_28Qt5Gxa3F^P( zPSi+`puVP8P!-=n&FFnp2R*c|nP`u?pMdI!A0zEL6nNGYpuTQXQ5~3Wt-_w1*P^C! z8>*saR0ms7BRy_Cjr^Gle5l@g$VNBuysUZys2S{o8i12$3uUM&sY5-u6t&w|qekRf zccV6I3u;DABeQFMv*-6w^*lm#(93kG13py8vr!$+MV87jL+y=`s29grOHd7$VIEea zrus8f2lt>lasV~er%-$5G_t?U71SpF2eq{E-J=~yM?D|J4*LG*P|ySUsLfN1>c~WV z38$iZz5~_s-KaHfMm>KJ^~3WWY6%ZpTdl{e$5GE!lHNqe`}+QmjuhEkIDk}brx{eGZPaaD+d507dEvuGBops9j_?Wy$^d0{Hm}Co@ ziE1*4G?3xsBcg+uH3P{yQcISRIbVh}QiD zvW9GP4<;nLKP1FBZ*cZ5nMW28ZL=c6!kR7Q6_QEjlj%f@O9PQ@?8O;GTXzM~v7U@2 z8_7g6g{&f5iH^-t%&4f2SxZ6R>Yn7a=*7sERPjSHl@yRNqAgudbo3&9BPC;A#YD1z zj3XhUFYuGco17GrWKu%-5ivs}-J|dfnM`!lL@^6-xh;#qWG3ma6MMXcv4kIn=n)LJ zCB}K&w4|>+^=_`O*?r(^ahp4phhzN#PrK5F(4uAG;mJXd8|u8${j+mk%-ovVT6btl zdAKQMlP5edb&kjVw(F&KqpRve_3q=eHaC!d*Y#)Ag*Rq2dg97Lbqzm$vp5tE29|r= Uy_x&m${v&5Gd*Uzg;`nu0eD&^bpQYW literal 8539 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 YjYO7C?ia=UQ*eK34yHs4Q%lYN0{$iKiU0rr diff --git a/locale/fr_FR/LC_MESSAGES/petersql.po b/locale/fr_FR/LC_MESSAGES/petersql.po index 70ce0fb..5ba12c3 100644 --- a/locale/fr_FR/LC_MESSAGES/petersql.po +++ b/locale/fr_FR/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-12 19:04+0100\n" +"POT-Creation-Date: 2026-03-23 10:07+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: fr_FR\n" @@ -47,66 +47,62 @@ msgstr "To" msgid "OpenSSH client not found." msgstr "Client OpenSSH introuvable." -#: structures/engines/mariadb/context.py:592 -#: structures/engines/mysql/context.py:563 -#: structures/engines/postgresql/context.py:579 -#: structures/engines/sqlite/context.py:518 +#: structures/engines/mariadb/context.py:611 +#: structures/engines/mysql/context.py:622 +#: structures/engines/postgresql/context.py:645 +#: structures/engines/sqlite/context.py:524 #, python-brace-format msgid "Table{table_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:620 -#: structures/engines/mysql/context.py:591 -#: structures/engines/postgresql/context.py:604 -#: structures/engines/sqlite/context.py:542 +#: structures/engines/mariadb/context.py:639 +#: structures/engines/mysql/context.py:650 +#: structures/engines/postgresql/context.py:670 +#: structures/engines/sqlite/context.py:548 #, python-brace-format msgid "Column{column_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:638 -#: structures/engines/mysql/context.py:609 -#: structures/engines/postgresql/context.py:622 -#: structures/engines/sqlite/context.py:560 +#: structures/engines/mariadb/context.py:657 +#: structures/engines/mysql/context.py:668 +#: structures/engines/postgresql/context.py:688 +#: structures/engines/sqlite/context.py:566 #, python-brace-format msgid "Index{index_number:03}" msgstr "" -#: structures/engines/mariadb/context.py:678 -#: structures/engines/mysql/context.py:649 -#: structures/engines/postgresql/context.py:662 -#: structures/engines/sqlite/context.py:602 +#: structures/engines/mariadb/context.py:697 +#: structures/engines/mysql/context.py:706 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:608 #, python-brace-format msgid "ForeignKey{foreign_key_number:03}" msgstr "" -#: structures/engines/mariadb/context.py:711 -#: structures/engines/mysql/context.py:680 -#: structures/engines/postgresql/context.py:692 -#: structures/engines/sqlite/context.py:630 +#: structures/engines/mariadb/context.py:730 +#: structures/engines/mysql/context.py:737 +#: structures/engines/postgresql/context.py:758 +#: structures/engines/sqlite/context.py:636 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:762 +#: structures/engines/mariadb/context.py:781 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "" -#: windows/dialogs/connections/view.py:402 -#: windows/dialogs/connections/view.py:739 windows/main/controller.py:606 +#: windows/dialogs/connections/view.py:415 +#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 #: windows/views.py:33 msgid "Connection" msgstr "Connexion" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:1003 -#: windows/views.py:1353 windows/views.py:1460 windows/views.py:1843 -#: windows/views.py:2788 windows/views.py:2811 windows/views.py:2812 -#: windows/views.py:2813 windows/views.py:2814 windows/views.py:2815 -#: windows/views.py:2816 windows/views.py:2817 windows/views.py:2818 -#: windows/views.py:2819 windows/views.py:2823 windows/views.py:3017 -#: windows/views.py:3218 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1006 +#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 +#: windows/views.py:2821 msgid "Name" msgstr "Nom" @@ -114,12 +110,12 @@ msgstr "Nom" msgid "Last connection" msgstr "Dernière connexion" -#: windows/dialogs/connections/view.py:640 windows/views.py:61 +#: windows/dialogs/connections/view.py:653 windows/views.py:61 msgid "New directory" msgstr "Nouveau répertoire" #: windows/dialogs/connections/model.py:206 -#: windows/dialogs/connections/view.py:600 windows/views.py:65 +#: windows/dialogs/connections/view.py:613 windows/views.py:65 msgid "New connection" msgstr "Nouvelle connexion" @@ -133,14 +129,14 @@ msgstr "Nom" msgid "Clone connection" msgstr "Nouvelle connexion" -#: windows/views.py:81 windows/views.py:603 windows/views.py:1337 -#: windows/views.py:1378 windows/views.py:1753 windows/views.py:1785 -#: windows/views.py:2044 windows/views.py:3354 windows/views.py:3386 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1322 +#: windows/views.py:1365 windows/views.py:1773 windows/views.py:2032 +#: windows/views.py:2726 windows/views.py:2957 windows/views.py:2989 msgid "Delete" msgstr "Supprimer" -#: windows/views.py:111 windows/views.py:1358 windows/views.py:1515 -#: windows/views.py:2828 windows/views.py:3273 +#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 +#: windows/views.py:2876 msgid "Engine" msgstr "Moteur" @@ -152,7 +148,7 @@ msgstr "Hôte + port" msgid "Username" msgstr "Nom d'utilisateur" -#: windows/views.py:161 windows/views.py:1147 +#: windows/views.py:161 windows/views.py:1132 msgid "Password" msgstr "Mot de passe" @@ -173,27 +169,26 @@ msgstr "Utiliser un tunnel SSH" msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 windows/views.py:2749 +#: windows/views.py:233 msgid "Filename" msgstr "Nom de fichier" -#: windows/views.py:238 windows/views.py:358 windows/views.py:2754 -#: windows/views.py:2937 +#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 msgid "Select a file" msgstr "Sélectionner un fichier" -#: windows/views.py:238 windows/views.py:358 windows/views.py:2937 +#: windows/views.py:238 windows/views.py:358 #, fuzzy msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:255 windows/views.py:1360 windows/views.py:1473 -#: windows/views.py:2829 windows/views.py:3115 windows/views.py:3231 +#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 +#: windows/views.py:2834 msgid "Comments" msgstr "Commentaires" -#: windows/main/controller.py:236 windows/views.py:269 windows/views.py:730 +#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 #: windows/views.py:884 msgid "Settings" msgstr "Paramètres" @@ -251,7 +246,7 @@ msgstr "" msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:411 windows/views.py:1356 windows/views.py:2826 +#: windows/views.py:411 windows/views.py:1341 msgid "Created at" msgstr "Créé le" @@ -291,7 +286,7 @@ msgstr "Ouvrir le gestionnaire de connexions" msgid "Statistics" msgstr "Statistiques" -#: windows/views.py:584 windows/views.py:1719 +#: windows/views.py:584 windows/views.py:1730 msgid "Create" msgstr "Créer" @@ -305,14 +300,15 @@ msgstr "Dernière connexion" msgid "Create directory" msgstr "Nouveau répertoire" -#: windows/views.py:620 windows/views.py:844 windows/views.py:1375 -#: windows/views.py:1788 windows/views.py:2049 windows/views.py:2152 -#: windows/views.py:3162 windows/views.py:3389 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1360 +#: windows/views.py:1776 windows/views.py:2037 windows/views.py:2093 +#: windows/views.py:2702 windows/views.py:2992 windows/views.py:3129 msgid "Cancel" msgstr "Annuler" -#: windows/views.py:625 windows/views.py:2054 windows/views.py:3172 -#: windows/views.py:3394 +#: windows/main/controller.py:283 windows/main/controller.py:300 +#: windows/main/controller.py:301 windows/views.py:625 windows/views.py:2042 +#: windows/views.py:2235 windows/views.py:2997 windows/views.py:3135 msgid "Save" msgstr "Enregistrer" @@ -345,8 +341,9 @@ msgid "Locale" msgstr "Localisation" #: windows/views.py:780 -msgid "Edit Value" -msgstr "Modifier la valeur" +#, fuzzy +msgid "Column content" +msgstr "Nouvelle connexion" #: windows/views.py:790 msgid "Syntax" @@ -376,7 +373,7 @@ msgstr "Aide" msgid "Open connection manager" msgstr "Ouvrir le gestionnaire de connexions" -#: windows/views.py:900 +#: windows/views.py:902 msgid "Disconnect from server" msgstr "Se déconnecter du serveur" @@ -388,21 +385,21 @@ msgstr "outil" msgid "Refresh" msgstr "Actualiser" -#: windows/views.py:908 windows/views.py:910 +#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 +#: windows/views.py:2077 windows/views.py:2221 msgid "Add" msgstr "Ajouter" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2232 -#: windows/views.py:2735 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 msgid "MyMenuItem" msgstr "MonÉlémentMenu" -#: windows/views.py:951 windows/views.py:1816 windows/views.py:3417 +#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 msgid "MyMenu" msgstr "MonMenu" -#: windows/views.py:966 windows/views.py:1397 windows/views.py:1404 -#: windows/views.py:1411 +#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 +#: windows/views.py:1402 msgid "MyLabel" msgstr "MonÉtiquette" @@ -410,8 +407,7 @@ msgstr "MonÉtiquette" msgid "Databases" msgstr "Bases de données" -#: windows/views.py:973 windows/views.py:1355 windows/views.py:2797 -#: windows/views.py:2825 +#: windows/views.py:973 windows/views.py:1340 msgid "Size" msgstr "Taille" @@ -431,283 +427,269 @@ msgstr "Tables" msgid "System" msgstr "Système" -#: windows/views.py:1026 -#, fuzzy -msgid "Character set" -msgstr "Créé le" - #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1047 -#: windows/views.py:1359 windows/views.py:3061 +#: windows/components/dataview.py:89 windows/views.py:1029 +#: windows/views.py:1344 msgid "Collation" msgstr "Classement" -#: windows/views.py:1073 windows/views.py:2943 +#: windows/views.py:1058 msgid "Encryption" msgstr "" -#: windows/views.py:1085 +#: windows/views.py:1070 msgid "Read Only" msgstr "" -#: windows/views.py:1102 +#: windows/views.py:1087 #, fuzzy msgid "Tablespace" msgstr "Tables" -#: windows/views.py:1123 +#: windows/views.py:1108 #, fuzzy msgid "Connection limit" msgstr "Connexion perdue" -#: windows/views.py:1166 +#: windows/views.py:1151 #, fuzzy msgid "Profile" msgstr "Fichier" -#: windows/views.py:1192 +#: windows/views.py:1177 #, fuzzy msgid "Default tablespace" msgstr "Supprimer la table" -#: windows/views.py:1213 +#: windows/views.py:1198 #, fuzzy msgid "Temporary tablespace" msgstr "Temporaire" -#: windows/views.py:1239 +#: windows/views.py:1224 msgid "Quota" msgstr "" -#: windows/views.py:1258 +#: windows/views.py:1243 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1275 +#: windows/views.py:1260 msgid "Account status" msgstr "" -#: windows/views.py:1296 +#: windows/views.py:1281 #, fuzzy msgid "Password expire" msgstr "Mot de passe" -#: windows/views.py:1317 +#: windows/views.py:1302 msgid "Table:" msgstr "Table :" -#: windows/views.py:1325 windows/views.py:1598 windows/views.py:1642 -#: windows/views.py:1748 windows/views.py:3349 +#: 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:1330 +#: windows/views.py:1315 msgid "Clone" msgstr "Cloner" -#: windows/views.py:1354 +#: windows/views.py:1339 msgid "Rows" msgstr "Lignes" -#: windows/views.py:1357 windows/views.py:2827 +#: windows/views.py:1342 msgid "Updated at" msgstr "Mis à jour le" -#: windows/views.py:1381 windows/views.py:1793 windows/views.py:2159 -#: windows/views.py:2206 +#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 +#: windows/views.py:2173 windows/views.py:2709 msgid "Apply" msgstr "Appliquer" -#: windows/views.py:1391 windows/views.py:1550 windows/views.py:2003 -#: windows/views.py:3306 +#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 +#: windows/views.py:2909 msgid "Options" msgstr "Options" -#: windows/views.py:1422 +#: windows/views.py:1413 msgid "Diagram" msgstr "Diagramme" -#: windows/views.py:1433 windows/views.py:2796 +#: windows/views.py:1424 msgid "Database" msgstr "Base de données" -#: windows/views.py:1488 windows/views.py:3246 +#: windows/views.py:1479 windows/views.py:2849 msgid "Base" msgstr "Base" -#: windows/views.py:1502 windows/views.py:3260 +#: windows/views.py:1493 windows/views.py:2863 msgid "Auto Increment" msgstr "Auto incrément" -#: windows/views.py:1530 windows/views.py:3288 +#: windows/views.py:1521 windows/views.py:2891 msgid "Default Collation" msgstr "Classement par défaut" -#: windows/views.py:1562 windows/views.py:1603 windows/views.py:1647 +#: windows/views.py:1531 +msgid "Convert data" +msgstr "" + +#: windows/views.py:1539 +msgid "Row format" +msgstr "" + +#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 +#: windows/views.py:1756 windows/views.py:2081 msgid "Remove" msgstr "Supprimer" -#: windows/views.py:1569 windows/views.py:1610 windows/views.py:1654 +#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 msgid "Clear" msgstr "Effacer" -#: windows/views.py:1584 windows/views.py:3320 +#: windows/views.py:1595 windows/views.py:2923 msgid "Indexes" msgstr "Index" -#: windows/views.py:1628 +#: windows/views.py:1639 msgid "Foreign Keys" msgstr "Clés étrangères" -#: windows/views.py:1672 +#: windows/views.py:1683 msgid "Checks" msgstr "Contrôles" -#: windows/views.py:1740 windows/views.py:3341 +#: windows/views.py:1750 windows/views.py:2944 msgid "Columns:" msgstr "Colonnes :" -#: windows/views.py:1760 windows/views.py:3361 -msgid "Up" -msgstr "Haut" +#: windows/views.py:1760 +#, fuzzy +msgid "Move Up" +msgstr "Déplacer vers le haut\tCTRL+UP" -#: windows/views.py:1767 windows/views.py:3368 -msgid "Down" -msgstr "Bas" +#: windows/views.py:1762 +#, fuzzy +msgid "Move Down" +msgstr "Déplacer vers le bas\tCTRL+D" -#: windows/views.py:1806 windows/views.py:1813 windows/views.py:3407 -#: windows/views.py:3414 +#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 +#: windows/views.py:3017 msgid "Add Index" msgstr "Ajouter un index" -#: windows/views.py:1810 windows/views.py:3411 +#: windows/views.py:1798 windows/views.py:3014 msgid "Add PrimaryKey" msgstr "Ajouter une clé primaire" -#: windows/views.py:1827 +#: windows/views.py:1815 msgid "Table" msgstr "Table" -#: windows/views.py:1863 +#: windows/views.py:1851 #, fuzzy msgid "Definer" msgstr "Insérer" -#: windows/views.py:1883 +#: windows/views.py:1871 msgid "Schema" msgstr "" -#: windows/views.py:1909 +#: windows/views.py:1897 msgid "SQL security" msgstr "" -#: windows/views.py:1916 +#: windows/views.py:1904 #, fuzzy msgid "DEFINER" msgstr "Insérer" -#: windows/views.py:1916 +#: windows/views.py:1904 #, fuzzy msgid "INVOKER" msgstr "Insérer" -#: windows/views.py:1928 windows/views.py:2639 windows/views.py:2658 -#: windows/views.py:2901 +#: windows/views.py:1916 msgid "Algorithm" msgstr "" -#: windows/views.py:1930 windows/views.py:2624 windows/views.py:2657 -#: windows/views.py:2906 +#: windows/views.py:1918 #, fuzzy msgid "UNDEFINED" msgstr "Non signé" -#: windows/views.py:1933 windows/views.py:2627 windows/views.py:2657 -#: windows/views.py:2909 +#: windows/views.py:1921 msgid "MERGE" msgstr "" -#: windows/views.py:1936 windows/views.py:2636 windows/views.py:2657 -#: windows/views.py:2912 +#: windows/views.py:1924 #, fuzzy msgid "TEMPTABLE" msgstr "Table" -#: windows/views.py:1946 windows/views.py:2663 +#: windows/views.py:1934 msgid "View constraint" msgstr "" -#: windows/views.py:1948 windows/views.py:2662 +#: windows/views.py:1936 #, fuzzy msgid "None" msgstr "Cloner" -#: windows/views.py:1951 windows/views.py:2662 +#: windows/views.py:1939 #, fuzzy msgid "LOCAL" msgstr "Localisation" -#: windows/views.py:1954 +#: windows/views.py:1942 #, fuzzy msgid "CASCADE" msgstr "Annuler" -#: windows/views.py:1957 +#: windows/views.py:1945 #, fuzzy msgid "CHECK ONLY" msgstr "Vérifier" -#: windows/views.py:1960 windows/views.py:2662 +#: windows/views.py:1948 msgid "READ ONLY" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:1960 msgid "Force" msgstr "" -#: windows/views.py:1984 +#: windows/views.py:1972 msgid "Security barrier" msgstr "" -#: windows/views.py:2066 +#: windows/views.py:2054 msgid "Views" msgstr "Vues" -#: windows/views.py:2074 +#: windows/views.py:2062 msgid "Triggers" msgstr "Déclencheurs" -#: windows/views.py:2086 -#, python-brace-format -msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" - -#: windows/views.py:2094 +#: windows/views.py:2073 #, fuzzy -msgid "First" -msgstr "Filtres" - -#: windows/views.py:2112 -msgid "Last" -msgstr "" - -#: windows/views.py:2123 -msgid "Insert record" -msgstr "Insérer un enregistrement" +msgid "Refrsh" +msgstr "Actualiser" -#: windows/views.py:2128 -msgid "Duplicate record" +#: windows/views.py:2079 +#, fuzzy +msgid "Duplicate" msgstr "Dupliquer un enregistrement" -#: windows/views.py:2135 -msgid "Delete record" -msgstr "Supprimer un enregistrement" - -#: windows/views.py:2145 +#: windows/views.py:2085 msgid "Apply changes automatically" msgstr "Appliquer les modifications automatiquement" -#: windows/views.py:2147 windows/views.py:2148 +#: windows/views.py:2087 windows/views.py:2088 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -715,157 +697,152 @@ msgstr "" "Si activé, les modifications de la table sont appliquées immédiatement " "sans appuyer sur Appliquer ou Annuler" -#: windows/views.py:2169 +#: windows/views.py:2101 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" + +#: windows/views.py:2109 +#, fuzzy +msgid "First" +msgstr "Filtres" + +#: windows/views.py:2127 +msgid "Last" +msgstr "" + +#: windows/views.py:2136 msgid "Filters" msgstr "Filtres" -#: windows/views.py:2209 +#: windows/views.py:2176 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2229 +#: windows/views.py:2196 msgid "Insert row" msgstr "Insérer une ligne" -#: windows/views.py:2237 +#: windows/views.py:2204 msgid "Data" msgstr "Données" -#: windows/views.py:2291 windows/views.py:2341 -msgid "New" -msgstr "Nouveau" - -#: windows/views.py:2318 -msgid "Query" +#: windows/main/controller.py:278 windows/main/controller.py:287 +#: windows/main/controller.py:288 windows/views.py:2221 +#, fuzzy +msgid "New query" msgstr "Requête" -#: windows/views.py:2338 +#: windows/views.py:2223 windows/views.py:2660 msgid "Close" msgstr "Fermer" -#: windows/views.py:2351 -msgid "Query #2" -msgstr "Requête #2" +#: windows/main/controller.py:279 windows/main/controller.py:289 +#: windows/main/controller.py:290 windows/views.py:2223 +#, fuzzy +msgid "Close query" +msgstr "Requête" -#: windows/views.py:2618 -msgid "Column5" -msgstr "Colonne5" +#: windows/views.py:2227 +msgid "Run" +msgstr "" -#: windows/views.py:2629 -msgid "Import" -msgstr "Importer" +#: windows/main/controller.py:280 windows/main/controller.py:292 +#: windows/main/controller.py:293 windows/views.py:2227 +#, fuzzy +msgid "Execute" +msgstr "Exécutable SSH" -#: windows/views.py:2654 -msgid "Read only" +#: windows/views.py:2229 +msgid "Run all" msgstr "" -#: windows/views.py:2662 -msgid "CASCADED" +#: windows/main/controller.py:295 windows/views.py:2229 +msgid "Execute all statements" msgstr "" -#: windows/views.py:2662 -#, fuzzy -msgid "CHECK OPTION" -msgstr "connexion" +#: windows/main/controller.py:282 windows/main/controller.py:297 +#: windows/main/controller.py:298 windows/views.py:2231 +msgid "Stop" +msgstr "" -#: windows/views.py:2694 -msgid "collapsible" -msgstr "rétractable" +#: windows/views.py:2287 +msgid "a page" +msgstr "" -#: windows/views.py:2710 -msgid "Column3" -msgstr "Colonne3" +#: windows/views.py:2315 +msgid "Query" +msgstr "Requête" -#: windows/views.py:2711 -msgid "Column4" -msgstr "Colonne4" +#: windows/views.py:2626 +#, fuzzy +msgid "Character set" +msgstr "Créé le" -#: windows/views.py:2754 -msgid "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" -msgstr "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#: windows/views.py:2644 windows/views.py:2663 +msgid "New" +msgstr "Nouveau" -#: windows/views.py:2766 -msgid "Port" -msgstr "Port" +#: windows/views.py:2683 +msgid "Insert record" +msgstr "Insérer un enregistrement" -#: windows/views.py:2789 -msgid "Usage" -msgstr "Utilisation" +#: windows/views.py:2688 +msgid "Duplicate record" +msgstr "Dupliquer un enregistrement" -#: windows/views.py:2800 -#, python-format -msgid "%(total_rows)s" -msgstr "%(total_rows)s" +#: windows/views.py:2695 +msgid "Delete record" +msgstr "Supprimer un enregistrement" -#: windows/views.py:2805 -msgid "rows total" -msgstr "lignes au total" +#: windows/views.py:2733 windows/views.py:2964 +msgid "Up" +msgstr "Haut" -#: windows/views.py:2824 -msgid "Lines" -msgstr "Lignes" +#: windows/views.py:2740 windows/views.py:2971 +msgid "Down" +msgstr "Bas" -#: windows/views.py:2856 -msgid "Temporary" -msgstr "Temporaire" +#: windows/views.py:3100 +msgid "Save Starments" +msgstr "" -#: windows/views.py:2867 +#: windows/views.py:3108 #, fuzzy -msgid "Engine options" -msgstr "Options" +msgid "Location" +msgstr "Classement" -#: windows/views.py:2926 -msgid "RadioBtn" +#: windows/views.py:3115 +msgid "*.sql" msgstr "" -#: windows/views.py:3009 -msgid "Edit Column" -msgstr "Modifier la colonne" - -#: windows/views.py:3025 -msgid "Datatype" -msgstr "Type de données" - -#: windows/components/dataview.py:121 windows/views.py:3040 -msgid "Length/Set" -msgstr "Longueur/Ensemble" - -#: windows/components/dataview.py:51 windows/views.py:3079 -msgid "Unsigned" -msgstr "Non signé" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3085 +#: windows/components/dataview.py:75 msgid "Allow NULL" msgstr "Autoriser NULL" -#: windows/views.py:3091 -msgid "Zero Fill" -msgstr "Remplissage zéro" +#: windows/components/dataview.py:28 +msgid "Check" +msgstr "Vérifier" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3102 +#: windows/components/dataview.py:78 msgid "Default" msgstr "Par défaut" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3128 +#: windows/components/dataview.py:82 msgid "Virtuality" msgstr "Virtualité" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3143 msgid "Expression" msgstr "Expression" -#: windows/components/dataview.py:28 -msgid "Check" -msgstr "Vérifier" +#: windows/components/dataview.py:51 +msgid "Unsigned" +msgstr "Non signé" #: windows/components/dataview.py:53 msgid "Zerofill" @@ -879,6 +856,10 @@ msgstr "#" msgid "Data type" msgstr "Type de données" +#: windows/components/dataview.py:121 +msgid "Length/Set" +msgstr "Longueur/Ensemble" + #: windows/components/dataview.py:155 msgid "Add column\tCTRL+INS" msgstr "Ajouter une colonne\tCTRL+INS" @@ -955,111 +936,164 @@ msgstr "AUTO INCREMENT" msgid "Text/Expression" msgstr "Texte/Expression" -#: windows/dialogs/connections/view.py:122 windows/main/tabs/query.py:387 +#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:401 +#: windows/dialogs/connections/view.py:414 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:413 +#: windows/dialogs/connections/view.py:426 #, python-brace-format msgid "Do you want save the connection {connection_name}?" msgstr "" -#: windows/dialogs/connections/view.py:416 +#: windows/dialogs/connections/view.py:429 msgid "Confirm save" msgstr "Confirmer la sauvegarde" -#: windows/dialogs/connections/view.py:468 +#: windows/dialogs/connections/view.py:481 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:470 +#: windows/dialogs/connections/view.py:483 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:737 +#: windows/dialogs/connections/view.py:750 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:775 #, fuzzy, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "Erreur de connexion" -#: windows/dialogs/connections/view.py:763 +#: windows/dialogs/connections/view.py:776 msgid "Connection error" msgstr "Erreur de connexion" -#: windows/dialogs/connections/view.py:789 +#: windows/dialogs/connections/view.py:802 #, fuzzy, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" msgstr "Voulez-vous supprimer les enregistrements ?" -#: windows/dialogs/connections/view.py:792 -#: windows/dialogs/connections/view.py:809 +#: windows/dialogs/connections/view.py:805 +#: windows/dialogs/connections/view.py:822 msgid "Confirm delete" msgstr "Confirmer la suppression" -#: windows/dialogs/connections/view.py:806 +#: windows/dialogs/connections/view.py:819 #, fuzzy, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "Voulez-vous supprimer les enregistrements ?" -#: windows/main/controller.py:189 +#: windows/main/controller.py:275 +#, python-brace-format +msgid "{text} ({shortcut})" +msgstr "" + +#: windows/main/controller.py:281 windows/main/controller.py:294 +#, fuzzy +msgid "Execute all" +msgstr "Exécutable SSH" + +#: windows/main/controller.py:471 windows/main/controller.py:479 +#, fuzzy +msgid "Query (1)" +msgstr "Requête" + +#: windows/main/controller.py:497 +#, python-brace-format +msgid "Query ({query_number})" +msgstr "" + +#: windows/main/controller.py:530 +msgid "You have unsaved changes. Save before closing?" +msgstr "" + +#: windows/main/controller.py:531 +msgid "Unsaved query" +msgstr "" + +#: windows/main/controller.py:576 +#, fuzzy +msgid "Save query" +msgstr "Requête" + +#: windows/main/controller.py:579 +msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" +msgstr "" + +#: windows/main/controller.py:616 windows/main/controller.py:642 +#: windows/main/database/list.py:84 windows/main/database/view.py:256 +#: windows/main/database/view.py:282 windows/main/query/controller.py:177 +msgid "Error" +msgstr "Erreur" + +#: windows/main/controller.py:622 +#, python-brace-format +msgid "-- Saved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:647 +#, python-brace-format +msgid "-- Autosaved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:704 msgid "days" msgstr "jours" -#: windows/main/controller.py:190 +#: windows/main/controller.py:705 msgid "hours" msgstr "heures" -#: windows/main/controller.py:191 +#: windows/main/controller.py:706 msgid "minutes" msgstr "minutes" -#: windows/main/controller.py:192 +#: windows/main/controller.py:707 msgid "seconds" msgstr "secondes" -#: windows/main/controller.py:200 +#: windows/main/controller.py:715 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Mémoire utilisée : {used} ({percentage:.2%})" -#: windows/main/controller.py:236 +#: windows/main/controller.py:751 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:441 +#: windows/main/controller.py:952 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:443 +#: windows/main/controller.py:954 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:608 +#: windows/main/controller.py:1119 msgid "Version" msgstr "Version" -#: windows/main/controller.py:610 +#: windows/main/controller.py:1121 msgid "Uptime" msgstr "Temps de fonctionnement" -#: windows/main/controller.py:678 +#: windows/main/controller.py:1199 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:711 +#: windows/main/controller.py:1232 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1069,153 +1103,177 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:716 windows/main/controller.py:737 +#: windows/main/controller.py:1237 windows/main/controller.py:1258 #, fuzzy msgid "Delete database" msgstr "Supprimer la table" -#: windows/main/controller.py:722 +#: windows/main/controller.py:1243 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:723 +#: windows/main/controller.py:1244 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:736 +#: windows/main/controller.py:1257 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:751 +#: windows/main/controller.py:1272 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:752 windows/main/tabs/view.py:253 -#: windows/main/tabs/view.py:279 +#: windows/main/controller.py:1273 windows/main/database/view.py:253 +#: windows/main/database/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:871 +#: windows/main/controller.py:1392 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:897 +#: windows/main/controller.py:1418 #, fuzzy, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "Voulez-vous supprimer les enregistrements ?" -#: windows/main/controller.py:900 +#: windows/main/controller.py:1421 msgid "Delete table" msgstr "Supprimer la table" -#: windows/main/controller.py:919 +#: windows/main/controller.py:1440 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1017 +#: windows/main/controller.py:1563 msgid "Do you want delete the records?" msgstr "Voulez-vous supprimer les enregistrements ?" -#: windows/main/tabs/database.py:71 +#: windows/main/database/list.py:69 msgid "The connection to the database was lost." msgstr "La connexion à la base de données a été perdue." -#: windows/main/tabs/database.py:73 +#: windows/main/database/list.py:71 msgid "Do you want to reconnect?" msgstr "Voulez-vous vous reconnecter ?" -#: windows/main/tabs/database.py:75 +#: windows/main/database/list.py:73 msgid "Connection lost" msgstr "Connexion perdue" -#: windows/main/tabs/database.py:85 +#: windows/main/database/list.py:83 msgid "Reconnection failed:" msgstr "Échec de la reconnexion :" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:489 -#: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 -msgid "Error" -msgstr "Erreur" +#: windows/main/database/view.py:252 +msgid "View created successfully" +msgstr "" -#: windows/main/tabs/query.py:308 -#, python-brace-format -msgid "{affected_rows} rows affected" +#: windows/main/database/view.py:252 +msgid "View updated successfully" msgstr "" -#: windows/main/tabs/query.py:315 windows/main/tabs/query.py:339 +#: windows/main/database/view.py:256 #, python-brace-format -msgid "Query {query_number}" +msgid "Error saving view: {}" msgstr "" -#: windows/main/tabs/query.py:320 +#: windows/main/database/view.py:269 #, python-brace-format -msgid "Query {query_number} (Error)" +msgid "Are you sure you want to delete view '{}'?" msgstr "" -#: windows/main/tabs/query.py:334 -#, python-brace-format -msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" +#: windows/main/database/view.py:270 +#, fuzzy +msgid "Confirm Delete" +msgstr "Confirmer la suppression" + +#: windows/main/database/view.py:279 +msgid "View deleted successfully" msgstr "" -#: windows/main/tabs/query.py:360 +#: windows/main/database/view.py:282 #, python-brace-format -msgid "{rows_count} rows" +msgid "Error deleting view: {}" msgstr "" -#: windows/main/tabs/query.py:362 +#: windows/main/query/controller.py:110 #, python-brace-format -msgid "{elapsed_ms:.1f} ms" +msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/tabs/query.py:366 +#: windows/main/query/controller.py:112 #, python-brace-format -msgid "{warnings_count} warnings" +msgid "{elapsed_s:.2f} s" msgstr "" -#: windows/main/tabs/query.py:381 +#: windows/main/query/controller.py:115 #, fuzzy -msgid "Error:" -msgstr "Erreur" +msgid "none" +msgstr "Cloner" + +#: windows/main/query/controller.py:121 +#, python-brace-format +msgid "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." +msgstr "" + +#: windows/main/query/controller.py:134 +msgid "Query execution cancelled" +msgstr "" -#: windows/main/tabs/query.py:488 +#: windows/main/query/controller.py:176 #, fuzzy msgid "No active database connection" msgstr "Nouvelle connexion" -#: windows/main/tabs/view.py:252 -msgid "View created successfully" +#: windows/main/query/renderer.py:53 +#, python-brace-format +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View updated successfully" +#: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 +#, python-brace-format +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/view.py:256 +#: windows/main/query/renderer.py:65 #, python-brace-format -msgid "Error saving view: {}" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/view.py:269 +#: windows/main/query/renderer.py:79 #, python-brace-format -msgid "Are you sure you want to delete view '{}'?" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/view.py:270 -#, fuzzy -msgid "Confirm Delete" -msgstr "Confirmer la suppression" +#: windows/main/query/renderer.py:165 +#, python-brace-format +msgid "{rows_count} rows" +msgstr "" -#: windows/main/tabs/view.py:279 -msgid "View deleted successfully" +#: windows/main/query/renderer.py:167 +#, python-brace-format +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/view.py:282 +#: windows/main/query/renderer.py:171 #, python-brace-format -msgid "Error deleting view: {}" +msgid "{warnings_count} warnings" msgstr "" +#: windows/main/query/renderer.py:186 +#, fuzzy +msgid "Error:" +msgstr "Erreur" + #~ msgid "Created at:" #~ msgstr "" @@ -1281,3 +1339,73 @@ msgstr "" #~ msgid "{} warnings" #~ msgstr "" +#~ msgid "Edit Value" +#~ msgstr "Modifier la valeur" + +#~ msgid "Query #2" +#~ msgstr "Requête #2" + +#~ msgid "Column5" +#~ msgstr "Colonne5" + +#~ msgid "Import" +#~ msgstr "Importer" + +#~ msgid "Read only" +#~ msgstr "" + +#~ msgid "CASCADED" +#~ msgstr "" + +#~ msgid "CHECK OPTION" +#~ msgstr "connexion" + +#~ msgid "collapsible" +#~ msgstr "rétractable" + +#~ msgid "Column3" +#~ msgstr "Colonne3" + +#~ msgid "Column4" +#~ msgstr "Colonne4" + +#~ msgid "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#~ msgstr "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" + +#~ msgid "Port" +#~ msgstr "Port" + +#~ msgid "Usage" +#~ msgstr "Utilisation" + +#~ msgid "%(total_rows)s" +#~ msgstr "%(total_rows)s" + +#~ msgid "rows total" +#~ msgstr "lignes au total" + +#~ msgid "Lines" +#~ msgstr "Lignes" + +#~ msgid "Temporary" +#~ msgstr "Temporaire" + +#~ msgid "Engine options" +#~ msgstr "Options" + +#~ msgid "RadioBtn" +#~ msgstr "" + +#~ msgid "Edit Column" +#~ msgstr "Modifier la colonne" + +#~ msgid "Datatype" +#~ msgstr "Type de données" + +#~ msgid "Zero Fill" +#~ msgstr "Remplissage zéro" + diff --git a/locale/it_IT/LC_MESSAGES/petersql.mo b/locale/it_IT/LC_MESSAGES/petersql.mo index f796663057558033d16042c6afe3df5a45698fab..bbf4054a5b2fae8b7d8f42efbb3a75dfa6fd8409 100644 GIT binary patch delta 3663 zcmY+G2~d?)7={mlTPh$T2?|^k1trbg5(`Ml1<-J<1eXvli?IoaYhERVGEMV`B3#C0 z+{UrdFvn)h%*h&U(`+24%#5bVv21iwXELdt_i`BR4DbJ(_4~f_o%8>fkCN*WJ(q+0 z>I`Ko>WMnFG3GqH*MSFRd}m{PU<&kwY0w|0L#CS9wm%1INxmH~gI#H_gaNP)J`8u+ z_Cd&ck2yi52Lqo%8E%3KfX%QA^kY!-LM&sT z){lVNSTgK@e>0v+dpJcMFcZq58_G}-lwl7fDW)1Kuv(~j+hGXY2P5GL7z(dIIl2oK z$iGm20%^!zDD=pHi;7Yj3l-r&sD<%R3lgE`je~NW4rO2_)Vyri5$3~5umWoSDX4j0 zLIwUUl>H_(3FR;zY9pg8 zQ=u|C73#9hfO;E>p#rUfTDQ?dg+J54LlGZrlGI9>e z(QlTwV0YThP#Fy&9daBB@#itoRJ4;gsD&e;Qj-i7KpND7$##4uQ~+)}J`X0*E`?fm z(DEqM8J~cf{|VFvPuuacUUL2yZO0YMt8faRychi%^074x{jIZc&lp5N1cizEBaTLG64J)PhVXhuOAWXjuW3 zsb!GsV5*>YS_k!(Y=*M47vjn^LfJV3J*u3y6E4ASw10(c-ZVpv2eB;~>H#$#19=fl z0@RMup#sjf{rONET4LL4px&a*Q1f;}WpICY`~DxLLykU%a&!jb+gyO!`5#bsm|VYh7Qc`SDQm(x4n?*!~=-4OLn$g$jIy9rsjIQA+C}!JEBM@99aX zOLGcp2WO#D{1fEQ+~uJZhVWah9Yt8iKsgu$bu`1F>`t`pJgE80AxGsgHB@A16O@CU zPz#SiO*{`}@Dfx&O;9_%4i(rvs7!SZZ`nu~)cpQXnHdB%J{oF$I@D2Tz+n8FTq<(3 z6e^HamTPT)t!;0CGQ11Q@P61Ieh76GSE1Jb4duW$qGdw?P#KMdI=VQh`SH+S@BaiU zT9649NFJ1Ln$cnfdOT!0`;W!4r=aGtnn}J?O)o246iK5UGNTn97@z(ff)G{3|vUOn{ibpHepeRc(d21eH9;1?s zO3-qYfEti)xsE|)6?zjrj&zBZqEeKBR0gALG!YFz3(;sa2o<51k&51g*229t!}0Ge zy7|HA4OHdrx&N3%H4r7Ce8h2@7f}t;eQ}V=+b#ItQ#ZdH>B6Z@K=ZvdK6bFxzsaPr z3{A0v8rY0h+xAmXmu)QaMN7~;6o*=iej7TY^{6izhBl#Ds0_V^c;~%k1<#H0zn01Z z)CZ*^m4Fs_pObAp8W!5R-v3JU92$s*pz){}sl=cW=v5SkRA!=4s7?*aToi6{ymjLS zPa=IP>sz|K&$c|DMlKYI79*80Xe-k4Eocr>(TD3%)KLw}GkWxPR{HPhIjYiKmX+sr zWtS8eyR$0_N{U?-1x0RGY*BeXCnjK_b0naq$NfQ*T;|TIaJzB}%9veJw%8dKnBY_H z{1AA++1}-SXI4;2<2OMOK5Zs1aF;D^^bJn%aZn!>ob2)!nv;G9M@0wcGi^HP#svHQU(~75yI{Cu70@ literal 8639 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 *F6?8Di;RYZv2bf)vNO$MRMECE_eK~ZgSY2 zvC2&95lZ*Q5mFTzh`JI1bH#{D)J6v!mjp5I5=%($aV^xL1X60T^CV_jnpFv;PjtH}r1ymw z?WBhzDZE9V;ZsPw%VxIga8(i+u^GaL16*3(;g(8W{q!`rCLdJkWTu3WeA3J$+=Noz zk}bbVlg(WRZ{|;zQKHveeE4v*fpHzHGn0-6pE*U`*QA$ZsDh(Y<$XTIbdl|pXol7@ z`u{UcbmE<3s!a+I*(L|y%{D_*(OJqjN24(p)ks$6@LaZ-jJeEbp1u_RNT^z>1e6mt zM)pjO59-hKJw(^sS^jjE z(e>yTd#W;0C9X<4Qu4|&XVNlLZdzRw6Wz3}dqr0oenoOx`9}ysv&47~Ninb+NflEF z%@{`&W699G0lutR_fn?I!zZg*Zx%_LYoQa@W!KSq64t82%P*oymu8B#!9=Q@Rhbq( z7dJZ&NAZbQ*t;M9UKBG_F#MIg4MOPMieU&AamW zF*8&7T&7evB)BhEsxI$JQ~R|4gWyz^s z()Aj4?yA#ap^?(xdHz%7__3alV2O%8>G4^x9I$AUG5KMA7VHekJ}HXNf*s=%vKo*y VxG3{ZW!^)E44CAXyqA>s{{Y=V)O`Q| diff --git a/locale/it_IT/LC_MESSAGES/petersql.po b/locale/it_IT/LC_MESSAGES/petersql.po index 840dd2c..9855aa5 100644 --- a/locale/it_IT/LC_MESSAGES/petersql.po +++ b/locale/it_IT/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-12 19:04+0100\n" +"POT-Creation-Date: 2026-03-23 10:07+0100\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: it_IT\n" @@ -47,66 +47,62 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "Client OpenSSH non trovato." -#: structures/engines/mariadb/context.py:592 -#: structures/engines/mysql/context.py:563 -#: structures/engines/postgresql/context.py:579 -#: structures/engines/sqlite/context.py:518 +#: structures/engines/mariadb/context.py:611 +#: structures/engines/mysql/context.py:622 +#: structures/engines/postgresql/context.py:645 +#: structures/engines/sqlite/context.py:524 #, python-brace-format msgid "Table{table_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:620 -#: structures/engines/mysql/context.py:591 -#: structures/engines/postgresql/context.py:604 -#: structures/engines/sqlite/context.py:542 +#: structures/engines/mariadb/context.py:639 +#: structures/engines/mysql/context.py:650 +#: structures/engines/postgresql/context.py:670 +#: structures/engines/sqlite/context.py:548 #, python-brace-format msgid "Column{column_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:638 -#: structures/engines/mysql/context.py:609 -#: structures/engines/postgresql/context.py:622 -#: structures/engines/sqlite/context.py:560 +#: structures/engines/mariadb/context.py:657 +#: structures/engines/mysql/context.py:668 +#: structures/engines/postgresql/context.py:688 +#: structures/engines/sqlite/context.py:566 #, python-brace-format msgid "Index{index_number:03}" msgstr "" -#: structures/engines/mariadb/context.py:678 -#: structures/engines/mysql/context.py:649 -#: structures/engines/postgresql/context.py:662 -#: structures/engines/sqlite/context.py:602 +#: structures/engines/mariadb/context.py:697 +#: structures/engines/mysql/context.py:706 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:608 #, python-brace-format msgid "ForeignKey{foreign_key_number:03}" msgstr "" -#: structures/engines/mariadb/context.py:711 -#: structures/engines/mysql/context.py:680 -#: structures/engines/postgresql/context.py:692 -#: structures/engines/sqlite/context.py:630 +#: structures/engines/mariadb/context.py:730 +#: structures/engines/mysql/context.py:737 +#: structures/engines/postgresql/context.py:758 +#: structures/engines/sqlite/context.py:636 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:762 +#: structures/engines/mariadb/context.py:781 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "" -#: windows/dialogs/connections/view.py:402 -#: windows/dialogs/connections/view.py:739 windows/main/controller.py:606 +#: windows/dialogs/connections/view.py:415 +#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 #: windows/views.py:33 msgid "Connection" msgstr "Connessione" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:1003 -#: windows/views.py:1353 windows/views.py:1460 windows/views.py:1843 -#: windows/views.py:2788 windows/views.py:2811 windows/views.py:2812 -#: windows/views.py:2813 windows/views.py:2814 windows/views.py:2815 -#: windows/views.py:2816 windows/views.py:2817 windows/views.py:2818 -#: windows/views.py:2819 windows/views.py:2823 windows/views.py:3017 -#: windows/views.py:3218 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1006 +#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 +#: windows/views.py:2821 msgid "Name" msgstr "Nome" @@ -114,12 +110,12 @@ msgstr "Nome" msgid "Last connection" msgstr "Ultima connessione" -#: windows/dialogs/connections/view.py:640 windows/views.py:61 +#: windows/dialogs/connections/view.py:653 windows/views.py:61 msgid "New directory" msgstr "Nuova directory" #: windows/dialogs/connections/model.py:206 -#: windows/dialogs/connections/view.py:600 windows/views.py:65 +#: windows/dialogs/connections/view.py:613 windows/views.py:65 msgid "New connection" msgstr "Nuova connessione" @@ -131,14 +127,14 @@ msgstr "Rinomina" msgid "Clone connection" msgstr "Chiudi connessione" -#: windows/views.py:81 windows/views.py:603 windows/views.py:1337 -#: windows/views.py:1378 windows/views.py:1753 windows/views.py:1785 -#: windows/views.py:2044 windows/views.py:3354 windows/views.py:3386 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1322 +#: windows/views.py:1365 windows/views.py:1773 windows/views.py:2032 +#: windows/views.py:2726 windows/views.py:2957 windows/views.py:2989 msgid "Delete" msgstr "Elimina" -#: windows/views.py:111 windows/views.py:1358 windows/views.py:1515 -#: windows/views.py:2828 windows/views.py:3273 +#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 +#: windows/views.py:2876 msgid "Engine" msgstr "Motore" @@ -150,7 +146,7 @@ msgstr "Host + porta" msgid "Username" msgstr "Nome utente" -#: windows/views.py:161 windows/views.py:1147 +#: windows/views.py:161 windows/views.py:1132 msgid "Password" msgstr "Password" @@ -170,26 +166,25 @@ msgstr "Usa tunnel SSH" msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 windows/views.py:2749 +#: windows/views.py:233 msgid "Filename" msgstr "Nome file" -#: windows/views.py:238 windows/views.py:358 windows/views.py:2754 -#: windows/views.py:2937 +#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 msgid "Select a file" msgstr "Seleziona un file" -#: windows/views.py:238 windows/views.py:358 windows/views.py:2937 +#: windows/views.py:238 windows/views.py:358 msgid "*.*" msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:255 windows/views.py:1360 windows/views.py:1473 -#: windows/views.py:2829 windows/views.py:3115 windows/views.py:3231 +#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 +#: windows/views.py:2834 msgid "Comments" msgstr "Commenti" -#: windows/main/controller.py:236 windows/views.py:269 windows/views.py:730 +#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 #: windows/views.py:884 msgid "Settings" msgstr "Impostazioni" @@ -246,7 +241,7 @@ msgstr "" msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:411 windows/views.py:1356 windows/views.py:2826 +#: windows/views.py:411 windows/views.py:1341 msgid "Created at" msgstr "Creato il" @@ -282,7 +277,7 @@ msgstr "" msgid "Statistics" msgstr "Statistiche" -#: windows/views.py:584 windows/views.py:1719 +#: windows/views.py:584 windows/views.py:1730 msgid "Create" msgstr "Crea" @@ -294,14 +289,15 @@ msgstr "Nuova connessione" msgid "Create directory" msgstr "Nuova directory" -#: windows/views.py:620 windows/views.py:844 windows/views.py:1375 -#: windows/views.py:1788 windows/views.py:2049 windows/views.py:2152 -#: windows/views.py:3162 windows/views.py:3389 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1360 +#: windows/views.py:1776 windows/views.py:2037 windows/views.py:2093 +#: windows/views.py:2702 windows/views.py:2992 windows/views.py:3129 msgid "Cancel" msgstr "Annulla" -#: windows/views.py:625 windows/views.py:2054 windows/views.py:3172 -#: windows/views.py:3394 +#: windows/main/controller.py:283 windows/main/controller.py:300 +#: windows/main/controller.py:301 windows/views.py:625 windows/views.py:2042 +#: windows/views.py:2235 windows/views.py:2997 windows/views.py:3135 msgid "Save" msgstr "Salva" @@ -334,8 +330,9 @@ msgid "Locale" msgstr "Localizzazione" #: windows/views.py:780 -msgid "Edit Value" -msgstr "Modifica valore" +#, fuzzy +msgid "Column content" +msgstr "Chiudi connessione" #: windows/views.py:790 msgid "Syntax" @@ -365,7 +362,7 @@ msgstr "Aiuto" msgid "Open connection manager" msgstr "Apri gestore connessioni" -#: windows/views.py:900 +#: windows/views.py:902 msgid "Disconnect from server" msgstr "Disconnetti dal server" @@ -377,21 +374,21 @@ msgstr "strumento" msgid "Refresh" msgstr "Aggiorna" -#: windows/views.py:908 windows/views.py:910 +#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 +#: windows/views.py:2077 windows/views.py:2221 msgid "Add" msgstr "Aggiungi" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2232 -#: windows/views.py:2735 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 msgid "MyMenuItem" msgstr "IlMioElementoMenu" -#: windows/views.py:951 windows/views.py:1816 windows/views.py:3417 +#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 msgid "MyMenu" msgstr "IlMioMenu" -#: windows/views.py:966 windows/views.py:1397 windows/views.py:1404 -#: windows/views.py:1411 +#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 +#: windows/views.py:1402 msgid "MyLabel" msgstr "LaMiaEtichetta" @@ -399,8 +396,7 @@ msgstr "LaMiaEtichetta" msgid "Databases" msgstr "Database" -#: windows/views.py:973 windows/views.py:1355 windows/views.py:2797 -#: windows/views.py:2825 +#: windows/views.py:973 windows/views.py:1340 msgid "Size" msgstr "Dimensione" @@ -420,283 +416,269 @@ msgstr "Tabelle" msgid "System" msgstr "Sistema" -#: windows/views.py:1026 -#, fuzzy -msgid "Character set" -msgstr "Creato il" - #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1047 -#: windows/views.py:1359 windows/views.py:3061 +#: windows/components/dataview.py:89 windows/views.py:1029 +#: windows/views.py:1344 msgid "Collation" msgstr "Ordinamento" -#: windows/views.py:1073 windows/views.py:2943 +#: windows/views.py:1058 msgid "Encryption" msgstr "" -#: windows/views.py:1085 +#: windows/views.py:1070 msgid "Read Only" msgstr "" -#: windows/views.py:1102 +#: windows/views.py:1087 #, fuzzy msgid "Tablespace" msgstr "Tabelle" -#: windows/views.py:1123 +#: windows/views.py:1108 #, fuzzy msgid "Connection limit" msgstr "Connessione persa" -#: windows/views.py:1166 +#: windows/views.py:1151 #, fuzzy msgid "Profile" msgstr "File" -#: windows/views.py:1192 +#: windows/views.py:1177 #, fuzzy msgid "Default tablespace" msgstr "Elimina tabella" -#: windows/views.py:1213 +#: windows/views.py:1198 #, fuzzy msgid "Temporary tablespace" msgstr "Temporaneo" -#: windows/views.py:1239 +#: windows/views.py:1224 msgid "Quota" msgstr "" -#: windows/views.py:1258 +#: windows/views.py:1243 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1275 +#: windows/views.py:1260 msgid "Account status" msgstr "" -#: windows/views.py:1296 +#: windows/views.py:1281 #, fuzzy msgid "Password expire" msgstr "Password" -#: windows/views.py:1317 +#: windows/views.py:1302 msgid "Table:" msgstr "Tabella:" -#: windows/views.py:1325 windows/views.py:1598 windows/views.py:1642 -#: windows/views.py:1748 windows/views.py:3349 +#: 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:1330 +#: windows/views.py:1315 msgid "Clone" msgstr "Clona" -#: windows/views.py:1354 +#: windows/views.py:1339 msgid "Rows" msgstr "Righe" -#: windows/views.py:1357 windows/views.py:2827 +#: windows/views.py:1342 msgid "Updated at" msgstr "Aggiornato il" -#: windows/views.py:1381 windows/views.py:1793 windows/views.py:2159 -#: windows/views.py:2206 +#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 +#: windows/views.py:2173 windows/views.py:2709 msgid "Apply" msgstr "Applica" -#: windows/views.py:1391 windows/views.py:1550 windows/views.py:2003 -#: windows/views.py:3306 +#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 +#: windows/views.py:2909 msgid "Options" msgstr "Opzioni" -#: windows/views.py:1422 +#: windows/views.py:1413 msgid "Diagram" msgstr "Diagramma" -#: windows/views.py:1433 windows/views.py:2796 +#: windows/views.py:1424 msgid "Database" msgstr "Database" -#: windows/views.py:1488 windows/views.py:3246 +#: windows/views.py:1479 windows/views.py:2849 msgid "Base" msgstr "Base" -#: windows/views.py:1502 windows/views.py:3260 +#: windows/views.py:1493 windows/views.py:2863 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1530 windows/views.py:3288 +#: windows/views.py:1521 windows/views.py:2891 msgid "Default Collation" msgstr "Ordinamento predefinito" -#: windows/views.py:1562 windows/views.py:1603 windows/views.py:1647 +#: windows/views.py:1531 +msgid "Convert data" +msgstr "" + +#: windows/views.py:1539 +msgid "Row format" +msgstr "" + +#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 +#: windows/views.py:1756 windows/views.py:2081 msgid "Remove" msgstr "Rimuovi" -#: windows/views.py:1569 windows/views.py:1610 windows/views.py:1654 +#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 msgid "Clear" msgstr "Pulisci" -#: windows/views.py:1584 windows/views.py:3320 +#: windows/views.py:1595 windows/views.py:2923 msgid "Indexes" msgstr "Indici" -#: windows/views.py:1628 +#: windows/views.py:1639 msgid "Foreign Keys" msgstr "Chiavi esterne" -#: windows/views.py:1672 +#: windows/views.py:1683 msgid "Checks" msgstr "Vincoli" -#: windows/views.py:1740 windows/views.py:3341 +#: windows/views.py:1750 windows/views.py:2944 msgid "Columns:" msgstr "Colonne:" -#: windows/views.py:1760 windows/views.py:3361 -msgid "Up" -msgstr "Su" +#: windows/views.py:1760 +#, fuzzy +msgid "Move Up" +msgstr "Sposta su\tCTRL+UP" -#: windows/views.py:1767 windows/views.py:3368 -msgid "Down" -msgstr "Giù" +#: windows/views.py:1762 +#, fuzzy +msgid "Move Down" +msgstr "Sposta giù\tCTRL+D" -#: windows/views.py:1806 windows/views.py:1813 windows/views.py:3407 -#: windows/views.py:3414 +#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 +#: windows/views.py:3017 msgid "Add Index" msgstr "Aggiungi indice" -#: windows/views.py:1810 windows/views.py:3411 +#: windows/views.py:1798 windows/views.py:3014 msgid "Add PrimaryKey" msgstr "Aggiungi chiave primaria" -#: windows/views.py:1827 +#: windows/views.py:1815 msgid "Table" msgstr "Tabella" -#: windows/views.py:1863 +#: windows/views.py:1851 #, fuzzy msgid "Definer" msgstr "Inserisci" -#: windows/views.py:1883 +#: windows/views.py:1871 msgid "Schema" msgstr "" -#: windows/views.py:1909 +#: windows/views.py:1897 msgid "SQL security" msgstr "" -#: windows/views.py:1916 +#: windows/views.py:1904 #, fuzzy msgid "DEFINER" msgstr "Inserisci" -#: windows/views.py:1916 +#: windows/views.py:1904 #, fuzzy msgid "INVOKER" msgstr "Inserisci" -#: windows/views.py:1928 windows/views.py:2639 windows/views.py:2658 -#: windows/views.py:2901 +#: windows/views.py:1916 msgid "Algorithm" msgstr "" -#: windows/views.py:1930 windows/views.py:2624 windows/views.py:2657 -#: windows/views.py:2906 +#: windows/views.py:1918 #, fuzzy msgid "UNDEFINED" msgstr "Senza segno" -#: windows/views.py:1933 windows/views.py:2627 windows/views.py:2657 -#: windows/views.py:2909 +#: windows/views.py:1921 msgid "MERGE" msgstr "" -#: windows/views.py:1936 windows/views.py:2636 windows/views.py:2657 -#: windows/views.py:2912 +#: windows/views.py:1924 #, fuzzy msgid "TEMPTABLE" msgstr "Tabella" -#: windows/views.py:1946 windows/views.py:2663 +#: windows/views.py:1934 msgid "View constraint" msgstr "" -#: windows/views.py:1948 windows/views.py:2662 +#: windows/views.py:1936 #, fuzzy msgid "None" msgstr "Clona" -#: windows/views.py:1951 windows/views.py:2662 +#: windows/views.py:1939 #, fuzzy msgid "LOCAL" msgstr "Localizzazione" -#: windows/views.py:1954 +#: windows/views.py:1942 #, fuzzy msgid "CASCADE" msgstr "Annulla" -#: windows/views.py:1957 +#: windows/views.py:1945 #, fuzzy msgid "CHECK ONLY" msgstr "Verifica" -#: windows/views.py:1960 windows/views.py:2662 +#: windows/views.py:1948 msgid "READ ONLY" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:1960 msgid "Force" msgstr "" -#: windows/views.py:1984 +#: windows/views.py:1972 msgid "Security barrier" msgstr "" -#: windows/views.py:2066 +#: windows/views.py:2054 msgid "Views" msgstr "Viste" -#: windows/views.py:2074 +#: windows/views.py:2062 msgid "Triggers" msgstr "Trigger" -#: windows/views.py:2086 -#, python-brace-format -msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" - -#: windows/views.py:2094 +#: windows/views.py:2073 #, fuzzy -msgid "First" -msgstr "Filtri" - -#: windows/views.py:2112 -msgid "Last" -msgstr "" - -#: windows/views.py:2123 -msgid "Insert record" -msgstr "Inserisci record" +msgid "Refrsh" +msgstr "Aggiorna" -#: windows/views.py:2128 -msgid "Duplicate record" +#: windows/views.py:2079 +#, fuzzy +msgid "Duplicate" msgstr "Duplica record" -#: windows/views.py:2135 -msgid "Delete record" -msgstr "Elimina record" - -#: windows/views.py:2145 +#: windows/views.py:2085 msgid "Apply changes automatically" msgstr "Applica modifiche automaticamente" -#: windows/views.py:2147 windows/views.py:2148 +#: windows/views.py:2087 windows/views.py:2088 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -704,157 +686,152 @@ msgstr "" "Se abilitato, le modifiche alla tabella vengono applicate immediatamente " "senza premere Applica o Annulla" -#: windows/views.py:2169 +#: windows/views.py:2101 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "" + +#: windows/views.py:2109 +#, fuzzy +msgid "First" +msgstr "Filtri" + +#: windows/views.py:2127 +msgid "Last" +msgstr "" + +#: windows/views.py:2136 msgid "Filters" msgstr "Filtri" -#: windows/views.py:2209 +#: windows/views.py:2176 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2229 +#: windows/views.py:2196 msgid "Insert row" msgstr "Inserisci riga" -#: windows/views.py:2237 +#: windows/views.py:2204 msgid "Data" msgstr "Dati" -#: windows/views.py:2291 windows/views.py:2341 -msgid "New" -msgstr "Nuovo" - -#: windows/views.py:2318 -msgid "Query" +#: windows/main/controller.py:278 windows/main/controller.py:287 +#: windows/main/controller.py:288 windows/views.py:2221 +#, fuzzy +msgid "New query" msgstr "Query" -#: windows/views.py:2338 +#: windows/views.py:2223 windows/views.py:2660 msgid "Close" msgstr "Chiudi" -#: windows/views.py:2351 -msgid "Query #2" -msgstr "Query #2" +#: windows/main/controller.py:279 windows/main/controller.py:289 +#: windows/main/controller.py:290 windows/views.py:2223 +#, fuzzy +msgid "Close query" +msgstr "Query" -#: windows/views.py:2618 -msgid "Column5" -msgstr "Colonna5" +#: windows/views.py:2227 +msgid "Run" +msgstr "" -#: windows/views.py:2629 -msgid "Import" -msgstr "Importa" +#: windows/main/controller.py:280 windows/main/controller.py:292 +#: windows/main/controller.py:293 windows/views.py:2227 +#, fuzzy +msgid "Execute" +msgstr "Eseguibile SSH" -#: windows/views.py:2654 -msgid "Read only" +#: windows/views.py:2229 +msgid "Run all" msgstr "" -#: windows/views.py:2662 -msgid "CASCADED" +#: windows/main/controller.py:295 windows/views.py:2229 +msgid "Execute all statements" msgstr "" -#: windows/views.py:2662 -#, fuzzy -msgid "CHECK OPTION" -msgstr "connessione" +#: windows/main/controller.py:282 windows/main/controller.py:297 +#: windows/main/controller.py:298 windows/views.py:2231 +msgid "Stop" +msgstr "" -#: windows/views.py:2694 -msgid "collapsible" -msgstr "collassabile" +#: windows/views.py:2287 +msgid "a page" +msgstr "" -#: windows/views.py:2710 -msgid "Column3" -msgstr "Colonna3" +#: windows/views.py:2315 +msgid "Query" +msgstr "Query" -#: windows/views.py:2711 -msgid "Column4" -msgstr "Colonna4" +#: windows/views.py:2626 +#, fuzzy +msgid "Character set" +msgstr "Creato il" -#: windows/views.py:2754 -msgid "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" -msgstr "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#: windows/views.py:2644 windows/views.py:2663 +msgid "New" +msgstr "Nuovo" -#: windows/views.py:2766 -msgid "Port" -msgstr "Porta" +#: windows/views.py:2683 +msgid "Insert record" +msgstr "Inserisci record" -#: windows/views.py:2789 -msgid "Usage" -msgstr "Utilizzo" +#: windows/views.py:2688 +msgid "Duplicate record" +msgstr "Duplica record" -#: windows/views.py:2800 -#, python-format -msgid "%(total_rows)s" -msgstr "%(total_rows)s" +#: windows/views.py:2695 +msgid "Delete record" +msgstr "Elimina record" -#: windows/views.py:2805 -msgid "rows total" -msgstr "righe totali" +#: windows/views.py:2733 windows/views.py:2964 +msgid "Up" +msgstr "Su" -#: windows/views.py:2824 -msgid "Lines" -msgstr "Righe" +#: windows/views.py:2740 windows/views.py:2971 +msgid "Down" +msgstr "Giù" -#: windows/views.py:2856 -msgid "Temporary" -msgstr "Temporaneo" +#: windows/views.py:3100 +msgid "Save Starments" +msgstr "" -#: windows/views.py:2867 +#: windows/views.py:3108 #, fuzzy -msgid "Engine options" -msgstr "Opzioni" +msgid "Location" +msgstr "Ordinamento" -#: windows/views.py:2926 -msgid "RadioBtn" +#: windows/views.py:3115 +msgid "*.sql" msgstr "" -#: windows/views.py:3009 -msgid "Edit Column" -msgstr "Modifica colonna" - -#: windows/views.py:3025 -msgid "Datatype" -msgstr "Tipo di dati" - -#: windows/components/dataview.py:121 windows/views.py:3040 -msgid "Length/Set" -msgstr "Lunghezza/Insieme" - -#: windows/components/dataview.py:51 windows/views.py:3079 -msgid "Unsigned" -msgstr "Senza segno" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3085 +#: windows/components/dataview.py:75 msgid "Allow NULL" msgstr "Consenti NULL" -#: windows/views.py:3091 -msgid "Zero Fill" -msgstr "Riempimento zero" +#: windows/components/dataview.py:28 +msgid "Check" +msgstr "Verifica" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3102 +#: windows/components/dataview.py:78 msgid "Default" msgstr "Predefinito" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3128 +#: windows/components/dataview.py:82 msgid "Virtuality" msgstr "Virtualità" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3143 msgid "Expression" msgstr "Espressione" -#: windows/components/dataview.py:28 -msgid "Check" -msgstr "Verifica" +#: windows/components/dataview.py:51 +msgid "Unsigned" +msgstr "Senza segno" #: windows/components/dataview.py:53 msgid "Zerofill" @@ -868,6 +845,10 @@ msgstr "#" msgid "Data type" msgstr "Tipo di dati" +#: windows/components/dataview.py:121 +msgid "Length/Set" +msgstr "Lunghezza/Insieme" + #: windows/components/dataview.py:155 msgid "Add column\tCTRL+INS" msgstr "Aggiungi colonna\tCTRL+INS" @@ -944,111 +925,164 @@ msgstr "AUTO INCREMENTO" msgid "Text/Expression" msgstr "Testo/Espressione" -#: windows/dialogs/connections/view.py:122 windows/main/tabs/query.py:387 +#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:401 +#: windows/dialogs/connections/view.py:414 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:413 +#: windows/dialogs/connections/view.py:426 #, python-brace-format msgid "Do you want save the connection {connection_name}?" msgstr "" -#: windows/dialogs/connections/view.py:416 +#: windows/dialogs/connections/view.py:429 msgid "Confirm save" msgstr "Conferma salvataggio" -#: windows/dialogs/connections/view.py:468 +#: windows/dialogs/connections/view.py:481 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:470 +#: windows/dialogs/connections/view.py:483 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:737 +#: windows/dialogs/connections/view.py:750 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:775 #, fuzzy, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "Errore di connessione" -#: windows/dialogs/connections/view.py:763 +#: windows/dialogs/connections/view.py:776 msgid "Connection error" msgstr "Errore di connessione" -#: windows/dialogs/connections/view.py:789 +#: windows/dialogs/connections/view.py:802 #, fuzzy, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" msgstr "Vuoi eliminare i record?" -#: windows/dialogs/connections/view.py:792 -#: windows/dialogs/connections/view.py:809 +#: windows/dialogs/connections/view.py:805 +#: windows/dialogs/connections/view.py:822 msgid "Confirm delete" msgstr "Conferma eliminazione" -#: windows/dialogs/connections/view.py:806 +#: windows/dialogs/connections/view.py:819 #, fuzzy, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "Vuoi eliminare i record?" -#: windows/main/controller.py:189 +#: windows/main/controller.py:275 +#, python-brace-format +msgid "{text} ({shortcut})" +msgstr "" + +#: windows/main/controller.py:281 windows/main/controller.py:294 +#, fuzzy +msgid "Execute all" +msgstr "Eseguibile SSH" + +#: windows/main/controller.py:471 windows/main/controller.py:479 +#, fuzzy +msgid "Query (1)" +msgstr "Query" + +#: windows/main/controller.py:497 +#, python-brace-format +msgid "Query ({query_number})" +msgstr "" + +#: windows/main/controller.py:530 +msgid "You have unsaved changes. Save before closing?" +msgstr "" + +#: windows/main/controller.py:531 +msgid "Unsaved query" +msgstr "" + +#: windows/main/controller.py:576 +#, fuzzy +msgid "Save query" +msgstr "Query" + +#: windows/main/controller.py:579 +msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" +msgstr "" + +#: windows/main/controller.py:616 windows/main/controller.py:642 +#: windows/main/database/list.py:84 windows/main/database/view.py:256 +#: windows/main/database/view.py:282 windows/main/query/controller.py:177 +msgid "Error" +msgstr "Errore" + +#: windows/main/controller.py:622 +#, python-brace-format +msgid "-- Saved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:647 +#, python-brace-format +msgid "-- Autosaved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:704 msgid "days" msgstr "giorni" -#: windows/main/controller.py:190 +#: windows/main/controller.py:705 msgid "hours" msgstr "ore" -#: windows/main/controller.py:191 +#: windows/main/controller.py:706 msgid "minutes" msgstr "minuti" -#: windows/main/controller.py:192 +#: windows/main/controller.py:707 msgid "seconds" msgstr "secondi" -#: windows/main/controller.py:200 +#: windows/main/controller.py:715 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizzata: {used} ({percentage:.2%})" -#: windows/main/controller.py:236 +#: windows/main/controller.py:751 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:441 +#: windows/main/controller.py:952 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:443 +#: windows/main/controller.py:954 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:608 +#: windows/main/controller.py:1119 msgid "Version" msgstr "Versione" -#: windows/main/controller.py:610 +#: windows/main/controller.py:1121 msgid "Uptime" msgstr "Tempo di attività" -#: windows/main/controller.py:678 +#: windows/main/controller.py:1199 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:711 +#: windows/main/controller.py:1232 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1058,153 +1092,177 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:716 windows/main/controller.py:737 +#: windows/main/controller.py:1237 windows/main/controller.py:1258 #, fuzzy msgid "Delete database" msgstr "Elimina tabella" -#: windows/main/controller.py:722 +#: windows/main/controller.py:1243 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:723 +#: windows/main/controller.py:1244 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:736 +#: windows/main/controller.py:1257 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:751 +#: windows/main/controller.py:1272 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:752 windows/main/tabs/view.py:253 -#: windows/main/tabs/view.py:279 +#: windows/main/controller.py:1273 windows/main/database/view.py:253 +#: windows/main/database/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:871 +#: windows/main/controller.py:1392 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:897 +#: windows/main/controller.py:1418 #, fuzzy, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "Vuoi eliminare i record?" -#: windows/main/controller.py:900 +#: windows/main/controller.py:1421 msgid "Delete table" msgstr "Elimina tabella" -#: windows/main/controller.py:919 +#: windows/main/controller.py:1440 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1017 +#: windows/main/controller.py:1563 msgid "Do you want delete the records?" msgstr "Vuoi eliminare i record?" -#: windows/main/tabs/database.py:71 +#: windows/main/database/list.py:69 msgid "The connection to the database was lost." msgstr "La connessione al database è stata persa." -#: windows/main/tabs/database.py:73 +#: windows/main/database/list.py:71 msgid "Do you want to reconnect?" msgstr "Vuoi riconnetterti?" -#: windows/main/tabs/database.py:75 +#: windows/main/database/list.py:73 msgid "Connection lost" msgstr "Connessione persa" -#: windows/main/tabs/database.py:85 +#: windows/main/database/list.py:83 msgid "Reconnection failed:" msgstr "Riconnessione fallita:" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:489 -#: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 -msgid "Error" -msgstr "Errore" +#: windows/main/database/view.py:252 +msgid "View created successfully" +msgstr "" -#: windows/main/tabs/query.py:308 -#, python-brace-format -msgid "{affected_rows} rows affected" +#: windows/main/database/view.py:252 +msgid "View updated successfully" msgstr "" -#: windows/main/tabs/query.py:315 windows/main/tabs/query.py:339 +#: windows/main/database/view.py:256 #, python-brace-format -msgid "Query {query_number}" +msgid "Error saving view: {}" msgstr "" -#: windows/main/tabs/query.py:320 +#: windows/main/database/view.py:269 #, python-brace-format -msgid "Query {query_number} (Error)" +msgid "Are you sure you want to delete view '{}'?" msgstr "" -#: windows/main/tabs/query.py:334 -#, python-brace-format -msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" +#: windows/main/database/view.py:270 +#, fuzzy +msgid "Confirm Delete" +msgstr "Conferma eliminazione" + +#: windows/main/database/view.py:279 +msgid "View deleted successfully" msgstr "" -#: windows/main/tabs/query.py:360 +#: windows/main/database/view.py:282 #, python-brace-format -msgid "{rows_count} rows" +msgid "Error deleting view: {}" msgstr "" -#: windows/main/tabs/query.py:362 +#: windows/main/query/controller.py:110 #, python-brace-format -msgid "{elapsed_ms:.1f} ms" +msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/tabs/query.py:366 +#: windows/main/query/controller.py:112 #, python-brace-format -msgid "{warnings_count} warnings" +msgid "{elapsed_s:.2f} s" msgstr "" -#: windows/main/tabs/query.py:381 +#: windows/main/query/controller.py:115 #, fuzzy -msgid "Error:" -msgstr "Errore" +msgid "none" +msgstr "Clona" + +#: windows/main/query/controller.py:121 +#, python-brace-format +msgid "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." +msgstr "" + +#: windows/main/query/controller.py:134 +msgid "Query execution cancelled" +msgstr "" -#: windows/main/tabs/query.py:488 +#: windows/main/query/controller.py:176 #, fuzzy msgid "No active database connection" msgstr "Nuova connessione" -#: windows/main/tabs/view.py:252 -msgid "View created successfully" +#: windows/main/query/renderer.py:53 +#, python-brace-format +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View updated successfully" +#: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 +#, python-brace-format +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/view.py:256 +#: windows/main/query/renderer.py:65 #, python-brace-format -msgid "Error saving view: {}" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/view.py:269 +#: windows/main/query/renderer.py:79 #, python-brace-format -msgid "Are you sure you want to delete view '{}'?" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/view.py:270 -#, fuzzy -msgid "Confirm Delete" -msgstr "Conferma eliminazione" +#: windows/main/query/renderer.py:165 +#, python-brace-format +msgid "{rows_count} rows" +msgstr "" -#: windows/main/tabs/view.py:279 -msgid "View deleted successfully" +#: windows/main/query/renderer.py:167 +#, python-brace-format +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/view.py:282 +#: windows/main/query/renderer.py:171 #, python-brace-format -msgid "Error deleting view: {}" +msgid "{warnings_count} warnings" msgstr "" +#: windows/main/query/renderer.py:186 +#, fuzzy +msgid "Error:" +msgstr "Errore" + #~ msgid "Created at:" #~ msgstr "" @@ -1264,3 +1322,73 @@ msgstr "" #~ msgid "{} warnings" #~ msgstr "" +#~ msgid "Edit Value" +#~ msgstr "Modifica valore" + +#~ msgid "Query #2" +#~ msgstr "Query #2" + +#~ msgid "Column5" +#~ msgstr "Colonna5" + +#~ msgid "Import" +#~ msgstr "Importa" + +#~ msgid "Read only" +#~ msgstr "" + +#~ msgid "CASCADED" +#~ msgstr "" + +#~ msgid "CHECK OPTION" +#~ msgstr "connessione" + +#~ msgid "collapsible" +#~ msgstr "collassabile" + +#~ msgid "Column3" +#~ msgstr "Colonna3" + +#~ msgid "Column4" +#~ msgstr "Colonna4" + +#~ msgid "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#~ msgstr "" +#~ "Database " +#~ "(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" + +#~ msgid "Port" +#~ msgstr "Porta" + +#~ msgid "Usage" +#~ msgstr "Utilizzo" + +#~ msgid "%(total_rows)s" +#~ msgstr "%(total_rows)s" + +#~ msgid "rows total" +#~ msgstr "righe totali" + +#~ msgid "Lines" +#~ msgstr "Righe" + +#~ msgid "Temporary" +#~ msgstr "Temporaneo" + +#~ msgid "Engine options" +#~ msgstr "Opzioni" + +#~ msgid "RadioBtn" +#~ msgstr "" + +#~ msgid "Edit Column" +#~ msgstr "Modifica colonna" + +#~ msgid "Datatype" +#~ msgstr "Tipo di dati" + +#~ msgid "Zero Fill" +#~ msgstr "Riempimento zero" + diff --git a/locale/petersql.pot b/locale/petersql.pot index f6b6181..fcc7476 100644 --- a/locale/petersql.pot +++ b/locale/petersql.pot @@ -27,66 +27,62 @@ msgstr "" msgid "OpenSSH client not found." msgstr "" -#: structures/engines/mariadb/context.py:592 -#: structures/engines/mysql/context.py:563 -#: structures/engines/postgresql/context.py:579 -#: structures/engines/sqlite/context.py:518 +#: structures/engines/mariadb/context.py:611 +#: structures/engines/mysql/context.py:622 +#: structures/engines/postgresql/context.py:645 +#: structures/engines/sqlite/context.py:524 #, python-brace-format msgid "Table{table_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:620 -#: structures/engines/mysql/context.py:591 -#: structures/engines/postgresql/context.py:604 -#: structures/engines/sqlite/context.py:542 +#: structures/engines/mariadb/context.py:639 +#: structures/engines/mysql/context.py:650 +#: structures/engines/postgresql/context.py:670 +#: structures/engines/sqlite/context.py:548 #, python-brace-format msgid "Column{column_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:638 -#: structures/engines/mysql/context.py:609 -#: structures/engines/postgresql/context.py:622 -#: structures/engines/sqlite/context.py:560 +#: structures/engines/mariadb/context.py:657 +#: structures/engines/mysql/context.py:668 +#: structures/engines/postgresql/context.py:688 +#: structures/engines/sqlite/context.py:566 #, python-brace-format msgid "Index{index_number:03}" msgstr "" -#: structures/engines/mariadb/context.py:678 -#: structures/engines/mysql/context.py:649 -#: structures/engines/postgresql/context.py:662 -#: structures/engines/sqlite/context.py:602 +#: structures/engines/mariadb/context.py:697 +#: structures/engines/mysql/context.py:706 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:608 #, python-brace-format msgid "ForeignKey{foreign_key_number:03}" msgstr "" -#: structures/engines/mariadb/context.py:711 -#: structures/engines/mysql/context.py:680 -#: structures/engines/postgresql/context.py:692 -#: structures/engines/sqlite/context.py:630 +#: structures/engines/mariadb/context.py:730 +#: structures/engines/mysql/context.py:737 +#: structures/engines/postgresql/context.py:758 +#: structures/engines/sqlite/context.py:636 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:762 +#: structures/engines/mariadb/context.py:781 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "" -#: windows/dialogs/connections/view.py:402 -#: windows/dialogs/connections/view.py:739 windows/main/controller.py:606 +#: windows/dialogs/connections/view.py:415 +#: windows/dialogs/connections/view.py:752 windows/main/controller.py:1117 #: windows/views.py:33 msgid "Connection" msgstr "" #: windows/components/dataview.py:113 windows/components/dataview.py:225 #: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py:1003 -#: windows/views.py:1353 windows/views.py:1460 windows/views.py:1843 -#: windows/views.py:2788 windows/views.py:2811 windows/views.py:2812 -#: windows/views.py:2813 windows/views.py:2814 windows/views.py:2815 -#: windows/views.py:2816 windows/views.py:2817 windows/views.py:2818 -#: windows/views.py:2819 windows/views.py:2823 windows/views.py:3017 -#: windows/views.py:3218 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1006 +#: windows/views.py:1338 windows/views.py:1451 windows/views.py:1831 +#: windows/views.py:2821 msgid "Name" msgstr "" @@ -94,12 +90,12 @@ msgstr "" msgid "Last connection" msgstr "" -#: windows/dialogs/connections/view.py:640 windows/views.py:61 +#: windows/dialogs/connections/view.py:653 windows/views.py:61 msgid "New directory" msgstr "" #: windows/dialogs/connections/model.py:206 -#: windows/dialogs/connections/view.py:600 windows/views.py:65 +#: windows/dialogs/connections/view.py:613 windows/views.py:65 msgid "New connection" msgstr "" @@ -111,14 +107,14 @@ msgstr "" msgid "Clone connection" msgstr "" -#: windows/views.py:81 windows/views.py:603 windows/views.py:1337 -#: windows/views.py:1378 windows/views.py:1753 windows/views.py:1785 -#: windows/views.py:2044 windows/views.py:3354 windows/views.py:3386 +#: windows/views.py:81 windows/views.py:603 windows/views.py:1322 +#: windows/views.py:1365 windows/views.py:1773 windows/views.py:2032 +#: windows/views.py:2726 windows/views.py:2957 windows/views.py:2989 msgid "Delete" msgstr "" -#: windows/views.py:111 windows/views.py:1358 windows/views.py:1515 -#: windows/views.py:2828 windows/views.py:3273 +#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 +#: windows/views.py:2876 msgid "Engine" msgstr "" @@ -130,7 +126,7 @@ msgstr "" msgid "Username" msgstr "" -#: windows/views.py:161 windows/views.py:1147 +#: windows/views.py:161 windows/views.py:1132 msgid "Password" msgstr "" @@ -150,26 +146,25 @@ msgstr "" msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 windows/views.py:2749 +#: windows/views.py:233 msgid "Filename" msgstr "" -#: windows/views.py:238 windows/views.py:358 windows/views.py:2754 -#: windows/views.py:2937 +#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 msgid "Select a file" msgstr "" -#: windows/views.py:238 windows/views.py:358 windows/views.py:2937 +#: windows/views.py:238 windows/views.py:358 msgid "*.*" msgstr "" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:255 windows/views.py:1360 windows/views.py:1473 -#: windows/views.py:2829 windows/views.py:3115 windows/views.py:3231 +#: windows/views.py:255 windows/views.py:1345 windows/views.py:1464 +#: windows/views.py:2834 msgid "Comments" msgstr "" -#: windows/main/controller.py:236 windows/views.py:269 windows/views.py:730 +#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 #: windows/views.py:884 msgid "Settings" msgstr "" @@ -226,7 +221,7 @@ msgstr "" msgid "SSH Tunnel" msgstr "" -#: windows/views.py:411 windows/views.py:1356 windows/views.py:2826 +#: windows/views.py:411 windows/views.py:1341 msgid "Created at" msgstr "" @@ -262,7 +257,7 @@ msgstr "" msgid "Statistics" msgstr "" -#: windows/views.py:584 windows/views.py:1719 +#: windows/views.py:584 windows/views.py:1730 msgid "Create" msgstr "" @@ -274,14 +269,15 @@ msgstr "" msgid "Create directory" msgstr "" -#: windows/views.py:620 windows/views.py:844 windows/views.py:1375 -#: windows/views.py:1788 windows/views.py:2049 windows/views.py:2152 -#: windows/views.py:3162 windows/views.py:3389 +#: windows/views.py:620 windows/views.py:844 windows/views.py:1360 +#: windows/views.py:1776 windows/views.py:2037 windows/views.py:2093 +#: windows/views.py:2702 windows/views.py:2992 windows/views.py:3129 msgid "Cancel" msgstr "" -#: windows/views.py:625 windows/views.py:2054 windows/views.py:3172 -#: windows/views.py:3394 +#: windows/main/controller.py:283 windows/main/controller.py:300 +#: windows/main/controller.py:301 windows/views.py:625 windows/views.py:2042 +#: windows/views.py:2235 windows/views.py:2997 windows/views.py:3135 msgid "Save" msgstr "" @@ -314,7 +310,7 @@ msgid "Locale" msgstr "" #: windows/views.py:780 -msgid "Edit Value" +msgid "Column content" msgstr "" #: windows/views.py:790 @@ -345,7 +341,7 @@ msgstr "" msgid "Open connection manager" msgstr "" -#: windows/views.py:900 +#: windows/views.py:902 msgid "Disconnect from server" msgstr "" @@ -357,21 +353,21 @@ msgstr "" msgid "Refresh" msgstr "" -#: windows/views.py:908 windows/views.py:910 +#: windows/views.py:908 windows/views.py:910 windows/views.py:1754 +#: windows/views.py:2077 windows/views.py:2221 msgid "Add" msgstr "" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2232 -#: windows/views.py:2735 +#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 msgid "MyMenuItem" msgstr "" -#: windows/views.py:951 windows/views.py:1816 windows/views.py:3417 +#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 msgid "MyMenu" msgstr "" -#: windows/views.py:966 windows/views.py:1397 windows/views.py:1404 -#: windows/views.py:1411 +#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 +#: windows/views.py:1402 msgid "MyLabel" msgstr "" @@ -379,8 +375,7 @@ msgstr "" msgid "Databases" msgstr "" -#: windows/views.py:973 windows/views.py:1355 windows/views.py:2797 -#: windows/views.py:2825 +#: windows/views.py:973 windows/views.py:1340 msgid "Size" msgstr "" @@ -400,417 +395,394 @@ msgstr "" msgid "System" msgstr "" -#: windows/views.py:1026 -msgid "Character set" -msgstr "" - #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1047 -#: windows/views.py:1359 windows/views.py:3061 +#: windows/components/dataview.py:89 windows/views.py:1029 +#: windows/views.py:1344 msgid "Collation" msgstr "" -#: windows/views.py:1073 windows/views.py:2943 +#: windows/views.py:1058 msgid "Encryption" msgstr "" -#: windows/views.py:1085 +#: windows/views.py:1070 msgid "Read Only" msgstr "" -#: windows/views.py:1102 +#: windows/views.py:1087 msgid "Tablespace" msgstr "" -#: windows/views.py:1123 +#: windows/views.py:1108 msgid "Connection limit" msgstr "" -#: windows/views.py:1166 +#: windows/views.py:1151 msgid "Profile" msgstr "" -#: windows/views.py:1192 +#: windows/views.py:1177 msgid "Default tablespace" msgstr "" -#: windows/views.py:1213 +#: windows/views.py:1198 msgid "Temporary tablespace" msgstr "" -#: windows/views.py:1239 +#: windows/views.py:1224 msgid "Quota" msgstr "" -#: windows/views.py:1258 +#: windows/views.py:1243 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1275 +#: windows/views.py:1260 msgid "Account status" msgstr "" -#: windows/views.py:1296 +#: windows/views.py:1281 msgid "Password expire" msgstr "" -#: windows/views.py:1317 +#: windows/views.py:1302 msgid "Table:" msgstr "" -#: windows/views.py:1325 windows/views.py:1598 windows/views.py:1642 -#: windows/views.py:1748 windows/views.py:3349 +#: 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:1330 +#: windows/views.py:1315 msgid "Clone" msgstr "" -#: windows/views.py:1354 +#: windows/views.py:1339 msgid "Rows" msgstr "" -#: windows/views.py:1357 windows/views.py:2827 +#: windows/views.py:1342 msgid "Updated at" msgstr "" -#: windows/views.py:1381 windows/views.py:1793 windows/views.py:2159 -#: windows/views.py:2206 +#: windows/views.py:1370 windows/views.py:1781 windows/views.py:2091 +#: windows/views.py:2173 windows/views.py:2709 msgid "Apply" msgstr "" -#: windows/views.py:1391 windows/views.py:1550 windows/views.py:2003 -#: windows/views.py:3306 +#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 +#: windows/views.py:2909 msgid "Options" msgstr "" -#: windows/views.py:1422 +#: windows/views.py:1413 msgid "Diagram" msgstr "" -#: windows/views.py:1433 windows/views.py:2796 +#: windows/views.py:1424 msgid "Database" msgstr "" -#: windows/views.py:1488 windows/views.py:3246 +#: windows/views.py:1479 windows/views.py:2849 msgid "Base" msgstr "" -#: windows/views.py:1502 windows/views.py:3260 +#: windows/views.py:1493 windows/views.py:2863 msgid "Auto Increment" msgstr "" -#: windows/views.py:1530 windows/views.py:3288 +#: windows/views.py:1521 windows/views.py:2891 msgid "Default Collation" msgstr "" -#: windows/views.py:1562 windows/views.py:1603 windows/views.py:1647 +#: windows/views.py:1531 +msgid "Convert data" +msgstr "" + +#: windows/views.py:1539 +msgid "Row format" +msgstr "" + +#: windows/views.py:1573 windows/views.py:1614 windows/views.py:1658 +#: windows/views.py:1756 windows/views.py:2081 msgid "Remove" msgstr "" -#: windows/views.py:1569 windows/views.py:1610 windows/views.py:1654 +#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 msgid "Clear" msgstr "" -#: windows/views.py:1584 windows/views.py:3320 +#: windows/views.py:1595 windows/views.py:2923 msgid "Indexes" msgstr "" -#: windows/views.py:1628 +#: windows/views.py:1639 msgid "Foreign Keys" msgstr "" -#: windows/views.py:1672 +#: windows/views.py:1683 msgid "Checks" msgstr "" -#: windows/views.py:1740 windows/views.py:3341 +#: windows/views.py:1750 windows/views.py:2944 msgid "Columns:" msgstr "" -#: windows/views.py:1760 windows/views.py:3361 -msgid "Up" +#: windows/views.py:1760 +msgid "Move Up" msgstr "" -#: windows/views.py:1767 windows/views.py:3368 -msgid "Down" +#: windows/views.py:1762 +msgid "Move Down" msgstr "" -#: windows/views.py:1806 windows/views.py:1813 windows/views.py:3407 -#: windows/views.py:3414 +#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 +#: windows/views.py:3017 msgid "Add Index" msgstr "" -#: windows/views.py:1810 windows/views.py:3411 +#: windows/views.py:1798 windows/views.py:3014 msgid "Add PrimaryKey" msgstr "" -#: windows/views.py:1827 +#: windows/views.py:1815 msgid "Table" msgstr "" -#: windows/views.py:1863 +#: windows/views.py:1851 msgid "Definer" msgstr "" -#: windows/views.py:1883 +#: windows/views.py:1871 msgid "Schema" msgstr "" -#: windows/views.py:1909 +#: windows/views.py:1897 msgid "SQL security" msgstr "" -#: windows/views.py:1916 +#: windows/views.py:1904 msgid "DEFINER" msgstr "" -#: windows/views.py:1916 +#: windows/views.py:1904 msgid "INVOKER" msgstr "" -#: windows/views.py:1928 windows/views.py:2639 windows/views.py:2658 -#: windows/views.py:2901 +#: windows/views.py:1916 msgid "Algorithm" msgstr "" -#: windows/views.py:1930 windows/views.py:2624 windows/views.py:2657 -#: windows/views.py:2906 +#: windows/views.py:1918 msgid "UNDEFINED" msgstr "" -#: windows/views.py:1933 windows/views.py:2627 windows/views.py:2657 -#: windows/views.py:2909 +#: windows/views.py:1921 msgid "MERGE" msgstr "" -#: windows/views.py:1936 windows/views.py:2636 windows/views.py:2657 -#: windows/views.py:2912 +#: windows/views.py:1924 msgid "TEMPTABLE" msgstr "" -#: windows/views.py:1946 windows/views.py:2663 +#: windows/views.py:1934 msgid "View constraint" msgstr "" -#: windows/views.py:1948 windows/views.py:2662 +#: windows/views.py:1936 msgid "None" msgstr "" -#: windows/views.py:1951 windows/views.py:2662 +#: windows/views.py:1939 msgid "LOCAL" msgstr "" -#: windows/views.py:1954 +#: windows/views.py:1942 msgid "CASCADE" msgstr "" -#: windows/views.py:1957 +#: windows/views.py:1945 msgid "CHECK ONLY" msgstr "" -#: windows/views.py:1960 windows/views.py:2662 +#: windows/views.py:1948 msgid "READ ONLY" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:1960 msgid "Force" msgstr "" -#: windows/views.py:1984 +#: windows/views.py:1972 msgid "Security barrier" msgstr "" -#: windows/views.py:2066 +#: windows/views.py:2054 msgid "Views" msgstr "" -#: windows/views.py:2074 +#: windows/views.py:2062 msgid "Triggers" msgstr "" -#: windows/views.py:2086 -#, python-brace-format -msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" - -#: windows/views.py:2094 -msgid "First" +#: windows/views.py:2073 +msgid "Refrsh" msgstr "" -#: windows/views.py:2112 -msgid "Last" +#: windows/views.py:2079 +msgid "Duplicate" msgstr "" -#: windows/views.py:2123 -msgid "Insert record" +#: windows/views.py:2085 +msgid "Apply changes automatically" msgstr "" -#: windows/views.py:2128 -msgid "Duplicate record" +#: windows/views.py:2087 windows/views.py:2088 +msgid "" +"If enabled, table edits are applied immediately without pressing Apply or" +" Cancel" msgstr "" -#: windows/views.py:2135 -msgid "Delete record" +#: windows/views.py:2101 +#, python-brace-format +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" -#: windows/views.py:2145 -msgid "Apply changes automatically" +#: windows/views.py:2109 +msgid "First" msgstr "" -#: windows/views.py:2147 windows/views.py:2148 -msgid "" -"If enabled, table edits are applied immediately without pressing Apply or" -" Cancel" +#: windows/views.py:2127 +msgid "Last" msgstr "" -#: windows/views.py:2169 +#: windows/views.py:2136 msgid "Filters" msgstr "" -#: windows/views.py:2209 +#: windows/views.py:2176 msgid "CTRL+ENTER" msgstr "" -#: windows/views.py:2229 +#: windows/views.py:2196 msgid "Insert row" msgstr "" -#: windows/views.py:2237 +#: windows/views.py:2204 msgid "Data" msgstr "" -#: windows/views.py:2291 windows/views.py:2341 -msgid "New" +#: windows/main/controller.py:278 windows/main/controller.py:287 +#: windows/main/controller.py:288 windows/views.py:2221 +msgid "New query" msgstr "" -#: windows/views.py:2318 -msgid "Query" -msgstr "" - -#: windows/views.py:2338 +#: windows/views.py:2223 windows/views.py:2660 msgid "Close" msgstr "" -#: windows/views.py:2351 -msgid "Query #2" -msgstr "" - -#: windows/views.py:2618 -msgid "Column5" -msgstr "" - -#: windows/views.py:2629 -msgid "Import" -msgstr "" - -#: windows/views.py:2654 -msgid "Read only" -msgstr "" - -#: windows/views.py:2662 -msgid "CASCADED" +#: windows/main/controller.py:279 windows/main/controller.py:289 +#: windows/main/controller.py:290 windows/views.py:2223 +msgid "Close query" msgstr "" -#: windows/views.py:2662 -msgid "CHECK OPTION" +#: windows/views.py:2227 +msgid "Run" msgstr "" -#: windows/views.py:2694 -msgid "collapsible" +#: windows/main/controller.py:280 windows/main/controller.py:292 +#: windows/main/controller.py:293 windows/views.py:2227 +msgid "Execute" msgstr "" -#: windows/views.py:2710 -msgid "Column3" +#: windows/views.py:2229 +msgid "Run all" msgstr "" -#: windows/views.py:2711 -msgid "Column4" +#: windows/main/controller.py:295 windows/views.py:2229 +msgid "Execute all statements" msgstr "" -#: windows/views.py:2754 -msgid "" -"Database " -"(*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3)|*.db;*.db3;*.sdb;*.s3db;*.sqlite;*.sqlite3" +#: windows/main/controller.py:282 windows/main/controller.py:297 +#: windows/main/controller.py:298 windows/views.py:2231 +msgid "Stop" msgstr "" -#: windows/views.py:2766 -msgid "Port" +#: windows/views.py:2287 +msgid "a page" msgstr "" -#: windows/views.py:2789 -msgid "Usage" +#: windows/views.py:2315 +msgid "Query" msgstr "" -#: windows/views.py:2800 -#, python-format -msgid "%(total_rows)s" +#: windows/views.py:2626 +msgid "Character set" msgstr "" -#: windows/views.py:2805 -msgid "rows total" +#: windows/views.py:2644 windows/views.py:2663 +msgid "New" msgstr "" -#: windows/views.py:2824 -msgid "Lines" +#: windows/views.py:2683 +msgid "Insert record" msgstr "" -#: windows/views.py:2856 -msgid "Temporary" +#: windows/views.py:2688 +msgid "Duplicate record" msgstr "" -#: windows/views.py:2867 -msgid "Engine options" +#: windows/views.py:2695 +msgid "Delete record" msgstr "" -#: windows/views.py:2926 -msgid "RadioBtn" +#: windows/views.py:2733 windows/views.py:2964 +msgid "Up" msgstr "" -#: windows/views.py:3009 -msgid "Edit Column" +#: windows/views.py:2740 windows/views.py:2971 +msgid "Down" msgstr "" -#: windows/views.py:3025 -msgid "Datatype" +#: windows/views.py:3100 +msgid "Save Starments" msgstr "" -#: windows/components/dataview.py:121 windows/views.py:3040 -msgid "Length/Set" +#: windows/views.py:3108 +msgid "Location" msgstr "" -#: windows/components/dataview.py:51 windows/views.py:3079 -msgid "Unsigned" +#: windows/views.py:3115 +msgid "*.sql" msgstr "" #: windows/components/dataview.py:25 windows/components/dataview.py:52 -#: windows/components/dataview.py:75 windows/views.py:3085 +#: windows/components/dataview.py:75 msgid "Allow NULL" msgstr "" -#: windows/views.py:3091 -msgid "Zero Fill" +#: windows/components/dataview.py:28 +msgid "Check" msgstr "" #: windows/components/dataview.py:32 windows/components/dataview.py:56 -#: windows/components/dataview.py:78 windows/views.py:3102 +#: windows/components/dataview.py:78 msgid "Default" msgstr "" #: windows/components/dataview.py:36 windows/components/dataview.py:60 -#: windows/components/dataview.py:82 windows/views.py:3128 +#: windows/components/dataview.py:82 msgid "Virtuality" msgstr "" #: windows/components/dataview.py:39 windows/components/dataview.py:63 #: windows/components/dataview.py:85 windows/components/dataview.py:241 -#: windows/views.py:3143 msgid "Expression" msgstr "" -#: windows/components/dataview.py:28 -msgid "Check" +#: windows/components/dataview.py:51 +msgid "Unsigned" msgstr "" #: windows/components/dataview.py:53 @@ -825,6 +797,10 @@ msgstr "" msgid "Data type" msgstr "" +#: windows/components/dataview.py:121 +msgid "Length/Set" +msgstr "" + #: windows/components/dataview.py:155 msgid "Add column\tCTRL+INS" msgstr "" @@ -901,111 +877,161 @@ msgstr "" msgid "Text/Expression" msgstr "" -#: windows/dialogs/connections/view.py:122 windows/main/tabs/query.py:387 +#: windows/dialogs/connections/view.py:124 windows/main/query/renderer.py:192 msgid "Unknown error" msgstr "" -#: windows/dialogs/connections/view.py:401 +#: windows/dialogs/connections/view.py:414 msgid "Connection established successfully" msgstr "" -#: windows/dialogs/connections/view.py:413 +#: windows/dialogs/connections/view.py:426 #, python-brace-format msgid "Do you want save the connection {connection_name}?" msgstr "" -#: windows/dialogs/connections/view.py:416 +#: windows/dialogs/connections/view.py:429 msgid "Confirm save" msgstr "" -#: windows/dialogs/connections/view.py:468 +#: windows/dialogs/connections/view.py:481 msgid "You have unsaved changes. Do you want to save them before continuing?" msgstr "" -#: windows/dialogs/connections/view.py:470 +#: windows/dialogs/connections/view.py:483 msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:737 +#: windows/dialogs/connections/view.py:750 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:775 #, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "" -#: windows/dialogs/connections/view.py:763 +#: windows/dialogs/connections/view.py:776 msgid "Connection error" msgstr "" -#: windows/dialogs/connections/view.py:789 +#: windows/dialogs/connections/view.py:802 #, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" msgstr "" -#: windows/dialogs/connections/view.py:792 -#: windows/dialogs/connections/view.py:809 +#: windows/dialogs/connections/view.py:805 +#: windows/dialogs/connections/view.py:822 msgid "Confirm delete" msgstr "" -#: windows/dialogs/connections/view.py:806 +#: windows/dialogs/connections/view.py:819 #, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "" -#: windows/main/controller.py:189 +#: windows/main/controller.py:275 +#, python-brace-format +msgid "{text} ({shortcut})" +msgstr "" + +#: windows/main/controller.py:281 windows/main/controller.py:294 +msgid "Execute all" +msgstr "" + +#: windows/main/controller.py:471 windows/main/controller.py:479 +msgid "Query (1)" +msgstr "" + +#: windows/main/controller.py:497 +#, python-brace-format +msgid "Query ({query_number})" +msgstr "" + +#: windows/main/controller.py:530 +msgid "You have unsaved changes. Save before closing?" +msgstr "" + +#: windows/main/controller.py:531 +msgid "Unsaved query" +msgstr "" + +#: windows/main/controller.py:576 +msgid "Save query" +msgstr "" + +#: windows/main/controller.py:579 +msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" +msgstr "" + +#: windows/main/controller.py:616 windows/main/controller.py:642 +#: windows/main/database/list.py:84 windows/main/database/view.py:256 +#: windows/main/database/view.py:282 windows/main/query/controller.py:177 +msgid "Error" +msgstr "" + +#: windows/main/controller.py:622 +#, python-brace-format +msgid "-- Saved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:647 +#, python-brace-format +msgid "-- Autosaved query to {file_path}" +msgstr "" + +#: windows/main/controller.py:704 msgid "days" msgstr "" -#: windows/main/controller.py:190 +#: windows/main/controller.py:705 msgid "hours" msgstr "" -#: windows/main/controller.py:191 +#: windows/main/controller.py:706 msgid "minutes" msgstr "" -#: windows/main/controller.py:192 +#: windows/main/controller.py:707 msgid "seconds" msgstr "" -#: windows/main/controller.py:200 +#: windows/main/controller.py:715 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "" -#: windows/main/controller.py:236 +#: windows/main/controller.py:751 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:441 +#: windows/main/controller.py:952 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:443 +#: windows/main/controller.py:954 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:608 +#: windows/main/controller.py:1119 msgid "Version" msgstr "" -#: windows/main/controller.py:610 +#: windows/main/controller.py:1121 msgid "Uptime" msgstr "" -#: windows/main/controller.py:678 +#: windows/main/controller.py:1199 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:711 +#: windows/main/controller.py:1232 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1015,146 +1041,169 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:716 windows/main/controller.py:737 +#: windows/main/controller.py:1237 windows/main/controller.py:1258 msgid "Delete database" msgstr "" -#: windows/main/controller.py:722 +#: windows/main/controller.py:1243 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:723 +#: windows/main/controller.py:1244 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:736 +#: windows/main/controller.py:1257 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:751 +#: windows/main/controller.py:1272 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:752 windows/main/tabs/view.py:253 -#: windows/main/tabs/view.py:279 +#: windows/main/controller.py:1273 windows/main/database/view.py:253 +#: windows/main/database/view.py:279 msgid "Success" msgstr "" -#: windows/main/controller.py:871 +#: windows/main/controller.py:1392 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:897 +#: windows/main/controller.py:1418 #, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "" -#: windows/main/controller.py:900 +#: windows/main/controller.py:1421 msgid "Delete table" msgstr "" -#: windows/main/controller.py:919 +#: windows/main/controller.py:1440 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1017 +#: windows/main/controller.py:1563 msgid "Do you want delete the records?" msgstr "" -#: windows/main/tabs/database.py:71 +#: windows/main/database/list.py:69 msgid "The connection to the database was lost." msgstr "" -#: windows/main/tabs/database.py:73 +#: windows/main/database/list.py:71 msgid "Do you want to reconnect?" msgstr "" -#: windows/main/tabs/database.py:75 +#: windows/main/database/list.py:73 msgid "Connection lost" msgstr "" -#: windows/main/tabs/database.py:85 +#: windows/main/database/list.py:83 msgid "Reconnection failed:" msgstr "" -#: windows/main/tabs/database.py:86 windows/main/tabs/query.py:489 -#: windows/main/tabs/view.py:256 windows/main/tabs/view.py:282 -msgid "Error" +#: windows/main/database/view.py:252 +msgid "View created successfully" msgstr "" -#: windows/main/tabs/query.py:308 -#, python-brace-format -msgid "{affected_rows} rows affected" +#: windows/main/database/view.py:252 +msgid "View updated successfully" msgstr "" -#: windows/main/tabs/query.py:315 windows/main/tabs/query.py:339 +#: windows/main/database/view.py:256 #, python-brace-format -msgid "Query {query_number}" +msgid "Error saving view: {}" msgstr "" -#: windows/main/tabs/query.py:320 +#: windows/main/database/view.py:269 #, python-brace-format -msgid "Query {query_number} (Error)" +msgid "Are you sure you want to delete view '{}'?" msgstr "" -#: windows/main/tabs/query.py:334 +#: windows/main/database/view.py:270 +msgid "Confirm Delete" +msgstr "" + +#: windows/main/database/view.py:279 +msgid "View deleted successfully" +msgstr "" + +#: windows/main/database/view.py:282 #, python-brace-format -msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" +msgid "Error deleting view: {}" msgstr "" -#: windows/main/tabs/query.py:360 +#: windows/main/query/controller.py:110 #, python-brace-format -msgid "{rows_count} rows" +msgid "{elapsed_ms:.0f} ms" msgstr "" -#: windows/main/tabs/query.py:362 +#: windows/main/query/controller.py:112 #, python-brace-format -msgid "{elapsed_ms:.1f} ms" +msgid "{elapsed_s:.2f} s" +msgstr "" + +#: windows/main/query/controller.py:115 +msgid "none" msgstr "" -#: windows/main/tabs/query.py:366 +#: windows/main/query/controller.py:121 #, python-brace-format -msgid "{warnings_count} warnings" +msgid "" +"Query execution stopped after {elapsed}.\n" +"Completed statements: {completed}/{total}.\n" +"Successful: {success}.\n" +"Failed: {failed}.\n" +"Last statement: #{last}." msgstr "" -#: windows/main/tabs/query.py:381 -msgid "Error:" +#: windows/main/query/controller.py:134 +msgid "Query execution cancelled" msgstr "" -#: windows/main/tabs/query.py:488 +#: windows/main/query/controller.py:176 msgid "No active database connection" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View created successfully" +#: windows/main/query/renderer.py:53 +#, python-brace-format +msgid "{affected_rows} rows affected" msgstr "" -#: windows/main/tabs/view.py:252 -msgid "View updated successfully" +#: windows/main/query/renderer.py:60 windows/main/query/renderer.py:84 +#, python-brace-format +msgid "Query {query_number}" msgstr "" -#: windows/main/tabs/view.py:256 +#: windows/main/query/renderer.py:65 #, python-brace-format -msgid "Error saving view: {}" +msgid "Query {query_number} (Error)" msgstr "" -#: windows/main/tabs/view.py:269 +#: windows/main/query/renderer.py:79 #, python-brace-format -msgid "Are you sure you want to delete view '{}'?" +msgid "Query {query_number} ({rows_count} rows × {columns_count} cols)" msgstr "" -#: windows/main/tabs/view.py:270 -msgid "Confirm Delete" +#: windows/main/query/renderer.py:165 +#, python-brace-format +msgid "{rows_count} rows" msgstr "" -#: windows/main/tabs/view.py:279 -msgid "View deleted successfully" +#: windows/main/query/renderer.py:167 +#, python-brace-format +msgid "{elapsed_ms:.1f} ms" msgstr "" -#: windows/main/tabs/view.py:282 +#: windows/main/query/renderer.py:171 #, python-brace-format -msgid "Error deleting view: {}" +msgid "{warnings_count} warnings" +msgstr "" + +#: windows/main/query/renderer.py:186 +msgid "Error:" msgstr "" From c955a4aae3ef51ee38e45226bc388182d7c2ea83 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 23 Mar 2026 10:16:05 +0100 Subject: [PATCH 31/93] chore(project): update project files and local settings AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- PeterSQL.fbp | 4587 +++++++++++++++++++++++++++----------------------- settings.yml | 9 +- 2 files changed, 2483 insertions(+), 2113 deletions(-) diff --git a/PeterSQL.fbp b/PeterSQL.fbp index 2589fd0..924f4ae 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -11357,7 +11357,7 @@ Load From Embedded File; icons/16x16/table.png Base - 0 + 1 1 1 @@ -11697,7 +11697,7 @@ Load From File; icons/16x16/wrench.png Options - 1 + 0 1 1 @@ -13457,8 +13457,8 @@ - - + + 1 1 1 @@ -13510,95 +13510,222 @@ wxTAB_TRAVERSAL - + bSizer54 wxVERTICAL none + + 5 + wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + + 0 + + + 0 + + 1 + toolbar_columns + 1 + 1 + + + protected + 1 + + Resizable + 5 + 1 + + wxTB_HORIZONTAL|wxTB_HORZ_TEXT + ; ; forward_declare + 0 + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Columns: + 0 + + 0 + + + 0 + -1,-1 + 1 + m_staticText39 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + Load From File; icons/16x16/add.png + 0 + wxID_ANY + wxITEM_NORMAL + Add + tool_add_column + protected + + + on_insert_column + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Remove + tool_remove_column + protected + + + on_delete_column + + + protected + + + Load From File; icons/16x16/arrow_up.png + 0 + wxID_ANY + wxITEM_NORMAL + Move Up + tool_move_up_column + protected + + + on_move_up_column + + + Load From File; icons/16x16/arrow_down.png + 0 + wxID_ANY + wxITEM_NORMAL + Move Down + tool_move_down_column + protected + + + on_move_down_column + + + 5 wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + list_ctrl_table_columns + protected + + + + TableColumnsDataViewCtrl; .components.dataview; forward_declare + + + + + + + + 5 + wxEXPAND 0 - bSizer53 + bSizer52 wxHORIZONTAL none 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - 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 + wxALL 0 1 @@ -13612,7 +13739,7 @@ 0 - Load From File; icons/16x16/add.png + 1 0 @@ -13635,7 +13762,7 @@ 0 0 wxID_ANY - Insert + Delete 0 @@ -13645,7 +13772,7 @@ 0 1 - btn_insert_column + btn_delete_table 1 @@ -13657,7 +13784,7 @@ Resizable 1 - wxBORDER_NONE + ; ; forward_declare 0 @@ -13668,12 +13795,12 @@ - on_insert_column + on_delete_table - 2 - wxLEFT|wxRIGHT + 5 + wxALL 0 1 @@ -13687,7 +13814,7 @@ 0 - Load From File; icons/16x16/delete.png + 1 0 @@ -13710,7 +13837,7 @@ 0 0 wxID_ANY - Delete + Cancel 0 @@ -13720,7 +13847,7 @@ 0 1 - btn_delete_column + btn_cancel_table 1 @@ -13732,7 +13859,7 @@ Resizable 1 - wxBORDER_NONE + ; ; forward_declare 0 @@ -13743,12 +13870,12 @@ - on_delete_column + on_cancel_table - 2 - wxLEFT|wxRIGHT + 5 + wxALL 0 1 @@ -13762,7 +13889,7 @@ 0 - Load From File; icons/16x16/arrow_up.png + 1 0 @@ -13785,7 +13912,7 @@ 0 0 wxID_ANY - Up + Apply 0 @@ -13795,7 +13922,7 @@ 0 1 - btn_move_up_column + btn_apply_table 1 @@ -13807,7 +13934,7 @@ Resizable 1 - wxBORDER_NONE + ; ; forward_declare 0 @@ -13818,405 +13945,56 @@ - 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 + do_apply_table - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 + + + MyMenu + menu_table_columns + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Add Index + add_index + none + + + + + + MyMenu + m_menu21 + protected + + + 0 1 - - - 0 + wxID_ANY - - - list_ctrl_table_columns - protected - - - - TableColumnsDataViewCtrl; .components.dataview; forward_declare - - - - + wxITEM_NORMAL + Add PrimaryKey + m_menuItem8 + none + + - - - 5 - wxEXPAND - 0 - - - bSizer52 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Delete - - 0 - - 0 - - - 0 - - 1 - btn_delete_table - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_table - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Cancel - - 0 - - 0 - - - 0 - - 1 - btn_cancel_table - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_cancel_table - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Apply - - 0 - - 0 - - - 0 - - 1 - btn_apply_table - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - do_apply_table - - - - - - - MyMenu - menu_table_columns - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Add Index - add_index - none - - - - - - MyMenu - m_menu21 - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Add PrimaryKey - m_menuItem8 - none - - - - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Add Index - m_menuItem9 + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Add Index + m_menuItem9 none @@ -14234,7 +14012,7 @@ Load From File; icons/16x16/view.png Views 0 - + 1 1 1 @@ -16434,10 +16212,10 @@ wxTAB_TRAVERSAL - + Load From File; icons/16x16/text_columns.png Data - 0 + 1 1 1 @@ -16495,6 +16273,208 @@ bSizer61 wxVERTICAL none + + 5 + wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + + 0 + + + 0 + + 1 + m_toolBar3 + 1 + 1 + + + protected + 1 + + Resizable + 5 + 1 + + wxTB_HORIZONTAL|wxTB_HORZ_TEXT + ; ; forward_declare + 0 + + + + + + Load From File; icons/16x16/arrow_refresh.png + 0 + wxID_ANY + wxITEM_NORMAL + Refrsh + tool_refresh_records + protected + + + on_refresh_records + + + protected + + + Load From File; icons/16x16/add.png + 0 + wxID_ANY + wxITEM_NORMAL + Add + tool_insert_record + protected + + + on_insert_record + + + Load From File; icons/16x16/page_copy_columns.png + 0 + wxID_ANY + wxITEM_NORMAL + Duplicate + tool_duplicate_record + protected + + + on_duplicate_record + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Remove + tool_delete_record + protected + + + on_delete_record + + + protected + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + 1 + If enabled, table edits are applied immediately without pressing Apply or Cancel + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Apply changes automatically + + 0 + + + 0 + + 1 + chb_auto_apply + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + If enabled, table edits are applied immediately without pressing Apply or Cancel + + wxFILTER_NONE + wxDefaultValidator + + + + + on_auto_apply + + + Load From File; icons/16x16/tick.png + 0 + wxID_ANY + wxITEM_NORMAL + Apply + tool_apply_record + protected + + + on_apply_record + + + Load From File; icons/16x16/cross.png + 0 + wxID_ANY + wxITEM_NORMAL + Cancel + tool_cancel_record + protected + + + on_cancel_record + + + 5 wxEXPAND @@ -16943,168 +16923,406 @@ 5 - wxEXPAND + 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 - bSizer83 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/add.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Insert record - - 0 - - 0 - - - 0 - - 1 - btn_insert_record - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_insert_record + 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 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/add.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Duplicate record - - 0 - - 0 - - - 0 - - 1 - btn_duplicate_record - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_duplicate_record - - - - 5 - wxALIGN_CENTER|wxALL - 0 - + + + + 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 @@ -17113,35 +17331,26 @@ 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 record - - 0 0 @@ -17149,766 +17358,28 @@ 0 1 - btn_delete_record + m_panel52 1 protected 1 - - Resizable 1 - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_record - - - - 5 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_staticline3 - 1 - - - protected - 1 - - Resizable - 1 - - wxLI_VERTICAL - ; ; forward_declare - 0 - - - - - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - 1 - If enabled, table edits are applied immediately without pressing Apply or Cancel - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Apply changes automatically - - 0 - - - 0 - - 1 - chb_auto_apply - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - If enabled, table edits are applied immediately without pressing Apply or Cancel - - wxFILTER_NONE - wxDefaultValidator - - - - - on_auto_apply - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/cancel.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Cancel - - 0 - - 0 - - - 0 - - 1 - btn_cancel_record - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE ; ; forward_declare 0 - - wxFILTER_NONE - wxDefaultValidator - - - - - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/disk.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Apply - - 0 - - 0 - - - 0 - - 1 - btn_apply_record - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - 0 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Filters - - 0 - - - 0 - - 1 - m_collapsiblePane1 - 1 - - - protected - 1 - - Resizable - 1 - - wxCP_DEFAULT_STYLE|wxCP_NO_TLW_RESIZE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - wxFULL_REPAINT_ON_RESIZE - on_collapsible_pane_changed - - - bSizer831 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 1 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - 0 - - 0 - 0 - wxID_ANY - 1 - 0 - - 0 - -1,-1 - - 0 - - 1 - sql_query_filters - 1 - - - protected - 1 - - 0 - Resizable - 1 - -1,100 - ; ; forward_declare - 1 - 4 - 0 - - 1 - 0 - 0 - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/tick.png - - 1 - 0 - 1 - CTRL+ENTER - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Apply - - 0 - - 0 - - - 0 - - 1 - m_button41 - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_apply_filters - - - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - ,90,400,10,70,0 - 0 - wxID_ANY - - - list_ctrl_table_records - protected - - - wxDV_MULTIPLE - TableRecordsDataViewCtrl; .components.dataview; forward_declare - - - - - - - - - MyMenu - m_menu10 - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - Insert row - m_menuItem13 - none - Ins - - - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - MyMenuItem - m_menuItem14 - none - - - - - - - - Load From File; icons/16x16/arrow_right.png - Query - 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_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 - + wxTAB_TRAVERSAL + bSizer125 wxVERTICAL none - + 5 wxEXPAND 0 @@ -17962,7 +17433,7 @@ 5 1 - wxTB_HORIZONTAL + wxTB_HORIZONTAL|wxTB_HORZ_TEXT ; ; forward_declare 0 @@ -17974,7 +17445,7 @@ 0 wxID_ANY wxITEM_NORMAL - New query + Add new_query protected @@ -17986,7 +17457,7 @@ 0 wxID_ANY wxITEM_NORMAL - Close query + Close close_query protected @@ -18001,7 +17472,7 @@ 0 wxID_ANY wxITEM_NORMAL - Execute + Run execute_statement protected @@ -18013,7 +17484,7 @@ 0 wxID_ANY wxITEM_NORMAL - Execute all + Run all execute_all_statements protected @@ -18040,7 +17511,7 @@ 0 wxID_ANY wxITEM_NORMAL - tool + Save save protected @@ -18049,11 +17520,11 @@ - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -18062,9 +17533,9 @@ 0 0 - 1 + 1 0 @@ -18079,13 +17550,10 @@ 1 1 - 1 0 0 wxID_ANY - 1 - 1 0 @@ -18093,70 +17561,197 @@ 0 1 - sql_query_editor + notebook_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 + + + 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 @@ -18173,7 +17768,7 @@ wxTAB_TRAVERSAL - + bSizer1261 wxVERTICAL @@ -18219,7 +17814,7 @@ 0 1 - notebook_sql_results + notebook_query_results 1 @@ -18246,304 +17841,6 @@ - - - Query #2 - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 1 - wxID_ANY - - 0 - - - 0 - - 1 - QueryPanelTpl - 1 - - - protected - 1 - - Resizable - 1 - - - 0 - - - - wxTAB_TRAVERSAL - - - bSizer263 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - 0 - - 0 - - 1 - m_textCtrl101 - 1 - - - protected - 1 - - Resizable - 1 - - wxTE_MULTILINE|wxTE_RICH|wxTE_RICH2 - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer49 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Close - - 0 - - 0 - - - 0 - - 1 - m_button17 - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - New - - 0 - - 0 - - - 0 - - 1 - m_button121 - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - @@ -18554,8 +17851,684 @@ - - + + + 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 @@ -18564,15 +18537,20 @@ 0 0 + 0 + 1 0 1 1 + + 0 0 + Dock 0 Left @@ -18580,107 +18558,44 @@ 1 1 + 0 0 wxID_ANY + New + + 0 0 - -1,-1 + 0 1 - panel_sql_log + m_button121 1 protected 1 + + Resizable 1 - -1,-1 + + ; ; forward_declare 0 + + wxFILTER_NONE + wxDefaultValidator + - 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 - - - - - - + @@ -18688,66 +18603,380 @@ - - - - - 1 - 0 - 1 - - 4 - - 0 - wxID_ANY - - - status_bar - protected - - - wxSTB_SIZEGRIP - - - - - - - - - 0 - wxAUI_MGR_DEFAULT - - - 1 - 0 - 1 - impl_virtual - - - 0 - wxID_ANY - - - Trash - - 500,300 - ; ; forward_declare - - 0 - - - wxTAB_TRAVERSAL - - - bSizer144 - wxVERTICAL - none - + + 5 + wxEXPAND + 0 + + + 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 - wxALIGN_CENTER - 1 - + 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 @@ -18756,26 +18985,35 @@ 0 0 + 0 + Load From File; icons/16x16/disk.png 1 0 1 1 + + 0 0 + Dock 0 Left 0 - 1 + 0 1 + 0 0 wxID_ANY + Apply + + 0 0 @@ -18783,229 +19021,360 @@ 0 1 - database_character_set_panel + btn_apply_record 1 protected 1 + + Resizable 1 + wxBORDER_NONE ; ; forward_declare 0 + + wxFILTER_NONE + wxDefaultValidator + - 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|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 - - 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 - - - - - + + + 2 + wxLEFT|wxRIGHT + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/arrow_down.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Down + + 0 + + 0 + + + 0 + + 1 + btn_move_down_column + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_move_down_column + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 - - - - 5 - 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 - - - - diff --git a/settings.yml b/settings.yml index adaeed6..bb68983 100755 --- a/settings.yml +++ b/settings.yml @@ -2,11 +2,11 @@ language: en_US ui: window: size: - - 2256 - - 1472 + - 1769 + - 967 position: - - 0 - - 0 + - 26 + - 23 appearance: theme: petersql mode: auto @@ -15,6 +15,7 @@ ui: expanded_directories: - - 0 - - 3 + - - 18 shortcuts: query: execute_current: Ctrl+Enter From 495ff8678335356edf50048b2e3416b8bcde57cb Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 27 Apr 2026 15:21:07 +0200 Subject: [PATCH 32/93] update AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- helpers/dataview.py | 4 +++- windows/main/controller.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/helpers/dataview.py b/helpers/dataview.py index 2205906..5f2a271 100644 --- a/helpers/dataview.py +++ b/helpers/dataview.py @@ -141,7 +141,9 @@ def HasValue(self, item, col): if fields := self._get_column_fields(): return fields[col].has_value(self.get_data_by_item(item)) - return getattr(self.get_data_by_item(item), col, None) is not None + # print(self.get_data_by_item(item), col) + # return getattr(self.get_data_by_item(item), col, None) is not None + return self.get_data_by_item(item) is not None class BaseDataViewListModel(_DataViewListValueMixin, BaseDataModel, wx.dataview.DataViewIndexListModel): diff --git a/windows/main/controller.py b/windows/main/controller.py index ee75069..c3a287a 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -840,7 +840,10 @@ def _count_table_records( context.execute(query) row = context.fetchone() or {} - total_rows = row.get("total_rows") + try : + total_rows = dict(row).get("total_rows") + except Exception as ex : + logger.error(ex) if total_rows is None and row: total_rows = next(iter(row.values()), 0) From 167e9240f358fdd10d059e05ace74374d85be46c Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 27 Apr 2026 15:41:20 +0200 Subject: [PATCH 33/93] fix(dataviewer): fix record apply and COUNT(*) crash - Implement on_apply_record and on_cancel_record in MainFrameController (previously stubs in the base view); changes pending in NEW_RECORDS are now saved on Apply and discarded on Cancel - Add do_apply_records and do_cancel_records to TableRecordsController - Fix UnboundLocalError in _count_table_records: total_rows was left unassigned when dict(row) raised an exception - Wrap _load_records_page and _update_records_label calls inside _on_records_count_complete with try/except to prevent unhandled exceptions crashing the app via wx.CallAfter AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- windows/main/controller.py | 28 ++++++++++++++++++++++------ windows/main/table/records.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/windows/main/controller.py b/windows/main/controller.py index c3a287a..a3d17b5 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -840,12 +840,16 @@ def _count_table_records( context.execute(query) row = context.fetchone() or {} - try : + total_rows = None + try: total_rows = dict(row).get("total_rows") - except Exception as ex : + except Exception as ex: logger.error(ex) if total_rows is None and row: - total_rows = next(iter(row.values()), 0) + try: + total_rows = next(iter(row.values()), 0) + except Exception: + total_rows = 0 return int(total_rows or 0) @@ -1028,11 +1032,17 @@ def _on_records_count_complete( if self._records_offset > last_offset: self._records_offset = last_offset - self._load_records_page() + try: + self._load_records_page() + except Exception as ex: + logger.error(f"Error reloading records page after count: {ex}", exc_info=True) return - self._update_records_label(table) - self._set_records_paging_buttons(table) + try: + self._update_records_label(table) + self._set_records_paging_buttons(table) + except Exception as ex: + logger.error(f"Error updating records label: {ex}", exc_info=True) def _get_records_last_offset(self, limit: int) -> int: total_rows = int(self._records_total_rows or 0) @@ -1552,6 +1562,12 @@ def _on_current_records(self, records: list[SQLRecord]): self.m_toolBar3.EnableTool(self.tool_duplicate_record.GetId(), len(records) == 1) self.m_toolBar3.EnableTool(self.tool_delete_record.GetId(), len(records) > 0) + def on_apply_record(self, event): + self.controller_list_table_records.do_apply_records() + + def on_cancel_record(self, event): + self.controller_list_table_records.do_cancel_records() + def on_insert_record(self, event): self.controller_list_table_records.do_insert_record() diff --git a/windows/main/table/records.py b/windows/main/table/records.py index 7712260..fe1d747 100644 --- a/windows/main/table/records.py +++ b/windows/main/table/records.py @@ -1,7 +1,9 @@ import datetime +from gettext import gettext as _ from typing import Optional +import wx import wx.dataview import wx.stc @@ -317,6 +319,37 @@ def _do_new_empty_record(self, index: int, copy_from_selected: bool = False, use self._do_edit(new_empty_item, 1) + def do_apply_records(self): + """Save all pending records from NEW_RECORDS.""" + records = list(NEW_RECORDS) + if not records: + return + + errors = [] + for record in records: + try: + record.save() + except Exception as ex: + logger.error(f"Error saving record: {ex}", exc_info=True) + errors.append(str(ex)) + + NEW_RECORDS.clear() + + if errors: + wx.MessageBox( + "\n".join(errors), + _("Error saving records"), + wx.OK | wx.ICON_ERROR, + ) + + self.load_records_async() + + def do_cancel_records(self): + """Discard all pending changes in NEW_RECORDS.""" + NEW_RECORDS.clear() + if self.table: + self.load_model() + def do_refresh_records(self): """Refresh records from database.""" if self.table: From 5cffe510965505ba31cc7589d1936b8cf988e5d2 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 27 Apr 2026 15:41:32 +0200 Subject: [PATCH 34/93] docs: update README and PROJECT_STATUS to April 2026 - Remove broken link to ROADMAP.md (merged into PROJECT_STATUS in March) - Refresh README "Recent updates" with latest features - Bump PROJECT_STATUS last-updated date to 2026-04-27 - Add 7 entries to "Recently Added": autocomplete, table execution flow, row_format/convert_data, windows/main refactoring, ColumnContentDialog, database action buttons, tree explorer state preservation AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- PROJECT_STATUS.md | 9 ++++++++- README.md | 11 +++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 7c15d9b..339790a 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # PeterSQL — Project Status -> **Last Updated:** 2026-03-16 +> **Last Updated:** 2026-04-27 > **Status Rule:** newly implemented features are tracked as **PARTIAL** until validated across supported versions. > **Definition of DONE:** engine methods implemented, integration tests pass on target versions, UI workflow exists (if user-facing), no known regressions, documentation updated. @@ -181,6 +181,13 @@ ## 6. Recently Added +- SQL autocomplete extended to INSERT / UPDATE / DELETE and string literals; parser improved with JSON and multi-table coverage. +- Table execution flow updated in the records UI. +- `row_format` and `convert_data` options added to the MySQL/MariaDB table editor. +- `windows/main/` modules restructured into subdirectories (`database/`, `table/`, `query/`) for better separation of concerns. +- Advanced cell editor replaced with a dedicated `ColumnContentDialog` for displaying and editing large cell content. +- Database options action buttons now update live when options change. +- Tree explorer preserves expanded state after a failed connection attempt. - Multi-tab query editor with per-tab dirty tracking, autosave before execution, and close/save confirmation dialogs. - Cancelable query execution with background thread, per-statement result rendering, and execution summary. - Configurable keyboard shortcuts for all query editor actions (execute, stop, new tab, close tab, save, save-as). diff --git a/README.md b/README.md index b651e06..70f9067 100644 --- a/README.md +++ b/README.md @@ -35,15 +35,14 @@ Use at your own risk and **do not rely on this project in production environment For a detailed status snapshot, see: - [PROJECT_STATUS.md](PROJECT_STATUS.md) -- [ROADMAP.md](ROADMAP.md) ### Recent updates -- PostgreSQL engine now includes **Function** and **Procedure** classes with CRUD-style operations. -- Check constraint support was added for **MySQL**, **MariaDB**, and **PostgreSQL** engine layers. -- Connection manager now tracks **persistent connection statistics** (attempts, success/failure, timing). -- Empty database passwords are now accepted for local setups. -- MySQL/MariaDB connections can auto-retry by enabling TLS when required by the server. +- SQL autocomplete extended to INSERT / UPDATE / DELETE and string literals; parser improved with JSON and multi-table coverage. +- Table execution flow updated in the records UI. +- `row_format` and `convert_data` options added to the MySQL/MariaDB table editor. +- `windows/main/` modules restructured into subdirectories (`database/`, `table/`, `query/`). +- Advanced cell editor replaced with a dedicated `ColumnContentDialog` for large content. --- From f36bc15cb24982456b18431631542f98aa5cd724 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 27 Apr 2026 17:02:19 +0200 Subject: [PATCH 35/93] fix(logging): improve wx crash diagnostics AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- helpers/logger.py | 94 +++++++++++++++++++++++++++- main.py | 12 +++- tests/ui/test_main_exception_hook.py | 18 ++++++ 3 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 tests/ui/test_main_exception_hook.py 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/main.py b/main.py index 325e4f3..c441ef4 100755 --- a/main.py +++ b/main.py @@ -10,7 +10,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 @@ -49,6 +49,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") @@ -138,5 +143,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/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 From 20aa8ba5f0058b45a43a2b0c8b67456da45e276c Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 27 Apr 2026 17:18:10 +0200 Subject: [PATCH 36/93] feat(views): add new view creation flow - on_insert_view: builds an empty view via build_empty_view() and sets CURRENT_VIEW, opening the view editor panel - _on_current_view: disables Delete button for unsaved (is_new) views - do_save_view: captures is_new before save, then reloads the created view from the database into CURRENT_VIEW after successful creation - tests/ui/test_view_editor.py: 5 unit tests covering button states, _has_changes, save flow, and post-save reload AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- tests/ui/test_view_editor.py | 139 ++++++++++++++++++++++++++++++++++ windows/main/controller.py | 13 +++- windows/main/database/view.py | 10 ++- 3 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 tests/ui/test_view_editor.py 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/windows/main/controller.py b/windows/main/controller.py index a3d17b5..f7e01f9 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -1295,7 +1295,18 @@ def on_delete_database(self, event: wx.Event): def _on_current_view(self, current: SQLView): self.toggle_panel(current) - self.btn_delete_view.Enable(current is not None) + self.btn_delete_view.Enable(current is not None and not current.is_new) + + 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): diff --git a/windows/main/database/view.py b/windows/main/database/view.py index 5c1cb44..68c492d 100644 --- a/windows/main/database/view.py +++ b/windows/main/database/view.py @@ -247,10 +247,18 @@ 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) + 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) From 1038399439f0aac638ae94034a9a681c2058785c Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 28 Apr 2026 16:02:36 +0200 Subject: [PATCH 37/93] feat(views): add database views list panel Add ListDatabaseView to windows/main/database/list.py: - ModelDatabaseView shows Name and Definition (truncated) columns - subscribes to CURRENT_DATABASE to populate on db change - subscribes to CURRENT_VIEW to sync selection in the list - wired to list_ctrl_database_views in MainFrameController AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- windows/main/controller.py | 83 +++++++++++++++++++++++++++-- windows/main/database/list.py | 99 ++++++++++++++++++++++++++++++++++- 2 files changed, 176 insertions(+), 6 deletions(-) diff --git a/windows/main/controller.py b/windows/main/controller.py index f7e01f9..2226719 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -37,7 +37,7 @@ from windows.main.explorer import TreeExplorerController -from windows.main.database.list import ListDatabaseTable +from windows.main.database.list import ListDatabaseTable, ListDatabaseView from windows.main.database.view import ViewEditorController from windows.main.database.options import DatabaseOptionsController @@ -75,6 +75,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) @@ -117,6 +118,13 @@ 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 @@ -143,6 +151,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 @@ -184,6 +193,12 @@ def _update_database_action_buttons(self) -> None: 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() @@ -752,6 +767,10 @@ def on_open_settings(self, event): def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, SQLTrigger]] = 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() @@ -1066,12 +1085,24 @@ def _load_records_page(self): self._records_offset = min(max(self._records_offset, 0), last_offset) + logger.debug( + "ui trace: records._load_records_page start table=%s limit=%s offset=%s filters=%s", + table.name, + limit, + self._records_offset, + filters, + ) with Loader.cursor_wait(): + logger.debug("ui trace: records._load_records_page before table.load_records table=%s", table.name) table.load_records(filters=filters, limit=limit, offset=self._records_offset) + logger.debug("ui trace: records._load_records_page after table.load_records table=%s", table.name) + logger.debug("ui trace: records._load_records_page before controller.load_model table=%s", table.name) self.controller_list_table_records.load_model() + logger.debug("ui trace: records._load_records_page after controller.load_model table=%s", table.name) self._update_records_label(table) self._set_records_paging_buttons(table) + logger.debug("ui trace: records._load_records_page end table=%s", table.name) def _update_records_label(self, table: SQLTable): rows_count = self._get_loaded_records_count(table) @@ -1122,6 +1153,11 @@ def on_page_chaged(self, event): self._load_records_page() 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) @@ -1154,6 +1190,15 @@ def _on_current_session(self, session: Session): stc_ctrl.Colourise(0, -1) 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() @@ -1293,6 +1338,11 @@ 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 and not current.is_new) @@ -1310,10 +1360,18 @@ def on_insert_view(self, event): # 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) # 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 @@ -1323,6 +1381,7 @@ def _on_current_table(self, table: SQLTable): 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) @@ -1345,8 +1404,8 @@ def _on_current_table(self, table: SQLTable): if self.MainFrameNotebook.GetSelection() == 5: self._load_records_page() - self.btn_clone_table.Enable(table is not None) - self.btn_delete_table.Enable(table is not None) + self.tool_clone_table.Enable(table is not None) + self.tool_delete_table.Enable(table is not None) def _on_new_table(self, table: SQLTable): self.btn_apply_table.Enable(bool(table is not None and table.is_valid)) @@ -1358,7 +1417,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() @@ -1585,6 +1644,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 == 5: + self.controller_list_table_records.do_refresh_records() + def on_duplicate_record(self, event): self.controller_list_table_records.do_duplicate_record() diff --git a/windows/main/database/list.py b/windows/main/database/list.py index 53fabd5..43a8761 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 -from windows.main import CURRENT_DATABASE, CURRENT_TABLE, CURRENT_SESSION +from windows.main import CURRENT_DATABASE, CURRENT_TABLE, CURRENT_SESSION, CURRENT_VIEW class ModelDatabaseTable(BaseObservableDataViewListModel): @@ -52,6 +53,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 +93,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): @@ -107,3 +118,87 @@ def _on_item_activated(self, event: wx.dataview.DataViewEvent): if table := self.model.get_data_by_item(item): 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: + _app = wx.GetApp() + + 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 = 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_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_VIEW.set_value(view.copy()) From 1763d1a956527d95cc7e660a9c86b05387b81877 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 28 Apr 2026 16:07:14 +0200 Subject: [PATCH 38/93] feat(views): implement clone view action - on_clone_view: builds a copy of the selected view with name "{name}_copy" and the same statement, opens it as a new view in the editor - _on_current_view: enables tool_clone_view only for existing (non-new) views AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- windows/main/controller.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/windows/main/controller.py b/windows/main/controller.py index 2226719..25b7681 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -1345,7 +1345,21 @@ def _on_current_view(self, current: SQLView): ) self.toggle_panel(current) - self.btn_delete_view.Enable(current is not None and not current.is_new) + 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) + + 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() From e85d9a90043a2c9caaaddf2777f15150dacb8ab2 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 28 Apr 2026 16:29:07 +0200 Subject: [PATCH 39/93] fix(views): clear CURRENT_TABLE on view selection and vice versa Selecting a view resets CURRENT_TABLE to None; selecting a table resets CURRENT_VIEW to None, ensuring mutual exclusivity. AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- windows/main/database/list.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/windows/main/database/list.py b/windows/main/database/list.py index 43a8761..748691d 100644 --- a/windows/main/database/list.py +++ b/windows/main/database/list.py @@ -109,6 +109,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): @@ -117,6 +118,7 @@ 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()) @@ -193,6 +195,7 @@ def _on_selection_changed(self, event: wx.dataview.DataViewEvent): 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): @@ -201,4 +204,5 @@ def _on_item_activated(self, event: wx.dataview.DataViewEvent): return if view := self.model.get_data_by_item(item): + CURRENT_TABLE.set_value(None) CURRENT_VIEW.set_value(view.copy()) From 625f2b22b0aebf92de0af1171c015e81d93008c1 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 30 Apr 2026 14:36:14 +0200 Subject: [PATCH 40/93] update runtest AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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: | From 82b2b80fb7b46b3bcf4f5b07b167526712e6a9a9 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 30 Apr 2026 14:36:22 +0200 Subject: [PATCH 41/93] update icon size AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- icons/16x16/code-folding.png | Bin 5969 -> 856 bytes icons/16x16/js-96.png | Bin 1630 -> 404 bytes icons/16x16/markdown-96.png | Bin 1817 -> 598 bytes icons/16x16/php-100.png | Bin 3609 -> 708 bytes icons/16x16/server-firebird.png | Bin 7274 -> 887 bytes icons/16x16/server-interbase.png | Bin 4360 -> 769 bytes icons/16x16/server-memsql.png | Bin 13126 -> 528 bytes icons/16x16/server-oracle.png | Bin 4926 -> 533 bytes icons/16x16/server-proxysqladmin.png | Bin 14294 -> 961 bytes icons/16x16/server-rds-mysql.png | Bin 5123 -> 850 bytes icons/16x16/sql-96.png | Bin 2584 -> 619 bytes icons/16x16/textile-lang.png | Bin 1804 -> 448 bytes 12 files changed, 0 insertions(+), 0 deletions(-) diff --git a/icons/16x16/code-folding.png b/icons/16x16/code-folding.png index ca97e238f9fb017ccd609df1c6b2358242ce857d..d9128383e5cb1a53ab3e9c42ace5536969a2aa96 100644 GIT binary patch literal 856 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 diff --git a/icons/16x16/js-96.png b/icons/16x16/js-96.png index ac67e6e35c48dfd5556d22ee86c4bf1158a1726b..11b3e69d217e1db684206c5c41f47b039c5d145b 100644 GIT binary patch literal 404 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 YUfF-=(4>~6`#OiCsj90|0kaDK4_43nH~;_u diff --git a/icons/16x16/server-firebird.png b/icons/16x16/server-firebird.png index ef6fb715f3106b8b0acdcb6c283be024b5eba4ba..2220dffee4e70020851e7ad4664948609883d329 100644 GIT binary patch literal 887 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 literal 4360 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 Bt(O1* diff --git a/icons/16x16/server-oracle.png b/icons/16x16/server-oracle.png index 4a1aea92bbac83b9be77d0a28f69a7f925438192..0e6111511f23d1c822735869270719b6d9cae866 100644 GIT binary patch literal 533 zcmV+w0_y#VP)8VKwY?M)s=n$L6Gi6aM_R03gXVaJ8`G9h|8{ofZ)P~ zJ1KQzaiL-|L(-)GOy*q|PB0}65f7ZryZ5~J?!D&@X@kG|k#uTT$ zuBsfVzVE}v#cuLe!ILfUZils3vFF|ExNO(Kwm3~3U<7rF7(rDeK)GHqa<>4q)IC`2 zrq4-i^`Tbl+6kjKU$|15)q{2=T0OM!*TaQ)*a@Q*+BVwj+uG63x%g7v(4vArg$01` zm1y;JA=cE^cIew!6B~WV#b(wIM$>AC(Z|%*PECF&vxFlw|K1=?Q#eQ})tjO5wg)g&l}%AF zZ5zFejlb4$!_PzHwSjb0@~A19YDt6@E9Q;?TA91Q)JtDTUccY9(PJN3pEo9$R)40- ziR!y!h8y*6x2fQrvGPH=To>=cvU5_Nu(D)>OwGRSC3}onc{G}TUM2-OvrOZ`|5@kX XNS($+2(u4b00000NkvXXu0mjfc+mZ+ literal 4926 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 WLaNJ&Pit!c`WWh)=#}a?Ui}|wDuulO diff --git a/icons/16x16/server-proxysqladmin.png b/icons/16x16/server-proxysqladmin.png index 7bdbbfa5700767492ab64676711294d7204bead5..afe17c152a67f473f1ba07a296c6df1df0ddf641 100644 GIT binary patch literal 961 zcmV;y13vtTP)0IM=`2gvtS`xuwri8-{hZ^469gal+&!Pmhwy*+;r{+X zi5{V!*JUk$C+3<_I^nti)M#0z# z!GL^b12rs)2P@mRR&(x;FI|XCghC-THa4ax91e?-k&*I=lkb00>9(Fu#ANJ%<{nQxNlHBzpH|`R+LfY zF(5dQD@jPHU=;+v%Yu4S<3N*xTicGRoTaY4me0H*UhX@1YFjz9S-Ts}ri9BTz)0ib zOcKAxHeqOueT`mpKjVYPE>P!|@CVwk;_lE#di)=X^k&-6tAgY+9dKJUh=6=i;rw(8 zSLdI=mfc33n#ZTd_G9q5YPbx*=dqF1*G6WY|FhB#y6eRZ6o8YDs zMaEHAA`;mS_#F}fuMM0LP8|xu>ogGb7!ZO( zOO5pc$9if>&;GHhVvCkZ=B%u_=yEw(X8thJQtzG&^y{ylxVu^~uar^&hB0Lldy;+n;9#)vw-&0Rww$*7r47U$NghG0acwwF5 zOACdYiH&m;m&Q*&{V4jkO06}BNWXRL_@Uu6eK?0#rD8@SX?n`Xv3!~@G jp8X*l)@)>iGywfKgh7g?X!S_m00000NkvXXu0mjfsdm>G literal 14294 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 Ef(`f%dH?_b diff --git a/icons/16x16/server-rds-mysql.png b/icons/16x16/server-rds-mysql.png index 6040183c150272db42fad22302bea3723db20d94..7e35a78aa645574c42fe80f7855dc28693a7cc78 100644 GIT binary patch literal 850 zcmV-Y1FigtP)10wvXUAcg0RYZAOdiN1mDX^UueY|hKbik{=7*?Mk^}&WXWpMV zI{V6xFTOCX7M5@Vb4Yo7z17S*+lxVm0DzWT+xOZ3uvXY;>R z{$Bpz?b%|G*=Lk+p7MQc^aEV}wT{r205gh(Q6zN$0^<5sM{RE2ucQ;fw5V=3$HPDt zE?-}DtxjLg|NJLb?*7Z#m-z72gRqX_7$EEn!DAacFcnOX$Auv_z7+BLR+|9esaz6w zDqAQoRj^uX@MTg28OoAj}8vxesn<{*h6@c$7zd0-qPmkQDB z4Dj-i0ud2>NvLdg(d@>UnHs}Js}Dg0`Y(V1C@E0C{}5BfQGE2yvzQnQ(HS_TLJ#vd zYB-$NbZlxAedA!6chd+6$$(=i1RnMVxc2)RKL7Rxk7$LA_CN$YXb*7lrwU6+IPu(G zC@J8;Owwb|aqLNuV#lllMJs>QLB2-55I}hZ20(%^qCFfvl*P(lO*m!-lM)nr0-kLE zSc8m1FbD;M*ZN&*h{kv4>y~WD=U=M}L?zj{^yO7}!JBO3WuXE=QVu&J8ZH15 zo*2obMYG*o-q>!xRlRWH>aI+Wbw(7)1ODtO6=u%pY<`mYiO$91o_pC;c+OtB{Z+X< cXCGVq2w$UXxZlAWk^lez07*qoM6N<$g7OxLx&QzG literal 5123 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 diff --git a/icons/16x16/sql-96.png b/icons/16x16/sql-96.png index 6acb7a69b9f2428e05906ce4142fbf85246248d3..1a68db8c6db90c5ee8038ddc96150773d99122e4 100644 GIT binary patch literal 619 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 n$#+~w9LkvSCKUDlXh5e>@vLA=k=9Y{I#tM Date: Thu, 30 Apr 2026 14:36:32 +0200 Subject: [PATCH 42/93] update runtest AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- scripts/runtest.py | 62 ++++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/scripts/runtest.py b/scripts/runtest.py index 35f862a..a3507a3 100755 --- a/scripts/runtest.py +++ b/scripts/runtest.py @@ -276,34 +276,54 @@ def update_badges(): 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( + '--suite', + choices=['unit', 'integration', 'all'], + default='unit', + help='Select test suite: unit, integration, or all (default: unit)', + ) + parser.add_argument( + '--engine', + choices=[engine.value.name.lower() for engine in ConnectionEngine], + help='Filter tests by engine suite (tests/engines//)', + ) parser.add_argument('--update', action='store_true', help='Run all tests (unit + integration) and update README badges') args = parser.parse_args() - if args.all: - print("Running ALL tests (unit + integration)...") - result = subprocess.run(['uv', 'run', 'pytest', 'tests/', '--tb=no']) - exit_code = result.returncode + tests_target = f"tests/engines/{args.engine}/" if args.engine else 'tests/' + + pytest_command = ['uv', 'run', 'pytest', tests_target] + + if args.suite == 'unit': + pytest_command.extend(['--tb=short', '-m', 'not integration']) + elif args.suite == 'integration': + pytest_command.extend(['--tb=short', '-m', 'integration']) + else: + pytest_command.extend(['--tb=no']) + + if args.update and args.suite != 'all': + print("Error: --update requires --suite all") + return 2 - elif args.update: + if 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: + update_pytest_command = [ + 'uv', + 'run', + 'pytest', + tests_target, + '--tb=no', + '--junitxml', + JUNIT_FILE, + ] process = subprocess.Popen( - [ - 'uv', - 'run', - 'pytest', - 'tests/', - '--tb=no', - '--junitxml', - JUNIT_FILE, - ], + update_pytest_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True @@ -324,14 +344,14 @@ def main(): exit_code = 1 else: - print("Running unit tests...") - result = subprocess.run([ - 'uv', 'run', 'pytest', 'tests/', '--tb=short', '-m', 'not integration' - ]) + suite_name = args.suite.upper() if args.suite == 'all' else args.suite + engine_info = f" for engine '{args.engine}'" if args.engine else "" + print(f"Running {suite_name} tests{engine_info}...") + result = subprocess.run(pytest_command) 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.") + print("\nNote: Use --suite integration or --suite all to include integration tests. Use --update --suite all to update badges.") print(f"\nDone. Pytest exit code: {exit_code}") return exit_code From 6173f65ff2351a5ff71040d06653649ffd10b0ba Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 30 Apr 2026 14:43:48 +0200 Subject: [PATCH 43/93] added read_only options added sql views update index update datatype category update dataview bug update icon bug update directory rename update sorting by custom col AI-Assisted-By: Cline AI-Contribution: 80% Tracked-By: CodeShield AI --- PeterSQL.fbp | 2664 +++++++++++++---- structures/connection.py | 2 + structures/engines/context.py | 10 + structures/engines/database.py | 32 +- structures/engines/mariadb/context.py | 7 + structures/engines/mariadb/datatype.py | 6 +- structures/engines/mariadb/indextype.py | 4 +- structures/engines/mysql/context.py | 7 + structures/engines/mysql/datatype.py | 6 +- structures/engines/postgresql/context.py | 43 + structures/engines/postgresql/database.py | 4 +- structures/engines/sqlite/context.py | 42 +- structures/engines/sqlite/database.py | 21 +- tests/engines/base_readonly_tests.py | 82 + tests/engines/base_view_tests.py | 380 ++- .../engines/mariadb/test_integration_suite.py | 65 +- tests/engines/mysql/test_integration_suite.py | 62 +- .../postgresql/test_integration_suite.py | 70 +- .../engines/sqlite/test_integration_suite.py | 129 +- windows/components/__init__.py | 33 +- windows/components/dataview.py | 87 +- windows/components/renders.py | 2 + .../stc/autocomplete/autocomplete_popup.py | 2 +- windows/dialogs/connections/controller.py | 21 +- windows/dialogs/connections/model.py | 6 + windows/dialogs/connections/repository.py | 1 + windows/dialogs/connections/view.py | 12 + windows/main/query.py | 17 - windows/state.py | 4 +- windows/views.py | 306 +- 30 files changed, 3398 insertions(+), 729 deletions(-) create mode 100644 tests/engines/base_readonly_tests.py delete mode 100644 windows/main/query.py diff --git a/PeterSQL.fbp b/PeterSQL.fbp index 924f4ae..67c9425 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -31,7 +31,7 @@ 1 0 0 - + 0 wxAUI_MGR_DEFAULT @@ -60,16 +60,16 @@ on_close - + bSizer34 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -385,8 +385,8 @@ - - + + 1 1 1 @@ -438,16 +438,16 @@ wxTAB_TRAVERSAL - + bSizer36 wxVERTICAL none - + 5 wxALL|wxEXPAND 1 - + 1 1 1 @@ -1658,6 +1658,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 @@ -2236,11 +2322,11 @@ - + SSH Tunnel 0 - + 1 1 1 @@ -2267,7 +2353,7 @@ 1 0 - 1 + 0 wxID_ANY 0 @@ -2292,7 +2378,7 @@ wxTAB_TRAVERSAL - + bSizer102 wxVERTICAL @@ -6423,7 +6509,7 @@ - + File m_menu2 protected @@ -6442,7 +6528,7 @@ on_settings - + Help m_menu4 protected @@ -6462,7 +6548,7 @@ - + 1 1 1 @@ -6832,8 +6918,8 @@ - - + + 1 1 1 @@ -6884,8 +6970,8 @@ - wxTAB_TRAVERSAL - + wxFULL_REPAINT_ON_RESIZE|wxTAB_TRAVERSAL + bSizer24 wxHORIZONTAL @@ -6959,11 +7045,11 @@ - + wxFULL_REPAINT_ON_RESIZE - + MyMenu m_menu5 protected @@ -6980,7 +7066,7 @@ - + MyMenu m_menu1 @@ -7570,8 +7656,8 @@ - - + + 1 1 1 @@ -7623,7 +7709,7 @@ wxTAB_TRAVERSAL - + bSizer158 wxVERTICAL @@ -7777,20 +7863,20 @@ none - + 5 wxEXPAND 0 - + bSizer142 wxHORIZONTAL none - + 5 wxALIGN_CENTER 1 - + 1 1 1 @@ -7977,11 +8063,11 @@ - + 5 wxEXPAND 1 - + 0 protected 0 @@ -7989,11 +8075,11 @@ - + 5 wxEXPAND 0 - + bSizer13911 wxHORIZONTAL @@ -10100,8 +10186,8 @@ - - + + 1 1 1 @@ -10137,7 +10223,7 @@ 0 1 - m_panel55 + m_panel651 1 @@ -10153,25 +10239,74 @@ 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 + m_notebook10 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + + + Tables + 0 + 1 1 1 @@ -10200,16 +10335,14 @@ 0 0 wxID_ANY - Table: - 0 0 0 - -1,-1 + 1 - m_staticText391 + m_panel55 1 @@ -10219,106 +10352,474 @@ Resizable 1 - ; ; forward_declare 0 - - -1 - - - - 5 - wxEXPAND - 0 - - 0 - protected - 100 - - - - 2 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/add.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Insert - - 0 - - 0 - - - 0 - - 1 - btn_insert_table - 1 + 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 + wxEXPAND + 1 + + + bSizer152 + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + list_ctrl_database_tables + protected + + + + ; ; forward_declare + + + + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Name + wxDATAVIEW_CELL_INERT + 0 + m_dataViewColumn12 + protected + Text + -1 + + + wxALIGN_RIGHT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Rows + wxDATAVIEW_CELL_INERT + 1 + m_dataViewColumn13 + protected + Text + -1 + + + wxALIGN_RIGHT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Size + wxDATAVIEW_CELL_INERT + 2 + m_dataViewColumn14 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Created at + wxDATAVIEW_CELL_INERT + 3 + m_dataViewColumn15 + protected + Date + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Updated at + wxDATAVIEW_CELL_INERT + 4 + m_dataViewColumn16 + protected + Date + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Engine + wxDATAVIEW_CELL_INERT + 5 + m_dataViewColumn17 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Collation + wxDATAVIEW_CELL_INERT + 6 + m_dataViewColumn19 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Comments + wxDATAVIEW_CELL_INERT + 7 + m_dataViewColumn18 + protected + Text + -1 + + + + + + + + + + + Views + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel65 + 1 protected 1 - - Resizable 1 - wxBORDER_NONE ; ; forward_declare 0 - - wxFILTER_NONE - wxDefaultValidator - - - on_insert_table + 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 + + + + - - 5 - wxALL|wxEXPAND - 0 - + + + Procedures + 0 + 1 1 1 @@ -10327,35 +10828,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,37 +10855,616 @@ 0 1 - btn_clone_table + m_panel652 1 protected 1 - - Resizable 1 - wxBORDER_NONE ; ; forward_declare 0 - - wxFILTER_NONE - wxDefaultValidator - - - on_clone_table + 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_view + + + Load From File; icons/16x16/page_copy.png + 0 + wxID_ANY + wxITEM_NORMAL + Clone view + tool_clone_procedure + protected + Clone procedure + Clone procedure + on_clone_view + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Delete procedure + tool_delete_procedure + protected + Delete procedure + Delete procedure + on_delete_view + + + + + 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 + + + + + + + + + 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 + + 1 + m_panel6521 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + 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_view + + + 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_view + + + Load From File; icons/16x16/delete.png + 0 + wxID_ANY + wxITEM_NORMAL + Delete function + tool_delete_function + protected + Delete function + Delete function + on_delete_view + + + + + 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 + + + + + + + + + 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 + + + + - - 2 - wxALL|wxEXPAND - 0 - + + + Events + 0 + 1 1 1 @@ -10402,35 +11473,26 @@ 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,174 +11500,178 @@ 0 1 - btn_delete_table1 + m_panel652111 1 protected 1 - - Resizable 1 - wxBORDER_NONE ; ; forward_declare 0 - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_table - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - - - 5 - wxEXPAND - 1 - - - bSizer152 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - list_ctrl_database_tables - protected - - - - ; ; forward_declare - - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn12 - protected - Text - -1 - - - wxALIGN_RIGHT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Rows - wxDATAVIEW_CELL_INERT - 1 - m_dataViewColumn13 - protected - Text - -1 - - - wxALIGN_RIGHT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Size - wxDATAVIEW_CELL_INERT - 2 - m_dataViewColumn14 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Created at - wxDATAVIEW_CELL_INERT - 3 - m_dataViewColumn15 - protected - Date - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Updated at - wxDATAVIEW_CELL_INERT - 4 - m_dataViewColumn16 - protected - Date - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Engine - wxDATAVIEW_CELL_INERT - 5 - m_dataViewColumn17 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Collation - wxDATAVIEW_CELL_INERT - 6 - m_dataViewColumn19 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Comments - wxDATAVIEW_CELL_INERT - 7 - m_dataViewColumn18 - protected - Text - -1 + 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 + + + @@ -10616,6 +11682,17 @@ + + 5 + wxEXPAND + 1 + + + bSizer147 + wxVERTICAL + none + + 5 wxEXPAND @@ -10855,11 +11932,11 @@ - + Diagram 0 - + 1 1 1 @@ -10911,7 +11988,7 @@ wxTAB_TRAVERSAL - + bSizer82 wxVERTICAL @@ -11115,11 +12192,11 @@ - + Load From File; icons/16x16/table.png Table 0 - + 1 1 1 @@ -11171,16 +12248,16 @@ wxTAB_TRAVERSAL - + bSizer251 wxVERTICAL none - + 0 wxEXPAND 1 - + 1 1 1 @@ -11238,8 +12315,8 @@ - - + + 1 1 1 @@ -11291,16 +12368,16 @@ wxTAB_TRAVERSAL - + bSizer55 wxVERTICAL none - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -11694,11 +12771,11 @@ - + Load From File; icons/16x16/wrench.png Options 0 - + 1 1 1 @@ -11750,16 +12827,16 @@ wxTAB_TRAVERSAL - + bSizer261 wxVERTICAL none - + 5 wxEXPAND 0 - + 2 0 @@ -11905,11 +12982,11 @@ - + 5 wxEXPAND 0 - + bSizer2712 wxHORIZONTAL @@ -12043,11 +13120,11 @@ - + 5 wxEXPAND 0 - + bSizer2721 wxHORIZONTAL @@ -12246,20 +13323,20 @@ - + 5 wxEXPAND 1 - + bSizer145 wxHORIZONTAL none - + 5 wxALIGN_CENTER|wxALL 0 - + 1 1 1 @@ -12317,11 +13394,11 @@ -1 - + 5 wxALL 1 - + 1 1 1 @@ -13457,8 +14534,8 @@ - - + + 1 1 1 @@ -13510,16 +14587,16 @@ wxTAB_TRAVERSAL - + bSizer54 wxVERTICAL none - + 5 wxEXPAND 0 - + 1 1 1 @@ -13633,7 +14710,7 @@ -1 - + Load From File; icons/16x16/add.png 0 wxID_ANY @@ -13645,7 +14722,7 @@ on_insert_column - + Load From File; icons/16x16/delete.png 0 wxID_ANY @@ -13657,10 +14734,10 @@ on_delete_column - + protected - + Load From File; icons/16x16/arrow_up.png 0 wxID_ANY @@ -13672,7 +14749,7 @@ on_move_up_column - + Load From File; icons/16x16/arrow_down.png 0 wxID_ANY @@ -14008,7 +15085,7 @@ - + Load From File; icons/16x16/view.png Views 0 @@ -16212,7 +17289,7 @@ wxTAB_TRAVERSAL - + Load From File; icons/16x16/text_columns.png Data 1 @@ -16334,19 +17411,19 @@ - + Load From File; icons/16x16/arrow_refresh.png 0 wxID_ANY wxITEM_NORMAL - Refrsh + Refresh tool_refresh_records protected on_refresh_records - + protected @@ -17379,7 +18456,7 @@ bSizer125 wxVERTICAL none - + 5 wxEXPAND 0 @@ -17522,67 +18599,18 @@ 5 - wxEXPAND | 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 - notebook_query_editor - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - - - a page - 0 - + bSizer150 + wxHORIZONTAL + none + + 5 + wxEXPAND | wxALL + 1 + 1 1 1 @@ -17593,6 +18621,7 @@ 0 + 1 0 @@ -17606,108 +18635,195 @@ 0 1 - 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 + + + + + + + + + + + + 5 + wxALL|wxEXPAND + 0 + + + + 1 + 0 + 1 + - 0 0 wxID_ANY - - 0 - - 0 - - 1 - m_panel63 - 1 - - + 200,-1 + m_dataViewTreeCtrl1 protected - 1 - Resizable - 1 + wxDV_NO_HEADER|wxDV_ROW_LINES ; ; 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 - - - - - - + @@ -17715,8 +18831,8 @@ - - + + 1 1 1 @@ -17768,7 +18884,7 @@ wxTAB_TRAVERSAL - + bSizer1261 wxVERTICAL @@ -18231,6 +19347,134 @@ + + 5 + wxALL + 0 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + m_dataViewListCtrl2 + protected + + + wxDV_ROW_LINES + ; ; forward_declare + + + + + + + + 5 + wxALL + 0 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + m_dataViewCtrl10 + protected + + + + ; ; forward_declare + + + + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Comments + wxDATAVIEW_CELL_INERT + 7 + m_dataViewColumn181 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Collation + wxDATAVIEW_CELL_INERT + 6 + m_dataViewColumn191 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Engine + wxDATAVIEW_CELL_INERT + 5 + m_dataViewColumn171 + protected + Text + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Updated at + wxDATAVIEW_CELL_INERT + 4 + m_dataViewColumn161 + protected + Date + -1 + + + wxALIGN_LEFT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Created at + wxDATAVIEW_CELL_INERT + 3 + m_dataViewColumn151 + protected + Date + -1 + + + wxALIGN_RIGHT + + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE + Size + wxDATAVIEW_CELL_INERT + 2 + m_dataViewColumn141 + protected + Text + -1 + + + 5 wxALIGN_RIGHT|wxALL @@ -18305,7 +19549,7 @@ - + 5 wxEXPAND | wxALL 1 @@ -18603,11 +19847,11 @@ - + 5 wxEXPAND 0 - + bSizer83 wxHORIZONTAL @@ -18673,7 +19917,7 @@ - + 5 wxALL 0 @@ -18748,7 +19992,7 @@ on_insert_record - + 5 wxALL 0 @@ -18823,7 +20067,7 @@ on_duplicate_record - + 5 wxALL 0 @@ -18898,7 +20142,7 @@ on_delete_record - + 5 wxALL 0 @@ -18972,7 +20216,7 @@ - + 5 wxALL 0 @@ -19046,11 +20290,11 @@ - + 5 wxALL|wxEXPAND 0 - + bSizer53 wxHORIZONTAL @@ -19062,7 +20306,157 @@ 0 protected - 100 + 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 @@ -19081,7 +20475,7 @@ 0 - Load From File; icons/16x16/add.png + Load From File; icons/16x16/arrow_up.png 1 0 @@ -19096,7 +20490,7 @@ 0 Left 0 - 1 + 0 1 @@ -19104,7 +20498,7 @@ 0 0 wxID_ANY - Insert + Up 0 @@ -19114,7 +20508,7 @@ 0 1 - btn_insert_column + btn_move_up_column 1 @@ -19137,7 +20531,7 @@ - on_insert_column + on_move_up_column @@ -19156,7 +20550,7 @@ 0 - Load From File; icons/16x16/delete.png + Load From File; icons/16x16/arrow_down.png 1 0 @@ -19179,7 +20573,7 @@ 0 0 wxID_ANY - Delete + Down 0 @@ -19189,7 +20583,7 @@ 0 1 - btn_delete_column + btn_move_down_column 1 @@ -19212,12 +20606,105 @@ - on_delete_column + on_move_down_column + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + + + 5 + wxEXPAND + 0 + + + bSizer531 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER_VERTICAL|wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Table: + 0 + + 0 + + + 0 + -1,-1 + 1 + m_staticText391 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND + 0 + + 0 + protected + 100 2 - wxLEFT|wxRIGHT + wxALL|wxEXPAND 0 1 @@ -19231,7 +20718,82 @@ 0 - Load From File; icons/16x16/arrow_up.png + Load From File; icons/16x16/add.png + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Insert + + 0 + + 0 + + + 0 + + 1 + btn_insert_table + 1 + + + protected + 1 + + + + Resizable + 1 + + wxBORDER_NONE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_insert_table + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + Load From File; icons/16x16/table_multiple.png 1 0 @@ -19254,7 +20816,7 @@ 0 0 wxID_ANY - Up + Clone 0 @@ -19264,7 +20826,7 @@ 0 1 - btn_move_up_column + btn_clone_table 1 @@ -19287,12 +20849,12 @@ - on_move_up_column + on_clone_table 2 - wxLEFT|wxRIGHT + wxALL|wxEXPAND 0 1 @@ -19306,7 +20868,7 @@ 0 - Load From File; icons/16x16/arrow_down.png + Load From File; icons/16x16/delete.png 1 0 @@ -19329,7 +20891,7 @@ 0 0 wxID_ANY - Down + Delete 0 @@ -19339,7 +20901,7 @@ 0 1 - btn_move_down_column + btn_delete_table1 1 @@ -19362,7 +20924,7 @@ - on_move_down_column + on_delete_table diff --git a/structures/connection.py b/structures/connection.py index 2fcc64f..8f31fea 100755 --- a/structures/connection.py +++ b/structures/connection.py @@ -70,6 +70,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, @@ -112,6 +113,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, diff --git a/structures/engines/context.py b/structures/engines/context.py index 922345d..5663f95 100755 --- a/structures/engines/context.py +++ b/structures/engines/context.py @@ -2,6 +2,7 @@ import contextlib import re +from gettext import gettext as _ from typing import Any, Optional import yaml @@ -31,6 +32,11 @@ SQL_SAFE_NAME_REGEX = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$") +_WRITE_QUERY_RE = re.compile( + r"^\s*(INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TRUNCATE|REPLACE|GRANT|REVOKE|RENAME|LOCK)\b", + re.IGNORECASE, +) + class AbstractContext(abc.ABC): """Base context API for SQL engines.""" @@ -524,6 +530,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) diff --git a/structures/engines/database.py b/structures/engines/database.py index 4238062..54c337e 100755 --- a/structures/engines/database.py +++ b/structures/engines/database.py @@ -141,6 +141,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)) @@ -361,11 +370,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): @@ -562,7 +577,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 +696,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..7159dac 100755 --- a/structures/engines/mariadb/context.py +++ b/structures/engines/mariadb/context.py @@ -84,6 +84,9 @@ 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 _parse_type(self, column_type: str): """Parse a raw COLUMN_TYPE string from information_schema into structured field attributes. @@ -334,6 +337,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, ) ) @@ -734,6 +739,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/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..2cf4ce1 100644 --- a/structures/engines/mysql/context.py +++ b/structures/engines/mysql/context.py @@ -85,6 +85,9 @@ 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 _parse_type(self, column_type: str): """Parse a raw COLUMN_TYPE string from information_schema into structured field attributes. @@ -344,6 +347,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, ) ) @@ -741,6 +746,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/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..d6f7bef 100644 --- a/structures/engines/postgresql/context.py +++ b/structures/engines/postgresql/context.py @@ -74,6 +74,9 @@ 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 _load_custom_types(self) -> None: """Load user-defined enum types from the database.""" self.execute(""" @@ -254,6 +257,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 +302,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, ) ) @@ -760,8 +800,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..6ff6780 100644 --- a/structures/engines/postgresql/database.py +++ b/structures/engines/postgresql/database.py @@ -398,9 +398,11 @@ def delete(self) -> bool: @dataclasses.dataclass class PostgreSQLView(SQLView): + schema: 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..31b6e55 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}") @@ -481,6 +486,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 +519,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, ) ) @@ -596,8 +624,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 +640,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 +666,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..756eb27 100644 --- a/structures/engines/sqlite/database.py +++ b/structures/engines/sqlite/database.py @@ -364,7 +364,6 @@ def drop(self) -> bool: return False # sqlite_ UNIQUE is handled in table creation - 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}" ) @@ -377,7 +376,15 @@ def modify(self, new_index: Self): @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): @@ -445,6 +452,8 @@ def insert(self) -> bool: if raw_insert_record := self.raw_insert_record(): try: return transaction.execute(raw_insert_record) + except PermissionError: + raise except: return False @@ -455,6 +464,8 @@ def update(self) -> bool: if raw_update_record := self.raw_update_record(): try: return transaction.execute(raw_update_record) + except PermissionError: + raise except: return False @@ -465,6 +476,8 @@ def delete(self) -> bool: if raw_delete_record := self.raw_delete_record(): try: return transaction.execute(raw_delete_record) + except PermissionError: + raise except: return False @@ -472,12 +485,12 @@ def delete(self) -> bool: 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/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..fd87646 100644 --- a/tests/engines/mariadb/test_integration_suite.py +++ b/tests/engines/mariadb/test_integration_suite.py @@ -11,7 +11,17 @@ from tests.engines.base_check_tests import BaseCheckTests 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 @@ -77,10 +87,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 +123,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 +173,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..3f0b53c 100644 --- a/tests/engines/mysql/test_integration_suite.py +++ b/tests/engines/mysql/test_integration_suite.py @@ -11,7 +11,17 @@ from tests.engines.base_check_tests import BaseCheckTests 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 @@ -77,6 +87,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 +128,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 +173,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/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/autocomplete_popup.py b/windows/components/stc/autocomplete/autocomplete_popup.py index 671bbb9..8988af2 100644 --- a/windows/components/stc/autocomplete/autocomplete_popup.py +++ b/windows/components/stc/autocomplete/autocomplete_popup.py @@ -6,7 +6,7 @@ from windows.components.stc.theme_loader import ThemeLoader -class AutoCompletePopup(wx.PopupWindow): +class AutoCompletePopup(wx.PopupTransientWindow): def __init__(self, parent: wx.Window, settings: object = None, theme_loader: ThemeLoader = None) -> None: super().__init__(parent, wx.BORDER_SIMPLE) 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..2a7570b 100644 --- a/windows/dialogs/connections/model.py +++ b/windows/dialogs/connections/model.py @@ -37,6 +37,8 @@ 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]() @@ -63,6 +65,7 @@ def __init__(self): self.port, self.filename, self.comments, + self.read_only, self.created_at, self.last_connection_at, self.successful_connected, @@ -118,6 +121,7 @@ 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, @@ -144,6 +148,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 +239,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..1abb5a5 100644 --- a/windows/dialogs/connections/repository.py +++ b/windows/dialogs/connections/repository.py @@ -130,6 +130,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"), diff --git a/windows/dialogs/connections/view.py b/windows/dialogs/connections/view.py index 4534ecb..4af0db6 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, @@ -716,6 +718,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/query.py b/windows/main/query.py deleted file mode 100644 index 0d06e58..0000000 --- a/windows/main/query.py +++ /dev/null @@ -1,17 +0,0 @@ -import wx - - -class QueryMixin: - def on_new_query(self, event): - new_panel = wx.Panel(self.notebook_query_editor) - sizer = wx.BoxSizer(wx.VERTICAL) - stc = wx.stc.StyledTextCtrl(new_panel, style=0) - sizer.Add(stc, 1, wx.EXPAND) - new_panel.SetSizer(sizer) - n = self.notebook_query_editor.GetPageCount() + 1 - self.notebook_query_editor.AddPage(new_panel, f"Query #{n}", select=True) - - def on_close_query(self, event): - n = self.notebook_query_editor.GetSelection() - if n != wx.NOT_FOUND: - self.notebook_query_editor.DeletePage(n) \ No newline at end of file diff --git a/windows/state.py b/windows/state.py index 19a861e..5fc907f 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,7 +13,7 @@ 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() diff --git a/windows/views.py b/windows/views.py index acf2939..b429416 100755 --- a/windows/views.py +++ b/windows/views.py @@ -195,6 +195,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 +280,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 ) @@ -927,7 +937,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,43 +1304,23 @@ 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 ) - bSizer154 = wx.BoxSizer( wx.VERTICAL ) + self.m_panel651 = wx.Panel( self.m_splitter7, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer149 = wx.BoxSizer( wx.VERTICAL ) - bSizer531 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText391 = wx.StaticText( self.m_panel55, wx.ID_ANY, _(u"Table:"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText391.Wrap( -1 ) - - bSizer531.Add( self.m_staticText391, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - - bSizer531.Add( ( 100, 0), 0, 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.btn_clone_table = wx.Button( self.m_panel55, wx.ID_ANY, _(u"Clone"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_clone_table.SetBitmap( wx.Bitmap( u"icons/16x16/table_multiple.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_clone_table.Enable( False ) - - bSizer531.Add( self.btn_clone_table, 0, wx.ALL|wx.EXPAND, 5 ) - - self.btn_delete_table1 = wx.Button( self.m_panel55, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_delete_table1.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_table1.Enable( False ) + 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.Add( self.btn_delete_table1, 0, wx.ALL|wx.EXPAND, 2 ) + 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 ) - bSizer531.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + 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 ) + self.m_toolBar51.Realize() - bSizer154.Add( bSizer531, 0, wx.EXPAND, 5 ) + bSizer154.Add( self.m_toolBar51, 0, wx.EXPAND, 5 ) bSizer152 = wx.BoxSizer( wx.VERTICAL ) @@ -1352,9 +1342,142 @@ def __init__( self, parent ): self.m_panel55.SetSizer( bSizer154 ) self.m_panel55.Layout() bSizer154.Fit( self.m_panel55 ) - self.m_splitter7.SplitHorizontally( self.m_panel54, self.m_panel55, 200 ) + self.m_notebook10.AddPage( self.m_panel55, _(u"Tables"), False ) + self.m_panel65 = wx.Panel( self.m_notebook10, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer1482 = wx.BoxSizer( wx.VERTICAL ) + + 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.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 ) + + 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.m_toolBar5.Realize() + + bSizer1482.Add( self.m_toolBar5, 0, wx.EXPAND, 5 ) + + 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 ) + + + 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 ) + + self.tool_clone_procedure = self.m_toolBar52.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 procedure"), _(u"Clone procedure"), None ) + + 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.m_toolBar52.Realize() + + bSizer14821.Add( self.m_toolBar52, 0, 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_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 ) + bSizer147 = wx.BoxSizer( wx.VERTICAL ) + + + bSizer80.Add( bSizer147, 1, wx.EXPAND, 5 ) + bSizer138 = wx.BoxSizer( wx.HORIZONTAL ) self.btn_cancel_database = wx.Button( self.m_panel30, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) @@ -2070,7 +2193,7 @@ def __init__( self, parent ): 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"Refrsh"), wx.Bitmap( u"icons/16x16/arrow_refresh.png", wx.BITMAP_TYPE_ANY ), wx.NullBitmap, wx.ITEM_NORMAL, wx.EmptyString, wx.EmptyString, None ) + 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() @@ -2238,6 +2361,8 @@ def __init__( self, parent ): bSizer125.Add( self.m_toolBar2, 0, wx.EXPAND, 5 ) + bSizer150 = wx.BoxSizer( wx.HORIZONTAL ) + self.notebook_query_editor = wx.Notebook( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) self.m_panel63 = wx.Panel( self.notebook_query_editor, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer146 = wx.BoxSizer( wx.VERTICAL ) @@ -2286,7 +2411,15 @@ 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 ) + bSizer150.Add( self.notebook_query_editor, 1, wx.EXPAND | wx.ALL, 5 ) + + self.m_dataViewTreeCtrl1 = wx.dataview.DataViewTreeCtrl( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.DV_NO_HEADER|wx.dataview.DV_ROW_LINES ) + self.m_dataViewTreeCtrl1.SetMinSize( wx.Size( 200,-1 ) ) + + bSizer150.Add( self.m_dataViewTreeCtrl1, 0, wx.ALL|wx.EXPAND, 5 ) + + + bSizer125.Add( bSizer150, 1, wx.EXPAND, 5 ) self.m_panel52.SetSizer( bSizer125 ) @@ -2397,9 +2530,24 @@ def __init__( self, parent ): self.Bind( wx.EVT_TOOL, self.on_database_disconnect, id = self.m_tool4.GetId() ) self.Bind( wx.EVT_TOOL, self.on_database_refresh, id = self.database_refresh.GetId() ) self.MainFrameNotebook.Bind( wx.EVT_NOTEBOOK_PAGE_CHANGED, self.on_page_chaged ) - self.btn_insert_table.Bind( wx.EVT_BUTTON, self.on_insert_table ) - self.btn_clone_table.Bind( wx.EVT_BUTTON, self.on_clone_table ) - 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_view, id = self.tool_insert_procedure.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_clone_view, id = self.tool_clone_procedure.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_view, id = self.tool_delete_procedure.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_insert_view, id = self.tool_insert_function.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_clone_view, id = self.tool_clone_function.GetId() ) + self.Bind( wx.EVT_TOOL, self.on_delete_view, 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 ) @@ -2473,6 +2621,27 @@ def on_clone_table( self, event ): def on_delete_table( self, event ): event.Skip() + def on_insert_view( self, event ): + event.Skip() + + def on_clone_view( self, event ): + event.Skip() + + def on_delete_view( self, event ): + event.Skip() + + + + + + + + + + + + + def on_cancel_database( self, event ): event.Skip() @@ -2641,6 +2810,18 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. bSizer139.Fit( self.database_character_set_panel ) bSizer144.Add( self.database_character_set_panel, 1, wx.ALIGN_CENTER, 5 ) + self.m_dataViewListCtrl2 = wx.dataview.DataViewListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.DV_ROW_LINES ) + bSizer144.Add( self.m_dataViewListCtrl2, 0, wx.ALL, 5 ) + + self.m_dataViewCtrl10 = wx.dataview.DataViewCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_dataViewColumn181 = self.m_dataViewCtrl10.AppendTextColumn( _(u"Comments"), 7, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + self.m_dataViewColumn191 = self.m_dataViewCtrl10.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_dataViewColumn171 = self.m_dataViewCtrl10.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_dataViewColumn161 = self.m_dataViewCtrl10.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_dataViewColumn151 = self.m_dataViewCtrl10.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_dataViewColumn141 = self.m_dataViewCtrl10.AppendTextColumn( _(u"Size"), 2, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_RIGHT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) + bSizer144.Add( self.m_dataViewCtrl10, 0, wx.ALL, 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 ) @@ -2750,6 +2931,41 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. bSizer144.Add( bSizer53, 0, wx.ALL|wx.EXPAND, 5 ) + bSizer531 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText391 = wx.StaticText( self, wx.ID_ANY, _(u"Table:"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText391.Wrap( -1 ) + + bSizer531.Add( self.m_staticText391, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) + + + bSizer531.Add( ( 100, 0), 0, wx.EXPAND, 5 ) + + self.btn_insert_table = wx.Button( self, 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.btn_clone_table = wx.Button( self, wx.ID_ANY, _(u"Clone"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + + self.btn_clone_table.SetBitmap( wx.Bitmap( u"icons/16x16/table_multiple.png", wx.BITMAP_TYPE_ANY ) ) + self.btn_clone_table.Enable( False ) + + bSizer531.Add( self.btn_clone_table, 0, wx.ALL|wx.EXPAND, 5 ) + + self.btn_delete_table1 = wx.Button( self, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + + self.btn_delete_table1.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) + self.btn_delete_table1.Enable( False ) + + bSizer531.Add( self.btn_delete_table1, 0, wx.ALL|wx.EXPAND, 2 ) + + + bSizer531.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + + + bSizer144.Add( bSizer531, 0, wx.EXPAND, 5 ) + self.SetSizer( bSizer144 ) self.Layout() @@ -2762,6 +2978,9 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. self.btn_delete_column.Bind( wx.EVT_BUTTON, self.on_delete_column ) self.btn_move_up_column.Bind( wx.EVT_BUTTON, self.on_move_up_column ) self.btn_move_down_column.Bind( wx.EVT_BUTTON, self.on_move_down_column ) + self.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 ) def __del__( self ): pass @@ -2789,6 +3008,15 @@ def on_move_up_column( self, event ): def on_move_down_column( self, event ): event.Skip() + def on_insert_table( self, event ): + event.Skip() + + def on_clone_table( self, event ): + event.Skip() + + def on_delete_table( self, event ): + event.Skip() + ########################################################################### ## Class TablePanel From 07443082aa8ee68f316777c9a1c699a0edaee52f Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 2 May 2026 16:05:26 +0200 Subject: [PATCH 44/93] Add UI scenario test suite and screenshot refresh support --- screenshot/connection_dialog_configured.png | Bin 0 -> 43585 bytes .../connection_dialog_explorer_state.png | Bin 0 -> 43336 bytes scripts/runtest.py | 23 +- tests/conftest.py | 14 ++ tests/ui/README.md | 35 +++ tests/ui/scenario_helpers.py | 22 ++ tests/ui/test_scenarios.py | 206 ++++++++++++++++++ 7 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 screenshot/connection_dialog_configured.png create mode 100644 screenshot/connection_dialog_explorer_state.png create mode 100644 tests/ui/README.md create mode 100644 tests/ui/scenario_helpers.py create mode 100644 tests/ui/test_scenarios.py diff --git a/screenshot/connection_dialog_configured.png b/screenshot/connection_dialog_configured.png new file mode 100644 index 0000000000000000000000000000000000000000..8bb3ea8e15154ba676215954eda6fbc199fec99c GIT binary patch literal 43585 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 literal 0 HcmV?d00001 diff --git a/screenshot/connection_dialog_explorer_state.png b/screenshot/connection_dialog_explorer_state.png new file mode 100644 index 0000000000000000000000000000000000000000..77743ebcb5b4e3264541765b4b2e2b92b7b53fd8 GIT binary patch literal 43336 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 T<|_2=^oUIjtqdykA3gsI$O6qc literal 0 HcmV?d00001 diff --git a/scripts/runtest.py b/scripts/runtest.py index a3507a3..4dc95e3 100755 --- a/scripts/runtest.py +++ b/scripts/runtest.py @@ -278,9 +278,9 @@ def main(): parser = argparse.ArgumentParser(description='Unified test runner') parser.add_argument( '--suite', - choices=['unit', 'integration', 'all'], + choices=['unit', 'integration', 'ui', 'all'], default='unit', - help='Select test suite: unit, integration, or all (default: unit)', + help='Select test suite: unit, integration, ui, or all (default: unit)', ) parser.add_argument( '--engine', @@ -289,10 +289,18 @@ def main(): ) parser.add_argument('--update', action='store_true', help='Run all tests (unit + integration) and update README badges') + parser.add_argument( + '--refresh-screenshots', + action='store_true', + help='Refresh UI scenario screenshots (available with --suite ui)', + ) args = parser.parse_args() - tests_target = f"tests/engines/{args.engine}/" if args.engine else 'tests/' + if args.suite == 'ui': + tests_target = 'tests/ui/test_scenarios.py' + else: + tests_target = f"tests/engines/{args.engine}/" if args.engine else 'tests/' pytest_command = ['uv', 'run', 'pytest', tests_target] @@ -300,9 +308,18 @@ def main(): pytest_command.extend(['--tb=short', '-m', 'not integration']) elif args.suite == 'integration': pytest_command.extend(['--tb=short', '-m', 'integration']) + elif args.suite == 'ui': + pytest_command.extend(['--tb=short']) else: pytest_command.extend(['--tb=no']) + if args.refresh_screenshots and args.suite != 'ui': + print("Error: --refresh-screenshots requires --suite ui") + return 2 + + if args.refresh_screenshots: + pytest_command.append('--refresh-screenshots') + if args.update and args.suite != 'all': print("Error: --update requires --suite all") return 2 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/ui/README.md b/tests/ui/README.md new file mode 100644 index 0000000..583d0d2 --- /dev/null +++ b/tests/ui/README.md @@ -0,0 +1,35 @@ +# UI Scenario Tests + +This directory includes UI scenario tests that validate key interaction flows and can refresh screenshots. + +## Scenarios + +- `test_scenario_connection_dialog_configured` + - Create directory + - Create connection + - Save connection + - Rename connection + - Optional final screenshot + +- `test_scenario_connection_dialog_tree_state` + - Build a minimal connection tree state + - Save connection + - Optional final screenshot + +## 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 +``` + +## 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..b0dc51f --- /dev/null +++ b/tests/ui/scenario_helpers.py @@ -0,0 +1,22 @@ +from pathlib import Path + +import wx + + +def pump_ui(iterations: int = 10) -> None: + for _ in range(iterations): + wx.YieldIfNeeded() + + +def capture_window_screenshot(window: wx.TopLevelWindow, target_path: Path) -> None: + size = window.GetSize() + width, height = int(size.width), int(size.height) + bitmap = wx.Bitmap(width, height) + memory_dc = wx.MemoryDC(bitmap) + try: + memory_dc.Blit(0, 0, width, height, wx.ClientDC(window), 0, 0) + finally: + memory_dc.SelectObject(wx.NullBitmap) + + target_path.parent.mkdir(parents=True, exist_ok=True) + bitmap.SaveFile(str(target_path), wx.BITMAP_TYPE_PNG) diff --git a/tests/ui/test_scenarios.py b/tests/ui/test_scenarios.py new file mode 100644 index 0000000..cfad2fc --- /dev/null +++ b/tests/ui/test_scenarios.py @@ -0,0 +1,206 @@ +from pathlib import Path + +import pytest +import wx + +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 ConnectionDirectory +from windows.dialogs.connections.view import ConnectionsManager + +from tests.ui.scenario_helpers import capture_window_screenshot +from tests.ui.scenario_helpers import pump_ui + + +class _DummySettings: + def __init__(self): + self._data = {} + + def get_value(self, *keys, default=None): + if not keys: + return default + + node = self._data + for key in keys: + if not isinstance(node, dict) or key not in node: + return default + node = node[key] + + return node + + def set_value(self, *keys, value): + node = self._data + for key in keys[:-1]: + child = node.get(key) + if not isinstance(child, dict): + child = {} + node[key] = child + node = child + + node[keys[-1]] = value + + +class _DummyIconRegistry: + imagelist = None + + @staticmethod + def get_bitmap(_): + return wx.NullBitmap + + +@pytest.fixture +def scenario_environment(tmp_path, monkeypatch): + import windows.dialogs.connections.repository as repository_module + + monkeypatch.setattr( + repository_module, + "CONNECTIONS_CONFIG_FILE", + tmp_path / "connections.yml", + ) + + dummy_app = _DummySettingsApp() + monkeypatch.setattr(wx, "GetApp", lambda: dummy_app) + + 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 + + # After dialog.Destroy() the C++ widgets are gone but the dialog's Python + # callbacks are still subscribed. The next test creates a new dialog that + # subscribes fresh, but then sets the observables — which fires the destroyed + # dialog's callbacks too, causing RuntimeError on the deleted wx widgets. + # Fix: clear both the stored value (silently) and all registered callbacks. + for obs in (CURRENT_DIRECTORY, CURRENT_CONNECTION, PENDING_CONNECTION): + obs._value = None + for event_callbacks in obs.callbacks.values(): + event_callbacks.clear() + + +class _DummySettingsApp: + def __init__(self): + self.settings = _DummySettings() + self.icon_registry_16 = _DummyIconRegistry() + + +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 _prepare_dialog(show: bool = False) -> ConnectionsManager: + CURRENT_DIRECTORY(None) + CURRENT_CONNECTION(None) + PENDING_CONNECTION(None) + + dialog = ConnectionsManager(None) + dialog.SetSize(wx.Size(1100, 780)) + if show: + dialog.Show() + pump_ui() + return dialog + + +def test_scenario_connection_dialog_configured(refresh_screenshots, scenario_environment): + dialog = _prepare_dialog(show=refresh_screenshots) + try: + dialog.on_create_directory(None) + pump_ui() + _rename_current_selection(dialog, "Scenario Directory") + pump_ui() + + directory = CURRENT_DIRECTORY() + assert isinstance(directory, ConnectionDirectory) + + dialog.on_create(None) + pump_ui() + + connection = CURRENT_CONNECTION() + assert connection is not None + connection.name = "Scenario Connection" + connection.configuration = connection.configuration._replace( + hostname="localhost", + username="root", + password="root", + port=3306, + ) + PENDING_CONNECTION(connection) + pump_ui() + + saved = dialog.on_save(None) + assert saved is True + pump_ui() + + refreshed_connection = CURRENT_CONNECTION() + assert refreshed_connection is not None + refreshed_connection.name = "Scenario Connection Renamed" + dialog._on_item_renamed(refreshed_connection) + + if refresh_screenshots: + capture_window_screenshot( + dialog, + Path("screenshot") / "connection_dialog_configured.png", + ) + + pump_ui() + + assert CURRENT_CONNECTION().name == "Scenario Connection Renamed" + finally: + dialog.Destroy() + + +def test_scenario_connection_dialog_tree_state(refresh_screenshots, scenario_environment): + dialog = _prepare_dialog(show=refresh_screenshots) + try: + dialog.on_create_directory(None) + pump_ui() + _rename_current_selection(dialog, "Explorer Scenario Directory") + pump_ui() + + dialog.on_create(None) + pump_ui() + + connection = CURRENT_CONNECTION() + assert connection is not None + connection.name = "Explorer Scenario Connection" + connection.configuration = connection.configuration._replace( + hostname="localhost", + username="root", + password="root", + ) + PENDING_CONNECTION(connection) + pump_ui() + + saved = dialog.on_save(None) + assert saved is True + + if refresh_screenshots: + capture_window_screenshot( + dialog, + Path("screenshot") / "connection_dialog_explorer_state.png", + ) + + pump_ui() + + item = dialog.connections_tree_ctrl.GetSelection() + assert item.IsOk() + finally: + dialog.Destroy() From c43391136358aa7dea812c03c4a22f0ce084f725 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 2 May 2026 16:05:26 +0200 Subject: [PATCH 45/93] Add query history controller and open-from-history behavior --- PeterSQL.fbp | 315 ++++++++++++++++++++++++++------ tests/ui/test_query_history.py | 142 +++++++++++++++ windows/main/controller.py | 321 ++++++++++++++++++++++----------- windows/main/query/history.py | 103 +++++++++++ windows/views.py | 40 +++- 5 files changed, 753 insertions(+), 168 deletions(-) create mode 100644 tests/ui/test_query_history.py create mode 100644 windows/main/query/history.py diff --git a/PeterSQL.fbp b/PeterSQL.fbp index 67c9425..a7fd971 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -18606,11 +18606,11 @@ bSizer150 wxHORIZONTAL none - + 5 - wxEXPAND | wxALL + wxEXPAND 1 - + 1 1 1 @@ -18621,7 +18621,6 @@ 0 - 1 0 @@ -18643,11 +18642,12 @@ 0 + 0 0 1 - notebook_query_editor + m_splitter8 1 @@ -18655,19 +18655,20 @@ 1 Resizable + 1 + -480 + -1 1 - + wxSPLIT_VERTICAL + wxSP_3D ; ; forward_declare 0 - - - a page - 0 + 1 1 @@ -18704,7 +18705,7 @@ 0 1 - m_panel63 + m_panel70 1 @@ -18722,14 +18723,14 @@ wxTAB_TRAVERSAL - bSizer146 + bSizer157 wxVERTICAL none 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -18738,9 +18739,9 @@ 0 0 - 1 + 1 0 @@ -18755,13 +18756,10 @@ 1 1 - 1 0 0 wxID_ANY - 1 - 1 0 @@ -18769,25 +18767,241 @@ 0 1 - sql_query_editor + notebook_query_editor 1 protected 1 - 0 Resizable 1 + ; ; forward_declare - 1 - 4 0 - 1 - 0 - 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 + @@ -18796,34 +19010,6 @@ - - - - 5 - wxALL|wxEXPAND - 0 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - 200,-1 - m_dataViewTreeCtrl1 - protected - - - wxDV_NO_HEADER|wxDV_ROW_LINES - ; ; forward_declare - - - - @@ -19379,7 +19565,7 @@ 5 wxALL 0 - + 1 @@ -19401,7 +19587,7 @@ - + wxALIGN_LEFT wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -19413,7 +19599,7 @@ Text -1 - + wxALIGN_LEFT wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -19425,7 +19611,7 @@ Text -1 - + wxALIGN_LEFT wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -19437,7 +19623,7 @@ Text -1 - + wxALIGN_LEFT wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -19449,7 +19635,7 @@ Date -1 - + wxALIGN_LEFT wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -19461,7 +19647,7 @@ Date -1 - + wxALIGN_RIGHT wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -20939,6 +21125,17 @@ + + 5 + wxEXPAND + 1 + + + bSizer156 + wxVERTICAL + none + + 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/windows/main/controller.py b/windows/main/controller.py index 25b7681..549e635 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -24,7 +24,7 @@ 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 from windows.views import MainFrameView @@ -33,12 +33,13 @@ 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 from windows.main.explorer import TreeExplorerController -from windows.main.database.list import ListDatabaseTable, ListDatabaseView +from windows.main.database.list import ListDatabaseTable, ListDatabaseView, ListDatabaseProcedure, ListDatabaseFunction, ListDatabaseTrigger, ListDatabaseEvent from windows.main.database.view import ViewEditorController +from windows.main.database.procedure import ProcedureEditorController from windows.main.database.options import DatabaseOptionsController from windows.main.table.check import TableCheckController @@ -49,6 +50,7 @@ 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 +63,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() @@ -91,6 +97,11 @@ def __init__(self): self._setup_query_pages() self.controller_view_editor = ViewEditorController(self) + self.controller_procedure_editor = ProcedureEditorController(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) @@ -246,6 +257,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) @@ -297,69 +322,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: @@ -459,14 +421,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: @@ -499,6 +453,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 @@ -511,7 +466,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 = { @@ -533,6 +493,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: @@ -605,10 +576,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: @@ -634,6 +627,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 @@ -659,6 +653,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 @@ -675,6 +670,8 @@ def _setup_subscribers(self): CURRENT_VIEW.subscribe(self._on_current_view) + CURRENT_PROCEDURE.subscribe(self._on_current_procedure) + CURRENT_TRIGGER.subscribe(self._on_current_trigger) CURRENT_TABLE.subscribe(self._on_current_table) @@ -700,6 +697,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) @@ -765,7 +765,7 @@ 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]] = None): # self.MainFrameNotebook.SetSelection(0) logger.debug( "ui trace: toggle_panel current=%s", @@ -777,6 +777,8 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S current_table = CURRENT_TABLE.get_value() current_view = CURRENT_VIEW.get_value() current_trigger = CURRENT_TRIGGER.get_value() + current_procedure = CURRENT_PROCEDURE.get_value() + procedure_page_index = self.controller_procedure_editor.page_index total_pages = self.MainFrameNotebook.GetPageCount() @@ -800,6 +802,9 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S if not current_trigger: self.MainFrameNotebook.GetPage(4).Hide() + if not current_procedure: + self.MainFrameNotebook.GetPage(procedure_page_index).Hide() + return if isinstance(current, Session): @@ -823,6 +828,7 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S self.MainFrameNotebook.SetSelection(3) self.MainFrameNotebook.GetPage(5).Show() + logger.debug("ui trace: toggle_panel records page shown (load disabled isolation)") self.MainFrameNotebook.GetPage(6).Show() elif isinstance(current, SQLTrigger): @@ -831,6 +837,12 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S if self.MainFrameNotebook.GetSelection() < 4: self.MainFrameNotebook.SetSelection(3) + elif isinstance(current, SQLProcedure): + self.MainFrameNotebook.GetPage(procedure_page_index).Show() + self.MainFrameNotebook.GetPage(6).Show() + if self.MainFrameNotebook.GetSelection() != procedure_page_index: + self.MainFrameNotebook.SetSelection(procedure_page_index) + def _get_records_filters(self) -> str: return (self.sql_query_filters.GetSelectedText() or self.sql_query_filters.GetText()).strip() @@ -1026,13 +1038,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) @@ -1040,10 +1065,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) @@ -1051,6 +1083,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: @@ -1060,6 +1098,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) @@ -1072,37 +1116,39 @@ 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) logger.debug( - "ui trace: records._load_records_page start table=%s limit=%s offset=%s filters=%s", - table.name, + "ui trace: records._load_records_page start obj=%s limit=%s offset=%s filters=%s", + obj.name, limit, self._records_offset, filters, ) with Loader.cursor_wait(): - logger.debug("ui trace: records._load_records_page before table.load_records table=%s", table.name) - table.load_records(filters=filters, limit=limit, offset=self._records_offset) - logger.debug("ui trace: records._load_records_page after table.load_records table=%s", table.name) - logger.debug("ui trace: records._load_records_page before controller.load_model table=%s", table.name) - self.controller_list_table_records.load_model() - logger.debug("ui trace: records._load_records_page after controller.load_model table=%s", table.name) + logger.debug("ui trace: records._load_records_page before obj.load_records obj=%s", obj.name) + obj.load_records(filters=filters, limit=limit, offset=self._records_offset) + logger.debug("ui trace: records._load_records_page after obj.load_records obj=%s", obj.name) + logger.debug("ui trace: records._load_records_page before controller.load_model_for obj=%s", obj.name) + self.controller_list_table_records.load_model_for(obj) + logger.debug("ui trace: records._load_records_page after controller.load_model_for obj=%s", obj.name) - self._update_records_label(table) - self._set_records_paging_buttons(table) - logger.debug("ui trace: records._load_records_page end table=%s", table.name) + 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) @@ -1148,7 +1194,7 @@ def _set_records_paging_buttons(self, table: SQLTable): def on_page_chaged(self, event): if int(event.Selection) == 5: - if table := CURRENT_TABLE.get_value(): + if CURRENT_TABLE.get_value() or CURRENT_VIEW.get_value(): self._records_offset = 0 self._load_records_page() @@ -1179,15 +1225,11 @@ 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(): @@ -1345,6 +1387,17 @@ def _on_current_view(self, current: SQLView): ) self.toggle_panel(current) + 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() == 5: + 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) @@ -1380,6 +1433,48 @@ def _on_current_trigger(self, current: SQLTrigger): ) 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) + + can_act = current is not None and not current.is_new + self.controller_procedure_editor.btn_delete_procedure.Enable(can_act) + + 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) + procedure_page_index = self.controller_procedure_editor.page_index + self._toggle_panel(procedure_page_index, True) + self.MainFrameNotebook.SetSelection(procedure_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) + procedure_page_index = self.controller_procedure_editor.page_index + self._toggle_panel(procedure_page_index, True) + self.MainFrameNotebook.SetSelection(procedure_page_index) + # TABLE def _on_current_table(self, table: SQLTable): logger.debug( @@ -1390,6 +1485,11 @@ def _on_current_table(self, table: SQLTable): 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 @@ -1400,6 +1500,11 @@ def _on_current_table(self, table: SQLTable): self.toggle_panel(table) self._set_records_paging_buttons(table) + 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([]) @@ -1416,7 +1521,17 @@ def _on_current_table(self, table: SQLTable): ) if self.MainFrameNotebook.GetSelection() == 5: + 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) @@ -1695,8 +1810,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( @@ -1706,8 +1820,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) 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/views.py b/windows/views.py index b429416..2a6319f 100755 --- a/windows/views.py +++ b/windows/views.py @@ -2363,7 +2363,14 @@ def __init__( self, parent ): bSizer150 = wx.BoxSizer( wx.HORIZONTAL ) - self.notebook_query_editor = wx.Notebook( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + 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 ) @@ -2411,12 +2418,26 @@ def __init__( self, parent ): bSizer146.Fit( self.m_panel63 ) self.notebook_query_editor.AddPage( self.m_panel63, _(u"a page"), False ) - bSizer150.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 ) ) - self.m_dataViewTreeCtrl1 = wx.dataview.DataViewTreeCtrl( self.m_panel52, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.DV_NO_HEADER|wx.dataview.DV_ROW_LINES ) - self.m_dataViewTreeCtrl1.SetMinSize( wx.Size( 200,-1 ) ) + bSizer1581.Add( self.tree_ctrl_query_history, 1, wx.ALL|wx.EXPAND, 5 ) - bSizer150.Add( self.m_dataViewTreeCtrl1, 0, 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 ) @@ -2777,6 +2798,10 @@ def m_splitter6OnIdle( self, event ): self.m_splitter6.SetSashPosition( -300 ) self.m_splitter6.Unbind( wx.EVT_IDLE ) + def m_splitter8OnIdle( self, event ): + self.m_splitter8.SetSashPosition( -480 ) + self.m_splitter8.Unbind( wx.EVT_IDLE ) + ########################################################################### ## Class Trash @@ -2966,6 +2991,11 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. bSizer144.Add( bSizer531, 0, wx.EXPAND, 5 ) + bSizer156 = wx.BoxSizer( wx.VERTICAL ) + + + bSizer144.Add( bSizer156, 1, wx.EXPAND, 5 ) + self.SetSizer( bSizer144 ) self.Layout() From 5fde9aed2ca670feec81b001c9b62a697be31f2c Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 2 May 2026 16:05:26 +0200 Subject: [PATCH 46/93] Add procedure handling and expand database explorer coverage --- tests/ui/test_database_list.py | 233 +++++++++++++++++ tests/ui/test_explorer.py | 173 ++++++++++++ windows/main/database/list.py | 356 ++++++++++++++++++++++++- windows/main/database/procedure.py | 406 +++++++++++++++++++++++++++++ windows/main/database/view.py | 10 +- windows/main/explorer.py | 82 +++++- 6 files changed, 1249 insertions(+), 11 deletions(-) create mode 100644 tests/ui/test_database_list.py create mode 100644 tests/ui/test_explorer.py create mode 100644 windows/main/database/procedure.py 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/windows/main/database/list.py b/windows/main/database/list.py index 748691d..5ddb299 100644 --- a/windows/main/database/list.py +++ b/windows/main/database/list.py @@ -9,9 +9,9 @@ from helpers.dataview import BaseObservableDataViewListModel, ColumnField from helpers.logger import logger -from structures.engines.database import SQLTable, SQLDatabase, SQLView +from structures.engines.database import SQLTable, SQLDatabase, SQLView, SQLProcedure, SQLFunction, SQLTrigger, SQLEvent -from windows.main import CURRENT_DATABASE, CURRENT_TABLE, CURRENT_SESSION, CURRENT_VIEW +from windows.main import CURRENT_DATABASE, CURRENT_TABLE, CURRENT_SESSION, CURRENT_VIEW, CURRENT_PROCEDURE, CURRENT_FUNCTION, CURRENT_TRIGGER, CURRENT_EVENT class ModelDatabaseTable(BaseObservableDataViewListModel): @@ -37,6 +37,36 @@ 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() @@ -44,7 +74,6 @@ class ListDatabaseTable: def __init__(self, list_ctrl_database_tables: wx.dataview.DataViewCtrl): 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) @@ -126,7 +155,7 @@ def _truncate_statement(value: str) -> str: if not value: return "" value = " ".join(value.split()) - return value[:120] + "…" if len(value) > 120 else value + return value[:120] + "..." if len(value) > 120 else value class ModelDatabaseView(BaseObservableDataViewListModel): @@ -206,3 +235,322 @@ def _on_item_activated(self, event: wx.dataview.DataViewEvent): 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/procedure.py b/windows/main/database/procedure.py new file mode 100644 index 0000000..2f2b35e --- /dev/null +++ b/windows/main/database/procedure.py @@ -0,0 +1,406 @@ +from typing import Optional + +import wx +import wx.stc + +from gettext import gettext as _ + +from helpers.bindings import AbstractModel, wx_call_after_debounce +from helpers.logger import logger +from helpers.observables import Observable + +from structures.connection import ConnectionEngine +from structures.engines.database import SQLProcedure + +from windows.main import CURRENT_SESSION, CURRENT_DATABASE, CURRENT_PROCEDURE + + +class EditViewModel(AbstractModel): + def __init__(self): + self.name = Observable() + self.parameters = Observable() + self.language = Observable() + self.definer = Observable() + self.body = Observable() + + wx_call_after_debounce( + self.name, self.parameters, self.language, self.definer, self.body, + callback=self.update_procedure + ) + + CURRENT_PROCEDURE.subscribe(self._load_procedure) + + def _load_procedure(self, procedure: Optional[SQLProcedure]): + if procedure is None: + return + + self.name.set_initial(procedure.name) + self.parameters.set_initial(getattr(procedure, "parameters", "")) + self.body.set_initial(getattr(procedure, "statement", "")) + + session = CURRENT_SESSION.get_value() + if not session: + return + + engine = session.engine + if engine in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): + self._load_mysql_fields(procedure) + elif engine == ConnectionEngine.POSTGRESQL: + self._load_postgresql_fields(procedure) + + def update_procedure(self, *args): + if not any(args): + return + + procedure = CURRENT_PROCEDURE.get_value() + if not procedure: + return + + procedure.name = self.name.get_value() or procedure.name + if hasattr(procedure, "parameters"): + procedure.parameters = self.parameters.get_value() or "" + if hasattr(procedure, "statement"): + procedure.statement = self.body.get_value() or "" + + session = CURRENT_SESSION.get_value() + if not session: + return + + engine = session.engine + if engine in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): + self._update_mysql_fields(procedure) + elif engine == ConnectionEngine.POSTGRESQL: + self._update_postgresql_fields(procedure) + + def _load_mysql_fields(self, procedure: SQLProcedure): + if hasattr(procedure, "definer"): + self.definer.set_initial(procedure.definer) + + def _load_postgresql_fields(self, procedure: SQLProcedure): + if hasattr(procedure, "language"): + self.language.set_initial(procedure.language) + + def _update_mysql_fields(self, procedure: SQLProcedure): + if hasattr(procedure, "definer"): + procedure.definer = self.definer.get_value() or "" + + def _update_postgresql_fields(self, procedure: SQLProcedure): + if hasattr(procedure, "language"): + procedure.language = self.language.get_value() or "plpgsql" + + +class ProcedureEditorController: + def __init__(self, parent): + self.parent = parent + self._build_panel(parent) + self.model = EditViewModel() + self._bind_controls() + self._bind_buttons() + + wx_call_after_debounce( + self.model.name, self.model.parameters, + self.model.language, self.model.definer, self.model.body, + callback=self.update_button_states + ) + + CURRENT_PROCEDURE.subscribe(self.on_current_procedure_changed) + + # ------------------------------------------------------------------ + # Panel construction + # ------------------------------------------------------------------ + + def _build_panel(self, parent): + self.panel = wx.Panel(parent.MainFrameNotebook, wx.ID_ANY) + parent.MainFrameNotebook.AddPage(self.panel, _("Procedure"), False) + self.page_index = parent.MainFrameNotebook.GetPageCount() - 1 + + outer = wx.BoxSizer(wx.VERTICAL) + + # --- Options notebook (mirrors m_notebook7 in views) --- + self.options_notebook = wx.Notebook(self.panel, wx.ID_ANY) + self.pnl_options_root = wx.Panel(self.options_notebook, wx.ID_ANY) + self.options_notebook.AddPage(self.pnl_options_root, _("Options"), False) + + options_vsizer = wx.BoxSizer(wx.VERTICAL) + + # Name row (always visible) + pnl_name = wx.Panel(self.pnl_options_root, wx.ID_ANY) + name_sizer = wx.BoxSizer(wx.HORIZONTAL) + lbl_name = wx.StaticText(pnl_name, label=_("Name")) + lbl_name.SetMinSize(wx.Size(150, -1)) + name_sizer.Add(lbl_name, 0, wx.ALIGN_CENTER | wx.ALL, 5) + self.txt_procedure_name = wx.TextCtrl(pnl_name, wx.ID_ANY) + name_sizer.Add(self.txt_procedure_name, 1, wx.ALIGN_CENTER | wx.ALL, 5) + pnl_name.SetSizer(name_sizer) + options_vsizer.Add(pnl_name, 0, wx.EXPAND | wx.ALL, 2) + + # Definer row (MySQL/MariaDB) + self.pnl_row_definer = wx.Panel(self.pnl_options_root, wx.ID_ANY) + definer_sizer = wx.BoxSizer(wx.HORIZONTAL) + lbl_def = wx.StaticText(self.pnl_row_definer, label=_("Definer")) + lbl_def.SetMinSize(wx.Size(150, -1)) + definer_sizer.Add(lbl_def, 0, wx.ALIGN_CENTER | wx.ALL, 5) + self.cmb_procedure_definer = wx.ComboBox(self.pnl_row_definer, wx.ID_ANY, style=wx.CB_DROPDOWN) + definer_sizer.Add(self.cmb_procedure_definer, 1, wx.ALIGN_CENTER | wx.ALL, 5) + self.pnl_row_definer.SetSizer(definer_sizer) + options_vsizer.Add(self.pnl_row_definer, 0, wx.EXPAND | wx.ALL, 2) + + # Parameters row (always visible) + pnl_params = wx.Panel(self.pnl_options_root, wx.ID_ANY) + params_sizer = wx.BoxSizer(wx.HORIZONTAL) + lbl_params = wx.StaticText(pnl_params, label=_("Parameters")) + lbl_params.SetMinSize(wx.Size(150, -1)) + params_sizer.Add(lbl_params, 0, wx.ALIGN_CENTER | wx.ALL, 5) + self.txt_procedure_parameters = wx.TextCtrl(pnl_params, wx.ID_ANY) + params_sizer.Add(self.txt_procedure_parameters, 1, wx.ALIGN_CENTER | wx.ALL, 5) + pnl_params.SetSizer(params_sizer) + options_vsizer.Add(pnl_params, 0, wx.EXPAND | wx.ALL, 2) + + # Language row (PostgreSQL) + self.pnl_row_language = wx.Panel(self.pnl_options_root, wx.ID_ANY) + lang_sizer = wx.BoxSizer(wx.HORIZONTAL) + lbl_lang = wx.StaticText(self.pnl_row_language, label=_("Language")) + lbl_lang.SetMinSize(wx.Size(150, -1)) + lang_sizer.Add(lbl_lang, 0, wx.ALIGN_CENTER | wx.ALL, 5) + self.cho_procedure_language = wx.Choice(self.pnl_row_language, wx.ID_ANY, choices=["plpgsql", "sql"]) + self.cho_procedure_language.SetSelection(0) + lang_sizer.Add(self.cho_procedure_language, 1, wx.ALIGN_CENTER | wx.ALL, 5) + self.pnl_row_language.SetSizer(lang_sizer) + options_vsizer.Add(self.pnl_row_language, 0, wx.EXPAND | wx.ALL, 2) + + self.pnl_options_root.SetSizer(options_vsizer) + self.pnl_options_root.Layout() + + outer.Add(self.options_notebook, 0, wx.ALL | wx.EXPAND, 5) + + # --- Body editor --- + self.stc_procedure_body = wx.stc.StyledTextCtrl(self.panel, wx.ID_ANY, size=wx.Size(-1, -1)) + self.stc_procedure_body.SetMinSize(wx.Size(-1, 120)) + outer.Add(self.stc_procedure_body, 1, wx.ALL | wx.EXPAND, 5) + + # --- Buttons --- + btn_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.btn_delete_procedure = wx.Button(self.panel, wx.ID_ANY, _("Delete")) + self.btn_cancel_procedure = wx.Button(self.panel, wx.ID_ANY, _("Cancel")) + self.btn_save_procedure = wx.Button(self.panel, wx.ID_ANY, _("Save")) + btn_sizer.Add(self.btn_delete_procedure, 0, wx.RIGHT, 5) + btn_sizer.AddStretchSpacer() + btn_sizer.Add(self.btn_cancel_procedure, 0, wx.RIGHT, 5) + btn_sizer.Add(self.btn_save_procedure, 0) + outer.Add(btn_sizer, 0, wx.ALL | wx.EXPAND, 5) + + self.panel.SetSizer(outer) + + try: + from windows.components.stc.styles import apply_stc_theme + from windows.components.stc.profiles import SQL + apply_stc_theme(self.stc_procedure_body) + SQL.apply(self.stc_procedure_body) + except Exception: + pass + + # ------------------------------------------------------------------ + # Bindings + # ------------------------------------------------------------------ + + def _bind_controls(self): + self.model.bind_controls( + name=self.txt_procedure_name, + parameters=self.txt_procedure_parameters, + language=self.cho_procedure_language, + definer=self.cmb_procedure_definer, + body=self.stc_procedure_body, + ) + + def _bind_buttons(self): + self.btn_save_procedure.Bind(wx.EVT_BUTTON, self.on_save_procedure) + self.btn_delete_procedure.Bind(wx.EVT_BUTTON, self.on_delete_procedure) + self.btn_cancel_procedure.Bind(wx.EVT_BUTTON, self.on_cancel_procedure) + + # ------------------------------------------------------------------ + # Button state + # ------------------------------------------------------------------ + + def _get_original_procedure(self, procedure: SQLProcedure) -> Optional[SQLProcedure]: + if procedure.is_new: + return None + database = CURRENT_DATABASE.get_value() + if not database: + return None + return next((p for p in database.procedures if p.id == procedure.id), None) + + def _has_changes(self, procedure: SQLProcedure) -> bool: + if procedure.is_new: + return True + original = self._get_original_procedure(procedure) + if original is None: + return True + self.model.update_procedure(procedure) + return procedure != original + + def update_button_states(self, *args, **kwargs): + procedure = CURRENT_PROCEDURE.get_value() + logger.debug( + "ui trace: procedure.update_button_states procedure=%s is_new=%s", + getattr(procedure, "name", None) if procedure is not None else None, + getattr(procedure, "is_new", None) if procedure is not None else None, + ) + if procedure is None: + self.btn_save_procedure.Enable(False) + self.btn_cancel_procedure.Enable(False) + self.btn_delete_procedure.Enable(False) + else: + has_changes = self._has_changes(procedure) + self.btn_save_procedure.Enable(has_changes) + self.btn_cancel_procedure.Enable(has_changes) + self.btn_delete_procedure.Enable(not procedure.is_new) + + # ------------------------------------------------------------------ + # Actions + # ------------------------------------------------------------------ + + def on_save_procedure(self, event): + self.do_save_procedure() + + def on_delete_procedure(self, event): + self.do_delete_procedure() + + def on_cancel_procedure(self, event): + self.do_cancel_procedure() + + def do_save_procedure(self): + procedure = CURRENT_PROCEDURE.get_value() + if not procedure: + return + session = CURRENT_SESSION.get_value() + if not session: + return + + is_new = procedure.is_new + try: + procedure.save() + 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() + saved = next((p for p in database.procedures if p.name == procedure.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 procedure: {}").format(str(e)), _("Error"), wx.OK | wx.ICON_ERROR) + + def do_delete_procedure(self): + procedure = CURRENT_PROCEDURE.get_value() + if not procedure: + return + session = CURRENT_SESSION.get_value() + if not session: + return + + result = wx.MessageBox( + _("Are you sure you want to delete procedure '{}'?").format(procedure.name), + _("Confirm Delete"), + wx.YES_NO | wx.ICON_QUESTION, + ) + if result != wx.YES: + return + + try: + procedure.drop() + wx.MessageBox(_("Procedure deleted successfully"), _("Success"), wx.OK | wx.ICON_INFORMATION) + CURRENT_PROCEDURE.set_value(None) + database = CURRENT_DATABASE.get_value() + database.procedures.refresh() + self.parent.controller_tree_connections.refresh_current_database() + except Exception as e: + wx.MessageBox(_("Error deleting procedure: {}").format(str(e)), _("Error"), wx.OK | wx.ICON_ERROR) + + def do_cancel_procedure(self): + procedure = CURRENT_PROCEDURE.get_value() + if not procedure: + return + CURRENT_PROCEDURE.set_value(None) + CURRENT_PROCEDURE.set_value(procedure) + self.update_button_states() + + # ------------------------------------------------------------------ + # Current procedure changed + # ------------------------------------------------------------------ + + def on_current_procedure_changed(self, procedure: Optional[SQLProcedure]): + logger.debug( + "ui trace: procedure.on_current_procedure_changed procedure=%s is_new=%s", + getattr(procedure, "name", None) if procedure is not None else None, + getattr(procedure, "is_new", None) if procedure is not None else None, + ) + self.update_button_states() + + if procedure 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): + if engine not in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): + return + try: + logger.debug("ui trace: procedure._populate_definers start engine=%s", engine.name) + definers = session.context.get_definers() + self.cmb_procedure_definer.Clear() + for definer in definers: + self.cmb_procedure_definer.Append(definer) + logger.debug("ui trace: procedure._populate_definers done count=%s", len(definers)) + except Exception: + pass + + def apply_engine_visibility(self, engine: ConnectionEngine): + logger.debug("ui trace: procedure.apply_engine_visibility engine=%s", engine.name) + if engine in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): + self._apply_mysql_visibility() + elif engine == ConnectionEngine.POSTGRESQL: + self._apply_postgresql_visibility() + else: + self._apply_default_visibility() + + self.pnl_options_root.GetSizer().Layout() + self.options_notebook.SetMinSize(wx.Size(-1, -1)) + self.options_notebook.Fit() + self.panel.Layout() + + def _apply_mysql_visibility(self): + self._batch_show_hide( + show=[self.pnl_row_definer], + hide=[self.pnl_row_language], + ) + + def _apply_postgresql_visibility(self): + self._batch_show_hide( + show=[self.pnl_row_language], + hide=[self.pnl_row_definer], + ) + + def _apply_default_visibility(self): + self._batch_show_hide( + show=[], + hide=[self.pnl_row_definer, self.pnl_row_language], + ) + + def _batch_show_hide(self, show: list[wx.Window], hide: list[wx.Window]): + 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) diff --git a/windows/main/database/view.py b/windows/main/database/view.py index 68c492d..e43da3b 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 @@ -216,9 +217,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) @@ -252,6 +253,8 @@ def do_save_view(self): view.save() 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) @@ -286,6 +289,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..214e8fc 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) @@ -108,11 +111,12 @@ def _load_items(self, event: wx.lib.agw.hypertreelist.TreeEvent): 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 +129,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 +170,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,12 +183,19 @@ 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(): @@ -221,7 +233,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 +258,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) From eac7124d7b28cf11e6baa496826edad278cd02ea Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 2 May 2026 16:05:26 +0200 Subject: [PATCH 47/93] Refine table controllers and add focused UI coverage --- tests/ui/test_foreign_key_controller.py | 133 ++++++++++++++++++++++++ windows/main/table/column.py | 40 ++++++- windows/main/table/foreign_key.py | 6 +- windows/main/table/records.py | 41 ++++---- 4 files changed, 193 insertions(+), 27 deletions(-) create mode 100644 tests/ui/test_foreign_key_controller.py 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/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/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/records.py b/windows/main/table/records.py index fe1d747..bceb713 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,9 +140,7 @@ 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.""" @@ -155,7 +151,7 @@ def load_records_async(self, filters: Optional[str] = None, limit: int = 1000, o """Load records asynchronously using RecordsExecutor.""" if not self.executor or not self.table: return - + self.executor.load_records( table=self.table, on_complete=self._on_records_loaded, @@ -164,26 +160,26 @@ def load_records_async(self, filters: Optional[str] = None, limit: int = 1000, o offset=offset, orders=orders ) - + def _on_records_loaded(self, result: RecordsOperationResult): """Handle completion of records loading.""" if result.success and result.records is not None: self.table.records.set_value(result.records) - self.load_model() else: logger.error(f"Failed to load records: {result.error}") - # Fallback to synchronous loading try: self.table.load_records() - self.load_model() except Exception as ex: logger.error(f"Fallback loading also failed: {ex}", exc_info=True) - def load_model(self): - self.model = RecordsModel(self.table, len(self.table.columns)) - 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) @@ -219,24 +215,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, From a027ed753ce203c2ca3cbd978c9741a9c2175018 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 4 May 2026 16:22:58 +0200 Subject: [PATCH 48/93] test(ui): extend dialog scenarios and screenshot flow --- assets/database_options_matrix.md | 56 ++ main.py | 17 +- pyproject.toml | 3 + screenshot/connection_dialog_configured.png | Bin 43585 -> 0 bytes .../connection_dialog_explorer_state.png | Bin 43336 -> 0 bytes scripts/runtest.py | 2 +- tests/ui/README.md | 29 +- tests/ui/scenario_helpers.py | 184 +++++- tests/ui/test_bindings.py | 3 + tests/ui/test_scenarios.py | 549 ++++++++++++++---- uv.lock | 163 ++++++ 11 files changed, 875 insertions(+), 131 deletions(-) create mode 100644 assets/database_options_matrix.md delete mode 100644 screenshot/connection_dialog_configured.png delete mode 100644 screenshot/connection_dialog_explorer_state.png diff --git a/assets/database_options_matrix.md b/assets/database_options_matrix.md new file mode 100644 index 0000000..bfe5cb0 --- /dev/null +++ b/assets/database_options_matrix.md @@ -0,0 +1,56 @@ +# Database Options Matrix (Compact) + +## Legend + +- ✅ supported +- ⚠️ partial / indirect +- ❌ not supported + +------------------------------------------------------------------------ + +## Matrix + + Option MySQL MariaDB PostgreSQL SQLite + ------------------- ------- --------- ------------ -------- + Charset ✅ ✅ ❌ ❌ + Collation ✅ ✅ ⚠️ ❌ + Encoding ❌ ❌ ✅ ❌ + Locale (LC\_\*) ❌ ❌ ✅ ❌ + Owner ❌ ❌ ✅ ❌ + Template DB ❌ ❌ ✅ ❌ + Connection limit ❌ ❌ ✅ ❌ + Allow connections ❌ ❌ ✅ ❌ + Is template flag ❌ ❌ ✅ ❌ + Default engine ✅ ✅ ❌ ❌ + +------------------------------------------------------------------------ + +## Notes + +### MySQL / MariaDB + +- Focus on: + - charset + - collation + - default engine + +### PostgreSQL + +- Different model: + - encoding + - locale (LC_COLLATE, LC_CTYPE) + - owner + - template database + - connection rules + +### 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 diff --git a/main.py b/main.py index c441ef4..a0385e8 100755 --- a/main.py +++ b/main.py @@ -3,6 +3,7 @@ import os from pathlib import Path +from typing import Optional import wx @@ -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,6 +32,19 @@ 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) diff --git a/pyproject.toml b/pyproject.toml index e252424..4339ef0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,10 @@ dependencies = [ [project.optional-dependencies] dev = [ "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 deleted file mode 100644 index 8bb3ea8e15154ba676215954eda6fbc199fec99c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43585 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 diff --git a/screenshot/connection_dialog_explorer_state.png b/screenshot/connection_dialog_explorer_state.png deleted file mode 100644 index 77743ebcb5b4e3264541765b4b2e2b92b7b53fd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43336 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 T<|_2=^oUIjtqdykA3gsI$O6qc diff --git a/scripts/runtest.py b/scripts/runtest.py index 4dc95e3..09a3718 100755 --- a/scripts/runtest.py +++ b/scripts/runtest.py @@ -309,7 +309,7 @@ def main(): elif args.suite == 'integration': pytest_command.extend(['--tb=short', '-m', 'integration']) elif args.suite == 'ui': - pytest_command.extend(['--tb=short']) + pytest_command.extend(['--tb=short', '-n', '1']) else: pytest_command.extend(['--tb=no']) diff --git a/tests/ui/README.md b/tests/ui/README.md index 583d0d2..872309a 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -1,20 +1,25 @@ # 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_connection_dialog_configured` - - Create directory - - Create connection - - Save connection - - Rename connection - - Optional final screenshot - -- `test_scenario_connection_dialog_tree_state` - - Build a minimal connection tree state - - Save connection - - Optional final screenshot +- `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 @@ -24,6 +29,8 @@ Use the project test runner with the dedicated UI suite: 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`: diff --git a/tests/ui/scenario_helpers.py b/tests/ui/scenario_helpers.py index b0dc51f..d3ea8fc 100644 --- a/tests/ui/scenario_helpers.py +++ b/tests/ui/scenario_helpers.py @@ -1,5 +1,7 @@ from pathlib import Path +import time +import pyautogui import wx @@ -8,15 +10,183 @@ def pump_ui(iterations: int = 10) -> None: wx.YieldIfNeeded() -def capture_window_screenshot(window: wx.TopLevelWindow, target_path: Path) -> None: - size = window.GetSize() - width, height = int(size.width), int(size.height) - bitmap = wx.Bitmap(width, height) +# 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.Blit(0, 0, width, height, wx.ClientDC(window), 0, 0) + 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) - target_path.parent.mkdir(parents=True, exist_ok=True) - bitmap.SaveFile(str(target_path), wx.BITMAP_TYPE_PNG) + 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.Layout() + # window.Refresh() + # window.Update() + # pump_ui(20) + # + # target_path.parent.mkdir(parents=True, exist_ok=True) + # bitmap = capture_window_bitmap(window) + # bitmap.SaveFile(str(target_path), wx.BITMAP_TYPE_PNG) + window.Show() + window.Raise() + window.SetFocus() + window.Layout() + window.Refresh() + window.Update() + + for _ in range(10): + wx.GetApp().ProcessPendingEvents() + wx.YieldIfNeeded() + time.sleep(0.05) + + rect = window.GetScreenRect() + + image = pyautogui.screenshot(region=( + rect.x, + rect.y, + rect.width, + rect.height, + )) + + image.save(target_path) 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_scenarios.py b/tests/ui/test_scenarios.py index cfad2fc..cbe43b6 100644 --- a/tests/ui/test_scenarios.py +++ b/tests/ui/test_scenarios.py @@ -1,66 +1,75 @@ 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 -class _DummySettings: - def __init__(self): - self._data = {} - - def get_value(self, *keys, default=None): - if not keys: - return default - - node = self._data - for key in keys: - if not isinstance(node, dict) or key not in node: - return default - node = node[key] - - return node - - def set_value(self, *keys, value): - node = self._data - for key in keys[:-1]: - child = node.get(key) - if not isinstance(child, dict): - child = {} - node[key] = child - node = child - - node[keys[-1]] = value - - -class _DummyIconRegistry: - imagelist = None - - @staticmethod - def get_bitmap(_): - return wx.NullBitmap - - -@pytest.fixture -def scenario_environment(tmp_path, monkeypatch): +@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", - tmp_path / "connections.yml", + config_file, ) - dummy_app = _DummySettingsApp() - monkeypatch.setattr(wx, "GetApp", lambda: dummy_app) + 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 @@ -78,21 +87,86 @@ def Destroy(self): yield - # After dialog.Destroy() the C++ widgets are gone but the dialog's Python - # callbacks are still subscribed. The next test creates a new dialog that - # subscribes fresh, but then sets the observables — which fires the destroyed - # dialog's callbacks too, causing RuntimeError on the deleted wx widgets. - # Fix: clear both the stored value (silently) and all registered callbacks. - for obs in (CURRENT_DIRECTORY, CURRENT_CONNECTION, PENDING_CONNECTION): - obs._value = None + 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) + -class _DummySettingsApp: - def __init__(self): - self.settings = _DummySettings() - self.icon_registry_16 = _DummyIconRegistry() +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: @@ -106,101 +180,358 @@ def _rename_current_selection(dialog: ConnectionsManager, target_name: str) -> N dialog._on_item_renamed(obj) -def _prepare_dialog(show: bool = False) -> ConnectionsManager: +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)) - if show: - dialog.Show() - pump_ui() + dialog.Show() + dialog.Raise() + dialog.SetFocus() + pump_ui(10) return dialog -def test_scenario_connection_dialog_configured(refresh_screenshots, scenario_environment): - dialog = _prepare_dialog(show=refresh_screenshots) +def test_scenario_a_connection_dialog_root_flow(scenario_environment): + dialog = _prepare_dialog() try: - dialog.on_create_directory(None) - pump_ui() - _rename_current_selection(dialog, "Scenario Directory") - pump_ui() - - directory = CURRENT_DIRECTORY() - assert isinstance(directory, ConnectionDirectory) + _clear_tree_selection(dialog) - dialog.on_create(None) + _click_button_with_simulator(dialog.btn_create) pump_ui() - connection = CURRENT_CONNECTION() - assert connection is not None - connection.name = "Scenario Connection" - connection.configuration = connection.configuration._replace( - hostname="localhost", - username="root", - password="root", - port=3306, - ) - PENDING_CONNECTION(connection) + 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() - saved = dialog.on_save(None) - assert saved is True - pump_ui() + _select_directory_by_name(dialog, "Scenario Root Directory") + _click_button_with_simulator(dialog.btn_delete) + pump_ui(10) - refreshed_connection = CURRENT_CONNECTION() - assert refreshed_connection is not None - refreshed_connection.name = "Scenario Connection Renamed" - dialog._on_item_renamed(refreshed_connection) + 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) - if refresh_screenshots: - capture_window_screenshot( - dialog, - Path("screenshot") / "connection_dialog_configured.png", - ) - - pump_ui() - - assert CURRENT_CONNECTION().name == "Scenario Connection Renamed" + 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_connection_dialog_tree_state(refresh_screenshots, scenario_environment): - dialog = _prepare_dialog(show=refresh_screenshots) +def test_scenario_b_connection_dialog_nested_flow(refresh_screenshots, scenario_environment): + dialog = _prepare_dialog() try: - dialog.on_create_directory(None) - pump_ui() - _rename_current_selection(dialog, "Explorer Scenario Directory") + _click_button_with_simulator(dialog.btn_create_directory) + pump_ui(10) + _rename_current_selection(dialog, "Scenario Nested Directory") pump_ui() - dialog.on_create(None) + 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 - connection.name = "Explorer Scenario Connection" - connection.configuration = connection.configuration._replace( - hostname="localhost", - username="root", - password="root", - ) - PENDING_CONNECTION(connection) + _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() - saved = dialog.on_save(None) + _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_explorer_state.png", + 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() - item = dialog.connections_tree_ctrl.GetSelection() - assert item.IsOk() + 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/uv.lock b/uv.lock index 6ed37a4..e7c64f6 100644 --- a/uv.lock +++ b/uv.lock @@ -290,6 +290,17 @@ 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 = "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" @@ -382,7 +393,10 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "mypy" }, + { name = "pillow" }, { name = "pre-commit" }, + { name = "pyautogui" }, + { name = "pyscreeze" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-mock" }, @@ -399,10 +413,13 @@ requires-dist = [ { name = "babel", specifier = ">=2.18.0" }, { 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 +435,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 +543,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 +568,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 +586,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 +604,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 +739,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" @@ -648,6 +802,15 @@ 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 = "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 = "sqlglot" version = "29.0.1" From 0c193bad68efc7c4c68564f5f120330b51a202d0 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 4 May 2026 16:23:39 +0200 Subject: [PATCH 49/93] i18n: update translation catalogs --- locale/de_DE/LC_MESSAGES/petersql.po | 771 ++++++++++++++++----------- locale/en_US/LC_MESSAGES/petersql.po | 752 +++++++++++++++----------- locale/es_ES/LC_MESSAGES/petersql.po | 771 ++++++++++++++++----------- locale/fr_FR/LC_MESSAGES/petersql.po | 771 ++++++++++++++++----------- locale/it_IT/LC_MESSAGES/petersql.po | 771 ++++++++++++++++----------- locale/petersql.pot | 739 ++++++++++++++----------- 6 files changed, 2738 insertions(+), 1837 deletions(-) diff --git a/locale/de_DE/LC_MESSAGES/petersql.po b/locale/de_DE/LC_MESSAGES/petersql.po index 22cec8b..30bcd7d 100644 --- a/locale/de_DE/LC_MESSAGES/petersql.po +++ b/locale/de_DE/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-23 10:07+0100\n" +"POT-Creation-Date: 2026-05-02 16:08+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: de_DE\n" @@ -47,75 +47,81 @@ 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:535 +msgid "This connection is read-only." +msgstr "" + +#: structures/engines/mariadb/context.py:616 +#: structures/engines/mysql/context.py:627 +#: structures/engines/postgresql/context.py:685 +#: structures/engines/sqlite/context.py:552 #, 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:644 +#: structures/engines/mysql/context.py:655 +#: structures/engines/postgresql/context.py:710 +#: structures/engines/sqlite/context.py:576 #, 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:662 +#: structures/engines/mysql/context.py:673 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:594 #, 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:702 +#: structures/engines/mysql/context.py:711 +#: structures/engines/postgresql/context.py:768 +#: structures/engines/sqlite/context.py:634 #, 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:735 +#: structures/engines/mysql/context.py:742 +#: structures/engines/postgresql/context.py:798 +#: structures/engines/sqlite/context.py:662 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:781 +#: structures/engines/mariadb/context.py:788 #, 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/dialogs/connections/view.py:417 +#: windows/dialogs/connections/view.py:764 windows/main/controller.py:1212 #: windows/views.py:33 msgid "Connection" msgstr "Verbindung" -#: windows/components/dataview.py:113 windows/components/dataview.py:225 -#: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py: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/main/database/procedure.py:129 windows/views.py:47 +#: windows/views.py:97 windows/views.py:1016 windows/views.py:1328 +#: windows/views.py:1361 windows/views.py:1385 windows/views.py:1409 +#: windows/views.py:1433 windows/views.py:1457 windows/views.py:1574 +#: windows/views.py:1954 windows/views.py:3079 msgid "Name" msgstr "Name" -#: windows/views.py:48 windows/views.py:428 +#: windows/views.py:48 windows/views.py:438 msgid "Last connection" msgstr "Letzte Verbindung" -#: windows/dialogs/connections/view.py:653 windows/views.py:61 +#: windows/dialogs/connections/view.py:655 windows/views.py:61 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:211 +#: windows/dialogs/connections/view.py:615 windows/views.py:65 msgid "New connection" msgstr "Neue Verbindung" @@ -129,14 +135,15 @@ msgstr "Name" msgid "Clone connection" msgstr "Neue Verbindung" -#: 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/main/database/procedure.py:183 windows/views.py:81 +#: windows/views.py:613 windows/views.py:1488 windows/views.py:1896 +#: windows/views.py:2155 windows/views.py:2932 windows/views.py:2981 +#: windows/views.py:3215 windows/views.py:3247 msgid "Delete" msgstr "Löschen" -#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 -#: windows/views.py:2876 +#: windows/views.py:111 windows/views.py:1333 windows/views.py:1629 +#: windows/views.py:2844 windows/views.py:3134 msgid "Engine" msgstr "Engine" @@ -148,7 +155,7 @@ msgstr "Host + Port" msgid "Username" msgstr "Benutzername" -#: windows/views.py:161 windows/views.py:1132 +#: windows/views.py:161 windows/views.py:1142 msgid "Password" msgstr "Passwort" @@ -162,534 +169,631 @@ msgid "Use TLS" msgstr "TLS verwenden" #: windows/views.py:203 +msgid "Mark read only" +msgstr "" + +#: windows/views.py:214 msgid "Use SSH tunnel" msgstr "SSH-Tunnel verwenden" -#: windows/views.py:214 +#: windows/views.py:225 msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 +#: windows/views.py:244 msgid "Filename" msgstr "Dateiname" -#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 +#: windows/views.py:249 windows/views.py:368 windows/views.py:3373 msgid "Select a file" msgstr "Datei auswählen" -#: windows/views.py:238 windows/views.py:358 +#: windows/views.py:249 windows/views.py:368 #, fuzzy 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:266 windows/views.py:1335 windows/views.py:1587 +#: windows/views.py:2842 windows/views.py:3092 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:766 windows/views.py:280 windows/views.py:740 +#: windows/views.py:894 msgid "Settings" msgstr "Einstellungen" -#: windows/views.py:278 +#: windows/views.py:288 msgid "SSH executable" msgstr "SSH-Executable" -#: windows/views.py:283 +#: windows/views.py:293 msgid "ssh" msgstr "ssh" -#: windows/views.py:291 +#: windows/views.py:301 msgid "SSH host + port" msgstr "SSH-Host + Port" -#: windows/views.py:303 +#: windows/views.py:313 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:322 msgid "SSH username" msgstr "SSH-Benutzername" -#: windows/views.py:325 +#: windows/views.py:335 msgid "SSH password" msgstr "SSH-Passwort" -#: windows/views.py:338 +#: windows/views.py:348 msgid "Local port" msgstr "Lokaler Port" -#: windows/views.py:344 +#: windows/views.py:354 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:363 msgid "Identity file" msgstr "Identitätsdatei" -#: windows/views.py:369 +#: windows/views.py:379 #, fuzzy msgid "Remote host + port" msgstr "Host + Port" -#: windows/views.py:381 +#: windows/views.py:391 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:400 msgid "SSH extra args" msgstr "" -#: windows/views.py:405 +#: windows/views.py:415 msgid "SSH Tunnel" msgstr "SSH-Tunnel" -#: windows/views.py:411 windows/views.py:1341 +#: windows/views.py:421 windows/views.py:1331 windows/views.py:2846 msgid "Created at" msgstr "Erstellt am" -#: windows/views.py:445 +#: windows/views.py:455 msgid "Successful connections" msgstr "Erfolgreiche Verbindungen" -#: windows/views.py:462 +#: windows/views.py:472 #, fuzzy msgid "Last successful connection" msgstr "Erfolgreiche Verbindungen" -#: windows/views.py:479 +#: windows/views.py:489 msgid "Unsuccessful connections" msgstr "Erfolglose Verbindungen" -#: windows/views.py:496 +#: windows/views.py:506 msgid "Last failure reason" msgstr "" -#: windows/views.py:513 +#: windows/views.py:523 #, fuzzy msgid "Total connection attempts" msgstr "Letzte Verbindung" -#: windows/views.py:530 +#: windows/views.py:540 #, fuzzy msgid "Average connection time (ms)" msgstr "Wiederverbindung fehlgeschlagen:" -#: windows/views.py:547 +#: windows/views.py:557 #, fuzzy msgid "Most recent connection duration" msgstr "Verbindungsmanager öffnen" -#: windows/views.py:566 +#: windows/views.py:576 msgid "Statistics" msgstr "Statistiken" -#: windows/views.py:584 windows/views.py:1730 +#: windows/views.py:594 windows/views.py:1853 msgid "Create" msgstr "Erstellen" -#: windows/views.py:588 +#: windows/views.py:598 #, fuzzy msgid "Create connection" msgstr "Letzte Verbindung" -#: windows/views.py:591 +#: windows/views.py:601 #, fuzzy 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/main/database/procedure.py:184 windows/views.py:630 +#: windows/views.py:854 windows/views.py:1483 windows/views.py:1899 +#: windows/views.py:2160 windows/views.py:2216 windows/views.py:2908 +#: windows/views.py:3250 windows/views.py:3387 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:323 windows/main/database/procedure.py:185 +#: windows/views.py:635 windows/views.py:2165 windows/views.py:2358 +#: windows/views.py:3255 windows/views.py:3393 msgid "Save" msgstr "Speichern" -#: windows/views.py:632 +#: windows/views.py:642 msgid "Test" msgstr "Testen" -#: windows/views.py:639 +#: windows/views.py:649 msgid "Connect" msgstr "Verbinden" -#: windows/views.py:742 +#: windows/main/database/procedure.py:162 windows/views.py:752 msgid "Language" msgstr "Sprache" -#: windows/views.py:747 +#: windows/views.py:757 msgid "English" msgstr "Englisch" -#: windows/views.py:747 +#: windows/views.py:757 msgid "Italian" msgstr "Italienisch" -#: windows/views.py:747 +#: windows/views.py:757 msgid "French" msgstr "Französisch" -#: windows/views.py:759 +#: windows/views.py:769 msgid "Locale" msgstr "Lokale" -#: windows/views.py:780 +#: windows/views.py:790 #, fuzzy msgid "Column content" msgstr "Neue Verbindung" -#: windows/views.py:790 +#: windows/views.py:800 msgid "Syntax" msgstr "Syntax" -#: windows/views.py:847 +#: windows/views.py:857 msgid "Ok" msgstr "Ok" -#: windows/views.py:878 +#: windows/views.py:888 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:887 +#: windows/views.py:897 msgid "File" msgstr "Datei" -#: windows/views.py:890 +#: windows/views.py:900 msgid "About" msgstr "Über" -#: windows/views.py:893 +#: windows/views.py:903 msgid "Help" msgstr "Hilfe" -#: windows/views.py:898 +#: windows/views.py:908 msgid "Open connection manager" msgstr "Verbindungsmanager öffnen" -#: windows/views.py:902 +#: windows/views.py:912 msgid "Disconnect from server" msgstr "Vom Server trennen" -#: windows/views.py:904 +#: windows/views.py:914 msgid "tool" msgstr "Werkzeug" -#: windows/views.py:904 +#: windows/views.py:914 windows/views.py:2196 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:918 windows/views.py:920 windows/views.py:1877 +#: windows/views.py:2200 windows/views.py:2344 msgid "Add" msgstr "Hinzufügen" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 +#: windows/views.py:954 windows/views.py:958 windows/views.py:2322 msgid "MyMenuItem" msgstr "MeinMenüElement" -#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 +#: windows/views.py:961 windows/views.py:1927 windows/views.py:3278 msgid "MyMenu" msgstr "MeinMenü" -#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 -#: windows/views.py:1402 +#: windows/views.py:976 windows/views.py:1511 windows/views.py:1518 +#: windows/views.py:1525 msgid "MyLabel" msgstr "MeinLabel" -#: windows/views.py:972 +#: windows/views.py:982 msgid "Databases" msgstr "Datenbanken" -#: windows/views.py:973 windows/views.py:1340 +#: windows/views.py:983 windows/views.py:1330 windows/views.py:2847 msgid "Size" msgstr "Größe" -#: windows/views.py:974 +#: windows/views.py:984 msgid "Elements" msgstr "Elemente" -#: windows/views.py:975 +#: windows/views.py:985 msgid "Modified at" msgstr "Geändert am" -#: windows/views.py:976 +#: windows/views.py:986 windows/views.py:1345 msgid "Tables" msgstr "Tabellen" -#: windows/views.py:983 +#: windows/views.py:993 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:1039 +#: windows/views.py:1334 windows/views.py:2843 msgid "Collation" msgstr "Sortierung" -#: windows/views.py:1058 +#: windows/views.py:1068 msgid "Encryption" msgstr "" -#: windows/views.py:1070 +#: windows/views.py:1080 msgid "Read Only" msgstr "" -#: windows/views.py:1087 +#: windows/views.py:1097 #, fuzzy msgid "Tablespace" msgstr "Tabellen" -#: windows/views.py:1108 +#: windows/views.py:1118 #, fuzzy msgid "Connection limit" msgstr "Verbindung verloren" -#: windows/views.py:1151 +#: windows/views.py:1161 #, fuzzy msgid "Profile" msgstr "Datei" -#: windows/views.py:1177 +#: windows/views.py:1187 #, fuzzy msgid "Default tablespace" msgstr "Tabelle löschen" -#: windows/views.py:1198 +#: windows/views.py:1208 #, fuzzy msgid "Temporary tablespace" msgstr "Temporär" -#: windows/views.py:1224 +#: windows/views.py:1234 msgid "Quota" msgstr "" -#: windows/views.py:1243 +#: windows/views.py:1253 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1260 +#: windows/views.py:1270 msgid "Account status" msgstr "" -#: windows/views.py:1281 +#: windows/views.py:1291 #, fuzzy msgid "Password expire" msgstr "Passwort" -#: windows/views.py:1302 -msgid "Table:" -msgstr "Tabelle:" +#: windows/views.py:1315 +#, fuzzy +msgid "Add new table" +msgstr "Tabelle löschen" -#: 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:1317 +#, fuzzy +msgid "Clone table" +msgstr "Tabelle löschen" -#: windows/views.py:1315 -msgid "Clone" -msgstr "Klonen" +#: windows/main/controller.py:1633 windows/views.py:1319 +msgid "Delete table" +msgstr "Tabelle löschen" -#: windows/views.py:1339 +#: windows/views.py:1329 msgid "Rows" msgstr "Zeilen" -#: windows/views.py:1342 +#: windows/views.py:1332 windows/views.py:2845 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:1350 +msgid "Add new view" +msgstr "" + +#: windows/views.py:1352 windows/views.py:1376 +#, fuzzy +msgid "Clone view" +msgstr "Klonen" + +#: windows/views.py:1354 +#, fuzzy +msgid "Delete view" +msgstr "Löschen" + +#: windows/views.py:1362 windows/views.py:1386 windows/views.py:1410 +#: windows/views.py:1434 windows/views.py:1458 +#, fuzzy +msgid "Definition" +msgstr "Bedingung" + +#: windows/views.py:1369 windows/views.py:2177 +msgid "Views" +msgstr "Ansichten" + +#: windows/views.py:1374 +msgid "Add new procedure" +msgstr "" + +#: windows/views.py:1376 +msgid "Clone procedure" +msgstr "" + +#: windows/views.py:1378 +#, fuzzy +msgid "Delete procedure" +msgstr "Datensatz löschen" + +#: windows/views.py:1393 +msgid "Procedures" +msgstr "" + +#: windows/views.py:1398 +#, fuzzy +msgid "Add new function" +msgstr "Neue Verbindung" + +#: windows/views.py:1400 +#, fuzzy +msgid "Clone function" +msgstr "Neue Verbindung" + +#: windows/views.py:1402 +#, fuzzy +msgid "Delete function" +msgstr "Datensatz löschen" + +#: windows/views.py:1417 +#, fuzzy +msgid "Functions" +msgstr "Verbindung" + +#: windows/views.py:1422 +#, fuzzy +msgid "Add new trigger" +msgstr "Trigger" + +#: windows/views.py:1424 +#, fuzzy +msgid "Clone trigger" +msgstr "Trigger" + +#: windows/views.py:1426 +#, fuzzy +msgid "Delete trigger" +msgstr "Datensatz löschen" + +#: windows/views.py:1441 windows/views.py:2185 +msgid "Triggers" +msgstr "Trigger" + +#: windows/views.py:1446 +msgid "Add new event" +msgstr "" + +#: windows/views.py:1448 +#, fuzzy +msgid "Clone event" +msgstr "Klonen" + +#: windows/views.py:1450 +#, fuzzy +msgid "Delete event" +msgstr "Tabelle löschen" + +#: windows/views.py:1465 +#, fuzzy +msgid "Events" +msgstr "Elemente" + +#: windows/views.py:1493 windows/views.py:1904 windows/views.py:2214 +#: windows/views.py:2296 windows/views.py:2915 msgid "Apply" msgstr "Anwenden" -#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 -#: windows/views.py:2909 +#: windows/main/database/procedure.py:122 windows/views.py:1505 +#: windows/views.py:1684 windows/views.py:2114 windows/views.py:3167 msgid "Options" msgstr "Optionen" -#: windows/views.py:1413 +#: windows/views.py:1536 msgid "Diagram" msgstr "Diagramm" -#: windows/views.py:1424 +#: windows/views.py:1547 msgid "Database" msgstr "Datenbank" -#: windows/views.py:1479 windows/views.py:2849 +#: windows/views.py:1602 windows/views.py:3107 msgid "Base" msgstr "Basis" -#: windows/views.py:1493 windows/views.py:2863 +#: windows/views.py:1616 windows/views.py:3121 msgid "Auto Increment" msgstr "Auto Inkrement" -#: windows/views.py:1521 windows/views.py:2891 +#: windows/views.py:1644 windows/views.py:3149 msgid "Default Collation" msgstr "Standard-Sortierung" -#: windows/views.py:1531 +#: windows/views.py:1654 msgid "Convert data" msgstr "" -#: windows/views.py:1539 +#: windows/views.py:1662 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:1696 windows/views.py:1737 windows/views.py:1781 +#: windows/views.py:1879 windows/views.py:2204 msgid "Remove" msgstr "Entfernen" -#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 +#: windows/views.py:1703 windows/views.py:1744 windows/views.py:1788 msgid "Clear" msgstr "Löschen" -#: windows/views.py:1595 windows/views.py:2923 +#: windows/views.py:1718 windows/views.py:3181 msgid "Indexes" msgstr "Indizes" -#: windows/views.py:1639 +#: windows/views.py:1732 windows/views.py:1776 windows/views.py:2927 +#: windows/views.py:2969 windows/views.py:3210 +msgid "Insert" +msgstr "Einfügen" + +#: windows/views.py:1762 msgid "Foreign Keys" msgstr "Fremdschlüssel" -#: windows/views.py:1683 +#: windows/views.py:1806 msgid "Checks" msgstr "Prüfungen" -#: windows/views.py:1750 windows/views.py:2944 +#: windows/views.py:1873 windows/views.py:3202 msgid "Columns:" msgstr "Spalten:" -#: windows/views.py:1760 +#: windows/views.py:1883 #, fuzzy msgid "Move Up" msgstr "Nach oben bewegen\tCTRL+UP" -#: windows/views.py:1762 +#: windows/views.py:1885 #, fuzzy 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:1917 windows/views.py:1924 windows/views.py:3268 +#: windows/views.py:3275 msgid "Add Index" msgstr "Index hinzufügen" -#: windows/views.py:1798 windows/views.py:3014 +#: windows/views.py:1921 windows/views.py:3272 msgid "Add PrimaryKey" msgstr "Primärschlüssel hinzufügen" -#: windows/views.py:1815 +#: windows/views.py:1938 msgid "Table" msgstr "Tabelle" -#: windows/views.py:1851 +#: windows/main/database/procedure.py:140 windows/views.py:1974 #, fuzzy msgid "Definer" msgstr "Einfügen" -#: windows/views.py:1871 +#: windows/views.py:1994 msgid "Schema" msgstr "" -#: windows/views.py:1897 +#: windows/views.py:2020 msgid "SQL security" msgstr "" -#: windows/views.py:1904 +#: windows/views.py:2027 #, fuzzy msgid "DEFINER" msgstr "Einfügen" -#: windows/views.py:1904 +#: windows/views.py:2027 #, fuzzy msgid "INVOKER" msgstr "Einfügen" -#: windows/views.py:1916 +#: windows/views.py:2039 msgid "Algorithm" msgstr "" -#: windows/views.py:1918 +#: windows/views.py:2041 #, fuzzy msgid "UNDEFINED" msgstr "Ohne Vorzeichen" -#: windows/views.py:1921 +#: windows/views.py:2044 msgid "MERGE" msgstr "" -#: windows/views.py:1924 +#: windows/views.py:2047 #, fuzzy msgid "TEMPTABLE" msgstr "Tabelle" -#: windows/views.py:1934 +#: windows/views.py:2057 msgid "View constraint" msgstr "" -#: windows/views.py:1936 +#: windows/views.py:2059 #, fuzzy msgid "None" msgstr "Klonen" -#: windows/views.py:1939 +#: windows/views.py:2062 #, fuzzy msgid "LOCAL" msgstr "Lokale" -#: windows/views.py:1942 +#: windows/views.py:2065 #, fuzzy msgid "CASCADE" msgstr "Abbrechen" -#: windows/views.py:1945 +#: windows/views.py:2068 #, fuzzy msgid "CHECK ONLY" msgstr "Prüfen" -#: windows/views.py:1948 +#: windows/views.py:2071 msgid "READ ONLY" msgstr "" -#: windows/views.py:1960 +#: windows/views.py:2083 msgid "Force" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:2095 msgid "Security barrier" msgstr "" -#: windows/views.py:2054 -msgid "Views" -msgstr "Ansichten" - -#: windows/views.py:2062 -msgid "Triggers" -msgstr "Trigger" - -#: windows/views.py:2073 -#, fuzzy -msgid "Refrsh" -msgstr "Aktualisieren" - -#: windows/views.py:2079 +#: windows/views.py:2202 #, fuzzy msgid "Duplicate" msgstr "Datensatz duplizieren" -#: windows/views.py:2085 +#: windows/views.py:2208 msgid "Apply changes automatically" msgstr "Änderungen automatisch anwenden" -#: windows/views.py:2087 windows/views.py:2088 +#: windows/views.py:2210 windows/views.py:2211 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -697,122 +801,126 @@ msgstr "" "Wenn aktiviert, werden Tabellenbearbeitungen sofort angewendet, ohne auf " "Anwenden oder Abbrechen zu drücken" -#: windows/views.py:2101 +#: windows/views.py:2224 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" -#: windows/views.py:2109 +#: windows/views.py:2232 #, fuzzy msgid "First" msgstr "Filter" -#: windows/views.py:2127 +#: windows/views.py:2250 msgid "Last" msgstr "" -#: windows/views.py:2136 +#: windows/views.py:2259 msgid "Filters" msgstr "Filter" -#: windows/views.py:2176 +#: windows/views.py:2299 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2196 +#: windows/views.py:2319 msgid "Insert row" msgstr "Zeile einfügen" -#: windows/views.py:2204 +#: windows/views.py:2327 msgid "Data" msgstr "Daten" -#: windows/main/controller.py:278 windows/main/controller.py:287 -#: windows/main/controller.py:288 windows/views.py:2221 +#: windows/main/controller.py:318 windows/views.py:2344 #, fuzzy msgid "New query" msgstr "Abfrage" -#: windows/views.py:2223 windows/views.py:2660 +#: windows/views.py:2346 windows/views.py:2866 msgid "Close" msgstr "Schließen" -#: windows/main/controller.py:279 windows/main/controller.py:289 -#: windows/main/controller.py:290 windows/views.py:2223 +#: windows/main/controller.py:319 windows/views.py:2346 #, fuzzy msgid "Close query" msgstr "Abfrage" -#: windows/views.py:2227 +#: windows/views.py:2350 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:320 windows/views.py:2350 #, fuzzy msgid "Execute" msgstr "SSH-Executable" -#: windows/views.py:2229 +#: windows/views.py:2352 msgid "Run all" msgstr "" -#: windows/main/controller.py:295 windows/views.py:2229 +#: windows/views.py:2352 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:322 windows/views.py:2354 msgid "Stop" msgstr "" -#: windows/views.py:2287 +#: windows/views.py:2419 msgid "a page" msgstr "" -#: windows/views.py:2315 +#: windows/views.py:2469 msgid "Query" msgstr "Abfrage" -#: windows/views.py:2626 +#: windows/views.py:2820 #, fuzzy msgid "Character set" msgstr "Erstellt am" -#: windows/views.py:2644 windows/views.py:2663 +#: windows/views.py:2850 windows/views.py:2869 msgid "New" msgstr "Neu" -#: windows/views.py:2683 +#: windows/views.py:2889 msgid "Insert record" msgstr "Datensatz einfügen" -#: windows/views.py:2688 +#: windows/views.py:2894 msgid "Duplicate record" msgstr "Datensatz duplizieren" -#: windows/views.py:2695 +#: windows/views.py:2901 msgid "Delete record" msgstr "Datensatz löschen" -#: windows/views.py:2733 windows/views.py:2964 +#: windows/views.py:2939 windows/views.py:3222 msgid "Up" msgstr "Hoch" -#: windows/views.py:2740 windows/views.py:2971 +#: windows/views.py:2946 windows/views.py:3229 msgid "Down" msgstr "Runter" -#: windows/views.py:3100 +#: windows/views.py:2961 +msgid "Table:" +msgstr "Tabelle:" + +#: windows/views.py:2974 +msgid "Clone" +msgstr "Klonen" + +#: windows/views.py:3358 msgid "Save Starments" msgstr "" -#: windows/views.py:3108 +#: windows/views.py:3366 #, fuzzy msgid "Location" msgstr "Sortierung" -#: windows/views.py:3115 +#: windows/views.py:3373 msgid "*.sql" msgstr "" @@ -836,7 +944,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 +956,75 @@ msgstr "Ohne Vorzeichen" msgid "Zerofill" msgstr "Nullfüllung" -#: windows/components/dataview.py:109 +#: windows/components/dataview.py:111 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" -#: windows/components/dataview.py:161 +#: windows/components/dataview.py:175 msgid "Remove column\tCTRL+DEL" msgstr "Spalte entfernen\tCTRL+DEL" -#: windows/components/dataview.py:169 +#: windows/components/dataview.py:183 msgid "Move up\tCTRL+UP" msgstr "Nach oben bewegen\tCTRL+UP" -#: windows/components/dataview.py:176 +#: windows/components/dataview.py:190 msgid "Move down\tCTRL+D" msgstr "Nach unten bewegen\tCTRL+D" -#: 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" @@ -936,164 +1044,166 @@ msgstr "AUTO INCREMENT" 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 "" -#: 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 "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 "" -#: 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:762 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:775 +#: windows/dialogs/connections/view.py:787 #, fuzzy, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "Verbindungsfehler" -#: windows/dialogs/connections/view.py:776 +#: windows/dialogs/connections/view.py:788 msgid "Connection error" msgstr "Verbindungsfehler" -#: windows/dialogs/connections/view.py:802 +#: windows/dialogs/connections/view.py:814 #, fuzzy, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" msgstr "Möchten Sie die Datensätze löschen?" -#: windows/dialogs/connections/view.py:805 -#: windows/dialogs/connections/view.py:822 +#: windows/dialogs/connections/view.py:817 +#: windows/dialogs/connections/view.py:834 msgid "Confirm delete" msgstr "Löschen bestätigen" -#: windows/dialogs/connections/view.py:819 +#: windows/dialogs/connections/view.py:831 #, fuzzy, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "Möchten Sie die Datensätze löschen?" -#: windows/main/controller.py:275 +#: windows/main/controller.py:315 #, python-brace-format msgid "{text} ({shortcut})" msgstr "" -#: windows/main/controller.py:281 windows/main/controller.py:294 +#: windows/main/controller.py:321 #, fuzzy msgid "Execute all" msgstr "SSH-Executable" -#: windows/main/controller.py:471 windows/main/controller.py:479 +#: windows/main/controller.py:440 windows/main/controller.py:448 #, fuzzy msgid "Query (1)" msgstr "Abfrage" -#: windows/main/controller.py:497 +#: windows/main/controller.py:467 #, python-brace-format msgid "Query ({query_number})" msgstr "" -#: windows/main/controller.py:530 +#: windows/main/controller.py:516 msgid "You have unsaved changes. Save before closing?" msgstr "" -#: windows/main/controller.py:531 +#: windows/main/controller.py:517 msgid "Unsaved query" msgstr "" -#: windows/main/controller.py:576 +#: windows/main/controller.py:562 #, fuzzy msgid "Save query" msgstr "Abfrage" -#: windows/main/controller.py:579 +#: windows/main/controller.py:565 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:593 windows/main/controller.py:624 +#: windows/main/controller.py:651 windows/main/database/list.py:119 +#: windows/main/database/procedure.py:294 +#: windows/main/database/procedure.py:320 windows/main/database/view.py:267 +#: windows/main/database/view.py:296 windows/main/query/controller.py:177 msgid "Error" msgstr "Fehler" -#: windows/main/controller.py:622 +#: windows/main/controller.py:631 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "" -#: windows/main/controller.py:647 +#: windows/main/controller.py:657 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "" -#: windows/main/controller.py:704 +#: windows/main/controller.py:719 msgid "days" msgstr "Tage" -#: windows/main/controller.py:705 +#: windows/main/controller.py:720 msgid "hours" msgstr "Stunden" -#: windows/main/controller.py:706 +#: windows/main/controller.py:721 msgid "minutes" msgstr "Minuten" -#: windows/main/controller.py:707 +#: windows/main/controller.py:722 msgid "seconds" msgstr "Sekunden" -#: windows/main/controller.py:715 +#: windows/main/controller.py:730 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Verwendeter Speicher: {used} ({percentage:.2%})" -#: windows/main/controller.py:751 +#: windows/main/controller.py:766 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:952 +#: windows/main/controller.py:990 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:954 +#: windows/main/controller.py:992 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:1119 +#: windows/main/controller.py:1214 msgid "Version" msgstr "Version" -#: windows/main/controller.py:1121 +#: windows/main/controller.py:1216 msgid "Uptime" msgstr "Betriebszeit" -#: windows/main/controller.py:1199 +#: windows/main/controller.py:1299 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:1232 +#: windows/main/controller.py:1332 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1103,99 +1213,132 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:1237 windows/main/controller.py:1258 +#: windows/main/controller.py:1337 windows/main/controller.py:1358 #, fuzzy msgid "Delete database" msgstr "Tabelle löschen" -#: windows/main/controller.py:1243 +#: windows/main/controller.py:1343 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:1244 +#: windows/main/controller.py:1344 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:1257 +#: windows/main/controller.py:1357 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:1272 +#: windows/main/controller.py:1372 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:1373 windows/main/database/procedure.py:283 +#: windows/main/database/procedure.py:314 windows/main/database/view.py:255 +#: windows/main/database/view.py:290 msgid "Success" msgstr "" -#: windows/main/controller.py:1392 +#: windows/main/controller.py:1604 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:1418 +#: windows/main/controller.py:1630 #, fuzzy, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "Möchten Sie die Datensätze löschen?" -#: windows/main/controller.py:1421 -msgid "Delete table" -msgstr "Tabelle löschen" - -#: windows/main/controller.py:1440 +#: windows/main/controller.py:1652 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1563 +#: windows/main/controller.py:1797 msgid "Do you want delete the records?" msgstr "Möchten Sie die Datensätze löschen?" -#: windows/main/database/list.py:69 +#: 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 +#: windows/main/database/list.py:108 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/procedure.py:114 +msgid "Procedure" +msgstr "" + +#: windows/main/database/procedure.py:151 +#, fuzzy +msgid "Parameters" +msgstr "PeterSQL" + +#: windows/main/database/procedure.py:282 +msgid "Procedure created successfully" +msgstr "" + +#: windows/main/database/procedure.py:282 +msgid "Procedure updated successfully" +msgstr "" + +#: windows/main/database/procedure.py:294 +#, python-brace-format +msgid "Error saving procedure: {}" +msgstr "" + +#: windows/main/database/procedure.py:305 +#, fuzzy, python-brace-format +msgid "Are you sure you want to delete procedure '{}'?" +msgstr "Möchten Sie die Datensätze löschen?" + +#: windows/main/database/procedure.py:306 windows/main/database/view.py:281 +#, fuzzy +msgid "Confirm Delete" +msgstr "Löschen bestätigen" + +#: windows/main/database/procedure.py:314 +msgid "Procedure deleted successfully" +msgstr "" + +#: windows/main/database/procedure.py:320 +#, python-brace-format +msgid "Error deleting procedure: {}" +msgstr "" + +#: windows/main/database/view.py:254 msgid "View created successfully" msgstr "" -#: windows/main/database/view.py:252 +#: windows/main/database/view.py:254 msgid "View updated successfully" msgstr "" -#: windows/main/database/view.py:256 +#: windows/main/database/view.py:267 #, python-brace-format msgid "Error saving view: {}" msgstr "" -#: windows/main/database/view.py:269 +#: windows/main/database/view.py:280 #, python-brace-format msgid "Are you sure you want to delete view '{}'?" msgstr "" -#: 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:290 msgid "View deleted successfully" msgstr "" -#: windows/main/database/view.py:282 +#: windows/main/database/view.py:296 #, python-brace-format msgid "Error deleting view: {}" msgstr "" @@ -1234,6 +1377,11 @@ msgstr "" msgid "No active database connection" msgstr "Neue Verbindung" +#: windows/main/query/history.py:55 +#, fuzzy +msgid "(empty query)" +msgstr "Abfrage" + #: windows/main/query/renderer.py:53 #, python-brace-format msgid "{affected_rows} rows affected" @@ -1274,6 +1422,10 @@ msgstr "" msgid "Error:" msgstr "Fehler" +#: windows/main/table/records.py:336 +msgid "Error saving records" +msgstr "" + #~ msgid "Created at:" #~ msgstr "" @@ -1409,3 +1561,6 @@ msgstr "Fehler" #~ msgid "Zero Fill" #~ msgstr "Nullfüllung" +#~ msgid "Refrsh" +#~ msgstr "Aktualisieren" + diff --git a/locale/en_US/LC_MESSAGES/petersql.po b/locale/en_US/LC_MESSAGES/petersql.po index 5e23444..afeb887 100644 --- a/locale/en_US/LC_MESSAGES/petersql.po +++ b/locale/en_US/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-23 10:07+0100\n" +"POT-Creation-Date: 2026-05-02 16:08+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: en_US\n" @@ -47,75 +47,81 @@ 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:535 +msgid "This connection is read-only." +msgstr "" + +#: structures/engines/mariadb/context.py:616 +#: structures/engines/mysql/context.py:627 +#: structures/engines/postgresql/context.py:685 +#: structures/engines/sqlite/context.py:552 #, 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:644 +#: structures/engines/mysql/context.py:655 +#: structures/engines/postgresql/context.py:710 +#: structures/engines/sqlite/context.py:576 #, 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:662 +#: structures/engines/mysql/context.py:673 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:594 #, 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:702 +#: structures/engines/mysql/context.py:711 +#: structures/engines/postgresql/context.py:768 +#: structures/engines/sqlite/context.py:634 #, 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:735 +#: structures/engines/mysql/context.py:742 +#: structures/engines/postgresql/context.py:798 +#: structures/engines/sqlite/context.py:662 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:781 +#: structures/engines/mariadb/context.py:788 #, 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/dialogs/connections/view.py:417 +#: windows/dialogs/connections/view.py:764 windows/main/controller.py:1212 #: windows/views.py:33 msgid "Connection" msgstr "Connection" -#: windows/components/dataview.py:113 windows/components/dataview.py:225 -#: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py: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/main/database/procedure.py:129 windows/views.py:47 +#: windows/views.py:97 windows/views.py:1016 windows/views.py:1328 +#: windows/views.py:1361 windows/views.py:1385 windows/views.py:1409 +#: windows/views.py:1433 windows/views.py:1457 windows/views.py:1574 +#: windows/views.py:1954 windows/views.py:3079 msgid "Name" msgstr "Name" -#: windows/views.py:48 windows/views.py:428 +#: windows/views.py:48 windows/views.py:438 msgid "Last connection" msgstr "Last connection" -#: windows/dialogs/connections/view.py:653 windows/views.py:61 +#: windows/dialogs/connections/view.py:655 windows/views.py:61 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:211 +#: windows/dialogs/connections/view.py:615 windows/views.py:65 msgid "New connection" msgstr "New connection" @@ -127,14 +133,15 @@ msgstr "Rename" 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/main/database/procedure.py:183 windows/views.py:81 +#: windows/views.py:613 windows/views.py:1488 windows/views.py:1896 +#: windows/views.py:2155 windows/views.py:2932 windows/views.py:2981 +#: windows/views.py:3215 windows/views.py:3247 msgid "Delete" msgstr "Delete" -#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 -#: windows/views.py:2876 +#: windows/views.py:111 windows/views.py:1333 windows/views.py:1629 +#: windows/views.py:2844 windows/views.py:3134 msgid "Engine" msgstr "Engine" @@ -146,7 +153,7 @@ msgstr "Host + port" msgid "Username" msgstr "Username" -#: windows/views.py:161 windows/views.py:1132 +#: windows/views.py:161 windows/views.py:1142 msgid "Password" msgstr "Password" @@ -160,625 +167,718 @@ msgid "Use TLS" msgstr "Use TLS" #: windows/views.py:203 +msgid "Mark read only" +msgstr "" + +#: windows/views.py:214 msgid "Use SSH tunnel" msgstr "Use SSH tunnel" -#: windows/views.py:214 +#: windows/views.py:225 msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 +#: windows/views.py:244 msgid "Filename" msgstr "Filename" -#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 +#: windows/views.py:249 windows/views.py:368 windows/views.py:3373 msgid "Select a file" msgstr "Select a file" -#: windows/views.py:238 windows/views.py:358 +#: windows/views.py:249 windows/views.py:368 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:266 windows/views.py:1335 windows/views.py:1587 +#: windows/views.py:2842 windows/views.py:3092 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:766 windows/views.py:280 windows/views.py:740 +#: windows/views.py:894 msgid "Settings" msgstr "Settings" -#: windows/views.py:278 +#: windows/views.py:288 msgid "SSH executable" msgstr "SSH executable" -#: windows/views.py:283 +#: windows/views.py:293 msgid "ssh" msgstr "ssh" -#: windows/views.py:291 +#: windows/views.py:301 msgid "SSH host + port" msgstr "SSH host + port" -#: windows/views.py:303 +#: windows/views.py:313 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:322 msgid "SSH username" msgstr "SSH username" -#: windows/views.py:325 +#: windows/views.py:335 msgid "SSH password" msgstr "SSH password" -#: windows/views.py:338 +#: windows/views.py:348 msgid "Local port" msgstr "Local port" -#: windows/views.py:344 +#: windows/views.py:354 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:363 msgid "Identity file" msgstr "Identity file" -#: windows/views.py:369 +#: windows/views.py:379 msgid "Remote host + port" msgstr "Remote host + port" -#: windows/views.py:381 +#: windows/views.py:391 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:400 msgid "SSH extra args" msgstr "" -#: windows/views.py:405 +#: windows/views.py:415 msgid "SSH Tunnel" msgstr "SSH Tunnel" -#: windows/views.py:411 windows/views.py:1341 +#: windows/views.py:421 windows/views.py:1331 windows/views.py:2846 msgid "Created at" msgstr "Created at" -#: windows/views.py:445 +#: windows/views.py:455 msgid "Successful connections" msgstr "" -#: windows/views.py:462 +#: windows/views.py:472 msgid "Last successful connection" msgstr "" -#: windows/views.py:479 +#: windows/views.py:489 msgid "Unsuccessful connections" msgstr "" -#: windows/views.py:496 +#: windows/views.py:506 msgid "Last failure reason" msgstr "" -#: windows/views.py:513 +#: windows/views.py:523 msgid "Total connection attempts" msgstr "" -#: windows/views.py:530 +#: windows/views.py:540 #, fuzzy msgid "Average connection time (ms)" msgstr "Connection" -#: windows/views.py:547 +#: windows/views.py:557 msgid "Most recent connection duration" msgstr "" -#: windows/views.py:566 +#: windows/views.py:576 msgid "Statistics" msgstr "" -#: windows/views.py:584 windows/views.py:1730 +#: windows/views.py:594 windows/views.py:1853 msgid "Create" msgstr "" -#: windows/views.py:588 +#: windows/views.py:598 msgid "Create connection" msgstr "" -#: windows/views.py:591 +#: windows/views.py:601 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/main/database/procedure.py:184 windows/views.py:630 +#: windows/views.py:854 windows/views.py:1483 windows/views.py:1899 +#: windows/views.py:2160 windows/views.py:2216 windows/views.py:2908 +#: windows/views.py:3250 windows/views.py:3387 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:323 windows/main/database/procedure.py:185 +#: windows/views.py:635 windows/views.py:2165 windows/views.py:2358 +#: windows/views.py:3255 windows/views.py:3393 msgid "Save" msgstr "" -#: windows/views.py:632 +#: windows/views.py:642 msgid "Test" msgstr "" -#: windows/views.py:639 +#: windows/views.py:649 msgid "Connect" msgstr "" -#: windows/views.py:742 +#: windows/main/database/procedure.py:162 windows/views.py:752 msgid "Language" msgstr "" -#: windows/views.py:747 +#: windows/views.py:757 msgid "English" msgstr "" -#: windows/views.py:747 +#: windows/views.py:757 msgid "Italian" msgstr "" -#: windows/views.py:747 +#: windows/views.py:757 msgid "French" msgstr "" -#: windows/views.py:759 +#: windows/views.py:769 msgid "Locale" msgstr "" -#: windows/views.py:780 +#: windows/views.py:790 #, fuzzy msgid "Column content" msgstr "Clone connection" -#: windows/views.py:790 +#: windows/views.py:800 msgid "Syntax" msgstr "" -#: windows/views.py:847 +#: windows/views.py:857 msgid "Ok" msgstr "" -#: windows/views.py:878 +#: windows/views.py:888 msgid "PeterSQL" msgstr "" -#: windows/views.py:887 +#: windows/views.py:897 msgid "File" msgstr "" -#: windows/views.py:890 +#: windows/views.py:900 msgid "About" msgstr "" -#: windows/views.py:893 +#: windows/views.py:903 msgid "Help" msgstr "" -#: windows/views.py:898 +#: windows/views.py:908 msgid "Open connection manager" msgstr "" -#: windows/views.py:902 +#: windows/views.py:912 msgid "Disconnect from server" msgstr "" -#: windows/views.py:904 +#: windows/views.py:914 msgid "tool" msgstr "" -#: windows/views.py:904 +#: windows/views.py:914 windows/views.py:2196 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:918 windows/views.py:920 windows/views.py:1877 +#: windows/views.py:2200 windows/views.py:2344 msgid "Add" msgstr "" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 +#: windows/views.py:954 windows/views.py:958 windows/views.py:2322 msgid "MyMenuItem" msgstr "" -#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 +#: windows/views.py:961 windows/views.py:1927 windows/views.py:3278 msgid "MyMenu" msgstr "" -#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 -#: windows/views.py:1402 +#: windows/views.py:976 windows/views.py:1511 windows/views.py:1518 +#: windows/views.py:1525 msgid "MyLabel" msgstr "" -#: windows/views.py:972 +#: windows/views.py:982 msgid "Databases" msgstr "" -#: windows/views.py:973 windows/views.py:1340 +#: windows/views.py:983 windows/views.py:1330 windows/views.py:2847 msgid "Size" msgstr "" -#: windows/views.py:974 +#: windows/views.py:984 msgid "Elements" msgstr "" -#: windows/views.py:975 +#: windows/views.py:985 msgid "Modified at" msgstr "" -#: windows/views.py:976 +#: windows/views.py:986 windows/views.py:1345 msgid "Tables" msgstr "" -#: windows/views.py:983 +#: windows/views.py:993 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:1039 +#: windows/views.py:1334 windows/views.py:2843 msgid "Collation" msgstr "" -#: windows/views.py:1058 +#: windows/views.py:1068 msgid "Encryption" msgstr "" -#: windows/views.py:1070 +#: windows/views.py:1080 msgid "Read Only" msgstr "" -#: windows/views.py:1087 +#: windows/views.py:1097 msgid "Tablespace" msgstr "" -#: windows/views.py:1108 +#: windows/views.py:1118 msgid "Connection limit" msgstr "" -#: windows/views.py:1151 +#: windows/views.py:1161 msgid "Profile" msgstr "" -#: windows/views.py:1177 +#: windows/views.py:1187 msgid "Default tablespace" msgstr "" -#: windows/views.py:1198 +#: windows/views.py:1208 msgid "Temporary tablespace" msgstr "" -#: windows/views.py:1224 +#: windows/views.py:1234 msgid "Quota" msgstr "" -#: windows/views.py:1243 +#: windows/views.py:1253 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1260 +#: windows/views.py:1270 msgid "Account status" msgstr "" -#: windows/views.py:1281 +#: windows/views.py:1291 msgid "Password expire" msgstr "" -#: windows/views.py:1302 -msgid "Table:" +#: windows/views.py:1315 +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:1317 +msgid "Clone table" msgstr "" -#: windows/views.py:1315 -msgid "Clone" +#: windows/main/controller.py:1633 windows/views.py:1319 +msgid "Delete table" msgstr "" -#: windows/views.py:1339 +#: windows/views.py:1329 msgid "Rows" msgstr "" -#: windows/views.py:1342 +#: windows/views.py:1332 windows/views.py:2845 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:1350 +msgid "Add new view" +msgstr "" + +#: windows/views.py:1352 windows/views.py:1376 +msgid "Clone view" +msgstr "" + +#: windows/views.py:1354 +#, fuzzy +msgid "Delete view" +msgstr "Delete" + +#: windows/views.py:1362 windows/views.py:1386 windows/views.py:1410 +#: windows/views.py:1434 windows/views.py:1458 +msgid "Definition" +msgstr "" + +#: windows/views.py:1369 windows/views.py:2177 +msgid "Views" +msgstr "" + +#: windows/views.py:1374 +msgid "Add new procedure" +msgstr "" + +#: windows/views.py:1376 +msgid "Clone procedure" +msgstr "" + +#: windows/views.py:1378 +msgid "Delete procedure" +msgstr "" + +#: windows/views.py:1393 +msgid "Procedures" +msgstr "" + +#: windows/views.py:1398 +#, fuzzy +msgid "Add new function" +msgstr "New connection" + +#: windows/views.py:1400 +#, fuzzy +msgid "Clone function" +msgstr "Clone connection" + +#: windows/views.py:1402 +#, fuzzy +msgid "Delete function" +msgstr "Last connection" + +#: windows/views.py:1417 +#, fuzzy +msgid "Functions" +msgstr "Connection" + +#: windows/views.py:1422 +msgid "Add new trigger" +msgstr "" + +#: windows/views.py:1424 +msgid "Clone trigger" +msgstr "" + +#: windows/views.py:1426 +#, fuzzy +msgid "Delete trigger" +msgstr "Delete" + +#: windows/views.py:1441 windows/views.py:2185 +msgid "Triggers" +msgstr "" + +#: windows/views.py:1446 +msgid "Add new event" +msgstr "" + +#: windows/views.py:1448 +msgid "Clone event" +msgstr "" + +#: windows/views.py:1450 +#, fuzzy +msgid "Delete event" +msgstr "Delete" + +#: windows/views.py:1465 +msgid "Events" +msgstr "" + +#: windows/views.py:1493 windows/views.py:1904 windows/views.py:2214 +#: windows/views.py:2296 windows/views.py:2915 msgid "Apply" msgstr "" -#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 -#: windows/views.py:2909 +#: windows/main/database/procedure.py:122 windows/views.py:1505 +#: windows/views.py:1684 windows/views.py:2114 windows/views.py:3167 msgid "Options" msgstr "" -#: windows/views.py:1413 +#: windows/views.py:1536 msgid "Diagram" msgstr "" -#: windows/views.py:1424 +#: windows/views.py:1547 msgid "Database" msgstr "" -#: windows/views.py:1479 windows/views.py:2849 +#: windows/views.py:1602 windows/views.py:3107 msgid "Base" msgstr "" -#: windows/views.py:1493 windows/views.py:2863 +#: windows/views.py:1616 windows/views.py:3121 msgid "Auto Increment" msgstr "" -#: windows/views.py:1521 windows/views.py:2891 +#: windows/views.py:1644 windows/views.py:3149 msgid "Default Collation" msgstr "" -#: windows/views.py:1531 +#: windows/views.py:1654 msgid "Convert data" msgstr "" -#: windows/views.py:1539 +#: windows/views.py:1662 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:1696 windows/views.py:1737 windows/views.py:1781 +#: windows/views.py:1879 windows/views.py:2204 msgid "Remove" msgstr "" -#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 +#: windows/views.py:1703 windows/views.py:1744 windows/views.py:1788 msgid "Clear" msgstr "" -#: windows/views.py:1595 windows/views.py:2923 +#: windows/views.py:1718 windows/views.py:3181 msgid "Indexes" msgstr "" -#: windows/views.py:1639 +#: windows/views.py:1732 windows/views.py:1776 windows/views.py:2927 +#: windows/views.py:2969 windows/views.py:3210 +msgid "Insert" +msgstr "" + +#: windows/views.py:1762 msgid "Foreign Keys" msgstr "" -#: windows/views.py:1683 +#: windows/views.py:1806 msgid "Checks" msgstr "" -#: windows/views.py:1750 windows/views.py:2944 +#: windows/views.py:1873 windows/views.py:3202 msgid "Columns:" msgstr "" -#: windows/views.py:1760 +#: windows/views.py:1883 msgid "Move Up" msgstr "" -#: windows/views.py:1762 +#: windows/views.py:1885 msgid "Move Down" msgstr "" -#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 -#: windows/views.py:3017 +#: windows/views.py:1917 windows/views.py:1924 windows/views.py:3268 +#: windows/views.py:3275 msgid "Add Index" msgstr "" -#: windows/views.py:1798 windows/views.py:3014 +#: windows/views.py:1921 windows/views.py:3272 msgid "Add PrimaryKey" msgstr "" -#: windows/views.py:1815 +#: windows/views.py:1938 msgid "Table" msgstr "" -#: windows/views.py:1851 +#: windows/main/database/procedure.py:140 windows/views.py:1974 msgid "Definer" msgstr "" -#: windows/views.py:1871 +#: windows/views.py:1994 msgid "Schema" msgstr "" -#: windows/views.py:1897 +#: windows/views.py:2020 msgid "SQL security" msgstr "" -#: windows/views.py:1904 +#: windows/views.py:2027 msgid "DEFINER" msgstr "" -#: windows/views.py:1904 +#: windows/views.py:2027 msgid "INVOKER" msgstr "" -#: windows/views.py:1916 +#: windows/views.py:2039 msgid "Algorithm" msgstr "" -#: windows/views.py:1918 +#: windows/views.py:2041 msgid "UNDEFINED" msgstr "" -#: windows/views.py:1921 +#: windows/views.py:2044 msgid "MERGE" msgstr "" -#: windows/views.py:1924 +#: windows/views.py:2047 msgid "TEMPTABLE" msgstr "" -#: windows/views.py:1934 +#: windows/views.py:2057 msgid "View constraint" msgstr "" -#: windows/views.py:1936 +#: windows/views.py:2059 msgid "None" msgstr "" -#: windows/views.py:1939 +#: windows/views.py:2062 msgid "LOCAL" msgstr "" -#: windows/views.py:1942 +#: windows/views.py:2065 msgid "CASCADE" msgstr "" -#: windows/views.py:1945 +#: windows/views.py:2068 msgid "CHECK ONLY" msgstr "" -#: windows/views.py:1948 +#: windows/views.py:2071 msgid "READ ONLY" msgstr "" -#: windows/views.py:1960 +#: windows/views.py:2083 msgid "Force" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:2095 msgid "Security barrier" msgstr "" -#: windows/views.py:2054 -msgid "Views" -msgstr "" - -#: windows/views.py:2062 -msgid "Triggers" -msgstr "" - -#: windows/views.py:2073 -msgid "Refrsh" -msgstr "" - -#: windows/views.py:2079 +#: windows/views.py:2202 msgid "Duplicate" msgstr "" -#: windows/views.py:2085 +#: windows/views.py:2208 msgid "Apply changes automatically" msgstr "" -#: windows/views.py:2087 windows/views.py:2088 +#: windows/views.py:2210 windows/views.py:2211 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" msgstr "" -#: windows/views.py:2101 +#: windows/views.py:2224 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" -#: windows/views.py:2109 +#: windows/views.py:2232 msgid "First" msgstr "" -#: windows/views.py:2127 +#: windows/views.py:2250 msgid "Last" msgstr "" -#: windows/views.py:2136 +#: windows/views.py:2259 msgid "Filters" msgstr "" -#: windows/views.py:2176 +#: windows/views.py:2299 msgid "CTRL+ENTER" msgstr "" -#: windows/views.py:2196 +#: windows/views.py:2319 msgid "Insert row" msgstr "" -#: windows/views.py:2204 +#: windows/views.py:2327 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:318 windows/views.py:2344 #, fuzzy msgid "New query" msgstr "New directory" -#: windows/views.py:2223 windows/views.py:2660 +#: windows/views.py:2346 windows/views.py:2866 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:319 windows/views.py:2346 msgid "Close query" msgstr "" -#: windows/views.py:2227 +#: windows/views.py:2350 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:320 windows/views.py:2350 #, fuzzy msgid "Execute" msgstr "SSH executable" -#: windows/views.py:2229 +#: windows/views.py:2352 msgid "Run all" msgstr "" -#: windows/main/controller.py:295 windows/views.py:2229 +#: windows/views.py:2352 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:322 windows/views.py:2354 msgid "Stop" msgstr "" -#: windows/views.py:2287 +#: windows/views.py:2419 msgid "a page" msgstr "" -#: windows/views.py:2315 +#: windows/views.py:2469 msgid "Query" msgstr "" -#: windows/views.py:2626 +#: windows/views.py:2820 msgid "Character set" msgstr "" -#: windows/views.py:2644 windows/views.py:2663 +#: windows/views.py:2850 windows/views.py:2869 msgid "New" msgstr "" -#: windows/views.py:2683 +#: windows/views.py:2889 msgid "Insert record" msgstr "" -#: windows/views.py:2688 +#: windows/views.py:2894 msgid "Duplicate record" msgstr "" -#: windows/views.py:2695 +#: windows/views.py:2901 msgid "Delete record" msgstr "" -#: windows/views.py:2733 windows/views.py:2964 +#: windows/views.py:2939 windows/views.py:3222 msgid "Up" msgstr "" -#: windows/views.py:2740 windows/views.py:2971 +#: windows/views.py:2946 windows/views.py:3229 msgid "Down" msgstr "" -#: windows/views.py:3100 +#: windows/views.py:2961 +msgid "Table:" +msgstr "" + +#: windows/views.py:2974 +msgid "Clone" +msgstr "" + +#: windows/views.py:3358 msgid "Save Starments" msgstr "" -#: windows/views.py:3108 +#: windows/views.py:3366 msgid "Location" msgstr "" -#: windows/views.py:3115 +#: windows/views.py:3373 msgid "*.sql" msgstr "" @@ -802,7 +902,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 "" @@ -814,75 +914,75 @@ msgstr "" msgid "Zerofill" msgstr "" -#: windows/components/dataview.py:109 +#: windows/components/dataview.py:111 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 "" @@ -902,162 +1002,164 @@ 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:762 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:775 +#: windows/dialogs/connections/view.py:787 #, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "" -#: windows/dialogs/connections/view.py:776 +#: windows/dialogs/connections/view.py:788 msgid "Connection error" msgstr "" -#: windows/dialogs/connections/view.py:802 +#: windows/dialogs/connections/view.py:814 #, 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:817 +#: windows/dialogs/connections/view.py:834 msgid "Confirm delete" msgstr "" -#: windows/dialogs/connections/view.py:819 +#: windows/dialogs/connections/view.py:831 #, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "" -#: windows/main/controller.py:275 +#: windows/main/controller.py:315 #, python-brace-format msgid "{text} ({shortcut})" msgstr "" -#: windows/main/controller.py:281 windows/main/controller.py:294 +#: windows/main/controller.py:321 #, fuzzy msgid "Execute all" msgstr "SSH executable" -#: windows/main/controller.py:471 windows/main/controller.py:479 +#: windows/main/controller.py:440 windows/main/controller.py:448 msgid "Query (1)" msgstr "" -#: windows/main/controller.py:497 +#: windows/main/controller.py:467 #, python-brace-format msgid "Query ({query_number})" msgstr "" -#: windows/main/controller.py:530 +#: windows/main/controller.py:516 msgid "You have unsaved changes. Save before closing?" msgstr "" -#: windows/main/controller.py:531 +#: windows/main/controller.py:517 msgid "Unsaved query" msgstr "" -#: windows/main/controller.py:576 +#: windows/main/controller.py:562 msgid "Save query" msgstr "" -#: windows/main/controller.py:579 +#: windows/main/controller.py:565 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:593 windows/main/controller.py:624 +#: windows/main/controller.py:651 windows/main/database/list.py:119 +#: windows/main/database/procedure.py:294 +#: windows/main/database/procedure.py:320 windows/main/database/view.py:267 +#: windows/main/database/view.py:296 windows/main/query/controller.py:177 msgid "Error" msgstr "" -#: windows/main/controller.py:622 +#: windows/main/controller.py:631 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "" -#: windows/main/controller.py:647 +#: windows/main/controller.py:657 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "" -#: windows/main/controller.py:704 +#: windows/main/controller.py:719 msgid "days" msgstr "" -#: windows/main/controller.py:705 +#: windows/main/controller.py:720 msgid "hours" msgstr "" -#: windows/main/controller.py:706 +#: windows/main/controller.py:721 msgid "minutes" msgstr "" -#: windows/main/controller.py:707 +#: windows/main/controller.py:722 msgid "seconds" msgstr "" -#: windows/main/controller.py:715 +#: windows/main/controller.py:730 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "" -#: windows/main/controller.py:751 +#: windows/main/controller.py:766 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:952 +#: windows/main/controller.py:990 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:954 +#: windows/main/controller.py:992 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:1119 +#: windows/main/controller.py:1214 msgid "Version" msgstr "" -#: windows/main/controller.py:1121 +#: windows/main/controller.py:1216 msgid "Uptime" msgstr "" -#: windows/main/controller.py:1199 +#: windows/main/controller.py:1299 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:1232 +#: windows/main/controller.py:1332 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1067,97 +1169,129 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:1237 windows/main/controller.py:1258 +#: windows/main/controller.py:1337 windows/main/controller.py:1358 msgid "Delete database" msgstr "" -#: windows/main/controller.py:1243 +#: windows/main/controller.py:1343 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:1244 +#: windows/main/controller.py:1344 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:1257 +#: windows/main/controller.py:1357 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:1272 +#: windows/main/controller.py:1372 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:1373 windows/main/database/procedure.py:283 +#: windows/main/database/procedure.py:314 windows/main/database/view.py:255 +#: windows/main/database/view.py:290 msgid "Success" msgstr "" -#: windows/main/controller.py:1392 +#: windows/main/controller.py:1604 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:1418 +#: windows/main/controller.py:1630 #, 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:1652 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1563 +#: windows/main/controller.py:1797 msgid "Do you want delete the records?" msgstr "" -#: windows/main/database/list.py:69 +#: 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 +#: windows/main/database/list.py:108 msgid "Connection lost" msgstr "" -#: windows/main/database/list.py:83 +#: windows/main/database/list.py:118 msgid "Reconnection failed:" msgstr "" -#: windows/main/database/view.py:252 +#: windows/main/database/procedure.py:114 +msgid "Procedure" +msgstr "" + +#: windows/main/database/procedure.py:151 +msgid "Parameters" +msgstr "" + +#: windows/main/database/procedure.py:282 +msgid "Procedure created successfully" +msgstr "" + +#: windows/main/database/procedure.py:282 +msgid "Procedure updated successfully" +msgstr "" + +#: windows/main/database/procedure.py:294 +#, python-brace-format +msgid "Error saving procedure: {}" +msgstr "" + +#: windows/main/database/procedure.py:305 +#, python-brace-format +msgid "Are you sure you want to delete procedure '{}'?" +msgstr "" + +#: windows/main/database/procedure.py:306 windows/main/database/view.py:281 +msgid "Confirm Delete" +msgstr "" + +#: windows/main/database/procedure.py:314 +msgid "Procedure deleted successfully" +msgstr "" + +#: windows/main/database/procedure.py:320 +#, python-brace-format +msgid "Error deleting procedure: {}" +msgstr "" + +#: windows/main/database/view.py:254 msgid "View created successfully" msgstr "" -#: windows/main/database/view.py:252 +#: windows/main/database/view.py:254 msgid "View updated successfully" msgstr "" -#: windows/main/database/view.py:256 +#: windows/main/database/view.py:267 #, python-brace-format msgid "Error saving view: {}" msgstr "" -#: windows/main/database/view.py:269 +#: windows/main/database/view.py:280 #, 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:290 msgid "View deleted successfully" msgstr "" -#: windows/main/database/view.py:282 +#: windows/main/database/view.py:296 #, python-brace-format msgid "Error deleting view: {}" msgstr "" @@ -1195,6 +1329,11 @@ msgstr "" msgid "No active database connection" msgstr "" +#: windows/main/query/history.py:55 +#, fuzzy +msgid "(empty query)" +msgstr "New directory" + #: windows/main/query/renderer.py:53 #, python-brace-format msgid "{affected_rows} rows affected" @@ -1234,6 +1373,10 @@ msgstr "" msgid "Error:" msgstr "" +#: windows/main/table/records.py:336 +msgid "Error saving records" +msgstr "" + #~ msgid "Created at:" #~ msgstr "" @@ -1374,3 +1517,6 @@ msgstr "" #~ msgid "Zero Fill" #~ msgstr "" +#~ msgid "Refrsh" +#~ msgstr "" + diff --git a/locale/es_ES/LC_MESSAGES/petersql.po b/locale/es_ES/LC_MESSAGES/petersql.po index e3de2d7..482a56c 100644 --- a/locale/es_ES/LC_MESSAGES/petersql.po +++ b/locale/es_ES/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-23 10:07+0100\n" +"POT-Creation-Date: 2026-05-02 16:08+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: es_ES\n" @@ -47,75 +47,81 @@ 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:535 +msgid "This connection is read-only." +msgstr "" + +#: structures/engines/mariadb/context.py:616 +#: structures/engines/mysql/context.py:627 +#: structures/engines/postgresql/context.py:685 +#: structures/engines/sqlite/context.py:552 #, 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:644 +#: structures/engines/mysql/context.py:655 +#: structures/engines/postgresql/context.py:710 +#: structures/engines/sqlite/context.py:576 #, 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:662 +#: structures/engines/mysql/context.py:673 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:594 #, 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:702 +#: structures/engines/mysql/context.py:711 +#: structures/engines/postgresql/context.py:768 +#: structures/engines/sqlite/context.py:634 #, 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:735 +#: structures/engines/mysql/context.py:742 +#: structures/engines/postgresql/context.py:798 +#: structures/engines/sqlite/context.py:662 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:781 +#: structures/engines/mariadb/context.py:788 #, 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/dialogs/connections/view.py:417 +#: windows/dialogs/connections/view.py:764 windows/main/controller.py:1212 #: windows/views.py:33 msgid "Connection" msgstr "Conexión" -#: windows/components/dataview.py:113 windows/components/dataview.py:225 -#: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py: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/main/database/procedure.py:129 windows/views.py:47 +#: windows/views.py:97 windows/views.py:1016 windows/views.py:1328 +#: windows/views.py:1361 windows/views.py:1385 windows/views.py:1409 +#: windows/views.py:1433 windows/views.py:1457 windows/views.py:1574 +#: windows/views.py:1954 windows/views.py:3079 msgid "Name" msgstr "Nombre" -#: windows/views.py:48 windows/views.py:428 +#: windows/views.py:48 windows/views.py:438 msgid "Last connection" msgstr "Última conexión" -#: windows/dialogs/connections/view.py:653 windows/views.py:61 +#: windows/dialogs/connections/view.py:655 windows/views.py:61 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:211 +#: windows/dialogs/connections/view.py:615 windows/views.py:65 msgid "New connection" msgstr "Nueva conexión" @@ -129,14 +135,15 @@ msgstr "Nombre" msgid "Clone connection" msgstr "Nueva 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/main/database/procedure.py:183 windows/views.py:81 +#: windows/views.py:613 windows/views.py:1488 windows/views.py:1896 +#: windows/views.py:2155 windows/views.py:2932 windows/views.py:2981 +#: windows/views.py:3215 windows/views.py:3247 msgid "Delete" msgstr "Eliminar" -#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 -#: windows/views.py:2876 +#: windows/views.py:111 windows/views.py:1333 windows/views.py:1629 +#: windows/views.py:2844 windows/views.py:3134 msgid "Engine" msgstr "Motor" @@ -148,7 +155,7 @@ msgstr "Host + puerto" msgid "Username" msgstr "Nombre de usuario" -#: windows/views.py:161 windows/views.py:1132 +#: windows/views.py:161 windows/views.py:1142 msgid "Password" msgstr "Contraseña" @@ -162,536 +169,633 @@ msgid "Use TLS" msgstr "Usar TLS" #: windows/views.py:203 +msgid "Mark read only" +msgstr "" + +#: windows/views.py:214 msgid "Use SSH tunnel" msgstr "Usar túnel SSH" -#: windows/views.py:214 +#: windows/views.py:225 msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 +#: windows/views.py:244 msgid "Filename" msgstr "Nombre de archivo" -#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 +#: windows/views.py:249 windows/views.py:368 windows/views.py:3373 msgid "Select a file" msgstr "Seleccionar un archivo" -#: windows/views.py:238 windows/views.py:358 +#: windows/views.py:249 windows/views.py:368 #, fuzzy 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:266 windows/views.py:1335 windows/views.py:1587 +#: windows/views.py:2842 windows/views.py:3092 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:766 windows/views.py:280 windows/views.py:740 +#: windows/views.py:894 msgid "Settings" msgstr "Configuraciones" -#: windows/views.py:278 +#: windows/views.py:288 msgid "SSH executable" msgstr "Ejecutable SSH" -#: windows/views.py:283 +#: windows/views.py:293 msgid "ssh" msgstr "ssh" -#: windows/views.py:291 +#: windows/views.py:301 msgid "SSH host + port" msgstr "Host SSH + puerto" -#: windows/views.py:303 +#: windows/views.py:313 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:322 msgid "SSH username" msgstr "Nombre de usuario SSH" -#: windows/views.py:325 +#: windows/views.py:335 msgid "SSH password" msgstr "Contraseña SSH" -#: windows/views.py:338 +#: windows/views.py:348 msgid "Local port" msgstr "Puerto local" -#: windows/views.py:344 +#: windows/views.py:354 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:363 msgid "Identity file" msgstr "Archivo de identidad" -#: windows/views.py:369 +#: windows/views.py:379 #, fuzzy msgid "Remote host + port" msgstr "Host + puerto" -#: windows/views.py:381 +#: windows/views.py:391 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:400 msgid "SSH extra args" msgstr "" -#: windows/views.py:405 +#: windows/views.py:415 msgid "SSH Tunnel" msgstr "Túnel SSH" -#: windows/views.py:411 windows/views.py:1341 +#: windows/views.py:421 windows/views.py:1331 windows/views.py:2846 msgid "Created at" msgstr "Creado en" -#: windows/views.py:445 +#: windows/views.py:455 msgid "Successful connections" msgstr "Conexiones exitosas" -#: windows/views.py:462 +#: windows/views.py:472 #, fuzzy msgid "Last successful connection" msgstr "Conexiones exitosas" -#: windows/views.py:479 +#: windows/views.py:489 msgid "Unsuccessful connections" msgstr "Conexiones fallidas" -#: windows/views.py:496 +#: windows/views.py:506 msgid "Last failure reason" msgstr "" -#: windows/views.py:513 +#: windows/views.py:523 #, fuzzy msgid "Total connection attempts" msgstr "Última conexión" -#: windows/views.py:530 +#: windows/views.py:540 #, fuzzy msgid "Average connection time (ms)" msgstr "Reconexión fallida:" -#: windows/views.py:547 +#: windows/views.py:557 #, fuzzy msgid "Most recent connection duration" msgstr "Abrir administrador de conexiones" -#: windows/views.py:566 +#: windows/views.py:576 msgid "Statistics" msgstr "Estadísticas" -#: windows/views.py:584 windows/views.py:1730 +#: windows/views.py:594 windows/views.py:1853 msgid "Create" msgstr "Crear" -#: windows/views.py:588 +#: windows/views.py:598 #, fuzzy msgid "Create connection" msgstr "Última conexión" -#: windows/views.py:591 +#: windows/views.py:601 #, fuzzy 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/main/database/procedure.py:184 windows/views.py:630 +#: windows/views.py:854 windows/views.py:1483 windows/views.py:1899 +#: windows/views.py:2160 windows/views.py:2216 windows/views.py:2908 +#: windows/views.py:3250 windows/views.py:3387 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:323 windows/main/database/procedure.py:185 +#: windows/views.py:635 windows/views.py:2165 windows/views.py:2358 +#: windows/views.py:3255 windows/views.py:3393 msgid "Save" msgstr "Guardar" -#: windows/views.py:632 +#: windows/views.py:642 msgid "Test" msgstr "Probar" -#: windows/views.py:639 +#: windows/views.py:649 msgid "Connect" msgstr "Conectar" -#: windows/views.py:742 +#: windows/main/database/procedure.py:162 windows/views.py:752 msgid "Language" msgstr "Idioma" -#: windows/views.py:747 +#: windows/views.py:757 msgid "English" msgstr "Inglés" -#: windows/views.py:747 +#: windows/views.py:757 msgid "Italian" msgstr "Italiano" -#: windows/views.py:747 +#: windows/views.py:757 msgid "French" msgstr "Francés" -#: windows/views.py:759 +#: windows/views.py:769 msgid "Locale" msgstr "Localización" -#: windows/views.py:780 +#: windows/views.py:790 #, fuzzy msgid "Column content" msgstr "Nueva conexión" -#: windows/views.py:790 +#: windows/views.py:800 msgid "Syntax" msgstr "Sintaxis" -#: windows/views.py:847 +#: windows/views.py:857 msgid "Ok" msgstr "Ok" -#: windows/views.py:878 +#: windows/views.py:888 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:887 +#: windows/views.py:897 msgid "File" msgstr "Archivo" -#: windows/views.py:890 +#: windows/views.py:900 msgid "About" msgstr "Acerca de" -#: windows/views.py:893 +#: windows/views.py:903 msgid "Help" msgstr "Ayuda" -#: windows/views.py:898 +#: windows/views.py:908 msgid "Open connection manager" msgstr "Abrir administrador de conexiones" -#: windows/views.py:902 +#: windows/views.py:912 msgid "Disconnect from server" msgstr "Desconectar del servidor" -#: windows/views.py:904 +#: windows/views.py:914 msgid "tool" msgstr "herramienta" -#: windows/views.py:904 +#: windows/views.py:914 windows/views.py:2196 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:918 windows/views.py:920 windows/views.py:1877 +#: windows/views.py:2200 windows/views.py:2344 msgid "Add" msgstr "Agregar" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 +#: windows/views.py:954 windows/views.py:958 windows/views.py:2322 msgid "MyMenuItem" msgstr "MiElementoMenu" -#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 +#: windows/views.py:961 windows/views.py:1927 windows/views.py:3278 msgid "MyMenu" msgstr "MiMenu" -#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 -#: windows/views.py:1402 +#: windows/views.py:976 windows/views.py:1511 windows/views.py:1518 +#: windows/views.py:1525 msgid "MyLabel" msgstr "MiEtiqueta" -#: windows/views.py:972 +#: windows/views.py:982 msgid "Databases" msgstr "Bases de datos" -#: windows/views.py:973 windows/views.py:1340 +#: windows/views.py:983 windows/views.py:1330 windows/views.py:2847 msgid "Size" msgstr "Tamaño" -#: windows/views.py:974 +#: windows/views.py:984 msgid "Elements" msgstr "Elementos" -#: windows/views.py:975 +#: windows/views.py:985 msgid "Modified at" msgstr "Modificado en" -#: windows/views.py:976 +#: windows/views.py:986 windows/views.py:1345 msgid "Tables" msgstr "Tablas" -#: windows/views.py:983 +#: windows/views.py:993 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:1039 +#: windows/views.py:1334 windows/views.py:2843 msgid "Collation" msgstr "Intercalación" -#: windows/views.py:1058 +#: windows/views.py:1068 msgid "Encryption" msgstr "" -#: windows/views.py:1070 +#: windows/views.py:1080 msgid "Read Only" msgstr "" -#: windows/views.py:1087 +#: windows/views.py:1097 #, fuzzy msgid "Tablespace" msgstr "Tablas" -#: windows/views.py:1108 +#: windows/views.py:1118 #, fuzzy msgid "Connection limit" msgstr "Conexión perdida" -#: windows/views.py:1151 +#: windows/views.py:1161 #, fuzzy msgid "Profile" msgstr "Archivo" -#: windows/views.py:1177 +#: windows/views.py:1187 #, fuzzy msgid "Default tablespace" msgstr "Eliminar tabla" -#: windows/views.py:1198 +#: windows/views.py:1208 #, fuzzy msgid "Temporary tablespace" msgstr "Temporal" -#: windows/views.py:1224 +#: windows/views.py:1234 msgid "Quota" msgstr "" -#: windows/views.py:1243 +#: windows/views.py:1253 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1260 +#: windows/views.py:1270 msgid "Account status" msgstr "" -#: windows/views.py:1281 +#: windows/views.py:1291 #, fuzzy msgid "Password expire" msgstr "Contraseña" -#: windows/views.py:1302 -msgid "Table:" -msgstr "Tabla:" +#: windows/views.py:1315 +#, fuzzy +msgid "Add new table" +msgstr "Eliminar 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:1317 +#, fuzzy +msgid "Clone table" +msgstr "Eliminar tabla" -#: windows/views.py:1315 -msgid "Clone" -msgstr "Clonar" +#: windows/main/controller.py:1633 windows/views.py:1319 +msgid "Delete table" +msgstr "Eliminar tabla" -#: windows/views.py:1339 +#: windows/views.py:1329 msgid "Rows" msgstr "Filas" -#: windows/views.py:1342 +#: windows/views.py:1332 windows/views.py:2845 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:1350 +msgid "Add new view" +msgstr "" + +#: windows/views.py:1352 windows/views.py:1376 +#, fuzzy +msgid "Clone view" +msgstr "Clonar" + +#: windows/views.py:1354 +#, fuzzy +msgid "Delete view" +msgstr "Eliminar" + +#: windows/views.py:1362 windows/views.py:1386 windows/views.py:1410 +#: windows/views.py:1434 windows/views.py:1458 +#, fuzzy +msgid "Definition" +msgstr "Condición" + +#: windows/views.py:1369 windows/views.py:2177 +msgid "Views" +msgstr "Vistas" + +#: windows/views.py:1374 +msgid "Add new procedure" +msgstr "" + +#: windows/views.py:1376 +msgid "Clone procedure" +msgstr "" + +#: windows/views.py:1378 +#, fuzzy +msgid "Delete procedure" +msgstr "Eliminar registro" + +#: windows/views.py:1393 +msgid "Procedures" +msgstr "" + +#: windows/views.py:1398 +#, fuzzy +msgid "Add new function" +msgstr "Nueva conexión" + +#: windows/views.py:1400 +#, fuzzy +msgid "Clone function" +msgstr "Nueva conexión" + +#: windows/views.py:1402 +#, fuzzy +msgid "Delete function" +msgstr "Eliminar registro" + +#: windows/views.py:1417 +#, fuzzy +msgid "Functions" +msgstr "Conexión" + +#: windows/views.py:1422 +#, fuzzy +msgid "Add new trigger" +msgstr "Disparadores" + +#: windows/views.py:1424 +#, fuzzy +msgid "Clone trigger" +msgstr "Disparadores" + +#: windows/views.py:1426 +#, fuzzy +msgid "Delete trigger" +msgstr "Eliminar registro" + +#: windows/views.py:1441 windows/views.py:2185 +msgid "Triggers" +msgstr "Disparadores" + +#: windows/views.py:1446 +msgid "Add new event" +msgstr "" + +#: windows/views.py:1448 +#, fuzzy +msgid "Clone event" +msgstr "Clonar" + +#: windows/views.py:1450 +#, fuzzy +msgid "Delete event" +msgstr "Eliminar tabla" + +#: windows/views.py:1465 +#, fuzzy +msgid "Events" +msgstr "Elementos" + +#: windows/views.py:1493 windows/views.py:1904 windows/views.py:2214 +#: windows/views.py:2296 windows/views.py:2915 msgid "Apply" msgstr "Aplicar" -#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 -#: windows/views.py:2909 +#: windows/main/database/procedure.py:122 windows/views.py:1505 +#: windows/views.py:1684 windows/views.py:2114 windows/views.py:3167 msgid "Options" msgstr "Opciones" -#: windows/views.py:1413 +#: windows/views.py:1536 msgid "Diagram" msgstr "Diagrama" -#: windows/views.py:1424 +#: windows/views.py:1547 msgid "Database" msgstr "Base de datos" -#: windows/views.py:1479 windows/views.py:2849 +#: windows/views.py:1602 windows/views.py:3107 msgid "Base" msgstr "Base" -#: windows/views.py:1493 windows/views.py:2863 +#: windows/views.py:1616 windows/views.py:3121 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1521 windows/views.py:2891 +#: windows/views.py:1644 windows/views.py:3149 msgid "Default Collation" msgstr "Intercalación predeterminada" -#: windows/views.py:1531 +#: windows/views.py:1654 msgid "Convert data" msgstr "" -#: windows/views.py:1539 +#: windows/views.py:1662 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:1696 windows/views.py:1737 windows/views.py:1781 +#: windows/views.py:1879 windows/views.py:2204 msgid "Remove" msgstr "Eliminar" -#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 +#: windows/views.py:1703 windows/views.py:1744 windows/views.py:1788 msgid "Clear" msgstr "Limpiar" -#: windows/views.py:1595 windows/views.py:2923 +#: windows/views.py:1718 windows/views.py:3181 msgid "Indexes" msgstr "Índices" -#: windows/views.py:1639 +#: windows/views.py:1732 windows/views.py:1776 windows/views.py:2927 +#: windows/views.py:2969 windows/views.py:3210 +msgid "Insert" +msgstr "Insertar" + +#: windows/views.py:1762 msgid "Foreign Keys" msgstr "Claves foráneas" -#: windows/views.py:1683 +#: windows/views.py:1806 msgid "Checks" msgstr "Comprobaciones" -#: windows/views.py:1750 windows/views.py:2944 +#: windows/views.py:1873 windows/views.py:3202 msgid "Columns:" msgstr "Columnas:" -#: windows/views.py:1760 +#: windows/views.py:1883 #, fuzzy msgid "Move Up" msgstr "Mover arriba\tCTRL+UP" -#: windows/views.py:1762 +#: windows/views.py:1885 #, fuzzy 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:1917 windows/views.py:1924 windows/views.py:3268 +#: windows/views.py:3275 msgid "Add Index" msgstr "Agregar índice" -#: windows/views.py:1798 windows/views.py:3014 +#: windows/views.py:1921 windows/views.py:3272 msgid "Add PrimaryKey" msgstr "Agregar clave primaria" -#: windows/views.py:1815 +#: windows/views.py:1938 msgid "Table" msgstr "Tabla" -#: windows/views.py:1851 +#: windows/main/database/procedure.py:140 windows/views.py:1974 #, fuzzy msgid "Definer" msgstr "Insertar" -#: windows/views.py:1871 +#: windows/views.py:1994 msgid "Schema" msgstr "" -#: windows/views.py:1897 +#: windows/views.py:2020 msgid "SQL security" msgstr "" -#: windows/views.py:1904 +#: windows/views.py:2027 #, fuzzy msgid "DEFINER" msgstr "Insertar" -#: windows/views.py:1904 +#: windows/views.py:2027 #, fuzzy msgid "INVOKER" msgstr "Insertar" -#: windows/views.py:1916 +#: windows/views.py:2039 msgid "Algorithm" msgstr "" -#: windows/views.py:1918 +#: windows/views.py:2041 #, fuzzy msgid "UNDEFINED" msgstr "Sin signo" -#: windows/views.py:1921 +#: windows/views.py:2044 msgid "MERGE" msgstr "" -#: windows/views.py:1924 +#: windows/views.py:2047 #, fuzzy msgid "TEMPTABLE" msgstr "Tabla" -#: windows/views.py:1934 +#: windows/views.py:2057 msgid "View constraint" msgstr "" -#: windows/views.py:1936 +#: windows/views.py:2059 #, fuzzy msgid "None" msgstr "Clonar" -#: windows/views.py:1939 +#: windows/views.py:2062 #, fuzzy msgid "LOCAL" msgstr "Localización" -#: windows/views.py:1942 +#: windows/views.py:2065 #, fuzzy msgid "CASCADE" msgstr "Cancelar" -#: windows/views.py:1945 +#: windows/views.py:2068 #, fuzzy msgid "CHECK ONLY" msgstr "Verificar" -#: windows/views.py:1948 +#: windows/views.py:2071 msgid "READ ONLY" msgstr "" -#: windows/views.py:1960 +#: windows/views.py:2083 msgid "Force" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:2095 msgid "Security barrier" msgstr "" -#: windows/views.py:2054 -msgid "Views" -msgstr "Vistas" - -#: windows/views.py:2062 -msgid "Triggers" -msgstr "Disparadores" - -#: windows/views.py:2073 -#, fuzzy -msgid "Refrsh" -msgstr "Actualizar" - -#: windows/views.py:2079 +#: windows/views.py:2202 #, fuzzy msgid "Duplicate" msgstr "Duplicar registro" -#: windows/views.py:2085 +#: windows/views.py:2208 msgid "Apply changes automatically" msgstr "Aplicar cambios automáticamente" -#: windows/views.py:2087 windows/views.py:2088 +#: windows/views.py:2210 windows/views.py:2211 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -699,122 +803,126 @@ msgstr "" "Si está habilitado, las ediciones de la tabla se aplican inmediatamente " "sin presionar Aplicar o Cancelar" -#: windows/views.py:2101 +#: windows/views.py:2224 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" -#: windows/views.py:2109 +#: windows/views.py:2232 #, fuzzy msgid "First" msgstr "Filtros" -#: windows/views.py:2127 +#: windows/views.py:2250 msgid "Last" msgstr "" -#: windows/views.py:2136 +#: windows/views.py:2259 msgid "Filters" msgstr "Filtros" -#: windows/views.py:2176 +#: windows/views.py:2299 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2196 +#: windows/views.py:2319 msgid "Insert row" msgstr "Insertar fila" -#: windows/views.py:2204 +#: windows/views.py:2327 msgid "Data" msgstr "Datos" -#: windows/main/controller.py:278 windows/main/controller.py:287 -#: windows/main/controller.py:288 windows/views.py:2221 +#: windows/main/controller.py:318 windows/views.py:2344 #, fuzzy msgid "New query" msgstr "Consulta" -#: windows/views.py:2223 windows/views.py:2660 +#: windows/views.py:2346 windows/views.py:2866 msgid "Close" msgstr "Cerrar" -#: windows/main/controller.py:279 windows/main/controller.py:289 -#: windows/main/controller.py:290 windows/views.py:2223 +#: windows/main/controller.py:319 windows/views.py:2346 #, fuzzy msgid "Close query" msgstr "Consulta" -#: windows/views.py:2227 +#: windows/views.py:2350 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:320 windows/views.py:2350 #, fuzzy msgid "Execute" msgstr "Ejecutable SSH" -#: windows/views.py:2229 +#: windows/views.py:2352 msgid "Run all" msgstr "" -#: windows/main/controller.py:295 windows/views.py:2229 +#: windows/views.py:2352 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:322 windows/views.py:2354 msgid "Stop" msgstr "" -#: windows/views.py:2287 +#: windows/views.py:2419 msgid "a page" msgstr "" -#: windows/views.py:2315 +#: windows/views.py:2469 msgid "Query" msgstr "Consulta" -#: windows/views.py:2626 +#: windows/views.py:2820 #, fuzzy msgid "Character set" msgstr "Creado en" -#: windows/views.py:2644 windows/views.py:2663 +#: windows/views.py:2850 windows/views.py:2869 msgid "New" msgstr "Nuevo" -#: windows/views.py:2683 +#: windows/views.py:2889 msgid "Insert record" msgstr "Insertar registro" -#: windows/views.py:2688 +#: windows/views.py:2894 msgid "Duplicate record" msgstr "Duplicar registro" -#: windows/views.py:2695 +#: windows/views.py:2901 msgid "Delete record" msgstr "Eliminar registro" -#: windows/views.py:2733 windows/views.py:2964 +#: windows/views.py:2939 windows/views.py:3222 msgid "Up" msgstr "Arriba" -#: windows/views.py:2740 windows/views.py:2971 +#: windows/views.py:2946 windows/views.py:3229 msgid "Down" msgstr "Abajo" -#: windows/views.py:3100 +#: windows/views.py:2961 +msgid "Table:" +msgstr "Tabla:" + +#: windows/views.py:2974 +msgid "Clone" +msgstr "Clonar" + +#: windows/views.py:3358 msgid "Save Starments" msgstr "" -#: windows/views.py:3108 +#: windows/views.py:3366 #, fuzzy msgid "Location" msgstr "Intercalación" -#: windows/views.py:3115 +#: windows/views.py:3373 msgid "*.sql" msgstr "" @@ -838,7 +946,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 +958,75 @@ msgstr "Sin signo" msgid "Zerofill" msgstr "Relleno cero" -#: windows/components/dataview.py:109 +#: windows/components/dataview.py:111 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" @@ -938,164 +1046,166 @@ 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 "" -#: 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 "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 "" -#: 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:762 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:775 +#: windows/dialogs/connections/view.py:787 #, fuzzy, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "Error de conexión" -#: windows/dialogs/connections/view.py:776 +#: windows/dialogs/connections/view.py:788 msgid "Connection error" msgstr "Error de conexión" -#: windows/dialogs/connections/view.py:802 +#: windows/dialogs/connections/view.py:814 #, fuzzy, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" msgstr "¿Quieres eliminar los registros?" -#: windows/dialogs/connections/view.py:805 -#: windows/dialogs/connections/view.py:822 +#: windows/dialogs/connections/view.py:817 +#: windows/dialogs/connections/view.py:834 msgid "Confirm delete" msgstr "Confirmar eliminar" -#: windows/dialogs/connections/view.py:819 +#: windows/dialogs/connections/view.py:831 #, fuzzy, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "¿Quieres eliminar los registros?" -#: windows/main/controller.py:275 +#: windows/main/controller.py:315 #, python-brace-format msgid "{text} ({shortcut})" msgstr "" -#: windows/main/controller.py:281 windows/main/controller.py:294 +#: windows/main/controller.py:321 #, fuzzy msgid "Execute all" msgstr "Ejecutable SSH" -#: windows/main/controller.py:471 windows/main/controller.py:479 +#: windows/main/controller.py:440 windows/main/controller.py:448 #, fuzzy msgid "Query (1)" msgstr "Consulta" -#: windows/main/controller.py:497 +#: windows/main/controller.py:467 #, python-brace-format msgid "Query ({query_number})" msgstr "" -#: windows/main/controller.py:530 +#: windows/main/controller.py:516 msgid "You have unsaved changes. Save before closing?" msgstr "" -#: windows/main/controller.py:531 +#: windows/main/controller.py:517 msgid "Unsaved query" msgstr "" -#: windows/main/controller.py:576 +#: windows/main/controller.py:562 #, fuzzy msgid "Save query" msgstr "Consulta" -#: windows/main/controller.py:579 +#: windows/main/controller.py:565 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:593 windows/main/controller.py:624 +#: windows/main/controller.py:651 windows/main/database/list.py:119 +#: windows/main/database/procedure.py:294 +#: windows/main/database/procedure.py:320 windows/main/database/view.py:267 +#: windows/main/database/view.py:296 windows/main/query/controller.py:177 msgid "Error" msgstr "Error" -#: windows/main/controller.py:622 +#: windows/main/controller.py:631 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "" -#: windows/main/controller.py:647 +#: windows/main/controller.py:657 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "" -#: windows/main/controller.py:704 +#: windows/main/controller.py:719 msgid "days" msgstr "días" -#: windows/main/controller.py:705 +#: windows/main/controller.py:720 msgid "hours" msgstr "horas" -#: windows/main/controller.py:706 +#: windows/main/controller.py:721 msgid "minutes" msgstr "minutos" -#: windows/main/controller.py:707 +#: windows/main/controller.py:722 msgid "seconds" msgstr "segundos" -#: windows/main/controller.py:715 +#: windows/main/controller.py:730 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizada: {used} ({percentage:.2%})" -#: windows/main/controller.py:751 +#: windows/main/controller.py:766 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:952 +#: windows/main/controller.py:990 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:954 +#: windows/main/controller.py:992 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:1119 +#: windows/main/controller.py:1214 msgid "Version" msgstr "Versión" -#: windows/main/controller.py:1121 +#: windows/main/controller.py:1216 msgid "Uptime" msgstr "Tiempo de actividad" -#: windows/main/controller.py:1199 +#: windows/main/controller.py:1299 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:1232 +#: windows/main/controller.py:1332 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1105,99 +1215,132 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:1237 windows/main/controller.py:1258 +#: windows/main/controller.py:1337 windows/main/controller.py:1358 #, fuzzy msgid "Delete database" msgstr "Eliminar tabla" -#: windows/main/controller.py:1243 +#: windows/main/controller.py:1343 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:1244 +#: windows/main/controller.py:1344 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:1257 +#: windows/main/controller.py:1357 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:1272 +#: windows/main/controller.py:1372 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:1373 windows/main/database/procedure.py:283 +#: windows/main/database/procedure.py:314 windows/main/database/view.py:255 +#: windows/main/database/view.py:290 msgid "Success" msgstr "" -#: windows/main/controller.py:1392 +#: windows/main/controller.py:1604 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:1418 +#: windows/main/controller.py:1630 #, fuzzy, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "¿Quieres eliminar los registros?" -#: windows/main/controller.py:1421 -msgid "Delete table" -msgstr "Eliminar tabla" - -#: windows/main/controller.py:1440 +#: windows/main/controller.py:1652 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1563 +#: windows/main/controller.py:1797 msgid "Do you want delete the records?" msgstr "¿Quieres eliminar los registros?" -#: windows/main/database/list.py:69 +#: 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 +#: windows/main/database/list.py:108 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/procedure.py:114 +msgid "Procedure" +msgstr "" + +#: windows/main/database/procedure.py:151 +#, fuzzy +msgid "Parameters" +msgstr "PeterSQL" + +#: windows/main/database/procedure.py:282 +msgid "Procedure created successfully" +msgstr "" + +#: windows/main/database/procedure.py:282 +msgid "Procedure updated successfully" +msgstr "" + +#: windows/main/database/procedure.py:294 +#, python-brace-format +msgid "Error saving procedure: {}" +msgstr "" + +#: windows/main/database/procedure.py:305 +#, fuzzy, python-brace-format +msgid "Are you sure you want to delete procedure '{}'?" +msgstr "¿Quieres eliminar los registros?" + +#: windows/main/database/procedure.py:306 windows/main/database/view.py:281 +#, fuzzy +msgid "Confirm Delete" +msgstr "Confirmar eliminar" + +#: windows/main/database/procedure.py:314 +msgid "Procedure deleted successfully" +msgstr "" + +#: windows/main/database/procedure.py:320 +#, python-brace-format +msgid "Error deleting procedure: {}" +msgstr "" + +#: windows/main/database/view.py:254 msgid "View created successfully" msgstr "" -#: windows/main/database/view.py:252 +#: windows/main/database/view.py:254 msgid "View updated successfully" msgstr "" -#: windows/main/database/view.py:256 +#: windows/main/database/view.py:267 #, python-brace-format msgid "Error saving view: {}" msgstr "" -#: windows/main/database/view.py:269 +#: windows/main/database/view.py:280 #, 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" - -#: windows/main/database/view.py:279 +#: windows/main/database/view.py:290 msgid "View deleted successfully" msgstr "" -#: windows/main/database/view.py:282 +#: windows/main/database/view.py:296 #, python-brace-format msgid "Error deleting view: {}" msgstr "" @@ -1236,6 +1379,11 @@ msgstr "" msgid "No active database connection" msgstr "Nueva conexión" +#: windows/main/query/history.py:55 +#, fuzzy +msgid "(empty query)" +msgstr "Consulta" + #: windows/main/query/renderer.py:53 #, python-brace-format msgid "{affected_rows} rows affected" @@ -1276,6 +1424,10 @@ msgstr "" msgid "Error:" msgstr "Error" +#: windows/main/table/records.py:336 +msgid "Error saving records" +msgstr "" + #~ msgid "Created at:" #~ msgstr "" @@ -1411,3 +1563,6 @@ msgstr "Error" #~ msgid "Zero Fill" #~ msgstr "Relleno cero" +#~ msgid "Refrsh" +#~ msgstr "Actualizar" + diff --git a/locale/fr_FR/LC_MESSAGES/petersql.po b/locale/fr_FR/LC_MESSAGES/petersql.po index 5ba12c3..e9d0ec5 100644 --- a/locale/fr_FR/LC_MESSAGES/petersql.po +++ b/locale/fr_FR/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-23 10:07+0100\n" +"POT-Creation-Date: 2026-05-02 16:08+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: fr_FR\n" @@ -47,75 +47,81 @@ 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:535 +msgid "This connection is read-only." +msgstr "" + +#: structures/engines/mariadb/context.py:616 +#: structures/engines/mysql/context.py:627 +#: structures/engines/postgresql/context.py:685 +#: structures/engines/sqlite/context.py:552 #, 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:644 +#: structures/engines/mysql/context.py:655 +#: structures/engines/postgresql/context.py:710 +#: structures/engines/sqlite/context.py:576 #, 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:662 +#: structures/engines/mysql/context.py:673 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:594 #, 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:702 +#: structures/engines/mysql/context.py:711 +#: structures/engines/postgresql/context.py:768 +#: structures/engines/sqlite/context.py:634 #, 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:735 +#: structures/engines/mysql/context.py:742 +#: structures/engines/postgresql/context.py:798 +#: structures/engines/sqlite/context.py:662 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:781 +#: structures/engines/mariadb/context.py:788 #, 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/dialogs/connections/view.py:417 +#: windows/dialogs/connections/view.py:764 windows/main/controller.py:1212 #: windows/views.py:33 msgid "Connection" msgstr "Connexion" -#: windows/components/dataview.py:113 windows/components/dataview.py:225 -#: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py: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/main/database/procedure.py:129 windows/views.py:47 +#: windows/views.py:97 windows/views.py:1016 windows/views.py:1328 +#: windows/views.py:1361 windows/views.py:1385 windows/views.py:1409 +#: windows/views.py:1433 windows/views.py:1457 windows/views.py:1574 +#: windows/views.py:1954 windows/views.py:3079 msgid "Name" msgstr "Nom" -#: windows/views.py:48 windows/views.py:428 +#: windows/views.py:48 windows/views.py:438 msgid "Last connection" msgstr "Dernière connexion" -#: windows/dialogs/connections/view.py:653 windows/views.py:61 +#: windows/dialogs/connections/view.py:655 windows/views.py:61 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:211 +#: windows/dialogs/connections/view.py:615 windows/views.py:65 msgid "New connection" msgstr "Nouvelle connexion" @@ -129,14 +135,15 @@ msgstr "Nom" msgid "Clone connection" msgstr "Nouvelle 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/main/database/procedure.py:183 windows/views.py:81 +#: windows/views.py:613 windows/views.py:1488 windows/views.py:1896 +#: windows/views.py:2155 windows/views.py:2932 windows/views.py:2981 +#: windows/views.py:3215 windows/views.py:3247 msgid "Delete" msgstr "Supprimer" -#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 -#: windows/views.py:2876 +#: windows/views.py:111 windows/views.py:1333 windows/views.py:1629 +#: windows/views.py:2844 windows/views.py:3134 msgid "Engine" msgstr "Moteur" @@ -148,7 +155,7 @@ msgstr "Hôte + port" msgid "Username" msgstr "Nom d'utilisateur" -#: windows/views.py:161 windows/views.py:1132 +#: windows/views.py:161 windows/views.py:1142 msgid "Password" msgstr "Mot de passe" @@ -162,534 +169,631 @@ msgid "Use TLS" msgstr "" #: windows/views.py:203 +msgid "Mark read only" +msgstr "" + +#: windows/views.py:214 msgid "Use SSH tunnel" msgstr "Utiliser un tunnel SSH" -#: windows/views.py:214 +#: windows/views.py:225 msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 +#: windows/views.py:244 msgid "Filename" msgstr "Nom de fichier" -#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 +#: windows/views.py:249 windows/views.py:368 windows/views.py:3373 msgid "Select a file" msgstr "Sélectionner un fichier" -#: windows/views.py:238 windows/views.py:358 +#: windows/views.py:249 windows/views.py:368 #, fuzzy 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:266 windows/views.py:1335 windows/views.py:1587 +#: windows/views.py:2842 windows/views.py:3092 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:766 windows/views.py:280 windows/views.py:740 +#: windows/views.py:894 msgid "Settings" msgstr "Paramètres" -#: windows/views.py:278 +#: windows/views.py:288 msgid "SSH executable" msgstr "Exécutable SSH" -#: windows/views.py:283 +#: windows/views.py:293 msgid "ssh" msgstr "ssh" -#: windows/views.py:291 +#: windows/views.py:301 msgid "SSH host + port" msgstr "Hôte SSH + port" -#: windows/views.py:303 +#: windows/views.py:313 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "" -#: windows/views.py:312 +#: windows/views.py:322 msgid "SSH username" msgstr "Nom d'utilisateur SSH" -#: windows/views.py:325 +#: windows/views.py:335 msgid "SSH password" msgstr "Mot de passe SSH" -#: windows/views.py:338 +#: windows/views.py:348 msgid "Local port" msgstr "Port local" -#: windows/views.py:344 +#: windows/views.py:354 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:363 msgid "Identity file" msgstr "" -#: windows/views.py:369 +#: windows/views.py:379 #, fuzzy msgid "Remote host + port" msgstr "Hôte + port" -#: windows/views.py:381 +#: windows/views.py:391 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "" -#: windows/views.py:390 +#: windows/views.py:400 msgid "SSH extra args" msgstr "" -#: windows/views.py:405 +#: windows/views.py:415 msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:411 windows/views.py:1341 +#: windows/views.py:421 windows/views.py:1331 windows/views.py:2846 msgid "Created at" msgstr "Créé le" -#: windows/views.py:445 +#: windows/views.py:455 msgid "Successful connections" msgstr "Connexions réussies" -#: windows/views.py:462 +#: windows/views.py:472 #, fuzzy msgid "Last successful connection" msgstr "Connexions réussies" -#: windows/views.py:479 +#: windows/views.py:489 msgid "Unsuccessful connections" msgstr "Connexions échouées" -#: windows/views.py:496 +#: windows/views.py:506 msgid "Last failure reason" msgstr "" -#: windows/views.py:513 +#: windows/views.py:523 #, fuzzy msgid "Total connection attempts" msgstr "Dernière connexion" -#: windows/views.py:530 +#: windows/views.py:540 #, fuzzy msgid "Average connection time (ms)" msgstr "Échec de la reconnexion :" -#: windows/views.py:547 +#: windows/views.py:557 #, fuzzy msgid "Most recent connection duration" msgstr "Ouvrir le gestionnaire de connexions" -#: windows/views.py:566 +#: windows/views.py:576 msgid "Statistics" msgstr "Statistiques" -#: windows/views.py:584 windows/views.py:1730 +#: windows/views.py:594 windows/views.py:1853 msgid "Create" msgstr "Créer" -#: windows/views.py:588 +#: windows/views.py:598 #, fuzzy msgid "Create connection" msgstr "Dernière connexion" -#: windows/views.py:591 +#: windows/views.py:601 #, fuzzy 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/main/database/procedure.py:184 windows/views.py:630 +#: windows/views.py:854 windows/views.py:1483 windows/views.py:1899 +#: windows/views.py:2160 windows/views.py:2216 windows/views.py:2908 +#: windows/views.py:3250 windows/views.py:3387 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:323 windows/main/database/procedure.py:185 +#: windows/views.py:635 windows/views.py:2165 windows/views.py:2358 +#: windows/views.py:3255 windows/views.py:3393 msgid "Save" msgstr "Enregistrer" -#: windows/views.py:632 +#: windows/views.py:642 msgid "Test" msgstr "Tester" -#: windows/views.py:639 +#: windows/views.py:649 msgid "Connect" msgstr "Connecter" -#: windows/views.py:742 +#: windows/main/database/procedure.py:162 windows/views.py:752 msgid "Language" msgstr "Langue" -#: windows/views.py:747 +#: windows/views.py:757 msgid "English" msgstr "Anglais" -#: windows/views.py:747 +#: windows/views.py:757 msgid "Italian" msgstr "Italien" -#: windows/views.py:747 +#: windows/views.py:757 msgid "French" msgstr "Français" -#: windows/views.py:759 +#: windows/views.py:769 msgid "Locale" msgstr "Localisation" -#: windows/views.py:780 +#: windows/views.py:790 #, fuzzy msgid "Column content" msgstr "Nouvelle connexion" -#: windows/views.py:790 +#: windows/views.py:800 msgid "Syntax" msgstr "Syntaxe" -#: windows/views.py:847 +#: windows/views.py:857 msgid "Ok" msgstr "Ok" -#: windows/views.py:878 +#: windows/views.py:888 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:887 +#: windows/views.py:897 msgid "File" msgstr "Fichier" -#: windows/views.py:890 +#: windows/views.py:900 msgid "About" msgstr "À propos" -#: windows/views.py:893 +#: windows/views.py:903 msgid "Help" msgstr "Aide" -#: windows/views.py:898 +#: windows/views.py:908 msgid "Open connection manager" msgstr "Ouvrir le gestionnaire de connexions" -#: windows/views.py:902 +#: windows/views.py:912 msgid "Disconnect from server" msgstr "Se déconnecter du serveur" -#: windows/views.py:904 +#: windows/views.py:914 msgid "tool" msgstr "outil" -#: windows/views.py:904 +#: windows/views.py:914 windows/views.py:2196 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:918 windows/views.py:920 windows/views.py:1877 +#: windows/views.py:2200 windows/views.py:2344 msgid "Add" msgstr "Ajouter" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 +#: windows/views.py:954 windows/views.py:958 windows/views.py:2322 msgid "MyMenuItem" msgstr "MonÉlémentMenu" -#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 +#: windows/views.py:961 windows/views.py:1927 windows/views.py:3278 msgid "MyMenu" msgstr "MonMenu" -#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 -#: windows/views.py:1402 +#: windows/views.py:976 windows/views.py:1511 windows/views.py:1518 +#: windows/views.py:1525 msgid "MyLabel" msgstr "MonÉtiquette" -#: windows/views.py:972 +#: windows/views.py:982 msgid "Databases" msgstr "Bases de données" -#: windows/views.py:973 windows/views.py:1340 +#: windows/views.py:983 windows/views.py:1330 windows/views.py:2847 msgid "Size" msgstr "Taille" -#: windows/views.py:974 +#: windows/views.py:984 msgid "Elements" msgstr "Éléments" -#: windows/views.py:975 +#: windows/views.py:985 msgid "Modified at" msgstr "Modifié le" -#: windows/views.py:976 +#: windows/views.py:986 windows/views.py:1345 msgid "Tables" msgstr "Tables" -#: windows/views.py:983 +#: windows/views.py:993 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:1039 +#: windows/views.py:1334 windows/views.py:2843 msgid "Collation" msgstr "Classement" -#: windows/views.py:1058 +#: windows/views.py:1068 msgid "Encryption" msgstr "" -#: windows/views.py:1070 +#: windows/views.py:1080 msgid "Read Only" msgstr "" -#: windows/views.py:1087 +#: windows/views.py:1097 #, fuzzy msgid "Tablespace" msgstr "Tables" -#: windows/views.py:1108 +#: windows/views.py:1118 #, fuzzy msgid "Connection limit" msgstr "Connexion perdue" -#: windows/views.py:1151 +#: windows/views.py:1161 #, fuzzy msgid "Profile" msgstr "Fichier" -#: windows/views.py:1177 +#: windows/views.py:1187 #, fuzzy msgid "Default tablespace" msgstr "Supprimer la table" -#: windows/views.py:1198 +#: windows/views.py:1208 #, fuzzy msgid "Temporary tablespace" msgstr "Temporaire" -#: windows/views.py:1224 +#: windows/views.py:1234 msgid "Quota" msgstr "" -#: windows/views.py:1243 +#: windows/views.py:1253 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1260 +#: windows/views.py:1270 msgid "Account status" msgstr "" -#: windows/views.py:1281 +#: windows/views.py:1291 #, fuzzy msgid "Password expire" msgstr "Mot de passe" -#: windows/views.py:1302 -msgid "Table:" -msgstr "Table :" +#: windows/views.py:1315 +#, fuzzy +msgid "Add new table" +msgstr "Supprimer la 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:1317 +#, fuzzy +msgid "Clone table" +msgstr "Supprimer la table" -#: windows/views.py:1315 -msgid "Clone" -msgstr "Cloner" +#: windows/main/controller.py:1633 windows/views.py:1319 +msgid "Delete table" +msgstr "Supprimer la table" -#: windows/views.py:1339 +#: windows/views.py:1329 msgid "Rows" msgstr "Lignes" -#: windows/views.py:1342 +#: windows/views.py:1332 windows/views.py:2845 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:1350 +msgid "Add new view" +msgstr "" + +#: windows/views.py:1352 windows/views.py:1376 +#, fuzzy +msgid "Clone view" +msgstr "Cloner" + +#: windows/views.py:1354 +#, fuzzy +msgid "Delete view" +msgstr "Supprimer" + +#: windows/views.py:1362 windows/views.py:1386 windows/views.py:1410 +#: windows/views.py:1434 windows/views.py:1458 +#, fuzzy +msgid "Definition" +msgstr "Condition" + +#: windows/views.py:1369 windows/views.py:2177 +msgid "Views" +msgstr "Vues" + +#: windows/views.py:1374 +msgid "Add new procedure" +msgstr "" + +#: windows/views.py:1376 +msgid "Clone procedure" +msgstr "" + +#: windows/views.py:1378 +#, fuzzy +msgid "Delete procedure" +msgstr "Supprimer un enregistrement" + +#: windows/views.py:1393 +msgid "Procedures" +msgstr "" + +#: windows/views.py:1398 +#, fuzzy +msgid "Add new function" +msgstr "Nouvelle connexion" + +#: windows/views.py:1400 +#, fuzzy +msgid "Clone function" +msgstr "Nouvelle connexion" + +#: windows/views.py:1402 +#, fuzzy +msgid "Delete function" +msgstr "Supprimer un enregistrement" + +#: windows/views.py:1417 +#, fuzzy +msgid "Functions" +msgstr "Connexion" + +#: windows/views.py:1422 +#, fuzzy +msgid "Add new trigger" +msgstr "Déclencheurs" + +#: windows/views.py:1424 +#, fuzzy +msgid "Clone trigger" +msgstr "Déclencheurs" + +#: windows/views.py:1426 +#, fuzzy +msgid "Delete trigger" +msgstr "Supprimer un enregistrement" + +#: windows/views.py:1441 windows/views.py:2185 +msgid "Triggers" +msgstr "Déclencheurs" + +#: windows/views.py:1446 +msgid "Add new event" +msgstr "" + +#: windows/views.py:1448 +#, fuzzy +msgid "Clone event" +msgstr "Cloner" + +#: windows/views.py:1450 +#, fuzzy +msgid "Delete event" +msgstr "Supprimer la table" + +#: windows/views.py:1465 +#, fuzzy +msgid "Events" +msgstr "Éléments" + +#: windows/views.py:1493 windows/views.py:1904 windows/views.py:2214 +#: windows/views.py:2296 windows/views.py:2915 msgid "Apply" msgstr "Appliquer" -#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 -#: windows/views.py:2909 +#: windows/main/database/procedure.py:122 windows/views.py:1505 +#: windows/views.py:1684 windows/views.py:2114 windows/views.py:3167 msgid "Options" msgstr "Options" -#: windows/views.py:1413 +#: windows/views.py:1536 msgid "Diagram" msgstr "Diagramme" -#: windows/views.py:1424 +#: windows/views.py:1547 msgid "Database" msgstr "Base de données" -#: windows/views.py:1479 windows/views.py:2849 +#: windows/views.py:1602 windows/views.py:3107 msgid "Base" msgstr "Base" -#: windows/views.py:1493 windows/views.py:2863 +#: windows/views.py:1616 windows/views.py:3121 msgid "Auto Increment" msgstr "Auto incrément" -#: windows/views.py:1521 windows/views.py:2891 +#: windows/views.py:1644 windows/views.py:3149 msgid "Default Collation" msgstr "Classement par défaut" -#: windows/views.py:1531 +#: windows/views.py:1654 msgid "Convert data" msgstr "" -#: windows/views.py:1539 +#: windows/views.py:1662 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:1696 windows/views.py:1737 windows/views.py:1781 +#: windows/views.py:1879 windows/views.py:2204 msgid "Remove" msgstr "Supprimer" -#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 +#: windows/views.py:1703 windows/views.py:1744 windows/views.py:1788 msgid "Clear" msgstr "Effacer" -#: windows/views.py:1595 windows/views.py:2923 +#: windows/views.py:1718 windows/views.py:3181 msgid "Indexes" msgstr "Index" -#: windows/views.py:1639 +#: windows/views.py:1732 windows/views.py:1776 windows/views.py:2927 +#: windows/views.py:2969 windows/views.py:3210 +msgid "Insert" +msgstr "Insérer" + +#: windows/views.py:1762 msgid "Foreign Keys" msgstr "Clés étrangères" -#: windows/views.py:1683 +#: windows/views.py:1806 msgid "Checks" msgstr "Contrôles" -#: windows/views.py:1750 windows/views.py:2944 +#: windows/views.py:1873 windows/views.py:3202 msgid "Columns:" msgstr "Colonnes :" -#: windows/views.py:1760 +#: windows/views.py:1883 #, fuzzy msgid "Move Up" msgstr "Déplacer vers le haut\tCTRL+UP" -#: windows/views.py:1762 +#: windows/views.py:1885 #, fuzzy 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:1917 windows/views.py:1924 windows/views.py:3268 +#: windows/views.py:3275 msgid "Add Index" msgstr "Ajouter un index" -#: windows/views.py:1798 windows/views.py:3014 +#: windows/views.py:1921 windows/views.py:3272 msgid "Add PrimaryKey" msgstr "Ajouter une clé primaire" -#: windows/views.py:1815 +#: windows/views.py:1938 msgid "Table" msgstr "Table" -#: windows/views.py:1851 +#: windows/main/database/procedure.py:140 windows/views.py:1974 #, fuzzy msgid "Definer" msgstr "Insérer" -#: windows/views.py:1871 +#: windows/views.py:1994 msgid "Schema" msgstr "" -#: windows/views.py:1897 +#: windows/views.py:2020 msgid "SQL security" msgstr "" -#: windows/views.py:1904 +#: windows/views.py:2027 #, fuzzy msgid "DEFINER" msgstr "Insérer" -#: windows/views.py:1904 +#: windows/views.py:2027 #, fuzzy msgid "INVOKER" msgstr "Insérer" -#: windows/views.py:1916 +#: windows/views.py:2039 msgid "Algorithm" msgstr "" -#: windows/views.py:1918 +#: windows/views.py:2041 #, fuzzy msgid "UNDEFINED" msgstr "Non signé" -#: windows/views.py:1921 +#: windows/views.py:2044 msgid "MERGE" msgstr "" -#: windows/views.py:1924 +#: windows/views.py:2047 #, fuzzy msgid "TEMPTABLE" msgstr "Table" -#: windows/views.py:1934 +#: windows/views.py:2057 msgid "View constraint" msgstr "" -#: windows/views.py:1936 +#: windows/views.py:2059 #, fuzzy msgid "None" msgstr "Cloner" -#: windows/views.py:1939 +#: windows/views.py:2062 #, fuzzy msgid "LOCAL" msgstr "Localisation" -#: windows/views.py:1942 +#: windows/views.py:2065 #, fuzzy msgid "CASCADE" msgstr "Annuler" -#: windows/views.py:1945 +#: windows/views.py:2068 #, fuzzy msgid "CHECK ONLY" msgstr "Vérifier" -#: windows/views.py:1948 +#: windows/views.py:2071 msgid "READ ONLY" msgstr "" -#: windows/views.py:1960 +#: windows/views.py:2083 msgid "Force" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:2095 msgid "Security barrier" msgstr "" -#: windows/views.py:2054 -msgid "Views" -msgstr "Vues" - -#: windows/views.py:2062 -msgid "Triggers" -msgstr "Déclencheurs" - -#: windows/views.py:2073 -#, fuzzy -msgid "Refrsh" -msgstr "Actualiser" - -#: windows/views.py:2079 +#: windows/views.py:2202 #, fuzzy msgid "Duplicate" msgstr "Dupliquer un enregistrement" -#: windows/views.py:2085 +#: windows/views.py:2208 msgid "Apply changes automatically" msgstr "Appliquer les modifications automatiquement" -#: windows/views.py:2087 windows/views.py:2088 +#: windows/views.py:2210 windows/views.py:2211 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -697,122 +801,126 @@ 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:2224 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" -#: windows/views.py:2109 +#: windows/views.py:2232 #, fuzzy msgid "First" msgstr "Filtres" -#: windows/views.py:2127 +#: windows/views.py:2250 msgid "Last" msgstr "" -#: windows/views.py:2136 +#: windows/views.py:2259 msgid "Filters" msgstr "Filtres" -#: windows/views.py:2176 +#: windows/views.py:2299 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2196 +#: windows/views.py:2319 msgid "Insert row" msgstr "Insérer une ligne" -#: windows/views.py:2204 +#: windows/views.py:2327 msgid "Data" msgstr "Données" -#: windows/main/controller.py:278 windows/main/controller.py:287 -#: windows/main/controller.py:288 windows/views.py:2221 +#: windows/main/controller.py:318 windows/views.py:2344 #, fuzzy msgid "New query" msgstr "Requête" -#: windows/views.py:2223 windows/views.py:2660 +#: windows/views.py:2346 windows/views.py:2866 msgid "Close" msgstr "Fermer" -#: windows/main/controller.py:279 windows/main/controller.py:289 -#: windows/main/controller.py:290 windows/views.py:2223 +#: windows/main/controller.py:319 windows/views.py:2346 #, fuzzy msgid "Close query" msgstr "Requête" -#: windows/views.py:2227 +#: windows/views.py:2350 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:320 windows/views.py:2350 #, fuzzy msgid "Execute" msgstr "Exécutable SSH" -#: windows/views.py:2229 +#: windows/views.py:2352 msgid "Run all" msgstr "" -#: windows/main/controller.py:295 windows/views.py:2229 +#: windows/views.py:2352 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:322 windows/views.py:2354 msgid "Stop" msgstr "" -#: windows/views.py:2287 +#: windows/views.py:2419 msgid "a page" msgstr "" -#: windows/views.py:2315 +#: windows/views.py:2469 msgid "Query" msgstr "Requête" -#: windows/views.py:2626 +#: windows/views.py:2820 #, fuzzy msgid "Character set" msgstr "Créé le" -#: windows/views.py:2644 windows/views.py:2663 +#: windows/views.py:2850 windows/views.py:2869 msgid "New" msgstr "Nouveau" -#: windows/views.py:2683 +#: windows/views.py:2889 msgid "Insert record" msgstr "Insérer un enregistrement" -#: windows/views.py:2688 +#: windows/views.py:2894 msgid "Duplicate record" msgstr "Dupliquer un enregistrement" -#: windows/views.py:2695 +#: windows/views.py:2901 msgid "Delete record" msgstr "Supprimer un enregistrement" -#: windows/views.py:2733 windows/views.py:2964 +#: windows/views.py:2939 windows/views.py:3222 msgid "Up" msgstr "Haut" -#: windows/views.py:2740 windows/views.py:2971 +#: windows/views.py:2946 windows/views.py:3229 msgid "Down" msgstr "Bas" -#: windows/views.py:3100 +#: windows/views.py:2961 +msgid "Table:" +msgstr "Table :" + +#: windows/views.py:2974 +msgid "Clone" +msgstr "Cloner" + +#: windows/views.py:3358 msgid "Save Starments" msgstr "" -#: windows/views.py:3108 +#: windows/views.py:3366 #, fuzzy msgid "Location" msgstr "Classement" -#: windows/views.py:3115 +#: windows/views.py:3373 msgid "*.sql" msgstr "" @@ -836,7 +944,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 +956,75 @@ msgstr "Non signé" msgid "Zerofill" msgstr "Remplissage zéro" -#: windows/components/dataview.py:109 +#: windows/components/dataview.py:111 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" @@ -936,164 +1044,166 @@ 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 "" -#: 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 "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 "" -#: 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:762 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:775 +#: windows/dialogs/connections/view.py:787 #, fuzzy, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "Erreur de connexion" -#: windows/dialogs/connections/view.py:776 +#: windows/dialogs/connections/view.py:788 msgid "Connection error" msgstr "Erreur de connexion" -#: windows/dialogs/connections/view.py:802 +#: windows/dialogs/connections/view.py:814 #, fuzzy, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" msgstr "Voulez-vous supprimer les enregistrements ?" -#: windows/dialogs/connections/view.py:805 -#: windows/dialogs/connections/view.py:822 +#: windows/dialogs/connections/view.py:817 +#: windows/dialogs/connections/view.py:834 msgid "Confirm delete" msgstr "Confirmer la suppression" -#: windows/dialogs/connections/view.py:819 +#: windows/dialogs/connections/view.py:831 #, fuzzy, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "Voulez-vous supprimer les enregistrements ?" -#: windows/main/controller.py:275 +#: windows/main/controller.py:315 #, python-brace-format msgid "{text} ({shortcut})" msgstr "" -#: windows/main/controller.py:281 windows/main/controller.py:294 +#: windows/main/controller.py:321 #, fuzzy msgid "Execute all" msgstr "Exécutable SSH" -#: windows/main/controller.py:471 windows/main/controller.py:479 +#: windows/main/controller.py:440 windows/main/controller.py:448 #, fuzzy msgid "Query (1)" msgstr "Requête" -#: windows/main/controller.py:497 +#: windows/main/controller.py:467 #, python-brace-format msgid "Query ({query_number})" msgstr "" -#: windows/main/controller.py:530 +#: windows/main/controller.py:516 msgid "You have unsaved changes. Save before closing?" msgstr "" -#: windows/main/controller.py:531 +#: windows/main/controller.py:517 msgid "Unsaved query" msgstr "" -#: windows/main/controller.py:576 +#: windows/main/controller.py:562 #, fuzzy msgid "Save query" msgstr "Requête" -#: windows/main/controller.py:579 +#: windows/main/controller.py:565 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:593 windows/main/controller.py:624 +#: windows/main/controller.py:651 windows/main/database/list.py:119 +#: windows/main/database/procedure.py:294 +#: windows/main/database/procedure.py:320 windows/main/database/view.py:267 +#: windows/main/database/view.py:296 windows/main/query/controller.py:177 msgid "Error" msgstr "Erreur" -#: windows/main/controller.py:622 +#: windows/main/controller.py:631 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "" -#: windows/main/controller.py:647 +#: windows/main/controller.py:657 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "" -#: windows/main/controller.py:704 +#: windows/main/controller.py:719 msgid "days" msgstr "jours" -#: windows/main/controller.py:705 +#: windows/main/controller.py:720 msgid "hours" msgstr "heures" -#: windows/main/controller.py:706 +#: windows/main/controller.py:721 msgid "minutes" msgstr "minutes" -#: windows/main/controller.py:707 +#: windows/main/controller.py:722 msgid "seconds" msgstr "secondes" -#: windows/main/controller.py:715 +#: windows/main/controller.py:730 #, 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:766 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:952 +#: windows/main/controller.py:990 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:954 +#: windows/main/controller.py:992 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:1119 +#: windows/main/controller.py:1214 msgid "Version" msgstr "Version" -#: windows/main/controller.py:1121 +#: windows/main/controller.py:1216 msgid "Uptime" msgstr "Temps de fonctionnement" -#: windows/main/controller.py:1199 +#: windows/main/controller.py:1299 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:1232 +#: windows/main/controller.py:1332 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1103,99 +1213,132 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:1237 windows/main/controller.py:1258 +#: windows/main/controller.py:1337 windows/main/controller.py:1358 #, fuzzy msgid "Delete database" msgstr "Supprimer la table" -#: windows/main/controller.py:1243 +#: windows/main/controller.py:1343 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:1244 +#: windows/main/controller.py:1344 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:1257 +#: windows/main/controller.py:1357 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:1272 +#: windows/main/controller.py:1372 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:1373 windows/main/database/procedure.py:283 +#: windows/main/database/procedure.py:314 windows/main/database/view.py:255 +#: windows/main/database/view.py:290 msgid "Success" msgstr "" -#: windows/main/controller.py:1392 +#: windows/main/controller.py:1604 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:1418 +#: windows/main/controller.py:1630 #, fuzzy, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "Voulez-vous supprimer les enregistrements ?" -#: windows/main/controller.py:1421 -msgid "Delete table" -msgstr "Supprimer la table" - -#: windows/main/controller.py:1440 +#: windows/main/controller.py:1652 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1563 +#: windows/main/controller.py:1797 msgid "Do you want delete the records?" msgstr "Voulez-vous supprimer les enregistrements ?" -#: windows/main/database/list.py:69 +#: 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 +#: windows/main/database/list.py:108 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/procedure.py:114 +msgid "Procedure" +msgstr "" + +#: windows/main/database/procedure.py:151 +#, fuzzy +msgid "Parameters" +msgstr "PeterSQL" + +#: windows/main/database/procedure.py:282 +msgid "Procedure created successfully" +msgstr "" + +#: windows/main/database/procedure.py:282 +msgid "Procedure updated successfully" +msgstr "" + +#: windows/main/database/procedure.py:294 +#, python-brace-format +msgid "Error saving procedure: {}" +msgstr "" + +#: windows/main/database/procedure.py:305 +#, fuzzy, python-brace-format +msgid "Are you sure you want to delete procedure '{}'?" +msgstr "Voulez-vous supprimer les enregistrements ?" + +#: windows/main/database/procedure.py:306 windows/main/database/view.py:281 +#, fuzzy +msgid "Confirm Delete" +msgstr "Confirmer la suppression" + +#: windows/main/database/procedure.py:314 +msgid "Procedure deleted successfully" +msgstr "" + +#: windows/main/database/procedure.py:320 +#, python-brace-format +msgid "Error deleting procedure: {}" +msgstr "" + +#: windows/main/database/view.py:254 msgid "View created successfully" msgstr "" -#: windows/main/database/view.py:252 +#: windows/main/database/view.py:254 msgid "View updated successfully" msgstr "" -#: windows/main/database/view.py:256 +#: windows/main/database/view.py:267 #, python-brace-format msgid "Error saving view: {}" msgstr "" -#: windows/main/database/view.py:269 +#: windows/main/database/view.py:280 #, 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" - -#: windows/main/database/view.py:279 +#: windows/main/database/view.py:290 msgid "View deleted successfully" msgstr "" -#: windows/main/database/view.py:282 +#: windows/main/database/view.py:296 #, python-brace-format msgid "Error deleting view: {}" msgstr "" @@ -1234,6 +1377,11 @@ msgstr "" msgid "No active database connection" msgstr "Nouvelle connexion" +#: windows/main/query/history.py:55 +#, fuzzy +msgid "(empty query)" +msgstr "Requête" + #: windows/main/query/renderer.py:53 #, python-brace-format msgid "{affected_rows} rows affected" @@ -1274,6 +1422,10 @@ msgstr "" msgid "Error:" msgstr "Erreur" +#: windows/main/table/records.py:336 +msgid "Error saving records" +msgstr "" + #~ msgid "Created at:" #~ msgstr "" @@ -1409,3 +1561,6 @@ msgstr "Erreur" #~ msgid "Zero Fill" #~ msgstr "Remplissage zéro" +#~ msgid "Refrsh" +#~ msgstr "Actualiser" + diff --git a/locale/it_IT/LC_MESSAGES/petersql.po b/locale/it_IT/LC_MESSAGES/petersql.po index 9855aa5..32db9ee 100644 --- a/locale/it_IT/LC_MESSAGES/petersql.po +++ b/locale/it_IT/LC_MESSAGES/petersql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-23 10:07+0100\n" +"POT-Creation-Date: 2026-05-02 16:08+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" "Language: it_IT\n" @@ -47,75 +47,81 @@ 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:535 +msgid "This connection is read-only." +msgstr "" + +#: structures/engines/mariadb/context.py:616 +#: structures/engines/mysql/context.py:627 +#: structures/engines/postgresql/context.py:685 +#: structures/engines/sqlite/context.py:552 #, 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:644 +#: structures/engines/mysql/context.py:655 +#: structures/engines/postgresql/context.py:710 +#: structures/engines/sqlite/context.py:576 #, 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:662 +#: structures/engines/mysql/context.py:673 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:594 #, 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:702 +#: structures/engines/mysql/context.py:711 +#: structures/engines/postgresql/context.py:768 +#: structures/engines/sqlite/context.py:634 #, 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:735 +#: structures/engines/mysql/context.py:742 +#: structures/engines/postgresql/context.py:798 +#: structures/engines/sqlite/context.py:662 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:781 +#: structures/engines/mariadb/context.py:788 #, 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/dialogs/connections/view.py:417 +#: windows/dialogs/connections/view.py:764 windows/main/controller.py:1212 #: windows/views.py:33 msgid "Connection" msgstr "Connessione" -#: windows/components/dataview.py:113 windows/components/dataview.py:225 -#: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py: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/main/database/procedure.py:129 windows/views.py:47 +#: windows/views.py:97 windows/views.py:1016 windows/views.py:1328 +#: windows/views.py:1361 windows/views.py:1385 windows/views.py:1409 +#: windows/views.py:1433 windows/views.py:1457 windows/views.py:1574 +#: windows/views.py:1954 windows/views.py:3079 msgid "Name" msgstr "Nome" -#: windows/views.py:48 windows/views.py:428 +#: windows/views.py:48 windows/views.py:438 msgid "Last connection" msgstr "Ultima connessione" -#: windows/dialogs/connections/view.py:653 windows/views.py:61 +#: windows/dialogs/connections/view.py:655 windows/views.py:61 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:211 +#: windows/dialogs/connections/view.py:615 windows/views.py:65 msgid "New connection" msgstr "Nuova connessione" @@ -127,14 +133,15 @@ msgstr "Rinomina" msgid "Clone connection" msgstr "Chiudi 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/main/database/procedure.py:183 windows/views.py:81 +#: windows/views.py:613 windows/views.py:1488 windows/views.py:1896 +#: windows/views.py:2155 windows/views.py:2932 windows/views.py:2981 +#: windows/views.py:3215 windows/views.py:3247 msgid "Delete" msgstr "Elimina" -#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 -#: windows/views.py:2876 +#: windows/views.py:111 windows/views.py:1333 windows/views.py:1629 +#: windows/views.py:2844 windows/views.py:3134 msgid "Engine" msgstr "Motore" @@ -146,7 +153,7 @@ msgstr "Host + porta" msgid "Username" msgstr "Nome utente" -#: windows/views.py:161 windows/views.py:1132 +#: windows/views.py:161 windows/views.py:1142 msgid "Password" msgstr "Password" @@ -159,526 +166,623 @@ msgid "Use TLS" msgstr "" #: windows/views.py:203 +msgid "Mark read only" +msgstr "" + +#: windows/views.py:214 msgid "Use SSH tunnel" msgstr "Usa tunnel SSH" -#: windows/views.py:214 +#: windows/views.py:225 msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 +#: windows/views.py:244 msgid "Filename" msgstr "Nome file" -#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 +#: windows/views.py:249 windows/views.py:368 windows/views.py:3373 msgid "Select a file" msgstr "Seleziona un file" -#: windows/views.py:238 windows/views.py:358 +#: windows/views.py:249 windows/views.py:368 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:266 windows/views.py:1335 windows/views.py:1587 +#: windows/views.py:2842 windows/views.py:3092 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:766 windows/views.py:280 windows/views.py:740 +#: windows/views.py:894 msgid "Settings" msgstr "Impostazioni" -#: windows/views.py:278 +#: windows/views.py:288 msgid "SSH executable" msgstr "Eseguibile SSH" -#: windows/views.py:283 +#: windows/views.py:293 msgid "ssh" msgstr "ssh" -#: windows/views.py:291 +#: windows/views.py:301 msgid "SSH host + port" msgstr "Host SSH + porta" -#: windows/views.py:303 +#: windows/views.py:313 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "" -#: windows/views.py:312 +#: windows/views.py:322 msgid "SSH username" msgstr "Nome utente SSH" -#: windows/views.py:325 +#: windows/views.py:335 msgid "SSH password" msgstr "Password SSH" -#: windows/views.py:338 +#: windows/views.py:348 msgid "Local port" msgstr "Porta locale" -#: windows/views.py:344 +#: windows/views.py:354 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:363 msgid "Identity file" msgstr "" -#: windows/views.py:369 +#: windows/views.py:379 msgid "Remote host + port" msgstr "Remoto host + porta" -#: windows/views.py:381 +#: windows/views.py:391 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "" -#: windows/views.py:390 +#: windows/views.py:400 msgid "SSH extra args" msgstr "" -#: windows/views.py:405 +#: windows/views.py:415 msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:411 windows/views.py:1341 +#: windows/views.py:421 windows/views.py:1331 windows/views.py:2846 msgid "Created at" msgstr "Creato il" -#: windows/views.py:445 +#: windows/views.py:455 msgid "Successful connections" msgstr "Connessioni riuscite" -#: windows/views.py:462 +#: windows/views.py:472 msgid "Last successful connection" msgstr "Ultima connessione riuscita" -#: windows/views.py:479 +#: windows/views.py:489 msgid "Unsuccessful connections" msgstr "Connessioni non riuscite" -#: windows/views.py:496 +#: windows/views.py:506 msgid "Last failure reason" msgstr "" -#: windows/views.py:513 +#: windows/views.py:523 msgid "Total connection attempts" msgstr "Totale connessioni" -#: windows/views.py:530 +#: windows/views.py:540 msgid "Average connection time (ms)" msgstr "Media in ms del tempo di connessione" -#: windows/views.py:547 +#: windows/views.py:557 msgid "Most recent connection duration" msgstr "" -#: windows/views.py:566 +#: windows/views.py:576 msgid "Statistics" msgstr "Statistiche" -#: windows/views.py:584 windows/views.py:1730 +#: windows/views.py:594 windows/views.py:1853 msgid "Create" msgstr "Crea" -#: windows/views.py:588 +#: windows/views.py:598 msgid "Create connection" msgstr "Nuova connessione" -#: windows/views.py:591 +#: windows/views.py:601 msgid "Create directory" msgstr "Nuova 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/main/database/procedure.py:184 windows/views.py:630 +#: windows/views.py:854 windows/views.py:1483 windows/views.py:1899 +#: windows/views.py:2160 windows/views.py:2216 windows/views.py:2908 +#: windows/views.py:3250 windows/views.py:3387 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:323 windows/main/database/procedure.py:185 +#: windows/views.py:635 windows/views.py:2165 windows/views.py:2358 +#: windows/views.py:3255 windows/views.py:3393 msgid "Save" msgstr "Salva" -#: windows/views.py:632 +#: windows/views.py:642 msgid "Test" msgstr "Testa" -#: windows/views.py:639 +#: windows/views.py:649 msgid "Connect" msgstr "Connetti" -#: windows/views.py:742 +#: windows/main/database/procedure.py:162 windows/views.py:752 msgid "Language" msgstr "Lingua" -#: windows/views.py:747 +#: windows/views.py:757 msgid "English" msgstr "Inglese" -#: windows/views.py:747 +#: windows/views.py:757 msgid "Italian" msgstr "Italiano" -#: windows/views.py:747 +#: windows/views.py:757 msgid "French" msgstr "Francese" -#: windows/views.py:759 +#: windows/views.py:769 msgid "Locale" msgstr "Localizzazione" -#: windows/views.py:780 +#: windows/views.py:790 #, fuzzy msgid "Column content" msgstr "Chiudi connessione" -#: windows/views.py:790 +#: windows/views.py:800 msgid "Syntax" msgstr "Sintassi" -#: windows/views.py:847 +#: windows/views.py:857 msgid "Ok" msgstr "Ok" -#: windows/views.py:878 +#: windows/views.py:888 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:887 +#: windows/views.py:897 msgid "File" msgstr "File" -#: windows/views.py:890 +#: windows/views.py:900 msgid "About" msgstr "Informazioni" -#: windows/views.py:893 +#: windows/views.py:903 msgid "Help" msgstr "Aiuto" -#: windows/views.py:898 +#: windows/views.py:908 msgid "Open connection manager" msgstr "Apri gestore connessioni" -#: windows/views.py:902 +#: windows/views.py:912 msgid "Disconnect from server" msgstr "Disconnetti dal server" -#: windows/views.py:904 +#: windows/views.py:914 msgid "tool" msgstr "strumento" -#: windows/views.py:904 +#: windows/views.py:914 windows/views.py:2196 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:918 windows/views.py:920 windows/views.py:1877 +#: windows/views.py:2200 windows/views.py:2344 msgid "Add" msgstr "Aggiungi" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 +#: windows/views.py:954 windows/views.py:958 windows/views.py:2322 msgid "MyMenuItem" msgstr "IlMioElementoMenu" -#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 +#: windows/views.py:961 windows/views.py:1927 windows/views.py:3278 msgid "MyMenu" msgstr "IlMioMenu" -#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 -#: windows/views.py:1402 +#: windows/views.py:976 windows/views.py:1511 windows/views.py:1518 +#: windows/views.py:1525 msgid "MyLabel" msgstr "LaMiaEtichetta" -#: windows/views.py:972 +#: windows/views.py:982 msgid "Databases" msgstr "Database" -#: windows/views.py:973 windows/views.py:1340 +#: windows/views.py:983 windows/views.py:1330 windows/views.py:2847 msgid "Size" msgstr "Dimensione" -#: windows/views.py:974 +#: windows/views.py:984 msgid "Elements" msgstr "Elementi" -#: windows/views.py:975 +#: windows/views.py:985 msgid "Modified at" msgstr "Modificato il" -#: windows/views.py:976 +#: windows/views.py:986 windows/views.py:1345 msgid "Tables" msgstr "Tabelle" -#: windows/views.py:983 +#: windows/views.py:993 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:1039 +#: windows/views.py:1334 windows/views.py:2843 msgid "Collation" msgstr "Ordinamento" -#: windows/views.py:1058 +#: windows/views.py:1068 msgid "Encryption" msgstr "" -#: windows/views.py:1070 +#: windows/views.py:1080 msgid "Read Only" msgstr "" -#: windows/views.py:1087 +#: windows/views.py:1097 #, fuzzy msgid "Tablespace" msgstr "Tabelle" -#: windows/views.py:1108 +#: windows/views.py:1118 #, fuzzy msgid "Connection limit" msgstr "Connessione persa" -#: windows/views.py:1151 +#: windows/views.py:1161 #, fuzzy msgid "Profile" msgstr "File" -#: windows/views.py:1177 +#: windows/views.py:1187 #, fuzzy msgid "Default tablespace" msgstr "Elimina tabella" -#: windows/views.py:1198 +#: windows/views.py:1208 #, fuzzy msgid "Temporary tablespace" msgstr "Temporaneo" -#: windows/views.py:1224 +#: windows/views.py:1234 msgid "Quota" msgstr "" -#: windows/views.py:1243 +#: windows/views.py:1253 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1260 +#: windows/views.py:1270 msgid "Account status" msgstr "" -#: windows/views.py:1281 +#: windows/views.py:1291 #, fuzzy msgid "Password expire" msgstr "Password" -#: windows/views.py:1302 -msgid "Table:" -msgstr "Tabella:" +#: windows/views.py:1315 +#, fuzzy +msgid "Add new table" +msgstr "Elimina 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:1317 +#, fuzzy +msgid "Clone table" +msgstr "Elimina tabella" -#: windows/views.py:1315 -msgid "Clone" -msgstr "Clona" +#: windows/main/controller.py:1633 windows/views.py:1319 +msgid "Delete table" +msgstr "Elimina tabella" -#: windows/views.py:1339 +#: windows/views.py:1329 msgid "Rows" msgstr "Righe" -#: windows/views.py:1342 +#: windows/views.py:1332 windows/views.py:2845 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:1350 +msgid "Add new view" +msgstr "" + +#: windows/views.py:1352 windows/views.py:1376 +#, fuzzy +msgid "Clone view" +msgstr "Clona" + +#: windows/views.py:1354 +#, fuzzy +msgid "Delete view" +msgstr "Elimina" + +#: windows/views.py:1362 windows/views.py:1386 windows/views.py:1410 +#: windows/views.py:1434 windows/views.py:1458 +#, fuzzy +msgid "Definition" +msgstr "Condizione" + +#: windows/views.py:1369 windows/views.py:2177 +msgid "Views" +msgstr "Viste" + +#: windows/views.py:1374 +msgid "Add new procedure" +msgstr "" + +#: windows/views.py:1376 +msgid "Clone procedure" +msgstr "" + +#: windows/views.py:1378 +#, fuzzy +msgid "Delete procedure" +msgstr "Elimina record" + +#: windows/views.py:1393 +msgid "Procedures" +msgstr "" + +#: windows/views.py:1398 +#, fuzzy +msgid "Add new function" +msgstr "Nuova connessione" + +#: windows/views.py:1400 +#, fuzzy +msgid "Clone function" +msgstr "Chiudi connessione" + +#: windows/views.py:1402 +#, fuzzy +msgid "Delete function" +msgstr "Elimina record" + +#: windows/views.py:1417 +#, fuzzy +msgid "Functions" +msgstr "Connessione" + +#: windows/views.py:1422 +#, fuzzy +msgid "Add new trigger" +msgstr "Trigger" + +#: windows/views.py:1424 +#, fuzzy +msgid "Clone trigger" +msgstr "Trigger" + +#: windows/views.py:1426 +#, fuzzy +msgid "Delete trigger" +msgstr "Elimina record" + +#: windows/views.py:1441 windows/views.py:2185 +msgid "Triggers" +msgstr "Trigger" + +#: windows/views.py:1446 +msgid "Add new event" +msgstr "" + +#: windows/views.py:1448 +#, fuzzy +msgid "Clone event" +msgstr "Clona" + +#: windows/views.py:1450 +#, fuzzy +msgid "Delete event" +msgstr "Elimina tabella" + +#: windows/views.py:1465 +#, fuzzy +msgid "Events" +msgstr "Elementi" + +#: windows/views.py:1493 windows/views.py:1904 windows/views.py:2214 +#: windows/views.py:2296 windows/views.py:2915 msgid "Apply" msgstr "Applica" -#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 -#: windows/views.py:2909 +#: windows/main/database/procedure.py:122 windows/views.py:1505 +#: windows/views.py:1684 windows/views.py:2114 windows/views.py:3167 msgid "Options" msgstr "Opzioni" -#: windows/views.py:1413 +#: windows/views.py:1536 msgid "Diagram" msgstr "Diagramma" -#: windows/views.py:1424 +#: windows/views.py:1547 msgid "Database" msgstr "Database" -#: windows/views.py:1479 windows/views.py:2849 +#: windows/views.py:1602 windows/views.py:3107 msgid "Base" msgstr "Base" -#: windows/views.py:1493 windows/views.py:2863 +#: windows/views.py:1616 windows/views.py:3121 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1521 windows/views.py:2891 +#: windows/views.py:1644 windows/views.py:3149 msgid "Default Collation" msgstr "Ordinamento predefinito" -#: windows/views.py:1531 +#: windows/views.py:1654 msgid "Convert data" msgstr "" -#: windows/views.py:1539 +#: windows/views.py:1662 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:1696 windows/views.py:1737 windows/views.py:1781 +#: windows/views.py:1879 windows/views.py:2204 msgid "Remove" msgstr "Rimuovi" -#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 +#: windows/views.py:1703 windows/views.py:1744 windows/views.py:1788 msgid "Clear" msgstr "Pulisci" -#: windows/views.py:1595 windows/views.py:2923 +#: windows/views.py:1718 windows/views.py:3181 msgid "Indexes" msgstr "Indici" -#: windows/views.py:1639 +#: windows/views.py:1732 windows/views.py:1776 windows/views.py:2927 +#: windows/views.py:2969 windows/views.py:3210 +msgid "Insert" +msgstr "Inserisci" + +#: windows/views.py:1762 msgid "Foreign Keys" msgstr "Chiavi esterne" -#: windows/views.py:1683 +#: windows/views.py:1806 msgid "Checks" msgstr "Vincoli" -#: windows/views.py:1750 windows/views.py:2944 +#: windows/views.py:1873 windows/views.py:3202 msgid "Columns:" msgstr "Colonne:" -#: windows/views.py:1760 +#: windows/views.py:1883 #, fuzzy msgid "Move Up" msgstr "Sposta su\tCTRL+UP" -#: windows/views.py:1762 +#: windows/views.py:1885 #, fuzzy 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:1917 windows/views.py:1924 windows/views.py:3268 +#: windows/views.py:3275 msgid "Add Index" msgstr "Aggiungi indice" -#: windows/views.py:1798 windows/views.py:3014 +#: windows/views.py:1921 windows/views.py:3272 msgid "Add PrimaryKey" msgstr "Aggiungi chiave primaria" -#: windows/views.py:1815 +#: windows/views.py:1938 msgid "Table" msgstr "Tabella" -#: windows/views.py:1851 +#: windows/main/database/procedure.py:140 windows/views.py:1974 #, fuzzy msgid "Definer" msgstr "Inserisci" -#: windows/views.py:1871 +#: windows/views.py:1994 msgid "Schema" msgstr "" -#: windows/views.py:1897 +#: windows/views.py:2020 msgid "SQL security" msgstr "" -#: windows/views.py:1904 +#: windows/views.py:2027 #, fuzzy msgid "DEFINER" msgstr "Inserisci" -#: windows/views.py:1904 +#: windows/views.py:2027 #, fuzzy msgid "INVOKER" msgstr "Inserisci" -#: windows/views.py:1916 +#: windows/views.py:2039 msgid "Algorithm" msgstr "" -#: windows/views.py:1918 +#: windows/views.py:2041 #, fuzzy msgid "UNDEFINED" msgstr "Senza segno" -#: windows/views.py:1921 +#: windows/views.py:2044 msgid "MERGE" msgstr "" -#: windows/views.py:1924 +#: windows/views.py:2047 #, fuzzy msgid "TEMPTABLE" msgstr "Tabella" -#: windows/views.py:1934 +#: windows/views.py:2057 msgid "View constraint" msgstr "" -#: windows/views.py:1936 +#: windows/views.py:2059 #, fuzzy msgid "None" msgstr "Clona" -#: windows/views.py:1939 +#: windows/views.py:2062 #, fuzzy msgid "LOCAL" msgstr "Localizzazione" -#: windows/views.py:1942 +#: windows/views.py:2065 #, fuzzy msgid "CASCADE" msgstr "Annulla" -#: windows/views.py:1945 +#: windows/views.py:2068 #, fuzzy msgid "CHECK ONLY" msgstr "Verifica" -#: windows/views.py:1948 +#: windows/views.py:2071 msgid "READ ONLY" msgstr "" -#: windows/views.py:1960 +#: windows/views.py:2083 msgid "Force" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:2095 msgid "Security barrier" msgstr "" -#: windows/views.py:2054 -msgid "Views" -msgstr "Viste" - -#: windows/views.py:2062 -msgid "Triggers" -msgstr "Trigger" - -#: windows/views.py:2073 -#, fuzzy -msgid "Refrsh" -msgstr "Aggiorna" - -#: windows/views.py:2079 +#: windows/views.py:2202 #, fuzzy msgid "Duplicate" msgstr "Duplica record" -#: windows/views.py:2085 +#: windows/views.py:2208 msgid "Apply changes automatically" msgstr "Applica modifiche automaticamente" -#: windows/views.py:2087 windows/views.py:2088 +#: windows/views.py:2210 windows/views.py:2211 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" @@ -686,122 +790,126 @@ msgstr "" "Se abilitato, le modifiche alla tabella vengono applicate immediatamente " "senza premere Applica o Annulla" -#: windows/views.py:2101 +#: windows/views.py:2224 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" -#: windows/views.py:2109 +#: windows/views.py:2232 #, fuzzy msgid "First" msgstr "Filtri" -#: windows/views.py:2127 +#: windows/views.py:2250 msgid "Last" msgstr "" -#: windows/views.py:2136 +#: windows/views.py:2259 msgid "Filters" msgstr "Filtri" -#: windows/views.py:2176 +#: windows/views.py:2299 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2196 +#: windows/views.py:2319 msgid "Insert row" msgstr "Inserisci riga" -#: windows/views.py:2204 +#: windows/views.py:2327 msgid "Data" msgstr "Dati" -#: windows/main/controller.py:278 windows/main/controller.py:287 -#: windows/main/controller.py:288 windows/views.py:2221 +#: windows/main/controller.py:318 windows/views.py:2344 #, fuzzy msgid "New query" msgstr "Query" -#: windows/views.py:2223 windows/views.py:2660 +#: windows/views.py:2346 windows/views.py:2866 msgid "Close" msgstr "Chiudi" -#: windows/main/controller.py:279 windows/main/controller.py:289 -#: windows/main/controller.py:290 windows/views.py:2223 +#: windows/main/controller.py:319 windows/views.py:2346 #, fuzzy msgid "Close query" msgstr "Query" -#: windows/views.py:2227 +#: windows/views.py:2350 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:320 windows/views.py:2350 #, fuzzy msgid "Execute" msgstr "Eseguibile SSH" -#: windows/views.py:2229 +#: windows/views.py:2352 msgid "Run all" msgstr "" -#: windows/main/controller.py:295 windows/views.py:2229 +#: windows/views.py:2352 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:322 windows/views.py:2354 msgid "Stop" msgstr "" -#: windows/views.py:2287 +#: windows/views.py:2419 msgid "a page" msgstr "" -#: windows/views.py:2315 +#: windows/views.py:2469 msgid "Query" msgstr "Query" -#: windows/views.py:2626 +#: windows/views.py:2820 #, fuzzy msgid "Character set" msgstr "Creato il" -#: windows/views.py:2644 windows/views.py:2663 +#: windows/views.py:2850 windows/views.py:2869 msgid "New" msgstr "Nuovo" -#: windows/views.py:2683 +#: windows/views.py:2889 msgid "Insert record" msgstr "Inserisci record" -#: windows/views.py:2688 +#: windows/views.py:2894 msgid "Duplicate record" msgstr "Duplica record" -#: windows/views.py:2695 +#: windows/views.py:2901 msgid "Delete record" msgstr "Elimina record" -#: windows/views.py:2733 windows/views.py:2964 +#: windows/views.py:2939 windows/views.py:3222 msgid "Up" msgstr "Su" -#: windows/views.py:2740 windows/views.py:2971 +#: windows/views.py:2946 windows/views.py:3229 msgid "Down" msgstr "Giù" -#: windows/views.py:3100 +#: windows/views.py:2961 +msgid "Table:" +msgstr "Tabella:" + +#: windows/views.py:2974 +msgid "Clone" +msgstr "Clona" + +#: windows/views.py:3358 msgid "Save Starments" msgstr "" -#: windows/views.py:3108 +#: windows/views.py:3366 #, fuzzy msgid "Location" msgstr "Ordinamento" -#: windows/views.py:3115 +#: windows/views.py:3373 msgid "*.sql" msgstr "" @@ -825,7 +933,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 +945,75 @@ msgstr "Senza segno" msgid "Zerofill" msgstr "Riempimento zero" -#: windows/components/dataview.py:109 +#: windows/components/dataview.py:111 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" @@ -925,164 +1033,166 @@ 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 "" -#: 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 "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 "" -#: 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:762 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:775 +#: windows/dialogs/connections/view.py:787 #, fuzzy, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "Errore di connessione" -#: windows/dialogs/connections/view.py:776 +#: windows/dialogs/connections/view.py:788 msgid "Connection error" msgstr "Errore di connessione" -#: windows/dialogs/connections/view.py:802 +#: windows/dialogs/connections/view.py:814 #, fuzzy, python-brace-format msgid "Do you want to delete the connection '{connection_name}'?" msgstr "Vuoi eliminare i record?" -#: windows/dialogs/connections/view.py:805 -#: windows/dialogs/connections/view.py:822 +#: windows/dialogs/connections/view.py:817 +#: windows/dialogs/connections/view.py:834 msgid "Confirm delete" msgstr "Conferma eliminazione" -#: windows/dialogs/connections/view.py:819 +#: windows/dialogs/connections/view.py:831 #, fuzzy, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "Vuoi eliminare i record?" -#: windows/main/controller.py:275 +#: windows/main/controller.py:315 #, python-brace-format msgid "{text} ({shortcut})" msgstr "" -#: windows/main/controller.py:281 windows/main/controller.py:294 +#: windows/main/controller.py:321 #, fuzzy msgid "Execute all" msgstr "Eseguibile SSH" -#: windows/main/controller.py:471 windows/main/controller.py:479 +#: windows/main/controller.py:440 windows/main/controller.py:448 #, fuzzy msgid "Query (1)" msgstr "Query" -#: windows/main/controller.py:497 +#: windows/main/controller.py:467 #, python-brace-format msgid "Query ({query_number})" msgstr "" -#: windows/main/controller.py:530 +#: windows/main/controller.py:516 msgid "You have unsaved changes. Save before closing?" msgstr "" -#: windows/main/controller.py:531 +#: windows/main/controller.py:517 msgid "Unsaved query" msgstr "" -#: windows/main/controller.py:576 +#: windows/main/controller.py:562 #, fuzzy msgid "Save query" msgstr "Query" -#: windows/main/controller.py:579 +#: windows/main/controller.py:565 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:593 windows/main/controller.py:624 +#: windows/main/controller.py:651 windows/main/database/list.py:119 +#: windows/main/database/procedure.py:294 +#: windows/main/database/procedure.py:320 windows/main/database/view.py:267 +#: windows/main/database/view.py:296 windows/main/query/controller.py:177 msgid "Error" msgstr "Errore" -#: windows/main/controller.py:622 +#: windows/main/controller.py:631 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "" -#: windows/main/controller.py:647 +#: windows/main/controller.py:657 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "" -#: windows/main/controller.py:704 +#: windows/main/controller.py:719 msgid "days" msgstr "giorni" -#: windows/main/controller.py:705 +#: windows/main/controller.py:720 msgid "hours" msgstr "ore" -#: windows/main/controller.py:706 +#: windows/main/controller.py:721 msgid "minutes" msgstr "minuti" -#: windows/main/controller.py:707 +#: windows/main/controller.py:722 msgid "seconds" msgstr "secondi" -#: windows/main/controller.py:715 +#: windows/main/controller.py:730 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizzata: {used} ({percentage:.2%})" -#: windows/main/controller.py:751 +#: windows/main/controller.py:766 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:952 +#: windows/main/controller.py:990 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:954 +#: windows/main/controller.py:992 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:1119 +#: windows/main/controller.py:1214 msgid "Version" msgstr "Versione" -#: windows/main/controller.py:1121 +#: windows/main/controller.py:1216 msgid "Uptime" msgstr "Tempo di attività" -#: windows/main/controller.py:1199 +#: windows/main/controller.py:1299 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:1232 +#: windows/main/controller.py:1332 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1092,99 +1202,132 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:1237 windows/main/controller.py:1258 +#: windows/main/controller.py:1337 windows/main/controller.py:1358 #, fuzzy msgid "Delete database" msgstr "Elimina tabella" -#: windows/main/controller.py:1243 +#: windows/main/controller.py:1343 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:1244 +#: windows/main/controller.py:1344 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:1257 +#: windows/main/controller.py:1357 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:1272 +#: windows/main/controller.py:1372 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:1373 windows/main/database/procedure.py:283 +#: windows/main/database/procedure.py:314 windows/main/database/view.py:255 +#: windows/main/database/view.py:290 msgid "Success" msgstr "" -#: windows/main/controller.py:1392 +#: windows/main/controller.py:1604 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:1418 +#: windows/main/controller.py:1630 #, fuzzy, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "Vuoi eliminare i record?" -#: windows/main/controller.py:1421 -msgid "Delete table" -msgstr "Elimina tabella" - -#: windows/main/controller.py:1440 +#: windows/main/controller.py:1652 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1563 +#: windows/main/controller.py:1797 msgid "Do you want delete the records?" msgstr "Vuoi eliminare i record?" -#: windows/main/database/list.py:69 +#: 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 +#: windows/main/database/list.py:108 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/procedure.py:114 +msgid "Procedure" +msgstr "" + +#: windows/main/database/procedure.py:151 +#, fuzzy +msgid "Parameters" +msgstr "PeterSQL" + +#: windows/main/database/procedure.py:282 +msgid "Procedure created successfully" +msgstr "" + +#: windows/main/database/procedure.py:282 +msgid "Procedure updated successfully" +msgstr "" + +#: windows/main/database/procedure.py:294 +#, python-brace-format +msgid "Error saving procedure: {}" +msgstr "" + +#: windows/main/database/procedure.py:305 +#, fuzzy, python-brace-format +msgid "Are you sure you want to delete procedure '{}'?" +msgstr "Vuoi eliminare i record?" + +#: windows/main/database/procedure.py:306 windows/main/database/view.py:281 +#, fuzzy +msgid "Confirm Delete" +msgstr "Conferma eliminazione" + +#: windows/main/database/procedure.py:314 +msgid "Procedure deleted successfully" +msgstr "" + +#: windows/main/database/procedure.py:320 +#, python-brace-format +msgid "Error deleting procedure: {}" +msgstr "" + +#: windows/main/database/view.py:254 msgid "View created successfully" msgstr "" -#: windows/main/database/view.py:252 +#: windows/main/database/view.py:254 msgid "View updated successfully" msgstr "" -#: windows/main/database/view.py:256 +#: windows/main/database/view.py:267 #, python-brace-format msgid "Error saving view: {}" msgstr "" -#: windows/main/database/view.py:269 +#: windows/main/database/view.py:280 #, 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" - -#: windows/main/database/view.py:279 +#: windows/main/database/view.py:290 msgid "View deleted successfully" msgstr "" -#: windows/main/database/view.py:282 +#: windows/main/database/view.py:296 #, python-brace-format msgid "Error deleting view: {}" msgstr "" @@ -1223,6 +1366,11 @@ msgstr "" msgid "No active database connection" msgstr "Nuova connessione" +#: windows/main/query/history.py:55 +#, fuzzy +msgid "(empty query)" +msgstr "Query" + #: windows/main/query/renderer.py:53 #, python-brace-format msgid "{affected_rows} rows affected" @@ -1263,6 +1411,10 @@ msgstr "" msgid "Error:" msgstr "Errore" +#: windows/main/table/records.py:336 +msgid "Error saving records" +msgstr "" + #~ msgid "Created at:" #~ msgstr "" @@ -1392,3 +1544,6 @@ msgstr "Errore" #~ msgid "Zero Fill" #~ msgstr "Riempimento zero" +#~ msgid "Refrsh" +#~ msgstr "Aggiorna" + diff --git a/locale/petersql.pot b/locale/petersql.pot index fcc7476..f155e7d 100644 --- a/locale/petersql.pot +++ b/locale/petersql.pot @@ -27,75 +27,81 @@ 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:535 +msgid "This connection is read-only." +msgstr "" + +#: structures/engines/mariadb/context.py:616 +#: structures/engines/mysql/context.py:627 +#: structures/engines/postgresql/context.py:685 +#: structures/engines/sqlite/context.py:552 #, 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:644 +#: structures/engines/mysql/context.py:655 +#: structures/engines/postgresql/context.py:710 +#: structures/engines/sqlite/context.py:576 #, 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:662 +#: structures/engines/mysql/context.py:673 +#: structures/engines/postgresql/context.py:728 +#: structures/engines/sqlite/context.py:594 #, 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:702 +#: structures/engines/mysql/context.py:711 +#: structures/engines/postgresql/context.py:768 +#: structures/engines/sqlite/context.py:634 #, 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:735 +#: structures/engines/mysql/context.py:742 +#: structures/engines/postgresql/context.py:798 +#: structures/engines/sqlite/context.py:662 #, python-brace-format msgid "View{view_index:03}" msgstr "" -#: structures/engines/mariadb/context.py:781 +#: structures/engines/mariadb/context.py:788 #, 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/dialogs/connections/view.py:417 +#: windows/dialogs/connections/view.py:764 windows/main/controller.py:1212 #: windows/views.py:33 msgid "Connection" msgstr "" -#: windows/components/dataview.py:113 windows/components/dataview.py:225 -#: windows/components/dataview.py:238 windows/components/dataview.py:253 -#: windows/views.py:47 windows/views.py:97 windows/views.py: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/main/database/procedure.py:129 windows/views.py:47 +#: windows/views.py:97 windows/views.py:1016 windows/views.py:1328 +#: windows/views.py:1361 windows/views.py:1385 windows/views.py:1409 +#: windows/views.py:1433 windows/views.py:1457 windows/views.py:1574 +#: windows/views.py:1954 windows/views.py:3079 msgid "Name" msgstr "" -#: windows/views.py:48 windows/views.py:428 +#: windows/views.py:48 windows/views.py:438 msgid "Last connection" msgstr "" -#: windows/dialogs/connections/view.py:653 windows/views.py:61 +#: windows/dialogs/connections/view.py:655 windows/views.py:61 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:211 +#: windows/dialogs/connections/view.py:615 windows/views.py:65 msgid "New connection" msgstr "" @@ -107,14 +113,15 @@ msgstr "" 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/main/database/procedure.py:183 windows/views.py:81 +#: windows/views.py:613 windows/views.py:1488 windows/views.py:1896 +#: windows/views.py:2155 windows/views.py:2932 windows/views.py:2981 +#: windows/views.py:3215 windows/views.py:3247 msgid "Delete" msgstr "" -#: windows/views.py:111 windows/views.py:1343 windows/views.py:1506 -#: windows/views.py:2876 +#: windows/views.py:111 windows/views.py:1333 windows/views.py:1629 +#: windows/views.py:2844 windows/views.py:3134 msgid "Engine" msgstr "" @@ -126,7 +133,7 @@ msgstr "" msgid "Username" msgstr "" -#: windows/views.py:161 windows/views.py:1132 +#: windows/views.py:161 windows/views.py:1142 msgid "Password" msgstr "" @@ -139,621 +146,707 @@ msgid "Use TLS" msgstr "" #: windows/views.py:203 -msgid "Use SSH tunnel" +msgid "Mark read only" msgstr "" #: windows/views.py:214 +msgid "Use SSH tunnel" +msgstr "" + +#: windows/views.py:225 msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:233 +#: windows/views.py:244 msgid "Filename" msgstr "" -#: windows/views.py:238 windows/views.py:358 windows/views.py:3115 +#: windows/views.py:249 windows/views.py:368 windows/views.py:3373 msgid "Select a file" msgstr "" -#: windows/views.py:238 windows/views.py:358 +#: windows/views.py:249 windows/views.py:368 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:266 windows/views.py:1335 windows/views.py:1587 +#: windows/views.py:2842 windows/views.py:3092 msgid "Comments" msgstr "" -#: windows/main/controller.py:751 windows/views.py:269 windows/views.py:730 -#: windows/views.py:884 +#: windows/main/controller.py:766 windows/views.py:280 windows/views.py:740 +#: windows/views.py:894 msgid "Settings" msgstr "" -#: windows/views.py:278 +#: windows/views.py:288 msgid "SSH executable" msgstr "" -#: windows/views.py:283 +#: windows/views.py:293 msgid "ssh" msgstr "" -#: windows/views.py:291 +#: windows/views.py:301 msgid "SSH host + port" msgstr "" -#: windows/views.py:303 +#: windows/views.py:313 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "" -#: windows/views.py:312 +#: windows/views.py:322 msgid "SSH username" msgstr "" -#: windows/views.py:325 +#: windows/views.py:335 msgid "SSH password" msgstr "" -#: windows/views.py:338 +#: windows/views.py:348 msgid "Local port" msgstr "" -#: windows/views.py:344 +#: windows/views.py:354 msgid "if the value is set to 0, the first available port will be used" msgstr "" -#: windows/views.py:353 +#: windows/views.py:363 msgid "Identity file" msgstr "" -#: windows/views.py:369 +#: windows/views.py:379 msgid "Remote host + port" msgstr "" -#: windows/views.py:381 +#: windows/views.py:391 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "" -#: windows/views.py:390 +#: windows/views.py:400 msgid "SSH extra args" msgstr "" -#: windows/views.py:405 +#: windows/views.py:415 msgid "SSH Tunnel" msgstr "" -#: windows/views.py:411 windows/views.py:1341 +#: windows/views.py:421 windows/views.py:1331 windows/views.py:2846 msgid "Created at" msgstr "" -#: windows/views.py:445 +#: windows/views.py:455 msgid "Successful connections" msgstr "" -#: windows/views.py:462 +#: windows/views.py:472 msgid "Last successful connection" msgstr "" -#: windows/views.py:479 +#: windows/views.py:489 msgid "Unsuccessful connections" msgstr "" -#: windows/views.py:496 +#: windows/views.py:506 msgid "Last failure reason" msgstr "" -#: windows/views.py:513 +#: windows/views.py:523 msgid "Total connection attempts" msgstr "" -#: windows/views.py:530 +#: windows/views.py:540 msgid "Average connection time (ms)" msgstr "" -#: windows/views.py:547 +#: windows/views.py:557 msgid "Most recent connection duration" msgstr "" -#: windows/views.py:566 +#: windows/views.py:576 msgid "Statistics" msgstr "" -#: windows/views.py:584 windows/views.py:1730 +#: windows/views.py:594 windows/views.py:1853 msgid "Create" msgstr "" -#: windows/views.py:588 +#: windows/views.py:598 msgid "Create connection" msgstr "" -#: windows/views.py:591 +#: windows/views.py:601 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/main/database/procedure.py:184 windows/views.py:630 +#: windows/views.py:854 windows/views.py:1483 windows/views.py:1899 +#: windows/views.py:2160 windows/views.py:2216 windows/views.py:2908 +#: windows/views.py:3250 windows/views.py:3387 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:323 windows/main/database/procedure.py:185 +#: windows/views.py:635 windows/views.py:2165 windows/views.py:2358 +#: windows/views.py:3255 windows/views.py:3393 msgid "Save" msgstr "" -#: windows/views.py:632 +#: windows/views.py:642 msgid "Test" msgstr "" -#: windows/views.py:639 +#: windows/views.py:649 msgid "Connect" msgstr "" -#: windows/views.py:742 +#: windows/main/database/procedure.py:162 windows/views.py:752 msgid "Language" msgstr "" -#: windows/views.py:747 +#: windows/views.py:757 msgid "English" msgstr "" -#: windows/views.py:747 +#: windows/views.py:757 msgid "Italian" msgstr "" -#: windows/views.py:747 +#: windows/views.py:757 msgid "French" msgstr "" -#: windows/views.py:759 +#: windows/views.py:769 msgid "Locale" msgstr "" -#: windows/views.py:780 +#: windows/views.py:790 msgid "Column content" msgstr "" -#: windows/views.py:790 +#: windows/views.py:800 msgid "Syntax" msgstr "" -#: windows/views.py:847 +#: windows/views.py:857 msgid "Ok" msgstr "" -#: windows/views.py:878 +#: windows/views.py:888 msgid "PeterSQL" msgstr "" -#: windows/views.py:887 +#: windows/views.py:897 msgid "File" msgstr "" -#: windows/views.py:890 +#: windows/views.py:900 msgid "About" msgstr "" -#: windows/views.py:893 +#: windows/views.py:903 msgid "Help" msgstr "" -#: windows/views.py:898 +#: windows/views.py:908 msgid "Open connection manager" msgstr "" -#: windows/views.py:902 +#: windows/views.py:912 msgid "Disconnect from server" msgstr "" -#: windows/views.py:904 +#: windows/views.py:914 msgid "tool" msgstr "" -#: windows/views.py:904 +#: windows/views.py:914 windows/views.py:2196 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:918 windows/views.py:920 windows/views.py:1877 +#: windows/views.py:2200 windows/views.py:2344 msgid "Add" msgstr "" -#: windows/views.py:944 windows/views.py:948 windows/views.py:2199 +#: windows/views.py:954 windows/views.py:958 windows/views.py:2322 msgid "MyMenuItem" msgstr "" -#: windows/views.py:951 windows/views.py:1804 windows/views.py:3020 +#: windows/views.py:961 windows/views.py:1927 windows/views.py:3278 msgid "MyMenu" msgstr "" -#: windows/views.py:966 windows/views.py:1388 windows/views.py:1395 -#: windows/views.py:1402 +#: windows/views.py:976 windows/views.py:1511 windows/views.py:1518 +#: windows/views.py:1525 msgid "MyLabel" msgstr "" -#: windows/views.py:972 +#: windows/views.py:982 msgid "Databases" msgstr "" -#: windows/views.py:973 windows/views.py:1340 +#: windows/views.py:983 windows/views.py:1330 windows/views.py:2847 msgid "Size" msgstr "" -#: windows/views.py:974 +#: windows/views.py:984 msgid "Elements" msgstr "" -#: windows/views.py:975 +#: windows/views.py:985 msgid "Modified at" msgstr "" -#: windows/views.py:976 +#: windows/views.py:986 windows/views.py:1345 msgid "Tables" msgstr "" -#: windows/views.py:983 +#: windows/views.py:993 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:1039 +#: windows/views.py:1334 windows/views.py:2843 msgid "Collation" msgstr "" -#: windows/views.py:1058 +#: windows/views.py:1068 msgid "Encryption" msgstr "" -#: windows/views.py:1070 +#: windows/views.py:1080 msgid "Read Only" msgstr "" -#: windows/views.py:1087 +#: windows/views.py:1097 msgid "Tablespace" msgstr "" -#: windows/views.py:1108 +#: windows/views.py:1118 msgid "Connection limit" msgstr "" -#: windows/views.py:1151 +#: windows/views.py:1161 msgid "Profile" msgstr "" -#: windows/views.py:1177 +#: windows/views.py:1187 msgid "Default tablespace" msgstr "" -#: windows/views.py:1198 +#: windows/views.py:1208 msgid "Temporary tablespace" msgstr "" -#: windows/views.py:1224 +#: windows/views.py:1234 msgid "Quota" msgstr "" -#: windows/views.py:1243 +#: windows/views.py:1253 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1260 +#: windows/views.py:1270 msgid "Account status" msgstr "" -#: windows/views.py:1281 +#: windows/views.py:1291 msgid "Password expire" msgstr "" -#: windows/views.py:1302 -msgid "Table:" +#: windows/views.py:1315 +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:1317 +msgid "Clone table" msgstr "" -#: windows/views.py:1315 -msgid "Clone" +#: windows/main/controller.py:1633 windows/views.py:1319 +msgid "Delete table" msgstr "" -#: windows/views.py:1339 +#: windows/views.py:1329 msgid "Rows" msgstr "" -#: windows/views.py:1342 +#: windows/views.py:1332 windows/views.py:2845 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:1350 +msgid "Add new view" +msgstr "" + +#: windows/views.py:1352 windows/views.py:1376 +msgid "Clone view" +msgstr "" + +#: windows/views.py:1354 +msgid "Delete view" +msgstr "" + +#: windows/views.py:1362 windows/views.py:1386 windows/views.py:1410 +#: windows/views.py:1434 windows/views.py:1458 +msgid "Definition" +msgstr "" + +#: windows/views.py:1369 windows/views.py:2177 +msgid "Views" +msgstr "" + +#: windows/views.py:1374 +msgid "Add new procedure" +msgstr "" + +#: windows/views.py:1376 +msgid "Clone procedure" +msgstr "" + +#: windows/views.py:1378 +msgid "Delete procedure" +msgstr "" + +#: windows/views.py:1393 +msgid "Procedures" +msgstr "" + +#: windows/views.py:1398 +msgid "Add new function" +msgstr "" + +#: windows/views.py:1400 +msgid "Clone function" +msgstr "" + +#: windows/views.py:1402 +msgid "Delete function" +msgstr "" + +#: windows/views.py:1417 +msgid "Functions" +msgstr "" + +#: windows/views.py:1422 +msgid "Add new trigger" +msgstr "" + +#: windows/views.py:1424 +msgid "Clone trigger" +msgstr "" + +#: windows/views.py:1426 +msgid "Delete trigger" +msgstr "" + +#: windows/views.py:1441 windows/views.py:2185 +msgid "Triggers" +msgstr "" + +#: windows/views.py:1446 +msgid "Add new event" +msgstr "" + +#: windows/views.py:1448 +msgid "Clone event" +msgstr "" + +#: windows/views.py:1450 +msgid "Delete event" +msgstr "" + +#: windows/views.py:1465 +msgid "Events" +msgstr "" + +#: windows/views.py:1493 windows/views.py:1904 windows/views.py:2214 +#: windows/views.py:2296 windows/views.py:2915 msgid "Apply" msgstr "" -#: windows/views.py:1382 windows/views.py:1561 windows/views.py:1991 -#: windows/views.py:2909 +#: windows/main/database/procedure.py:122 windows/views.py:1505 +#: windows/views.py:1684 windows/views.py:2114 windows/views.py:3167 msgid "Options" msgstr "" -#: windows/views.py:1413 +#: windows/views.py:1536 msgid "Diagram" msgstr "" -#: windows/views.py:1424 +#: windows/views.py:1547 msgid "Database" msgstr "" -#: windows/views.py:1479 windows/views.py:2849 +#: windows/views.py:1602 windows/views.py:3107 msgid "Base" msgstr "" -#: windows/views.py:1493 windows/views.py:2863 +#: windows/views.py:1616 windows/views.py:3121 msgid "Auto Increment" msgstr "" -#: windows/views.py:1521 windows/views.py:2891 +#: windows/views.py:1644 windows/views.py:3149 msgid "Default Collation" msgstr "" -#: windows/views.py:1531 +#: windows/views.py:1654 msgid "Convert data" msgstr "" -#: windows/views.py:1539 +#: windows/views.py:1662 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:1696 windows/views.py:1737 windows/views.py:1781 +#: windows/views.py:1879 windows/views.py:2204 msgid "Remove" msgstr "" -#: windows/views.py:1580 windows/views.py:1621 windows/views.py:1665 +#: windows/views.py:1703 windows/views.py:1744 windows/views.py:1788 msgid "Clear" msgstr "" -#: windows/views.py:1595 windows/views.py:2923 +#: windows/views.py:1718 windows/views.py:3181 msgid "Indexes" msgstr "" -#: windows/views.py:1639 +#: windows/views.py:1732 windows/views.py:1776 windows/views.py:2927 +#: windows/views.py:2969 windows/views.py:3210 +msgid "Insert" +msgstr "" + +#: windows/views.py:1762 msgid "Foreign Keys" msgstr "" -#: windows/views.py:1683 +#: windows/views.py:1806 msgid "Checks" msgstr "" -#: windows/views.py:1750 windows/views.py:2944 +#: windows/views.py:1873 windows/views.py:3202 msgid "Columns:" msgstr "" -#: windows/views.py:1760 +#: windows/views.py:1883 msgid "Move Up" msgstr "" -#: windows/views.py:1762 +#: windows/views.py:1885 msgid "Move Down" msgstr "" -#: windows/views.py:1794 windows/views.py:1801 windows/views.py:3010 -#: windows/views.py:3017 +#: windows/views.py:1917 windows/views.py:1924 windows/views.py:3268 +#: windows/views.py:3275 msgid "Add Index" msgstr "" -#: windows/views.py:1798 windows/views.py:3014 +#: windows/views.py:1921 windows/views.py:3272 msgid "Add PrimaryKey" msgstr "" -#: windows/views.py:1815 +#: windows/views.py:1938 msgid "Table" msgstr "" -#: windows/views.py:1851 +#: windows/main/database/procedure.py:140 windows/views.py:1974 msgid "Definer" msgstr "" -#: windows/views.py:1871 +#: windows/views.py:1994 msgid "Schema" msgstr "" -#: windows/views.py:1897 +#: windows/views.py:2020 msgid "SQL security" msgstr "" -#: windows/views.py:1904 +#: windows/views.py:2027 msgid "DEFINER" msgstr "" -#: windows/views.py:1904 +#: windows/views.py:2027 msgid "INVOKER" msgstr "" -#: windows/views.py:1916 +#: windows/views.py:2039 msgid "Algorithm" msgstr "" -#: windows/views.py:1918 +#: windows/views.py:2041 msgid "UNDEFINED" msgstr "" -#: windows/views.py:1921 +#: windows/views.py:2044 msgid "MERGE" msgstr "" -#: windows/views.py:1924 +#: windows/views.py:2047 msgid "TEMPTABLE" msgstr "" -#: windows/views.py:1934 +#: windows/views.py:2057 msgid "View constraint" msgstr "" -#: windows/views.py:1936 +#: windows/views.py:2059 msgid "None" msgstr "" -#: windows/views.py:1939 +#: windows/views.py:2062 msgid "LOCAL" msgstr "" -#: windows/views.py:1942 +#: windows/views.py:2065 msgid "CASCADE" msgstr "" -#: windows/views.py:1945 +#: windows/views.py:2068 msgid "CHECK ONLY" msgstr "" -#: windows/views.py:1948 +#: windows/views.py:2071 msgid "READ ONLY" msgstr "" -#: windows/views.py:1960 +#: windows/views.py:2083 msgid "Force" msgstr "" -#: windows/views.py:1972 +#: windows/views.py:2095 msgid "Security barrier" msgstr "" -#: windows/views.py:2054 -msgid "Views" -msgstr "" - -#: windows/views.py:2062 -msgid "Triggers" -msgstr "" - -#: windows/views.py:2073 -msgid "Refrsh" -msgstr "" - -#: windows/views.py:2079 +#: windows/views.py:2202 msgid "Duplicate" msgstr "" -#: windows/views.py:2085 +#: windows/views.py:2208 msgid "Apply changes automatically" msgstr "" -#: windows/views.py:2087 windows/views.py:2088 +#: windows/views.py:2210 windows/views.py:2211 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" msgstr "" -#: windows/views.py:2101 +#: windows/views.py:2224 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" -#: windows/views.py:2109 +#: windows/views.py:2232 msgid "First" msgstr "" -#: windows/views.py:2127 +#: windows/views.py:2250 msgid "Last" msgstr "" -#: windows/views.py:2136 +#: windows/views.py:2259 msgid "Filters" msgstr "" -#: windows/views.py:2176 +#: windows/views.py:2299 msgid "CTRL+ENTER" msgstr "" -#: windows/views.py:2196 +#: windows/views.py:2319 msgid "Insert row" msgstr "" -#: windows/views.py:2204 +#: windows/views.py:2327 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:318 windows/views.py:2344 msgid "New query" msgstr "" -#: windows/views.py:2223 windows/views.py:2660 +#: windows/views.py:2346 windows/views.py:2866 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:319 windows/views.py:2346 msgid "Close query" msgstr "" -#: windows/views.py:2227 +#: windows/views.py:2350 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:320 windows/views.py:2350 msgid "Execute" msgstr "" -#: windows/views.py:2229 +#: windows/views.py:2352 msgid "Run all" msgstr "" -#: windows/main/controller.py:295 windows/views.py:2229 +#: windows/views.py:2352 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:322 windows/views.py:2354 msgid "Stop" msgstr "" -#: windows/views.py:2287 +#: windows/views.py:2419 msgid "a page" msgstr "" -#: windows/views.py:2315 +#: windows/views.py:2469 msgid "Query" msgstr "" -#: windows/views.py:2626 +#: windows/views.py:2820 msgid "Character set" msgstr "" -#: windows/views.py:2644 windows/views.py:2663 +#: windows/views.py:2850 windows/views.py:2869 msgid "New" msgstr "" -#: windows/views.py:2683 +#: windows/views.py:2889 msgid "Insert record" msgstr "" -#: windows/views.py:2688 +#: windows/views.py:2894 msgid "Duplicate record" msgstr "" -#: windows/views.py:2695 +#: windows/views.py:2901 msgid "Delete record" msgstr "" -#: windows/views.py:2733 windows/views.py:2964 +#: windows/views.py:2939 windows/views.py:3222 msgid "Up" msgstr "" -#: windows/views.py:2740 windows/views.py:2971 +#: windows/views.py:2946 windows/views.py:3229 msgid "Down" msgstr "" -#: windows/views.py:3100 +#: windows/views.py:2961 +msgid "Table:" +msgstr "" + +#: windows/views.py:2974 +msgid "Clone" +msgstr "" + +#: windows/views.py:3358 msgid "Save Starments" msgstr "" -#: windows/views.py:3108 +#: windows/views.py:3366 msgid "Location" msgstr "" -#: windows/views.py:3115 +#: windows/views.py:3373 msgid "*.sql" msgstr "" @@ -777,7 +870,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 +882,75 @@ msgstr "" msgid "Zerofill" msgstr "" -#: windows/components/dataview.py:109 +#: windows/components/dataview.py:111 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 "" @@ -877,161 +970,163 @@ 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:762 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:775 +#: windows/dialogs/connections/view.py:787 #, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "" -#: windows/dialogs/connections/view.py:776 +#: windows/dialogs/connections/view.py:788 msgid "Connection error" msgstr "" -#: windows/dialogs/connections/view.py:802 +#: windows/dialogs/connections/view.py:814 #, 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:817 +#: windows/dialogs/connections/view.py:834 msgid "Confirm delete" msgstr "" -#: windows/dialogs/connections/view.py:819 +#: windows/dialogs/connections/view.py:831 #, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "" -#: windows/main/controller.py:275 +#: windows/main/controller.py:315 #, python-brace-format msgid "{text} ({shortcut})" msgstr "" -#: windows/main/controller.py:281 windows/main/controller.py:294 +#: windows/main/controller.py:321 msgid "Execute all" msgstr "" -#: windows/main/controller.py:471 windows/main/controller.py:479 +#: windows/main/controller.py:440 windows/main/controller.py:448 msgid "Query (1)" msgstr "" -#: windows/main/controller.py:497 +#: windows/main/controller.py:467 #, python-brace-format msgid "Query ({query_number})" msgstr "" -#: windows/main/controller.py:530 +#: windows/main/controller.py:516 msgid "You have unsaved changes. Save before closing?" msgstr "" -#: windows/main/controller.py:531 +#: windows/main/controller.py:517 msgid "Unsaved query" msgstr "" -#: windows/main/controller.py:576 +#: windows/main/controller.py:562 msgid "Save query" msgstr "" -#: windows/main/controller.py:579 +#: windows/main/controller.py:565 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:593 windows/main/controller.py:624 +#: windows/main/controller.py:651 windows/main/database/list.py:119 +#: windows/main/database/procedure.py:294 +#: windows/main/database/procedure.py:320 windows/main/database/view.py:267 +#: windows/main/database/view.py:296 windows/main/query/controller.py:177 msgid "Error" msgstr "" -#: windows/main/controller.py:622 +#: windows/main/controller.py:631 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "" -#: windows/main/controller.py:647 +#: windows/main/controller.py:657 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "" -#: windows/main/controller.py:704 +#: windows/main/controller.py:719 msgid "days" msgstr "" -#: windows/main/controller.py:705 +#: windows/main/controller.py:720 msgid "hours" msgstr "" -#: windows/main/controller.py:706 +#: windows/main/controller.py:721 msgid "minutes" msgstr "" -#: windows/main/controller.py:707 +#: windows/main/controller.py:722 msgid "seconds" msgstr "" -#: windows/main/controller.py:715 +#: windows/main/controller.py:730 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "" -#: windows/main/controller.py:751 +#: windows/main/controller.py:766 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:952 +#: windows/main/controller.py:990 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:954 +#: windows/main/controller.py:992 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:1119 +#: windows/main/controller.py:1214 msgid "Version" msgstr "" -#: windows/main/controller.py:1121 +#: windows/main/controller.py:1216 msgid "Uptime" msgstr "" -#: windows/main/controller.py:1199 +#: windows/main/controller.py:1299 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:1232 +#: windows/main/controller.py:1332 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1041,97 +1136,129 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:1237 windows/main/controller.py:1258 +#: windows/main/controller.py:1337 windows/main/controller.py:1358 msgid "Delete database" msgstr "" -#: windows/main/controller.py:1243 +#: windows/main/controller.py:1343 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:1244 +#: windows/main/controller.py:1344 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:1257 +#: windows/main/controller.py:1357 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:1272 +#: windows/main/controller.py:1372 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:1373 windows/main/database/procedure.py:283 +#: windows/main/database/procedure.py:314 windows/main/database/view.py:255 +#: windows/main/database/view.py:290 msgid "Success" msgstr "" -#: windows/main/controller.py:1392 +#: windows/main/controller.py:1604 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:1418 +#: windows/main/controller.py:1630 #, 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:1652 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1563 +#: windows/main/controller.py:1797 msgid "Do you want delete the records?" msgstr "" -#: windows/main/database/list.py:69 +#: 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 +#: windows/main/database/list.py:108 msgid "Connection lost" msgstr "" -#: windows/main/database/list.py:83 +#: windows/main/database/list.py:118 msgid "Reconnection failed:" msgstr "" -#: windows/main/database/view.py:252 +#: windows/main/database/procedure.py:114 +msgid "Procedure" +msgstr "" + +#: windows/main/database/procedure.py:151 +msgid "Parameters" +msgstr "" + +#: windows/main/database/procedure.py:282 +msgid "Procedure created successfully" +msgstr "" + +#: windows/main/database/procedure.py:282 +msgid "Procedure updated successfully" +msgstr "" + +#: windows/main/database/procedure.py:294 +#, python-brace-format +msgid "Error saving procedure: {}" +msgstr "" + +#: windows/main/database/procedure.py:305 +#, python-brace-format +msgid "Are you sure you want to delete procedure '{}'?" +msgstr "" + +#: windows/main/database/procedure.py:306 windows/main/database/view.py:281 +msgid "Confirm Delete" +msgstr "" + +#: windows/main/database/procedure.py:314 +msgid "Procedure deleted successfully" +msgstr "" + +#: windows/main/database/procedure.py:320 +#, python-brace-format +msgid "Error deleting procedure: {}" +msgstr "" + +#: windows/main/database/view.py:254 msgid "View created successfully" msgstr "" -#: windows/main/database/view.py:252 +#: windows/main/database/view.py:254 msgid "View updated successfully" msgstr "" -#: windows/main/database/view.py:256 +#: windows/main/database/view.py:267 #, python-brace-format msgid "Error saving view: {}" msgstr "" -#: windows/main/database/view.py:269 +#: windows/main/database/view.py:280 #, 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:290 msgid "View deleted successfully" msgstr "" -#: windows/main/database/view.py:282 +#: windows/main/database/view.py:296 #, python-brace-format msgid "Error deleting view: {}" msgstr "" @@ -1168,6 +1295,10 @@ msgstr "" msgid "No active database connection" 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 +1338,7 @@ msgstr "" msgid "Error:" msgstr "" +#: windows/main/table/records.py:336 +msgid "Error saving records" +msgstr "" + From 3cd077f71c5e27526e7f2cc1083c0b88a1738e42 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 4 May 2026 16:24:15 +0200 Subject: [PATCH 50/93] feat(database): align options flow across engines --- PeterSQL.fbp | 123 +++---- helpers/bindings.py | 49 ++- structures/engines/context.py | 5 + structures/engines/mariadb/context.py | 11 + structures/engines/mariadb/database.py | 18 +- structures/engines/mysql/context.py | 11 + structures/engines/mysql/database.py | 17 +- structures/engines/postgresql/context.py | 12 + structures/engines/postgresql/database.py | 14 +- structures/engines/sqlite/context.py | 3 + windows/dialogs/connections/model.py | 5 +- windows/dialogs/connections/repository.py | 1 + windows/dialogs/connections/view.py | 19 +- windows/main/controller.py | 86 +++-- windows/main/database/list.py | 8 +- windows/main/database/options.py | 399 ++++++++-------------- windows/main/database/procedure.py | 2 + windows/main/database/view.py | 1 + windows/main/table/options.py | 6 +- windows/state.py | 1 + windows/views.py | 8 +- 21 files changed, 420 insertions(+), 379 deletions(-) diff --git a/PeterSQL.fbp b/PeterSQL.fbp index a7fd971..5c00010 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -2326,7 +2326,7 @@ SSH Tunnel 0 - + 1 1 1 @@ -2378,7 +2378,7 @@ wxTAB_TRAVERSAL - + bSizer102 wxVERTICAL @@ -6487,7 +6487,7 @@ wxTAB_TRAVERSAL 1 do_close - + 1 @@ -6548,7 +6548,7 @@ - + 1 1 1 @@ -6653,10 +6653,11 @@ wxID_ANY wxITEM_NORMAL Add - database_add + tool_add_database protected + on_add_database Load From File; icons/16x16/database_delete.png @@ -7656,8 +7657,8 @@ - - + + 1 1 1 @@ -7709,16 +7710,16 @@ wxTAB_TRAVERSAL - + bSizer158 wxVERTICAL none - + 5 wxEXPAND 0 - + bSizer159 wxHORIZONTAL @@ -7863,11 +7864,11 @@ none - + 5 wxEXPAND 0 - + bSizer142 wxHORIZONTAL @@ -10876,11 +10877,11 @@ bSizer14821 wxVERTICAL none - + 5 wxEXPAND 0 - + 1 1 1 @@ -10937,7 +10938,7 @@ - + Load From File; icons/16x16/add.png 0 wxID_ANY @@ -10949,7 +10950,7 @@ Add new procedure on_insert_view - + Load From File; icons/16x16/page_copy.png 0 wxID_ANY @@ -10961,7 +10962,7 @@ Clone procedure on_clone_view - + Load From File; icons/16x16/delete.png 0 wxID_ANY @@ -10975,11 +10976,11 @@ - + 5 wxALL|wxEXPAND 1 - + 1 @@ -11001,7 +11002,7 @@ - + wxALIGN_LEFT wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -11013,7 +11014,7 @@ Text -1 - + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -11030,11 +11031,11 @@ - + Functions 0 - + 1 1 1 @@ -11086,16 +11087,16 @@ wxTAB_TRAVERSAL - + bSizer148211 wxVERTICAL none - + 5 wxEXPAND 0 - + 1 1 1 @@ -11152,7 +11153,7 @@ - + Load From File; icons/16x16/add.png 0 wxID_ANY @@ -11164,7 +11165,7 @@ Add new function on_insert_view - + Load From File; icons/16x16/page_copy.png 0 wxID_ANY @@ -11176,7 +11177,7 @@ Clone function on_clone_view - + Load From File; icons/16x16/delete.png 0 wxID_ANY @@ -11190,11 +11191,11 @@ - + 5 wxALL|wxEXPAND 1 - + 1 @@ -11216,7 +11217,7 @@ - + wxALIGN_LEFT wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -11228,7 +11229,7 @@ Text -1 - + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -11245,11 +11246,11 @@ - + Triggers 0 - + 1 1 1 @@ -11301,16 +11302,16 @@ wxTAB_TRAVERSAL - + bSizer1482111 wxVERTICAL none - + 5 wxEXPAND 0 - + 1 1 1 @@ -11367,7 +11368,7 @@ - + Load From File; icons/16x16/add.png 0 wxID_ANY @@ -11379,7 +11380,7 @@ Add new trigger on_insert_view - + Load From File; icons/16x16/page_copy.png 0 wxID_ANY @@ -11391,7 +11392,7 @@ Clone trigger on_clone_view - + Load From File; icons/16x16/delete.png 0 wxID_ANY @@ -11405,11 +11406,11 @@ - + 5 wxALL|wxEXPAND 1 - + 1 @@ -11431,7 +11432,7 @@ - + wxALIGN_LEFT wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -11443,7 +11444,7 @@ Text -1 - + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -11460,11 +11461,11 @@ - + Events 0 - + 1 1 1 @@ -11516,16 +11517,16 @@ wxTAB_TRAVERSAL - + bSizer14821111 wxVERTICAL none - + 5 wxEXPAND 0 - + 1 1 1 @@ -11582,7 +11583,7 @@ - + Load From File; icons/16x16/add.png 0 wxID_ANY @@ -11594,7 +11595,7 @@ Add new event on_insert_view - + Load From File; icons/16x16/page_copy.png 0 wxID_ANY @@ -11606,7 +11607,7 @@ Clone event on_clone_view - + Load From File; icons/16x16/delete.png 0 wxID_ANY @@ -11620,11 +11621,11 @@ - + 5 wxALL|wxEXPAND 1 - + 1 @@ -11646,7 +11647,7 @@ - + wxALIGN_LEFT wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -11658,7 +11659,7 @@ Text -1 - + wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE @@ -11932,11 +11933,11 @@ - + Diagram 0 - + 1 1 1 @@ -11988,7 +11989,7 @@ wxTAB_TRAVERSAL - + bSizer82 wxVERTICAL @@ -17292,7 +17293,7 @@ Load From File; icons/16x16/text_columns.png Data - 1 + 0 1 1 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/structures/engines/context.py b/structures/engines/context.py index 5663f95..65434cc 100755 --- a/structures/engines/context.py +++ b/structures/engines/context.py @@ -366,6 +366,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 diff --git a/structures/engines/mariadb/context.py b/structures/engines/mariadb/context.py index 7159dac..f6601c6 100755 --- a/structures/engines/mariadb/context.py +++ b/structures/engines/mariadb/context.py @@ -607,6 +607,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: diff --git a/structures/engines/mariadb/database.py b/structures/engines/mariadb/database.py index 5a52634..95e1ff2 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,14 @@ def create(self) -> bool: return self.context.execute(query) def alter(self) -> bool: - clauses = self._build_database_clauses() + clauses = self._build_database_clauses(self._changed_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 diff --git a/structures/engines/mysql/context.py b/structures/engines/mysql/context.py index 2cf4ce1..23d6f77 100644 --- a/structures/engines/mysql/context.py +++ b/structures/engines/mysql/context.py @@ -618,6 +618,17 @@ 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_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: diff --git a/structures/engines/mysql/database.py b/structures/engines/mysql/database.py index 399f4e3..da0c74f 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,14 @@ def create(self) -> bool: return self.context.execute(query) def alter(self) -> bool: - clauses = self._build_database_clauses() + clauses = self._build_database_clauses(self._changed_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 diff --git a/structures/engines/postgresql/context.py b/structures/engines/postgresql/context.py index d6f7bef..18dc9b3 100644 --- a/structures/engines/postgresql/context.py +++ b/structures/engines/postgresql/context.py @@ -676,6 +676,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: diff --git a/structures/engines/postgresql/database.py b/structures/engines/postgresql/database.py index 6ff6780..d4587e3 100644 --- a/structures/engines/postgresql/database.py +++ b/structures/engines/postgresql/database.py @@ -35,17 +35,23 @@ 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) + + if "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 "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: diff --git a/structures/engines/sqlite/context.py b/structures/engines/sqlite/context.py index 31b6e55..ce8a827 100755 --- a/structures/engines/sqlite/context.py +++ b/structures/engines/sqlite/context.py @@ -543,6 +543,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: diff --git a/windows/dialogs/connections/model.py b/windows/dialogs/connections/model.py index 2a7570b..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]() @@ -45,7 +46,7 @@ def __init__(self): 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) @@ -128,7 +129,7 @@ def clear(self, *args): 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, diff --git a/windows/dialogs/connections/repository.py b/windows/dialogs/connections/repository.py index 1abb5a5..ddb677f 100644 --- a/windows/dialogs/connections/repository.py +++ b/windows/dialogs/connections/repository.py @@ -227,6 +227,7 @@ def add_directory( self.connections.append(directory) self._write() + self.connections.refresh() def delete_directory(self, directory: ConnectionDirectory): self.connections.get_value() diff --git a/windows/dialogs/connections/view.py b/windows/dialogs/connections/view.py index 4af0db6..7f2d826 100644 --- a/windows/dialogs/connections/view.py +++ b/windows/dialogs/connections/view.py @@ -646,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 @@ -655,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) diff --git a/windows/main/controller.py b/windows/main/controller.py index 549e635..d5565f6 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -47,6 +47,7 @@ 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 @@ -140,20 +141,11 @@ 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: @@ -198,7 +190,7 @@ 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) @@ -687,6 +679,7 @@ 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) @@ -1263,6 +1256,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() @@ -1270,15 +1274,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) @@ -1536,6 +1581,9 @@ def _on_current_table(self, table: SQLTable): self.tool_clone_table.Enable(table is not None) self.tool_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)) self.btn_cancel_table.Enable(bool(table is not None)) diff --git a/windows/main/database/list.py b/windows/main/database/list.py index 5ddb299..99557af 100644 --- a/windows/main/database/list.py +++ b/windows/main/database/list.py @@ -69,9 +69,9 @@ def Compare(self, item1, item2, col, ascending): class ListDatabaseTable: - _app = wx.GetApp() - 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) @@ -175,9 +175,9 @@ def GetValueByRow(self, row, col): class ListDatabaseView: - _app = wx.GetApp() - 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) 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/procedure.py b/windows/main/database/procedure.py index 2f2b35e..b1b5444 100644 --- a/windows/main/database/procedure.py +++ b/windows/main/database/procedure.py @@ -17,6 +17,8 @@ class EditViewModel(AbstractModel): def __init__(self): + super().__init__() + self.name = Observable() self.parameters = Observable() self.language = Observable() diff --git a/windows/main/database/view.py b/windows/main/database/view.py index e43da3b..c450a44 100644 --- a/windows/main/database/view.py +++ b/windows/main/database/view.py @@ -18,6 +18,7 @@ class EditViewModel(AbstractModel): def __init__(self): + super().__init__() self.name = Observable() self.schema = Observable() self.definer = Observable() 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/state.py b/windows/state.py index 5fc907f..21ba18d 100644 --- a/windows/state.py +++ b/windows/state.py @@ -20,6 +20,7 @@ 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) diff --git a/windows/views.py b/windows/views.py index 2a6319f..b44d604 100755 --- a/windows/views.py +++ b/windows/views.py @@ -915,7 +915,7 @@ def __init__( self, parent ): 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 ) @@ -2324,7 +2324,7 @@ def __init__( self, parent ): 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 ) @@ -2550,6 +2550,7 @@ def __init__( self, parent ): 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_add_database, id = self.tool_add_database.GetId() ) self.MainFrameNotebook.Bind( wx.EVT_NOTEBOOK_PAGE_CHANGED, self.on_page_chaged ) 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() ) @@ -2630,6 +2631,9 @@ def on_database_disconnect( self, event ): def on_database_refresh( self, event ): event.Skip() + def on_add_database( self, event ): + event.Skip() + def on_page_chaged( self, event ): event.Skip() From 1eb0d36d8cdb8c46c29640d8df40e13b871953c4 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 5 May 2026 15:14:19 +0200 Subject: [PATCH 51/93] docs(database): update options matrix --- assets/database_options_matrix.md | 33 ++++++++++++++++--------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/assets/database_options_matrix.md b/assets/database_options_matrix.md index bfe5cb0..a87d7b1 100644 --- a/assets/database_options_matrix.md +++ b/assets/database_options_matrix.md @@ -14,34 +14,34 @@ ------------------- ------- --------- ------------ -------- Charset ✅ ✅ ❌ ❌ Collation ✅ ✅ ⚠️ ❌ - Encoding ❌ ❌ ✅ ❌ - Locale (LC\_\*) ❌ ❌ ✅ ❌ - Owner ❌ ❌ ✅ ❌ - Template DB ❌ ❌ ✅ ❌ + Encryption ✅ ❌ ❌ ❌ + Tablespace ❌ ❌ ✅ ❌ Connection limit ❌ ❌ ✅ ❌ - Allow connections ❌ ❌ ✅ ❌ - Is template flag ❌ ❌ ✅ ❌ - Default engine ✅ ✅ ❌ ❌ ------------------------------------------------------------------------ ## Notes -### MySQL / MariaDB +### MySQL - Focus on: - charset - collation - - default engine + - encryption (Y/N) + +### MariaDB + +- Focus on: + - charset + - collation +- Encryption NOT supported (MySQL-only syntax) ### PostgreSQL - Different model: - - encoding - - locale (LC_COLLATE, LC_CTYPE) - - owner - - template database - - connection rules + - tablespace + - connection limit +- Collation: shown in UI but `PostgreSQLDatabase` has no `default_collation` field — not functional yet ### SQLite @@ -52,5 +52,6 @@ ## Important Caveat -Collation in PostgreSQL is NOT equivalent to MySQL: - Derived from -locale (LC_COLLATE) - Not freely alterable like in MySQL/MariaDB +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 From 060383002471b44a5e7ac0c8dfc154a4b965804a Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 5 May 2026 15:14:27 +0200 Subject: [PATCH 52/93] test(ui): update screenshot capture and references --- screenshot/connection_dialog_configured.png | Bin 0 -> 46629 bytes screenshot/connection_dialog_ssh_tunnel.png | Bin 0 -> 42019 bytes screenshot/mysql_main_window_add_database.png | Bin 0 -> 107011 bytes tests/ui/scenario_helpers.py | 32 +++++++----------- 4 files changed, 12 insertions(+), 20 deletions(-) create mode 100644 screenshot/connection_dialog_configured.png create mode 100644 screenshot/connection_dialog_ssh_tunnel.png create mode 100644 screenshot/mysql_main_window_add_database.png diff --git a/screenshot/connection_dialog_configured.png b/screenshot/connection_dialog_configured.png new file mode 100644 index 0000000000000000000000000000000000000000..280d291bc94a1cbd9db604c59503ab858ea2c3a9 GIT binary patch literal 46629 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 ie}%L5zqCDA(h0qATVEQYkNooVh@qY-G#VPzsR% literal 0 HcmV?d00001 diff --git a/screenshot/mysql_main_window_add_database.png b/screenshot/mysql_main_window_add_database.png new file mode 100644 index 0000000000000000000000000000000000000000..b038c696d969a2e79386cd350ddd84c16b23383d GIT binary patch literal 107011 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 h1_=DWBP4w bool: def capture_window_screenshot(window: wx.TopLevelWindow, target_path: Path) -> None: - # window.Show() - # window.Raise() - # window.Layout() - # window.Refresh() - # window.Update() - # pump_ui(20) - # - # target_path.parent.mkdir(parents=True, exist_ok=True) - # bitmap = capture_window_bitmap(window) - # bitmap.SaveFile(str(target_path), wx.BITMAP_TYPE_PNG) window.Show() window.Raise() window.SetFocus() @@ -175,18 +165,20 @@ def capture_window_screenshot(window: wx.TopLevelWindow, target_path: Path) -> N window.Refresh() window.Update() - for _ in range(10): + for _ in range(20): wx.GetApp().ProcessPendingEvents() wx.YieldIfNeeded() - time.sleep(0.05) + wx.MilliSleep(50) - rect = window.GetScreenRect() + width, height = window.GetClientSize() + + bitmap = wx.Bitmap(width, height) + memory_dc = wx.MemoryDC(bitmap) - image = pyautogui.screenshot(region=( - rect.x, - rect.y, - rect.width, - rect.height, - )) + try: + source_dc = wx.ClientDC(window) + memory_dc.Blit(0, 0, width, height, source_dc, 0, 0) + finally: + memory_dc.SelectObject(wx.NullBitmap) - image.save(target_path) + bitmap.SaveFile(target_path, wx.BITMAP_TYPE_PNG) From ec95bf21ab0fb69b01517414cb67ddbcaa521715 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Fri, 15 May 2026 09:58:03 +0200 Subject: [PATCH 53/93] Improve SQL autocomplete popup behavior --- .../stc/autocomplete/auto_complete.py | 50 +++++++++++++++++ .../stc/autocomplete/autocomplete_popup.py | 54 ++++++++++++------- 2 files changed, 84 insertions(+), 20 deletions(-) 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 8988af2..1cc89ae 100644 --- a/windows/components/stc/autocomplete/autocomplete_popup.py +++ b/windows/components/stc/autocomplete/autocomplete_popup.py @@ -6,7 +6,7 @@ from windows.components.stc.theme_loader import ThemeLoader -class AutoCompletePopup(wx.PopupTransientWindow): +class AutoCompletePopup(wx.PopupWindow): def __init__(self, parent: wx.Window, settings: object = None, theme_loader: ThemeLoader = None) -> None: super().__init__(parent, wx.BORDER_SIMPLE) @@ -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 From a83fb89b3be77912220b309d7540863518fccc53 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Fri, 15 May 2026 09:58:15 +0200 Subject: [PATCH 54/93] Add temporary write override for read-only sessions --- PeterSQL.fbp | 514 +++++++++++++++++------ structures/engines/context.py | 4 + structures/engines/mariadb/context.py | 5 + structures/engines/mariadb/database.py | 11 +- structures/engines/mysql/context.py | 5 + structures/engines/mysql/database.py | 11 +- structures/engines/postgresql/context.py | 4 + structures/engines/sqlite/context.py | 9 + windows/main/__init__.py | 2 + windows/main/controller.py | 148 ++++++- windows/main/explorer.py | 5 + windows/main/table/executor.py | 5 +- windows/main/table/records.py | 38 +- windows/state.py | 2 + windows/views.py | 46 +- 15 files changed, 617 insertions(+), 192 deletions(-) diff --git a/PeterSQL.fbp b/PeterSQL.fbp index 5c00010..c72681c 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -31,7 +31,7 @@ 1 0 0 - + 0 wxAUI_MGR_DEFAULT @@ -60,16 +60,16 @@ on_close - + bSizer34 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -385,8 +385,8 @@ - - + + 1 1 1 @@ -438,16 +438,16 @@ wxTAB_TRAVERSAL - + bSizer36 wxVERTICAL none - + 5 wxALL|wxEXPAND 1 - + 1 1 1 @@ -2322,7 +2322,7 @@ - + SSH Tunnel 0 @@ -6638,11 +6638,11 @@ wxID_ANY wxITEM_NORMAL tool - database_refresh + tool_refresh_database protected Refresh Refresh - on_database_refresh + on_refresh_database protected @@ -6670,6 +6670,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 + @@ -6787,7 +6863,7 @@ Resizable 1 - -150 + -200 -1 1 @@ -6919,8 +6995,8 @@ - - + + 1 1 1 @@ -6972,7 +7048,7 @@ wxFULL_REPAINT_ON_RESIZE|wxTAB_TRAVERSAL - + bSizer24 wxHORIZONTAL @@ -7050,7 +7126,7 @@ - + MyMenu m_menu5 protected @@ -7067,7 +7143,7 @@ - + MyMenu m_menu1 @@ -7415,11 +7491,11 @@ - + Load From File; icons/16x16/database.png Database 0 - + 1 1 1 @@ -7471,16 +7547,16 @@ wxTAB_TRAVERSAL - + bSizer27 wxVERTICAL none - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -7534,11 +7610,11 @@ - + Options 0 - + 1 1 1 @@ -7590,16 +7666,16 @@ wxTAB_TRAVERSAL - + bSizer80 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -7657,8 +7733,8 @@ - - + + 1 1 1 @@ -7710,16 +7786,16 @@ wxTAB_TRAVERSAL - + bSizer158 wxVERTICAL none - + 5 wxEXPAND 0 - + bSizer159 wxHORIZONTAL @@ -7864,11 +7940,11 @@ none - + 5 wxEXPAND 0 - + bSizer142 wxHORIZONTAL @@ -10187,8 +10263,8 @@ - - + + 1 1 1 @@ -10240,16 +10316,16 @@ wxTAB_TRAVERSAL - + bSizer149 wxVERTICAL none - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -10601,11 +10677,11 @@ - + Views 0 - + 1 1 1 @@ -10657,7 +10733,7 @@ wxTAB_TRAVERSAL - + bSizer1482 wxVERTICAL @@ -10816,11 +10892,11 @@ - + Procedures 0 - + 1 1 1 @@ -10872,7 +10948,7 @@ wxTAB_TRAVERSAL - + bSizer14821 wxVERTICAL @@ -17290,11 +17366,11 @@ wxTAB_TRAVERSAL - + Load From File; icons/16x16/text_columns.png Data - 0 - + 1 + 1 1 1 @@ -17346,7 +17422,7 @@ wxTAB_TRAVERSAL - + bSizer61 wxVERTICAL @@ -17999,11 +18075,11 @@ - + 5 wxALL|wxEXPAND 0 - + 1 1 1 @@ -18018,7 +18094,7 @@ 1 0 1 - 1 + 0 1 0 @@ -18063,7 +18139,7 @@ wxFULL_REPAINT_ON_RESIZE on_collapsible_pane_changed - + bSizer831 wxVERTICAL @@ -18136,79 +18212,165 @@ - + 5 - wxALL + wxEXPAND 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 + 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 + + @@ -18276,11 +18438,11 @@ - + Load From File; icons/16x16/arrow_right.png Query 0 - + 1 1 1 @@ -18332,16 +18494,16 @@ wxTAB_TRAVERSAL - + bSizer26 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -18399,8 +18561,8 @@ - - + + 1 1 1 @@ -18452,7 +18614,7 @@ wxTAB_TRAVERSAL - + bSizer125 wxVERTICAL @@ -18598,20 +18760,20 @@ - + 5 wxEXPAND 1 - + bSizer150 wxHORIZONTAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -18922,8 +19084,8 @@ - - + + 1 1 1 @@ -18975,7 +19137,7 @@ wxTAB_TRAVERSAL - + bSizer1581 wxVERTICAL @@ -21137,6 +21299,80 @@ 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 + MyButton + + 0 + + 0 + + + 0 + + 1 + m_button57 + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + diff --git a/structures/engines/context.py b/structures/engines/context.py index 65434cc..ffd8d7a 100755 --- a/structures/engines/context.py +++ b/structures/engines/context.py @@ -120,6 +120,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: diff --git a/structures/engines/mariadb/context.py b/structures/engines/mariadb/context.py index f6601c6..570961e 100755 --- a/structures/engines/mariadb/context.py +++ b/structures/engines/mariadb/context.py @@ -87,6 +87,10 @@ def after_connect(self, *args, **kwargs): 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. @@ -205,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: diff --git a/structures/engines/mariadb/database.py b/structures/engines/mariadb/database.py index 95e1ff2..5f1b165 100644 --- a/structures/engines/mariadb/database.py +++ b/structures/engines/mariadb/database.py @@ -69,12 +69,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): diff --git a/structures/engines/mysql/context.py b/structures/engines/mysql/context.py index 23d6f77..0c0cab6 100644 --- a/structures/engines/mysql/context.py +++ b/structures/engines/mysql/context.py @@ -88,6 +88,10 @@ def after_connect(self, *args, **kwargs): 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. @@ -205,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: diff --git a/structures/engines/mysql/database.py b/structures/engines/mysql/database.py index da0c74f..c47ad13 100644 --- a/structures/engines/mysql/database.py +++ b/structures/engines/mysql/database.py @@ -84,12 +84,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): diff --git a/structures/engines/postgresql/context.py b/structures/engines/postgresql/context.py index 18dc9b3..0cfa5f6 100644 --- a/structures/engines/postgresql/context.py +++ b/structures/engines/postgresql/context.py @@ -77,6 +77,10 @@ def after_connect(self, *args, **kwargs): 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(""" diff --git a/structures/engines/sqlite/context.py b/structures/engines/sqlite/context.py index ce8a827..9cc481c 100755 --- a/structures/engines/sqlite/context.py +++ b/structures/engines/sqlite/context.py @@ -119,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 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 d5565f6..191ecee 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -19,7 +19,7 @@ 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 @@ -33,7 +33,7 @@ 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, CURRENT_PROCEDURE +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, WRITE_OVERRIDE from windows.main.explorer import TreeExplorerController @@ -121,6 +121,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 @@ -214,7 +220,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(): @@ -224,7 +231,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")) @@ -234,6 +241,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( @@ -683,6 +691,8 @@ def _setup_subscribers(self): AUTO_APPLY.subscribe(self._on_auto_apply) + WRITE_OVERRIDE.subscribe(self._on_write_override) + # Initialize record toolbar states self._initialize_record_toolbar_states() @@ -1131,13 +1141,14 @@ def _load_records_page(self): self._records_offset, filters, ) - with Loader.cursor_wait(): - logger.debug("ui trace: records._load_records_page before obj.load_records obj=%s", obj.name) - obj.load_records(filters=filters, limit=limit, offset=self._records_offset) - logger.debug("ui trace: records._load_records_page after obj.load_records obj=%s", obj.name) - logger.debug("ui trace: records._load_records_page before controller.load_model_for obj=%s", obj.name) - self.controller_list_table_records.load_model_for(obj) - logger.debug("ui trace: records._load_records_page after controller.load_model_for obj=%s", obj.name) + 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(obj) self._set_records_paging_buttons(obj) @@ -1191,6 +1202,62 @@ def on_page_chaged(self, event): 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") @@ -1199,14 +1266,32 @@ def _on_current_session(self, session: Session): from structures.session import Session + # 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.connection 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) keywords = " ".join(k.lower() for k in session.context.KEYWORDS) @@ -1447,6 +1532,9 @@ def _on_current_view(self, current: SQLView): 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() @@ -1545,6 +1633,7 @@ def _on_current_table(self, table: SQLTable): 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, @@ -1773,12 +1862,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) @@ -1805,9 +1908,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() @@ -1886,6 +1989,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() diff --git a/windows/main/explorer.py b/windows/main/explorer.py index 214e8fc..faf369b 100755 --- a/windows/main/explorer.py +++ b/windows/main/explorer.py @@ -106,6 +106,10 @@ 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, @@ -199,6 +203,7 @@ def reset_current_objects(self): 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) 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/records.py b/windows/main/table/records.py index bceb713..465c409 100644 --- a/windows/main/table/records.py +++ b/windows/main/table/records.py @@ -147,28 +147,30 @@ def _on_auto_apply_changed(self, auto_apply_enabled: bool): 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) + target.records.set_value(result.records) else: logger.error(f"Failed to load records: {result.error}") try: - self.table.load_records() + target.load_records() except Exception as ex: logger.error(f"Fallback loading also failed: {ex}", exc_info=True) @@ -186,7 +188,6 @@ def _do_edit(self, item, model_column: int = 1): 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() @@ -195,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() diff --git a/windows/state.py b/windows/state.py index 21ba18d..b061bb6 100644 --- a/windows/state.py +++ b/windows/state.py @@ -24,3 +24,5 @@ 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 b44d604..c33d58e 100755 --- a/windows/views.py +++ b/windows/views.py @@ -911,7 +911,7 @@ 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() @@ -919,6 +919,14 @@ def __init__( self, parent ): 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 ) @@ -2257,7 +2265,7 @@ def __init__( self, parent ): 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 ) + self.m_collapsiblePane1.Collapse( False ) bSizer831 = wx.BoxSizer( wx.VERTICAL ) @@ -2293,12 +2301,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 ) @@ -2324,7 +2343,7 @@ def __init__( self, parent ): self.panel_records.Bind( wx.EVT_RIGHT_DOWN, self.panel_recordsOnContextMenu ) - self.MainFrameNotebook.AddPage( self.panel_records, _(u"Data"), False ) + self.MainFrameNotebook.AddPage( self.panel_records, _(u"Data"), True ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/text_columns.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2527,7 +2546,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 ) @@ -2549,8 +2568,9 @@ 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.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() ) @@ -2601,6 +2621,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() ) @@ -2628,12 +2649,15 @@ 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 ): event.Skip() @@ -2752,6 +2776,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() @@ -2771,7 +2798,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 ): @@ -3000,6 +3027,9 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. bSizer144.Add( bSizer156, 1, wx.EXPAND, 5 ) + self.m_button57 = wx.Button( self, wx.ID_ANY, _(u"MyButton"), wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer144.Add( self.m_button57, 0, wx.ALL, 5 ) + self.SetSizer( bSizer144 ) self.Layout() From 66eed922dba619a2375d223a786540080ff15594 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 30 May 2026 10:30:04 +0200 Subject: [PATCH 55/93] Add memray dependency for profiling --- pyproject.toml | 1 + uv.lock | 158 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4339ef0..64b996d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.14" dependencies = [ "babel>=2.18.0", + "memray>=1.19.3", "oracledb>=3.4.2", "psutil>=7.2.2", "psycopg2-binary>=2.9.11", diff --git a/uv.lock b/uv.lock index e7c64f6..0e87750 100644 --- a/uv.lock +++ b/uv.lock @@ -256,6 +256,18 @@ 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 = "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 = "librt" version = "0.8.1" @@ -290,6 +302,111 @@ 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 = "mouseinfo" version = "0.1.3" @@ -381,6 +498,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "babel" }, + { name = "memray" }, { name = "oracledb" }, { name = "psutil" }, { name = "psycopg2-binary" }, @@ -411,6 +529,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "babel", specifier = ">=2.18.0" }, + { name = "memray", 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" }, @@ -802,6 +921,19 @@ 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" @@ -836,6 +968,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" @@ -881,6 +1030,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" From f7d83508e71dc3101638d571c178779ad2500965 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 30 May 2026 10:33:04 +0200 Subject: [PATCH 56/93] Add UNKNOW datatype to SQLite engine --- structures/engines/sqlite/datatype.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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"]) From bc95eddf645ae05b98b7a287c94767d66a7a7e99 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 30 May 2026 10:33:13 +0200 Subject: [PATCH 57/93] Refactor procedure editor UI to parent panel --- windows/main/database/procedure.py | 159 +++++++---------------------- windows/views.py | 138 +++++++++++++++++++++++-- 2 files changed, 167 insertions(+), 130 deletions(-) diff --git a/windows/main/database/procedure.py b/windows/main/database/procedure.py index b1b5444..1d65477 100644 --- a/windows/main/database/procedure.py +++ b/windows/main/database/procedure.py @@ -1,7 +1,6 @@ from typing import Optional import wx -import wx.stc from gettext import gettext as _ @@ -94,7 +93,15 @@ def _update_postgresql_fields(self, procedure: SQLProcedure): class ProcedureEditorController: def __init__(self, parent): self.parent = parent - self._build_panel(parent) + + try: + from windows.components.stc.styles import apply_stc_theme + from windows.components.stc.profiles import SQL + apply_stc_theme(self.parent.stc_procedure) + SQL.apply(self.parent.stc_procedure) + except Exception: + pass + self.model = EditViewModel() self._bind_controls() self._bind_buttons() @@ -107,117 +114,20 @@ def __init__(self, parent): CURRENT_PROCEDURE.subscribe(self.on_current_procedure_changed) - # ------------------------------------------------------------------ - # Panel construction - # ------------------------------------------------------------------ - - def _build_panel(self, parent): - self.panel = wx.Panel(parent.MainFrameNotebook, wx.ID_ANY) - parent.MainFrameNotebook.AddPage(self.panel, _("Procedure"), False) - self.page_index = parent.MainFrameNotebook.GetPageCount() - 1 - - outer = wx.BoxSizer(wx.VERTICAL) - - # --- Options notebook (mirrors m_notebook7 in views) --- - self.options_notebook = wx.Notebook(self.panel, wx.ID_ANY) - self.pnl_options_root = wx.Panel(self.options_notebook, wx.ID_ANY) - self.options_notebook.AddPage(self.pnl_options_root, _("Options"), False) - - options_vsizer = wx.BoxSizer(wx.VERTICAL) - - # Name row (always visible) - pnl_name = wx.Panel(self.pnl_options_root, wx.ID_ANY) - name_sizer = wx.BoxSizer(wx.HORIZONTAL) - lbl_name = wx.StaticText(pnl_name, label=_("Name")) - lbl_name.SetMinSize(wx.Size(150, -1)) - name_sizer.Add(lbl_name, 0, wx.ALIGN_CENTER | wx.ALL, 5) - self.txt_procedure_name = wx.TextCtrl(pnl_name, wx.ID_ANY) - name_sizer.Add(self.txt_procedure_name, 1, wx.ALIGN_CENTER | wx.ALL, 5) - pnl_name.SetSizer(name_sizer) - options_vsizer.Add(pnl_name, 0, wx.EXPAND | wx.ALL, 2) - - # Definer row (MySQL/MariaDB) - self.pnl_row_definer = wx.Panel(self.pnl_options_root, wx.ID_ANY) - definer_sizer = wx.BoxSizer(wx.HORIZONTAL) - lbl_def = wx.StaticText(self.pnl_row_definer, label=_("Definer")) - lbl_def.SetMinSize(wx.Size(150, -1)) - definer_sizer.Add(lbl_def, 0, wx.ALIGN_CENTER | wx.ALL, 5) - self.cmb_procedure_definer = wx.ComboBox(self.pnl_row_definer, wx.ID_ANY, style=wx.CB_DROPDOWN) - definer_sizer.Add(self.cmb_procedure_definer, 1, wx.ALIGN_CENTER | wx.ALL, 5) - self.pnl_row_definer.SetSizer(definer_sizer) - options_vsizer.Add(self.pnl_row_definer, 0, wx.EXPAND | wx.ALL, 2) - - # Parameters row (always visible) - pnl_params = wx.Panel(self.pnl_options_root, wx.ID_ANY) - params_sizer = wx.BoxSizer(wx.HORIZONTAL) - lbl_params = wx.StaticText(pnl_params, label=_("Parameters")) - lbl_params.SetMinSize(wx.Size(150, -1)) - params_sizer.Add(lbl_params, 0, wx.ALIGN_CENTER | wx.ALL, 5) - self.txt_procedure_parameters = wx.TextCtrl(pnl_params, wx.ID_ANY) - params_sizer.Add(self.txt_procedure_parameters, 1, wx.ALIGN_CENTER | wx.ALL, 5) - pnl_params.SetSizer(params_sizer) - options_vsizer.Add(pnl_params, 0, wx.EXPAND | wx.ALL, 2) - - # Language row (PostgreSQL) - self.pnl_row_language = wx.Panel(self.pnl_options_root, wx.ID_ANY) - lang_sizer = wx.BoxSizer(wx.HORIZONTAL) - lbl_lang = wx.StaticText(self.pnl_row_language, label=_("Language")) - lbl_lang.SetMinSize(wx.Size(150, -1)) - lang_sizer.Add(lbl_lang, 0, wx.ALIGN_CENTER | wx.ALL, 5) - self.cho_procedure_language = wx.Choice(self.pnl_row_language, wx.ID_ANY, choices=["plpgsql", "sql"]) - self.cho_procedure_language.SetSelection(0) - lang_sizer.Add(self.cho_procedure_language, 1, wx.ALIGN_CENTER | wx.ALL, 5) - self.pnl_row_language.SetSizer(lang_sizer) - options_vsizer.Add(self.pnl_row_language, 0, wx.EXPAND | wx.ALL, 2) - - self.pnl_options_root.SetSizer(options_vsizer) - self.pnl_options_root.Layout() - - outer.Add(self.options_notebook, 0, wx.ALL | wx.EXPAND, 5) - - # --- Body editor --- - self.stc_procedure_body = wx.stc.StyledTextCtrl(self.panel, wx.ID_ANY, size=wx.Size(-1, -1)) - self.stc_procedure_body.SetMinSize(wx.Size(-1, 120)) - outer.Add(self.stc_procedure_body, 1, wx.ALL | wx.EXPAND, 5) - - # --- Buttons --- - btn_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.btn_delete_procedure = wx.Button(self.panel, wx.ID_ANY, _("Delete")) - self.btn_cancel_procedure = wx.Button(self.panel, wx.ID_ANY, _("Cancel")) - self.btn_save_procedure = wx.Button(self.panel, wx.ID_ANY, _("Save")) - btn_sizer.Add(self.btn_delete_procedure, 0, wx.RIGHT, 5) - btn_sizer.AddStretchSpacer() - btn_sizer.Add(self.btn_cancel_procedure, 0, wx.RIGHT, 5) - btn_sizer.Add(self.btn_save_procedure, 0) - outer.Add(btn_sizer, 0, wx.ALL | wx.EXPAND, 5) - - self.panel.SetSizer(outer) - - try: - from windows.components.stc.styles import apply_stc_theme - from windows.components.stc.profiles import SQL - apply_stc_theme(self.stc_procedure_body) - SQL.apply(self.stc_procedure_body) - except Exception: - pass - # ------------------------------------------------------------------ # Bindings # ------------------------------------------------------------------ def _bind_controls(self): self.model.bind_controls( - name=self.txt_procedure_name, - parameters=self.txt_procedure_parameters, - language=self.cho_procedure_language, - definer=self.cmb_procedure_definer, - body=self.stc_procedure_body, + name=self.parent.txt_name_procedure, + body=self.parent.stc_procedure, ) def _bind_buttons(self): - self.btn_save_procedure.Bind(wx.EVT_BUTTON, self.on_save_procedure) - self.btn_delete_procedure.Bind(wx.EVT_BUTTON, self.on_delete_procedure) - self.btn_cancel_procedure.Bind(wx.EVT_BUTTON, self.on_cancel_procedure) + self.parent.btn_save_procedure.Bind(wx.EVT_BUTTON, self.on_save_procedure) + self.parent.btn_delete_procedure.Bind(wx.EVT_BUTTON, self.on_delete_procedure) + self.parent.btn_cancel_procedure.Bind(wx.EVT_BUTTON, self.on_cancel_procedure) # ------------------------------------------------------------------ # Button state @@ -248,14 +158,14 @@ def update_button_states(self, *args, **kwargs): getattr(procedure, "is_new", None) if procedure is not None else None, ) if procedure is None: - self.btn_save_procedure.Enable(False) - self.btn_cancel_procedure.Enable(False) - self.btn_delete_procedure.Enable(False) + self.parent.btn_save_procedure.Enable(False) + self.parent.btn_cancel_procedure.Enable(False) + self.parent.btn_delete_procedure.Enable(False) else: has_changes = self._has_changes(procedure) - self.btn_save_procedure.Enable(has_changes) - self.btn_cancel_procedure.Enable(has_changes) - self.btn_delete_procedure.Enable(not procedure.is_new) + self.parent.btn_save_procedure.Enable(has_changes) + self.parent.btn_cancel_procedure.Enable(has_changes) + self.parent.btn_delete_procedure.Enable(not procedure.is_new) # ------------------------------------------------------------------ # Actions @@ -353,12 +263,15 @@ def on_current_procedure_changed(self, procedure: Optional[SQLProcedure]): def _populate_definers(self, engine: ConnectionEngine, session): if engine not in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): return + cmb = getattr(self.parent, 'cmb_procedure_definer', None) + if cmb is None: + return try: logger.debug("ui trace: procedure._populate_definers start engine=%s", engine.name) definers = session.context.get_definers() - self.cmb_procedure_definer.Clear() + cmb.Clear() for definer in definers: - self.cmb_procedure_definer.Append(definer) + cmb.Append(definer) logger.debug("ui trace: procedure._populate_definers done count=%s", len(definers)) except Exception: pass @@ -372,27 +285,31 @@ def apply_engine_visibility(self, engine: ConnectionEngine): else: self._apply_default_visibility() - self.pnl_options_root.GetSizer().Layout() - self.options_notebook.SetMinSize(wx.Size(-1, -1)) - self.options_notebook.Fit() - self.panel.Layout() + self.parent.m_panel73.GetSizer().Layout() + self.parent.panel_procedures.Layout() def _apply_mysql_visibility(self): + definer = getattr(self.parent, 'pnl_procedure_row_definer', None) + language = getattr(self.parent, 'pnl_procedure_row_language', None) self._batch_show_hide( - show=[self.pnl_row_definer], - hide=[self.pnl_row_language], + show=[w for w in [definer] if w], + hide=[w for w in [language] if w], ) def _apply_postgresql_visibility(self): + definer = getattr(self.parent, 'pnl_procedure_row_definer', None) + language = getattr(self.parent, 'pnl_procedure_row_language', None) self._batch_show_hide( - show=[self.pnl_row_language], - hide=[self.pnl_row_definer], + show=[w for w in [language] if w], + hide=[w for w in [definer] if w], ) def _apply_default_visibility(self): + definer = getattr(self.parent, 'pnl_procedure_row_definer', None) + language = getattr(self.parent, 'pnl_procedure_row_language', None) self._batch_show_hide( show=[], - hide=[self.pnl_row_definer, self.pnl_row_language], + hide=[w for w in [definer, language] if w], ) def _batch_show_hide(self, show: list[wx.Window], hide: list[wx.Window]): diff --git a/windows/views.py b/windows/views.py index c33d58e..809ad81 100755 --- a/windows/views.py +++ b/windows/views.py @@ -2182,15 +2182,120 @@ 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_procedures = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer160 = wx.BoxSizer( wx.VERTICAL ) + + self.m_splitter9 = wx.SplitterWindow( self.panel_procedures, 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_panel73 = wx.Panel( self.m_splitter9, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer166 = wx.BoxSizer( wx.VERTICAL ) + + bSizer871 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText401 = wx.StaticText( self.m_panel73, wx.ID_ANY, _(u"Name"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText401.Wrap( -1 ) + + self.m_staticText401.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer871.Add( self.m_staticText401, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + self.txt_name_procedure = wx.TextCtrl( self.m_panel73, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer871.Add( self.txt_name_procedure, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) + + + bSizer166.Add( bSizer871, 0, wx.EXPAND, 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.stc_procedure = wx.stc.StyledTextCtrl( self.m_panel74, wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,-1 ), 0) + self.stc_procedure.SetUseTabs ( True ) + self.stc_procedure.SetTabWidth ( 4 ) + self.stc_procedure.SetIndent ( 4 ) + self.stc_procedure.SetTabIndents( True ) + self.stc_procedure.SetBackSpaceUnIndents( True ) + self.stc_procedure.SetViewEOL( False ) + self.stc_procedure.SetViewWhiteSpace( False ) + self.stc_procedure.SetMarginWidth( 2, 0 ) + self.stc_procedure.SetIndentationGuides( True ) + self.stc_procedure.SetReadOnly( False ) + self.stc_procedure.SetMarginWidth( 1, 0 ) + self.stc_procedure.SetMarginType( 0, wx.stc.STC_MARGIN_NUMBER ) + self.stc_procedure.SetMarginWidth( 0, self.stc_procedure.TextWidth( wx.stc.STC_STYLE_LINENUMBER, "_99999" ) ) + self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS ) + self.stc_procedure.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK) + self.stc_procedure.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE) + self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS ) + self.stc_procedure.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK ) + self.stc_procedure.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE ) + self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY ) + self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS ) + self.stc_procedure.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK ) + self.stc_procedure.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE ) + self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUS ) + self.stc_procedure.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.BLACK) + self.stc_procedure.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.WHITE) + self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY ) + self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY ) + self.stc_procedure.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) ) + self.stc_procedure.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) ) + self.stc_procedure.SetMinSize( wx.Size( -1,200 ) ) + + bSizer161.Add( self.stc_procedure, 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_delete_procedure = wx.Button( self.panel_procedures, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btn_delete_procedure.Enable( False ) + + bSizer911.Add( self.btn_delete_procedure, 0, wx.ALL, 5 ) + + self.btn_cancel_procedure = wx.Button( self.panel_procedures, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btn_cancel_procedure.Enable( False ) + + bSizer911.Add( self.btn_cancel_procedure, 0, wx.ALL, 5 ) + + self.btn_save_procedure = wx.Button( self.panel_procedures, wx.ID_ANY, _(u"Save"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btn_save_procedure.Enable( False ) + + bSizer911.Add( self.btn_save_procedure, 0, wx.ALL, 5 ) + + + bSizer160.Add( bSizer911, 0, wx.EXPAND, 5 ) + + + self.panel_procedures.SetSizer( bSizer160 ) + self.panel_procedures.Layout() + bSizer160.Fit( self.panel_procedures ) + self.MainFrameNotebook.AddPage( self.panel_procedures, _(u"Procedure"), 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"Triggers"), False ) + 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 ) @@ -2265,7 +2370,7 @@ def __init__( self, parent ): bSizer61.Add( bSizer94, 0, wx.EXPAND, 5 ) self.m_collapsiblePane1 = wx.CollapsiblePane( self.panel_records, wx.ID_ANY, _(u"Filters"), wx.DefaultPosition, wx.DefaultSize, wx.CP_DEFAULT_STYLE|wx.CP_NO_TLW_RESIZE|wx.FULL_REPAINT_ON_RESIZE ) - self.m_collapsiblePane1.Collapse( False ) + self.m_collapsiblePane1.Collapse( True ) bSizer831 = wx.BoxSizer( wx.VERTICAL ) @@ -2334,12 +2439,23 @@ 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.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.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_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 ) @@ -2822,8 +2938,12 @@ def m_splitter41OnIdle( self, event ): def panel_table_columnsOnContextMenu( self, event ): self.panel_table_columns.PopupMenu( self.menu_table_columns, event.GetPosition() ) + 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 ) From 399655383fb4ae39f4014188c02d8ea40b55091a Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 30 May 2026 10:33:24 +0200 Subject: [PATCH 58/93] Fix notebook page indices after procedure panel restructure --- windows/main/controller.py | 36 ++++++++++++++++++------------------ windows/main/explorer.py | 1 + 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/windows/main/controller.py b/windows/main/controller.py index 191ecee..aec1ec4 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -781,7 +781,7 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S current_view = CURRENT_VIEW.get_value() current_trigger = CURRENT_TRIGGER.get_value() current_procedure = CURRENT_PROCEDURE.get_value() - procedure_page_index = self.controller_procedure_editor.page_index + procedure_page_index = self.MainFrameNotebook.FindPage(self.panel_procedures) total_pages = self.MainFrameNotebook.GetPageCount() @@ -796,14 +796,14 @@ 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(procedure_page_index).Hide() @@ -816,7 +816,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): @@ -830,19 +830,19 @@ 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() - logger.debug("ui trace: toggle_panel records page shown (load disabled isolation)") 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(procedure_page_index).Show() - self.MainFrameNotebook.GetPage(6).Show() + self.MainFrameNotebook.GetPage(7).Show() if self.MainFrameNotebook.GetSelection() != procedure_page_index: self.MainFrameNotebook.SetSelection(procedure_page_index) @@ -1197,7 +1197,7 @@ 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 int(event.Selection) == 6: if CURRENT_TABLE.get_value() or CURRENT_VIEW.get_value(): self._records_offset = 0 self._load_records_page() @@ -1525,7 +1525,7 @@ def _on_current_view(self, current: SQLView): self._records_total_is_loading = False self._update_records_label(current) self._set_records_paging_buttons(current) - if self.MainFrameNotebook.GetSelection() == 5: + if self.MainFrameNotebook.GetSelection() == 6: self._load_records_page() can_act = current is not None and not current.is_new @@ -1576,7 +1576,7 @@ def _on_current_procedure(self, current: SQLProcedure): self.toggle_panel(current) can_act = current is not None and not current.is_new - self.controller_procedure_editor.btn_delete_procedure.Enable(can_act) + self.btn_delete_procedure.Enable(can_act) def on_insert_procedure(self): session = CURRENT_SESSION.get_value() @@ -1586,7 +1586,7 @@ def on_insert_procedure(self): CURRENT_PROCEDURE.set_value(None) new_proc = session.context.build_empty_procedure(database) CURRENT_PROCEDURE.set_value(new_proc) - procedure_page_index = self.controller_procedure_editor.page_index + procedure_page_index = self.MainFrameNotebook.FindPage(self.panel_procedures) self._toggle_panel(procedure_page_index, True) self.MainFrameNotebook.SetSelection(procedure_page_index) @@ -1604,7 +1604,7 @@ def on_clone_procedure(self): ) CURRENT_PROCEDURE.set_value(None) CURRENT_PROCEDURE.set_value(clone) - procedure_page_index = self.controller_procedure_editor.page_index + procedure_page_index = self.MainFrameNotebook.FindPage(self.panel_procedures) self._toggle_panel(procedure_page_index, True) self.MainFrameNotebook.SetSelection(procedure_page_index) @@ -1654,7 +1654,7 @@ 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, @@ -1937,7 +1937,7 @@ def _on_f5_refresh(self, event): page = self.MainFrameNotebook.GetSelection() if page == 2: self.controller_list_table_columns.do_refresh_columns() - elif page == 5: + elif page == 6: self.controller_list_table_records.do_refresh_records() def on_duplicate_record(self, event): diff --git a/windows/main/explorer.py b/windows/main/explorer.py index faf369b..2ddaef4 100755 --- a/windows/main/explorer.py +++ b/windows/main/explorer.py @@ -221,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) From 0e183ac5b6adbccf83f65c30be6b089fa6671f35 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 30 May 2026 10:34:39 +0200 Subject: [PATCH 59/93] Update UI layout in wxFormBuilder project --- PeterSQL.fbp | 1239 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 982 insertions(+), 257 deletions(-) diff --git a/PeterSQL.fbp b/PeterSQL.fbp index c72681c..6b6e5d9 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -15162,9 +15162,9 @@ - + Load From File; icons/16x16/view.png - Views + View 0 1 @@ -17308,9 +17308,9 @@ - - Load From File; icons/16x16/cog.png - Triggers + + Load From File; icons/16x16/code-folding.png + Procedure 0 1 @@ -17348,65 +17348,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_procedures 1 @@ -17422,16 +17364,16 @@ wxTAB_TRAVERSAL - + - bSizer61 + bSizer160 wxVERTICAL none 5 wxEXPAND - 0 - + 1 + 1 1 1 @@ -17442,7 +17384,6 @@ 0 - 1 0 @@ -17461,16 +17402,15 @@ 0 0 wxID_ANY - 0 + 0 0 1 - m_toolBar3 - 1 + m_splitter9 1 @@ -17478,171 +17418,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 - 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 @@ -17671,8 +17461,6 @@ 0 0 wxID_ANY - {database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows} - 0 0 @@ -17680,7 +17468,7 @@ 0 1 - name_database_table + m_panel73 1 @@ -17690,26 +17478,927 @@ Resizable 1 - ; ; forward_declare 0 - - -1 - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - + wxTAB_TRAVERSAL + + + bSizer166 + 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 + txt_name_procedure + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_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 + stc_procedure + 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_delete_procedure + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Cancel + + 0 + + 0 + + + 0 + + 1 + btn_cancel_procedure + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 0 + + 1 + + + 0 + 0 + wxID_ANY + Save + + 0 + + 0 + + + 0 + + 1 + btn_save_procedure + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + 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 + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + panel_records + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer61 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + + 0 + + + 0 + + 1 + m_toolBar3 + 1 + 1 + + + protected + 1 + + Resizable + 5 + 1 + + wxTB_HORIZONTAL|wxTB_HORZ_TEXT + ; ; forward_declare + 0 + + + + + + Load From File; icons/16x16/arrow_refresh.png + 0 + wxID_ANY + wxITEM_NORMAL + 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 @@ -18075,11 +18764,11 @@ - + 5 wxALL|wxEXPAND 0 - + 1 1 1 @@ -18094,7 +18783,7 @@ 1 0 1 - 0 + 1 1 0 @@ -18139,7 +18828,7 @@ wxFULL_REPAINT_ON_RESIZE on_collapsible_pane_changed - + bSizer831 wxVERTICAL @@ -18212,11 +18901,11 @@ - + 5 wxEXPAND 0 - + bSizer1591 wxHORIZONTAL @@ -18296,11 +18985,11 @@ on_apply_filters - + 5 wxALL 0 - + 1 1 1 @@ -18405,9 +19094,9 @@ - + MyMenu - m_menu10 + menu_table_records protected @@ -18435,6 +19124,42 @@ + + 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 + + + + From 0e1d93748074c43383d21c90bde1a875f003ac95 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 1 Jun 2026 09:03:10 +0200 Subject: [PATCH 60/93] Refactor routine editor integration --- PeterSQL.fbp | 16420 +++++++++++-------------- locale/de_DE/LC_MESSAGES/petersql.mo | Bin 7894 -> 18654 bytes locale/de_DE/LC_MESSAGES/petersql.po | 963 +- locale/en_US/LC_MESSAGES/petersql.mo | Bin 2349 -> 18212 bytes locale/en_US/LC_MESSAGES/petersql.po | 1179 +- locale/es_ES/LC_MESSAGES/petersql.mo | Bin 7897 -> 18627 bytes locale/es_ES/LC_MESSAGES/petersql.po | 962 +- locale/fr_FR/LC_MESSAGES/petersql.mo | Bin 7621 -> 18905 bytes locale/fr_FR/LC_MESSAGES/petersql.po | 967 +- locale/it_IT/LC_MESSAGES/petersql.mo | Bin 7957 -> 18518 bytes locale/it_IT/LC_MESSAGES/petersql.po | 951 +- locale/petersql.pot | 552 +- pyproject.toml | 2 +- uv.lock | 4 +- windows/main/controller.py | 84 +- windows/main/database/procedure.py | 325 - windows/main/database/routine.py | 651 + windows/main/explorer.py | 2 +- windows/main/table/records.py | 5 +- windows/views.py | 1328 +- 20 files changed, 11427 insertions(+), 12968 deletions(-) delete mode 100644 windows/main/database/procedure.py create mode 100644 windows/main/database/routine.py diff --git a/PeterSQL.fbp b/PeterSQL.fbp index 6b6e5d9..fc395c7 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -5738,7 +5738,7 @@ - + 0 wxAUI_MGR_DEFAULT @@ -6670,15 +6670,15 @@ - + protected - + 0 protected 0 - + 1 1 1 @@ -7226,7 +7226,7 @@ 5 wxALL|wxEXPAND - 1 + 0 1 1 @@ -11024,19 +11024,19 @@ protected Add new procedure Add new procedure - on_insert_view + on_insert_procedure Load From File; icons/16x16/page_copy.png 0 wxID_ANY wxITEM_NORMAL - Clone view + Clone procedure tool_clone_procedure protected Clone procedure Clone procedure - on_clone_view + on_clone_procedure Load From File; icons/16x16/delete.png @@ -11048,7 +11048,7 @@ protected Delete procedure Delete procedure - on_delete_view + on_delete_procedure @@ -11110,7 +11110,7 @@ Functions - 0 + 1 1 1 @@ -11239,7 +11239,7 @@ protected Add new function Add new function - on_insert_view + on_insert_function Load From File; icons/16x16/page_copy.png @@ -11251,7 +11251,7 @@ protected Clone function Clone function - on_clone_view + on_clone_function Load From File; icons/16x16/delete.png @@ -11263,7 +11263,7 @@ protected Delete function Delete function - on_delete_view + on_delete_function @@ -13606,162 +13606,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 @@ -13855,281 +13781,133 @@ bSizer77 - wxVERTICAL + wxHORIZONTAL none 5 wxEXPAND - 1 - + 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 - 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 - - - + 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 - - 0 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - dv_table_foreign_keys - protected - - - - TableForeignKeysDataViewCtrl; .components.dataview; forward_declare - - - - - + + Load From File; icons/16x16/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 + + + dv_table_foreign_keys + protected + + + + TableForeignKeysDataViewCtrl; .components.dataview; forward_declare + + + + @@ -14194,290 +13972,142 @@ bSizer771 - wxVERTICAL + wxHORIZONTAL none 5 wxEXPAND - 1 - - - bSizer781 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER - 0 - - - bSizer792 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/add.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Insert - - 0 - - 0 - - - 0 - - 1 - btn_insert_check - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_insert_foreign_key - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/delete.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Remove - - 0 - - 0 - - - 0 - - 1 - btn_delete_check - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_foreign_key - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/cross.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Clear - - 0 - - 0 - - - 0 - - 1 - btn_clear_check - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_clear_foreign_key - - - + 0 + + 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 - - 0 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - dv_table_checks - protected - - - - TableCheckDataViewCtrl; .components.dataview; forward_declare - - - - - + + 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 - - - - - Load From File; icons/16x16/code-folding.png - Create - 0 + + 0 + wxALL|wxEXPAND + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + dv_table_checks + protected + + + + TableCheckDataViewCtrl; .components.dataview; forward_declare + + + + + + + + + + + Load From File; icons/16x16/code-folding.png + Create + 0 1 1 @@ -15162,7 +14792,7 @@ - + Load From File; icons/16x16/view.png View 0 @@ -15225,9 +14855,9 @@ none 5 - wxEXPAND | wxALL - 0 - + wxEXPAND + 1 + 1 1 1 @@ -15238,7 +14868,6 @@ 0 - 1 0 @@ -15260,11 +14889,12 @@ 0 + 0 0 1 - m_notebook7 + m_splitter11 1 @@ -15272,19 +14902,20 @@ 1 Resizable + 0.0 + 0 + -1 1 - + wxSPLIT_HORIZONTAL + wxSP_3D ; ; forward_declare 0 - - - Options - 0 + 1 1 @@ -15321,7 +14952,7 @@ 0 1 - pnl_view_editor_root + m_panel79 1 @@ -15339,85 +14970,72 @@ wxTAB_TRAVERSAL - bSizer85 + bSizer170 wxVERTICAL none 5 - wxALL|wxEXPAND + wxEXPAND | wxALL 0 - + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 - 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 + m_notebook7 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + + + General + 0 + 1 1 1 @@ -15449,12 +15067,11 @@ 0 - 0 0 1 - txt_view_name + pnl_view_editor_root 1 @@ -15464,99 +15081,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 @@ -15592,7 +15134,7 @@ 0 0 wxID_ANY - Definer + Name 0 0 @@ -15601,7 +15143,7 @@ 0 150,-1 1 - lbl_view_definer + m_staticText40 1 @@ -15625,7 +15167,7 @@ 5 wxALIGN_CENTER|wxALL 1 - + 1 1 1 @@ -15639,7 +15181,6 @@ 1 0 - 1 1 @@ -15658,11 +15199,12 @@ 0 + 0 0 1 - cmb_view_definer + txt_view_name 1 @@ -15670,7 +15212,6 @@ 1 Resizable - -1 1 @@ -15689,1307 +15230,1638 @@ - - - 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 - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer8711 - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - pnl_row_sql_security - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - szr_view_sql_security - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - SQL security - 0 - - 0 - - - 0 - 150,-1 - 1 - lbl_view_sql_security - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALIGN_CENTER|wxALL - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - "DEFINER" "INVOKER" - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - cho_view_sql_security - 1 - - - protected - 1 - - Resizable - 0 - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - + 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 + + + + + + + + + - - 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 + + + + + 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_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 - 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 - - - + 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 - 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 + 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 - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - TEMPTABLE - - 0 - - - 0 - - 1 - rad_view_algorithm_temptable - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - 0 - - - + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + pnl_row_constraint + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + View constraint + + szr_view_constraint + wxVERTICAL + 1 + none + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + None + + 0 + + + 0 + + 1 + rad_view_constraint_none + 1 + + + protected + 1 + + Resizable + 1 + + wxRB_GROUP + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + LOCAL + + 0 + + + 0 + + 1 + rad_view_constraint_local + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + CASCADE + + 0 + + + 0 + + 1 + rad_view_constraint_cascaded + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + CHECK ONLY + + 0 + + + 0 + + 1 + rad_view_constraint_check_only + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + READ ONLY + + 0 + + + 0 + + 1 + rad_view_constraint_read_only + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + - - 5 - 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 - - - + + + + + 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 + + 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 - 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 + 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 - 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 + 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 - 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 + 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 - 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 - - - - + 1 + pnl_row_security_barrier + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer126 + wxVERTICAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Force + + 0 + + + 0 + + 1 + chk_view_force + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + - - - 5 - wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - pnl_row_force - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - + + 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 - 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 + + + + + @@ -17002,74 +16874,135 @@ - - - - 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 - - - + + + 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_panel80 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + 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 + + + + + + + + @@ -17310,9 +17243,9 @@ Load From File; icons/16x16/code-folding.png - Procedure - 0 - + Routine + 1 + 1 1 1 @@ -17348,7 +17281,7 @@ 0 1 - panel_procedures + panel_routine 1 @@ -17364,16 +17297,16 @@ wxTAB_TRAVERSAL - + bSizer160 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -17431,8 +17364,8 @@ - - + + 1 1 1 @@ -17484,215 +17417,16 @@ wxTAB_TRAVERSAL - + bSizer166 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 - txt_name_procedure - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - m_panel74 - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer161 - wxVERTICAL - none - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -17701,9 +17435,9 @@ 0 0 - 1 + 1 0 @@ -17718,6707 +17452,5179 @@ 1 1 - 0 0 0 wxID_ANY - 1 - 1 0 - -1,-1 + 0 - -1,200 + 1 - stc_procedure + m_notebook11 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_delete_procedure - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Cancel - - 0 - - 0 - - - 0 - - 1 - btn_cancel_procedure - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Save - - 0 - - 0 - - - 0 - - 1 - btn_save_procedure - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - - - - 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 - 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - panel_records - 1 - - - protected - 1 - - Resizable - 1 - - ; ; forward_declare - 0 - - - - wxTAB_TRAVERSAL - - - bSizer61 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - - 0 - - - 0 - - 1 - m_toolBar3 - 1 - 1 - - - protected - 1 - - Resizable - 5 - 1 - - wxTB_HORIZONTAL|wxTB_HORZ_TEXT - ; ; forward_declare - 0 - - - - - - Load From File; icons/16x16/arrow_refresh.png - 0 - wxID_ANY - wxITEM_NORMAL - 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 - - - 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 - wxALL - 0 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - m_dataViewListCtrl2 - protected - - - wxDV_ROW_LINES - ; ; forward_declare - - - - - - - - 5 - wxALL - 0 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - m_dataViewCtrl10 - protected - - - - ; ; forward_declare - - - - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Comments - wxDATAVIEW_CELL_INERT - 7 - m_dataViewColumn181 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Collation - wxDATAVIEW_CELL_INERT - 6 - m_dataViewColumn191 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Engine - wxDATAVIEW_CELL_INERT - 5 - m_dataViewColumn171 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Updated at - wxDATAVIEW_CELL_INERT - 4 - m_dataViewColumn161 - protected - Date - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Created at - wxDATAVIEW_CELL_INERT - 3 - m_dataViewColumn151 - protected - Date - -1 - - - wxALIGN_RIGHT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Size - wxDATAVIEW_CELL_INERT - 2 - m_dataViewColumn141 - protected - Text - -1 - - - - - 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 - - - - - - 5 - wxEXPAND - 0 - - - bSizer531 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Table: - 0 - - 0 - - - 0 - -1,-1 - 1 - m_staticText391 - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxEXPAND - 0 - - 0 - protected - 100 - - - - 2 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/add.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Insert - - 0 - - 0 - - - 0 - - 1 - btn_insert_table - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_insert_table - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/table_multiple.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Clone - - 0 - - 0 - - - 0 - - 1 - btn_clone_table - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_clone_table - - - - 2 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - Load From File; icons/16x16/delete.png - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 0 - - 1 - - - 0 - 0 - wxID_ANY - Delete - - 0 - - 0 - - - 0 - - 1 - btn_delete_table1 - 1 - - - protected - 1 - - - - Resizable - 1 - - wxBORDER_NONE - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - on_delete_table - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - - - 5 - wxEXPAND - 1 - - - bSizer156 - 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 - MyButton - - 0 - - 0 - - - 0 - - 1 - m_button57 - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - - 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 - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer165 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Cancel - - 0 - - 0 - - - 0 - - 1 - m_button57 - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - - - - - 1 - 0 - 1 - - 1 - - 0 - 0 - - Dock - 0 - Left - 0 - 1 - - 1 - - - 0 - 0 - wxID_ANY - Save - - 0 - - 0 - - - 0 - - 1 - m_button58 - 1 - - - protected - 1 - - - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - + + + 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 + + + + + + diff --git a/locale/de_DE/LC_MESSAGES/petersql.mo b/locale/de_DE/LC_MESSAGES/petersql.mo index a5c6beb378f983413a045b19681b22c4aee34bc8..338eebd6468f60b5df504e48ece710a376e6125f 100644 GIT binary patch literal 18654 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 literal 7894 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 diff --git a/locale/de_DE/LC_MESSAGES/petersql.po b/locale/de_DE/LC_MESSAGES/petersql.po index 30bcd7d..217b7b9 100644 --- a/locale/de_DE/LC_MESSAGES/petersql.po +++ b/locale/de_DE/LC_MESSAGES/petersql.po @@ -2,20 +2,19 @@ # 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-05-02 16:08+0200\n" +"POT-Creation-Date: 2026-05-31 12:36+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" -"Language: de_DE\n" "Language-Team: de_DE \n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: de_DE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.18.0\n" #: helpers/__init__.py:16 @@ -47,68 +46,68 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "OpenSSH-Client nicht gefunden." -#: structures/engines/context.py:535 +#: structures/engines/context.py:544 msgid "This connection is read-only." -msgstr "" +msgstr "Diese Verbindung ist schreibgeschützt." -#: structures/engines/mariadb/context.py:616 -#: structures/engines/mysql/context.py:627 -#: structures/engines/postgresql/context.py:685 -#: structures/engines/sqlite/context.py:552 +#: structures/engines/mariadb/context.py:632 +#: structures/engines/mysql/context.py:643 +#: 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:644 -#: structures/engines/mysql/context.py:655 -#: structures/engines/postgresql/context.py:710 -#: structures/engines/sqlite/context.py:576 +#: structures/engines/mariadb/context.py:660 +#: structures/engines/mysql/context.py:671 +#: 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:662 -#: structures/engines/mysql/context.py:673 -#: structures/engines/postgresql/context.py:728 -#: structures/engines/sqlite/context.py:594 +#: structures/engines/mariadb/context.py:678 +#: structures/engines/mysql/context.py:689 +#: 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:702 -#: structures/engines/mysql/context.py:711 -#: structures/engines/postgresql/context.py:768 -#: structures/engines/sqlite/context.py:634 +#: structures/engines/mariadb/context.py:718 +#: structures/engines/mysql/context.py:727 +#: 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:735 -#: structures/engines/mysql/context.py:742 -#: structures/engines/postgresql/context.py:798 -#: structures/engines/sqlite/context.py:662 +#: structures/engines/mariadb/context.py:751 +#: structures/engines/mysql/context.py:758 +#: 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:788 +#: structures/engines/mariadb/context.py:804 #, python-brace-format msgid "Trigger{trigger_index:03}" -msgstr "" +msgstr "Trigger{trigger_index:03}" #: windows/dialogs/connections/view.py:417 -#: windows/dialogs/connections/view.py:764 windows/main/controller.py:1212 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1290 #: windows/views.py:33 msgid "Connection" msgstr "Verbindung" #: windows/components/dataview.py:115 windows/components/dataview.py:240 #: windows/components/dataview.py:253 windows/components/dataview.py:268 -#: windows/main/database/procedure.py:129 windows/views.py:47 -#: windows/views.py:97 windows/views.py:1016 windows/views.py:1328 -#: windows/views.py:1361 windows/views.py:1385 windows/views.py:1409 -#: windows/views.py:1433 windows/views.py:1457 windows/views.py:1574 -#: windows/views.py:1954 windows/views.py:3079 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1024 +#: windows/views.py:1336 windows/views.py:1369 windows/views.py:1393 +#: windows/views.py:1417 windows/views.py:1441 windows/views.py:1465 +#: windows/views.py:1582 windows/views.py:1928 windows/views.py:2199 +#: windows/views.py:3293 windows/views.py:3515 msgid "Name" msgstr "Name" @@ -116,34 +115,32 @@ msgstr "Name" msgid "Last connection" msgstr "Letzte Verbindung" -#: windows/dialogs/connections/view.py:655 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:61 msgid "New directory" msgstr "Neues Verzeichnis" -#: windows/dialogs/connections/model.py:211 +#: windows/dialogs/connections/model.py:212 #: windows/dialogs/connections/view.py:615 windows/views.py:65 msgid "New connection" msgstr "Neue Verbindung" #: windows/views.py:71 -#, fuzzy msgid "Rename" -msgstr "Name" +msgstr "Umbenennen" #: windows/views.py:76 -#, fuzzy msgid "Clone connection" -msgstr "Neue Verbindung" +msgstr "Verbindung klonen" -#: windows/main/database/procedure.py:183 windows/views.py:81 -#: windows/views.py:613 windows/views.py:1488 windows/views.py:1896 -#: windows/views.py:2155 windows/views.py:2932 windows/views.py:2981 -#: windows/views.py:3215 windows/views.py:3247 +#: windows/views.py:81 windows/views.py:613 windows/views.py:1496 +#: windows/views.py:1864 windows/views.py:2154 windows/views.py:2378 +#: windows/views.py:3203 windows/views.py:3252 windows/views.py:3651 +#: windows/views.py:3683 msgid "Delete" msgstr "Löschen" -#: windows/views.py:111 windows/views.py:1333 windows/views.py:1629 -#: windows/views.py:2844 windows/views.py:3134 +#: windows/views.py:111 windows/views.py:1341 windows/views.py:1637 +#: windows/views.py:3115 windows/views.py:3570 msgid "Engine" msgstr "Engine" @@ -155,12 +152,11 @@ msgstr "Host + Port" msgid "Username" msgstr "Benutzername" -#: windows/views.py:161 windows/views.py:1142 +#: windows/views.py:161 windows/views.py:1150 msgid "Password" msgstr "Passwort" #: windows/views.py:174 -#, fuzzy msgid "Connection timeout" msgstr "Verbindung verloren" @@ -170,7 +166,7 @@ msgstr "TLS verwenden" #: windows/views.py:203 msgid "Mark read only" -msgstr "" +msgstr "Als schreibgeschützt markieren" #: windows/views.py:214 msgid "Use SSH tunnel" @@ -178,28 +174,27 @@ msgstr "SSH-Tunnel verwenden" #: windows/views.py:225 msgid "Compressed client/server protocol" -msgstr "" +msgstr "Komprimiertes Client/Server-Protokoll" #: windows/views.py:244 msgid "Filename" msgstr "Dateiname" -#: windows/views.py:249 windows/views.py:368 windows/views.py:3373 +#: windows/views.py:249 windows/views.py:368 windows/views.py:3809 msgid "Select a file" msgstr "Datei auswählen" #: windows/views.py:249 windows/views.py:368 -#, fuzzy msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:266 windows/views.py:1335 windows/views.py:1587 -#: windows/views.py:2842 windows/views.py:3092 +#: windows/views.py:266 windows/views.py:1343 windows/views.py:1595 +#: windows/views.py:3113 windows/views.py:3528 msgid "Comments" msgstr "Kommentare" -#: windows/main/controller.py:766 windows/views.py:280 windows/views.py:740 +#: windows/main/controller.py:769 windows/views.py:280 windows/views.py:740 #: windows/views.py:894 msgid "Settings" msgstr "Einstellungen" @@ -234,30 +229,31 @@ msgstr "Lokaler Port" #: windows/views.py:354 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" +msgstr "" +"wenn der Wert auf 0 gesetzt ist, wird der erste verfügbare Port verwendet" #: windows/views.py:363 msgid "Identity file" msgstr "Identitätsdatei" #: windows/views.py:379 -#, fuzzy msgid "Remote host + port" -msgstr "Host + Port" +msgstr "Remote-Host + Port" #: windows/views.py:391 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)." +msgstr "" +"Remote-Host/Port ist das eigentliche DB-Ziel (standardmäßig DB-Host/Port)." #: windows/views.py:400 msgid "SSH extra args" -msgstr "" +msgstr "Zusätzliche SSH-Argumente" #: windows/views.py:415 msgid "SSH Tunnel" msgstr "SSH-Tunnel" -#: windows/views.py:421 windows/views.py:1331 windows/views.py:2846 +#: windows/views.py:421 windows/views.py:1339 windows/views.py:3117 msgid "Created at" msgstr "Erstellt am" @@ -266,7 +262,6 @@ msgid "Successful connections" msgstr "Erfolgreiche Verbindungen" #: windows/views.py:472 -#, fuzzy msgid "Last successful connection" msgstr "Erfolgreiche Verbindungen" @@ -276,51 +271,46 @@ msgstr "Erfolglose Verbindungen" #: windows/views.py:506 msgid "Last failure reason" -msgstr "" +msgstr "Letzter Fehlergrund" #: windows/views.py:523 -#, fuzzy msgid "Total connection attempts" msgstr "Letzte Verbindung" #: windows/views.py:540 -#, fuzzy msgid "Average connection time (ms)" msgstr "Wiederverbindung fehlgeschlagen:" #: windows/views.py:557 -#, fuzzy msgid "Most recent connection duration" -msgstr "Verbindungsmanager öffnen" +msgstr "Dauer der letzten Verbindung" #: windows/views.py:576 msgid "Statistics" msgstr "Statistiken" -#: windows/views.py:594 windows/views.py:1853 +#: windows/views.py:594 windows/views.py:1821 msgid "Create" msgstr "Erstellen" #: windows/views.py:598 -#, fuzzy msgid "Create connection" -msgstr "Letzte Verbindung" +msgstr "Verbindung erstellen" #: windows/views.py:601 -#, fuzzy msgid "Create directory" msgstr "Neues Verzeichnis" -#: windows/main/database/procedure.py:184 windows/views.py:630 -#: windows/views.py:854 windows/views.py:1483 windows/views.py:1899 -#: windows/views.py:2160 windows/views.py:2216 windows/views.py:2908 -#: windows/views.py:3250 windows/views.py:3387 +#: windows/views.py:630 windows/views.py:854 windows/views.py:1491 +#: windows/views.py:1867 windows/views.py:2159 windows/views.py:2383 +#: windows/views.py:2439 windows/views.py:3179 windows/views.py:3686 +#: windows/views.py:3823 msgid "Cancel" msgstr "Abbrechen" -#: windows/main/controller.py:323 windows/main/database/procedure.py:185 -#: windows/views.py:635 windows/views.py:2165 windows/views.py:2358 -#: windows/views.py:3255 windows/views.py:3393 +#: windows/main/controller.py:323 windows/views.py:635 windows/views.py:2164 +#: windows/views.py:2388 windows/views.py:2603 windows/views.py:3691 +#: windows/views.py:3829 msgid "Save" msgstr "Speichern" @@ -332,7 +322,7 @@ msgstr "Testen" msgid "Connect" msgstr "Verbinden" -#: windows/main/database/procedure.py:162 windows/views.py:752 +#: windows/views.py:752 msgid "Language" msgstr "Sprache" @@ -353,9 +343,8 @@ msgid "Locale" msgstr "Lokale" #: windows/views.py:790 -#, fuzzy msgid "Column content" -msgstr "Neue Verbindung" +msgstr "Spalteninhalt" #: windows/views.py:800 msgid "Syntax" @@ -393,536 +382,558 @@ msgstr "Vom Server trennen" msgid "tool" msgstr "Werkzeug" -#: windows/views.py:914 windows/views.py:2196 +#: windows/views.py:914 windows/views.py:2419 msgid "Refresh" msgstr "Aktualisieren" -#: windows/views.py:918 windows/views.py:920 windows/views.py:1877 -#: windows/views.py:2200 windows/views.py:2344 +#: windows/views.py:918 windows/views.py:920 windows/views.py:1845 +#: windows/views.py:2423 windows/views.py:2589 msgid "Add" msgstr "Hinzufügen" -#: windows/views.py:954 windows/views.py:958 windows/views.py:2322 +#: windows/views.py:925 +#, python-brace-format +msgid "{mode}" +msgstr "{mode}" + +#: windows/views.py:962 windows/views.py:966 windows/views.py:2556 +#: windows/views.py:2561 msgid "MyMenuItem" msgstr "MeinMenüElement" -#: windows/views.py:961 windows/views.py:1927 windows/views.py:3278 +#: windows/views.py:969 windows/views.py:1895 windows/views.py:2568 +#: windows/views.py:3714 msgid "MyMenu" msgstr "MeinMenü" -#: windows/views.py:976 windows/views.py:1511 windows/views.py:1518 -#: windows/views.py:1525 +#: windows/views.py:984 windows/views.py:1519 windows/views.py:1526 +#: windows/views.py:1533 msgid "MyLabel" msgstr "MeinLabel" -#: windows/views.py:982 +#: windows/views.py:990 msgid "Databases" msgstr "Datenbanken" -#: windows/views.py:983 windows/views.py:1330 windows/views.py:2847 +#: windows/views.py:991 windows/views.py:1338 windows/views.py:3118 msgid "Size" msgstr "Größe" -#: windows/views.py:984 +#: windows/views.py:992 msgid "Elements" msgstr "Elemente" -#: windows/views.py:985 +#: windows/views.py:993 msgid "Modified at" msgstr "Geändert am" -#: windows/views.py:986 windows/views.py:1345 +#: windows/views.py:994 windows/views.py:1353 msgid "Tables" msgstr "Tabellen" -#: windows/views.py:993 +#: windows/views.py:1001 msgid "System" msgstr "System" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1039 -#: windows/views.py:1334 windows/views.py:2843 +#: windows/components/dataview.py:89 windows/views.py:1047 +#: windows/views.py:1342 windows/views.py:3114 msgid "Collation" msgstr "Sortierung" -#: windows/views.py:1068 +#: windows/views.py:1076 msgid "Encryption" -msgstr "" +msgstr "Verschlüsselung" -#: windows/views.py:1080 +#: windows/main/controller.py:1259 windows/main/controller.py:1281 +#: windows/main/controller.py:1285 windows/views.py:1088 msgid "Read Only" -msgstr "" +msgstr "Schreibgeschützt" -#: windows/views.py:1097 -#, fuzzy +#: windows/views.py:1105 msgid "Tablespace" -msgstr "Tabellen" +msgstr "Tablespace" -#: windows/views.py:1118 -#, fuzzy +#: windows/views.py:1126 msgid "Connection limit" -msgstr "Verbindung verloren" +msgstr "Verbindungslimit" -#: windows/views.py:1161 -#, fuzzy +#: windows/views.py:1169 msgid "Profile" -msgstr "Datei" +msgstr "Profil" -#: windows/views.py:1187 -#, fuzzy +#: windows/views.py:1195 msgid "Default tablespace" -msgstr "Tabelle löschen" +msgstr "Standard-Tablespace" -#: windows/views.py:1208 -#, fuzzy +#: windows/views.py:1216 msgid "Temporary tablespace" -msgstr "Temporär" +msgstr "Temporärer Tablespace" -#: windows/views.py:1234 +#: windows/views.py:1242 msgid "Quota" -msgstr "" +msgstr "Quota" -#: windows/views.py:1253 +#: windows/views.py:1261 msgid "Unlimited quota" -msgstr "" +msgstr "Unbegrenzte Quota" -#: windows/views.py:1270 +#: windows/views.py:1278 msgid "Account status" -msgstr "" +msgstr "Kontostatus" -#: windows/views.py:1291 -#, fuzzy +#: windows/views.py:1299 msgid "Password expire" -msgstr "Passwort" +msgstr "Passwortablauf" -#: windows/views.py:1315 -#, fuzzy +#: windows/views.py:1323 msgid "Add new table" -msgstr "Tabelle löschen" +msgstr "Neue Tabelle hinzufügen" -#: windows/views.py:1317 -#, fuzzy +#: windows/views.py:1325 msgid "Clone table" -msgstr "Tabelle löschen" +msgstr "Tabelle klonen" -#: windows/main/controller.py:1633 windows/views.py:1319 +#: windows/main/controller.py:1770 windows/views.py:1327 msgid "Delete table" msgstr "Tabelle löschen" -#: windows/views.py:1329 +#: windows/views.py:1337 msgid "Rows" msgstr "Zeilen" -#: windows/views.py:1332 windows/views.py:2845 +#: windows/views.py:1340 windows/views.py:3116 msgid "Updated at" msgstr "Aktualisiert am" -#: windows/views.py:1350 +#: windows/views.py:1358 msgid "Add new view" -msgstr "" +msgstr "Neue Ansicht hinzufügen" -#: windows/views.py:1352 windows/views.py:1376 -#, fuzzy +#: windows/views.py:1360 windows/views.py:1384 msgid "Clone view" msgstr "Klonen" -#: windows/views.py:1354 -#, fuzzy +#: windows/views.py:1362 msgid "Delete view" msgstr "Löschen" -#: windows/views.py:1362 windows/views.py:1386 windows/views.py:1410 -#: windows/views.py:1434 windows/views.py:1458 -#, fuzzy +#: windows/views.py:1370 windows/views.py:1394 windows/views.py:1418 +#: windows/views.py:1442 windows/views.py:1466 msgid "Definition" -msgstr "Bedingung" +msgstr "Definition" -#: windows/views.py:1369 windows/views.py:2177 +#: windows/views.py:1377 msgid "Views" msgstr "Ansichten" -#: windows/views.py:1374 +#: windows/views.py:1382 msgid "Add new procedure" -msgstr "" +msgstr "Neue Prozedur hinzufügen" -#: windows/views.py:1376 +#: windows/views.py:1384 msgid "Clone procedure" -msgstr "" +msgstr "Prozedur klonen" -#: windows/views.py:1378 -#, fuzzy +#: windows/views.py:1386 msgid "Delete procedure" -msgstr "Datensatz löschen" +msgstr "Prozedur löschen" -#: windows/views.py:1393 +#: windows/views.py:1401 msgid "Procedures" -msgstr "" +msgstr "Prozeduren" -#: windows/views.py:1398 -#, fuzzy +#: windows/views.py:1406 msgid "Add new function" -msgstr "Neue Verbindung" +msgstr "Neue Funktion hinzufügen" -#: windows/views.py:1400 -#, fuzzy +#: windows/views.py:1408 msgid "Clone function" -msgstr "Neue Verbindung" +msgstr "Funktion klonen" -#: windows/views.py:1402 -#, fuzzy +#: windows/views.py:1410 msgid "Delete function" -msgstr "Datensatz löschen" +msgstr "Funktion löschen" -#: windows/views.py:1417 -#, fuzzy +#: windows/views.py:1425 msgid "Functions" -msgstr "Verbindung" +msgstr "Funktionen" -#: windows/views.py:1422 -#, fuzzy +#: windows/views.py:1430 msgid "Add new trigger" -msgstr "Trigger" +msgstr "Neuen Trigger hinzufügen" -#: windows/views.py:1424 -#, fuzzy +#: windows/views.py:1432 msgid "Clone trigger" -msgstr "Trigger" +msgstr "Trigger klonen" -#: windows/views.py:1426 -#, fuzzy +#: windows/views.py:1434 msgid "Delete trigger" -msgstr "Datensatz löschen" +msgstr "Trigger löschen" -#: windows/views.py:1441 windows/views.py:2185 +#: windows/views.py:1449 msgid "Triggers" msgstr "Trigger" -#: windows/views.py:1446 +#: windows/views.py:1454 msgid "Add new event" -msgstr "" +msgstr "Neues Ereignis hinzufügen" -#: windows/views.py:1448 -#, fuzzy +#: windows/views.py:1456 msgid "Clone event" msgstr "Klonen" -#: windows/views.py:1450 -#, fuzzy +#: windows/views.py:1458 msgid "Delete event" -msgstr "Tabelle löschen" +msgstr "Ereignis löschen" -#: windows/views.py:1465 -#, fuzzy +#: windows/views.py:1473 msgid "Events" -msgstr "Elemente" +msgstr "Ereignisse" -#: windows/views.py:1493 windows/views.py:1904 windows/views.py:2214 -#: windows/views.py:2296 windows/views.py:2915 +#: windows/views.py:1501 windows/views.py:1872 windows/views.py:2437 +#: windows/views.py:2521 windows/views.py:3186 msgid "Apply" msgstr "Anwenden" -#: windows/main/database/procedure.py:122 windows/views.py:1505 -#: windows/views.py:1684 windows/views.py:2114 windows/views.py:3167 +#: windows/views.py:1513 windows/views.py:1692 windows/views.py:3603 msgid "Options" msgstr "Optionen" -#: windows/views.py:1536 +#: windows/views.py:1544 msgid "Diagram" msgstr "Diagramm" -#: windows/views.py:1547 +#: windows/views.py:1555 msgid "Database" msgstr "Datenbank" -#: windows/views.py:1602 windows/views.py:3107 +#: windows/views.py:1610 windows/views.py:3543 msgid "Base" msgstr "Basis" -#: windows/views.py:1616 windows/views.py:3121 +#: windows/views.py:1624 windows/views.py:3557 msgid "Auto Increment" msgstr "Auto Inkrement" -#: windows/views.py:1644 windows/views.py:3149 +#: windows/views.py:1652 windows/views.py:3585 msgid "Default Collation" msgstr "Standard-Sortierung" -#: windows/views.py:1654 +#: windows/views.py:1662 msgid "Convert data" -msgstr "" +msgstr "Daten konvertieren" -#: windows/views.py:1662 +#: windows/views.py:1670 msgid "Row format" -msgstr "" +msgstr "Zeilenformat" -#: windows/views.py:1696 windows/views.py:1737 windows/views.py:1781 -#: windows/views.py:1879 windows/views.py:2204 +#: windows/views.py:1703 windows/views.py:1731 windows/views.py:1759 +#: windows/views.py:1847 windows/views.py:2259 windows/views.py:2427 +#: windows/views.py:3313 windows/views.py:3330 windows/views.py:3354 +#: windows/views.py:3381 msgid "Remove" msgstr "Entfernen" -#: windows/views.py:1703 windows/views.py:1744 windows/views.py:1788 +#: windows/views.py:1705 windows/views.py:1733 windows/views.py:1761 +#: windows/views.py:2261 windows/views.py:2529 windows/views.py:3320 +#: windows/views.py:3337 windows/views.py:3361 windows/views.py:3388 msgid "Clear" msgstr "Löschen" -#: windows/views.py:1718 windows/views.py:3181 +#: windows/views.py:1718 windows/views.py:3617 msgid "Indexes" msgstr "Indizes" -#: windows/views.py:1732 windows/views.py:1776 windows/views.py:2927 -#: windows/views.py:2969 windows/views.py:3210 +#: windows/views.py:1729 windows/views.py:1757 windows/views.py:2257 +#: windows/views.py:3198 windows/views.py:3240 windows/views.py:3308 +#: windows/views.py:3349 windows/views.py:3376 windows/views.py:3646 msgid "Insert" msgstr "Einfügen" -#: windows/views.py:1762 +#: windows/views.py:1746 msgid "Foreign Keys" msgstr "Fremdschlüssel" -#: windows/views.py:1806 +#: windows/views.py:1774 msgid "Checks" msgstr "Prüfungen" -#: windows/views.py:1873 windows/views.py:3202 +#: windows/views.py:1841 windows/views.py:3638 msgid "Columns:" msgstr "Spalten:" -#: windows/views.py:1883 -#, fuzzy +#: windows/views.py:1851 msgid "Move Up" msgstr "Nach oben bewegen\tCTRL+UP" -#: windows/views.py:1885 -#, fuzzy +#: windows/views.py:1853 msgid "Move Down" msgstr "Nach unten bewegen\tCTRL+D" -#: windows/views.py:1917 windows/views.py:1924 windows/views.py:3268 -#: windows/views.py:3275 +#: windows/views.py:1885 windows/views.py:1892 windows/views.py:3704 +#: windows/views.py:3711 msgid "Add Index" msgstr "Index hinzufügen" -#: windows/views.py:1921 windows/views.py:3272 +#: windows/views.py:1889 windows/views.py:3708 msgid "Add PrimaryKey" msgstr "Primärschlüssel hinzufügen" -#: windows/views.py:1938 +#: windows/views.py:1906 msgid "Table" msgstr "Tabelle" -#: windows/main/database/procedure.py:140 windows/views.py:1974 -#, fuzzy -msgid "Definer" -msgstr "Einfügen" - -#: windows/views.py:1994 +#: windows/views.py:1948 windows/views.py:2219 msgid "Schema" -msgstr "" +msgstr "Schema" -#: windows/views.py:2020 -msgid "SQL security" -msgstr "" +#: windows/views.py:1976 windows/views.py:2247 +msgid "General" +msgstr "Allgemein" -#: windows/views.py:2027 -#, fuzzy -msgid "DEFINER" -msgstr "Einfügen" - -#: windows/views.py:2027 -#, fuzzy -msgid "INVOKER" -msgstr "Einfügen" - -#: windows/views.py:2039 +#: windows/views.py:1981 msgid "Algorithm" -msgstr "" +msgstr "Algorithmus" -#: windows/views.py:2041 -#, fuzzy +#: windows/views.py:1983 msgid "UNDEFINED" msgstr "Ohne Vorzeichen" -#: windows/views.py:2044 +#: windows/views.py:1986 msgid "MERGE" -msgstr "" +msgstr "MERGE" -#: windows/views.py:2047 -#, fuzzy +#: windows/views.py:1989 msgid "TEMPTABLE" -msgstr "Tabelle" +msgstr "TEMPTABLE" -#: windows/views.py:2057 +#: windows/views.py:1999 msgid "View constraint" -msgstr "" +msgstr "Ansichtseinschränkung" -#: windows/views.py:2059 -#, fuzzy +#: windows/views.py:2001 msgid "None" -msgstr "Klonen" +msgstr "Keiner" -#: windows/views.py:2062 -#, fuzzy +#: windows/views.py:2004 msgid "LOCAL" -msgstr "Lokale" +msgstr "LOKAL" -#: windows/views.py:2065 -#, fuzzy +#: windows/views.py:2007 msgid "CASCADE" -msgstr "Abbrechen" +msgstr "KASKADE" -#: windows/views.py:2068 -#, fuzzy +#: windows/views.py:2010 msgid "CHECK ONLY" -msgstr "Prüfen" +msgstr "NUR PRÜFEN" -#: windows/views.py:2071 +#: windows/views.py:2013 msgid "READ ONLY" -msgstr "" +msgstr "SCHREIBGESCHÜTZT" + +#: windows/views.py:2026 +msgid "Behavior" +msgstr "Verhalten" + +#: windows/views.py:2033 windows/views.py:2281 +msgid "Definer" +msgstr "Definierer" + +#: windows/views.py:2041 windows/views.py:2289 +msgid "*" +msgstr "*" + +#: windows/views.py:2053 windows/views.py:2301 +msgid "SQL security" +msgstr "SQL-Sicherheit" + +#: windows/views.py:2060 windows/views.py:2308 +msgid "DEFINER" +msgstr "DEFINIERER" + +#: windows/views.py:2060 windows/views.py:2308 +msgid "INVOKER" +msgstr "AUFRUFER" -#: windows/views.py:2083 +#: windows/views.py:2074 msgid "Force" -msgstr "" +msgstr "Erzwingen" -#: windows/views.py:2095 +#: windows/views.py:2086 msgid "Security barrier" -msgstr "" +msgstr "Sicherheitsbarriere" + +#: windows/views.py:2099 windows/views.py:2323 +msgid "Security" +msgstr "Sicherheit" -#: windows/views.py:2202 -#, fuzzy +#: windows/views.py:2176 +msgid "View" +msgstr "Ansichten" + +#: windows/views.py:2274 +msgid "Parameters" +msgstr "Parameter" + +#: windows/views.py:2400 +msgid "Procedure" +msgstr "Procedure" + +#: windows/views.py:2408 +msgid "Trigger" +msgstr "Trigger" + +#: windows/views.py:2425 msgid "Duplicate" -msgstr "Datensatz duplizieren" +msgstr "Duplizieren" -#: windows/views.py:2208 +#: windows/views.py:2431 msgid "Apply changes automatically" msgstr "Änderungen automatisch anwenden" -#: windows/views.py:2210 windows/views.py:2211 +#: windows/views.py:2433 windows/views.py:2434 msgid "" -"If enabled, table edits are applied immediately without pressing Apply or" -" Cancel" +"If enabled, table edits are applied immediately without pressing Apply or " +"Cancel" msgstr "" "Wenn aktiviert, werden Tabellenbearbeitungen sofort angewendet, ohne auf " "Anwenden oder Abbrechen zu drücken" -#: windows/views.py:2224 +#: windows/views.py:2447 #, python-brace-format -msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgid "" +"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" +"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -#: windows/views.py:2232 -#, fuzzy +#: windows/views.py:2455 msgid "First" -msgstr "Filter" +msgstr "Erste" -#: windows/views.py:2250 +#: windows/views.py:2473 msgid "Last" -msgstr "" +msgstr "Letzte" -#: windows/views.py:2259 +#: windows/views.py:2482 msgid "Filters" msgstr "Filter" -#: windows/views.py:2299 +#: windows/views.py:2524 +msgid "" +"Apply filters in data\n" +"CTRL+ENTER" +msgstr "" +"Filter in Daten anwenden\n" +"STRG+EINGABE" + +#: windows/views.py:2525 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2319 +#: windows/views.py:2553 msgid "Insert row" msgstr "Zeile einfügen" -#: windows/views.py:2327 +#: windows/components/popup.py:31 windows/views.py:2565 +msgid "NULL" +msgstr "NULL" + +#: windows/views.py:2572 msgid "Data" msgstr "Daten" -#: windows/main/controller.py:318 windows/views.py:2344 -#, fuzzy +#: windows/main/controller.py:318 windows/views.py:2589 msgid "New query" msgstr "Abfrage" -#: windows/views.py:2346 windows/views.py:2866 +#: windows/views.py:2591 windows/views.py:3137 msgid "Close" msgstr "Schließen" -#: windows/main/controller.py:319 windows/views.py:2346 -#, fuzzy +#: windows/main/controller.py:319 windows/views.py:2591 msgid "Close query" msgstr "Abfrage" -#: windows/views.py:2350 +#: windows/views.py:2595 msgid "Run" -msgstr "" +msgstr "Ausführen" -#: windows/main/controller.py:320 windows/views.py:2350 -#, fuzzy +#: windows/main/controller.py:320 windows/views.py:2595 msgid "Execute" -msgstr "SSH-Executable" +msgstr "Ausführen" -#: windows/views.py:2352 +#: windows/views.py:2597 msgid "Run all" -msgstr "" +msgstr "Alle ausführen" -#: windows/views.py:2352 +#: windows/views.py:2597 msgid "Execute all statements" -msgstr "" +msgstr "Alle Anweisungen ausführen" -#: windows/main/controller.py:322 windows/views.py:2354 +#: windows/main/controller.py:322 windows/views.py:2599 msgid "Stop" -msgstr "" +msgstr "Stopp" -#: windows/views.py:2419 +#: windows/views.py:2664 msgid "a page" -msgstr "" +msgstr "eine Seite" -#: windows/views.py:2469 +#: windows/views.py:2714 msgid "Query" msgstr "Abfrage" -#: windows/views.py:2820 -#, fuzzy +#: windows/views.py:3091 msgid "Character set" -msgstr "Erstellt am" +msgstr "Zeichensatz" -#: windows/views.py:2850 windows/views.py:2869 +#: windows/views.py:3121 windows/views.py:3140 msgid "New" msgstr "Neu" -#: windows/views.py:2889 +#: windows/views.py:3160 msgid "Insert record" msgstr "Datensatz einfügen" -#: windows/views.py:2894 +#: windows/views.py:3165 msgid "Duplicate record" msgstr "Datensatz duplizieren" -#: windows/views.py:2901 +#: windows/views.py:3172 msgid "Delete record" msgstr "Datensatz löschen" -#: windows/views.py:2939 windows/views.py:3222 +#: windows/views.py:3210 windows/views.py:3658 msgid "Up" msgstr "Hoch" -#: windows/views.py:2946 windows/views.py:3229 +#: windows/views.py:3217 windows/views.py:3665 msgid "Down" msgstr "Runter" -#: windows/views.py:2961 +#: windows/views.py:3232 msgid "Table:" msgstr "Tabelle:" -#: windows/views.py:2974 +#: windows/views.py:3245 msgid "Clone" msgstr "Klonen" -#: windows/views.py:3358 +#: windows/views.py:3270 +msgid "MyButton" +msgstr "MeinButton" + +#: windows/views.py:3794 msgid "Save Starments" -msgstr "" +msgstr "Anweisungen speichern" -#: windows/views.py:3366 -#, fuzzy +#: windows/views.py:3802 msgid "Location" -msgstr "Sortierung" +msgstr "Speicherort" -#: windows/views.py:3373 +#: windows/views.py:3809 msgid "*.sql" -msgstr "" +msgstr "*.sql" #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 @@ -1032,10 +1043,6 @@ 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" @@ -1046,16 +1053,16 @@ msgstr "Text/Ausdruck" #: windows/dialogs/connections/view.py:126 windows/main/query/renderer.py:192 msgid "Unknown error" -msgstr "" +msgstr "Unbekannter Fehler" #: windows/dialogs/connections/view.py:416 msgid "Connection established successfully" -msgstr "" +msgstr "Verbindung erfolgreich hergestellt" #: 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:431 msgid "Confirm save" @@ -1063,147 +1070,154 @@ msgstr "Speichern bestätigen" #: windows/dialogs/connections/view.py:483 msgid "You have unsaved changes. Do you want to save them before continuing?" -msgstr "" +msgstr "Sie haben ungespeicherte Änderungen. Möchten Sie sie vor dem Fortfahren speichern?" #: windows/dialogs/connections/view.py:485 msgid "Unsaved changes" -msgstr "" +msgstr "Ungespeicherte Änderungen" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:773 msgid "" -"This connection cannot work without TLS. TLS has been enabled " -"automatically." +"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:787 -#, 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:788 +#: windows/dialogs/connections/view.py:799 msgid "Connection error" msgstr "Verbindungsfehler" -#: windows/dialogs/connections/view.py:814 -#, 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:817 -#: windows/dialogs/connections/view.py:834 +#: 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:831 -#, 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:315 #, python-brace-format msgid "{text} ({shortcut})" -msgstr "" +msgstr "{text} ({shortcut})" #: windows/main/controller.py:321 -#, fuzzy msgid "Execute all" -msgstr "SSH-Executable" +msgstr "Alle ausführen" #: windows/main/controller.py:440 windows/main/controller.py:448 -#, fuzzy msgid "Query (1)" msgstr "Abfrage" #: windows/main/controller.py:467 #, python-brace-format msgid "Query ({query_number})" -msgstr "" +msgstr "Query ({query_number})" #: windows/main/controller.py:516 msgid "You have unsaved changes. Save before closing?" -msgstr "" +msgstr "Sie haben ungespeicherte Änderungen. Vor dem Schließen speichern?" #: windows/main/controller.py:517 msgid "Unsaved query" -msgstr "" +msgstr "Ungespeicherte Abfrage" #: windows/main/controller.py:562 -#, fuzzy msgid "Save query" -msgstr "Abfrage" +msgstr "Abfrage speichern" #: windows/main/controller.py:565 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" -msgstr "" +msgstr "SQL-Dateien (*.sql)|*.sql|Alle Dateien (*.*)|*.*" #: windows/main/controller.py:593 windows/main/controller.py:624 #: windows/main/controller.py:651 windows/main/database/list.py:119 -#: windows/main/database/procedure.py:294 -#: windows/main/database/procedure.py:320 windows/main/database/view.py:267 -#: windows/main/database/view.py:296 windows/main/query/controller.py:177 +#: windows/main/database/procedure.py:206 +#: windows/main/database/procedure.py:232 windows/main/database/view.py:268 +#: windows/main/database/view.py:297 windows/main/query/controller.py:177 msgid "Error" msgstr "Fehler" #: windows/main/controller.py:631 #, python-brace-format msgid "-- Saved query to {file_path}" -msgstr "" +msgstr "-- Abfrage gespeichert in {file_path}" #: windows/main/controller.py:657 #, python-brace-format msgid "-- Autosaved query to {file_path}" -msgstr "" +msgstr "-- Abfrage automatisch gespeichert in {file_path}" -#: windows/main/controller.py:719 +#: windows/main/controller.py:722 msgid "days" msgstr "Tage" -#: windows/main/controller.py:720 +#: windows/main/controller.py:723 msgid "hours" msgstr "Stunden" -#: windows/main/controller.py:721 +#: windows/main/controller.py:724 msgid "minutes" msgstr "Minuten" -#: windows/main/controller.py:722 +#: windows/main/controller.py:725 msgid "seconds" msgstr "Sekunden" -#: windows/main/controller.py:730 +#: windows/main/controller.py:733 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Verwendeter Speicher: {used} ({percentage:.2%})" -#: windows/main/controller.py:766 +#: windows/main/controller.py:769 msgid "Settings saved successfully" -msgstr "" +msgstr "Einstellungen erfolgreich gespeichert" -#: windows/main/controller.py:990 +#: windows/main/controller.py:993 #, python-brace-format msgid "~{estimated} (Loading...)" -msgstr "" +msgstr "~{estimated} (Wird geladen...)" -#: windows/main/controller.py:992 +#: windows/main/controller.py:995 msgid "~ (Loading...)" -msgstr "" +msgstr "~ (Wird geladen...)" + +#: windows/main/controller.py:1230 +msgid "Write Mode (2:00)" +msgstr "Schreibmodus (2:00)" -#: windows/main/controller.py:1214 +#: windows/main/controller.py:1281 +msgid "Write Mode" +msgstr "Schreibmodus" + +#: windows/main/controller.py:1292 msgid "Version" msgstr "Version" -#: windows/main/controller.py:1216 +#: windows/main/controller.py:1294 msgid "Uptime" msgstr "Betriebszeit" -#: windows/main/controller.py:1299 +#: windows/main/controller.py:1429 #, 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:1332 +#: windows/main/controller.py:1462 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1212,50 +1226,54 @@ 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:1337 windows/main/controller.py:1358 -#, fuzzy +#: windows/main/controller.py:1467 windows/main/controller.py:1488 msgid "Delete database" -msgstr "Tabelle löschen" +msgstr "Datenbank löschen" -#: windows/main/controller.py:1343 +#: windows/main/controller.py:1473 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:1344 +#: windows/main/controller.py:1474 msgid "Dump not available" -msgstr "" +msgstr "Dump nicht verfügbar" -#: windows/main/controller.py:1357 +#: windows/main/controller.py:1487 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:1372 +#: windows/main/controller.py:1502 msgid "Database deleted successfully" -msgstr "" +msgstr "Datenbank erfolgreich gelöscht" -#: windows/main/controller.py:1373 windows/main/database/procedure.py:283 -#: windows/main/database/procedure.py:314 windows/main/database/view.py:255 -#: windows/main/database/view.py:290 +#: windows/main/controller.py:1503 windows/main/database/procedure.py:195 +#: windows/main/database/procedure.py:226 windows/main/database/view.py:256 +#: windows/main/database/view.py:291 msgid "Success" -msgstr "" +msgstr "Erfolg" -#: windows/main/controller.py:1604 +#: windows/main/controller.py:1741 #, 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:1630 -#, fuzzy, python-brace-format +#: windows/main/controller.py:1767 +#, python-brace-format msgid "Do you want delete the table {table_name}?" -msgstr "Möchten Sie die Datensätze löschen?" +msgstr "Möchten Sie die Tabelle {table_name} löschen?" -#: windows/main/controller.py:1652 +#: windows/main/controller.py:1789 #, python-brace-format msgid "{table_name} (COPY)" -msgstr "" +msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1797 +#: windows/main/controller.py:1948 msgid "Do you want delete the records?" msgstr "Möchten Sie die Datensätze löschen?" @@ -1275,88 +1293,77 @@ msgstr "Verbindung verloren" msgid "Reconnection failed:" msgstr "Wiederverbindung fehlgeschlagen:" -#: windows/main/database/procedure.py:114 -msgid "Procedure" -msgstr "" - -#: windows/main/database/procedure.py:151 -#, fuzzy -msgid "Parameters" -msgstr "PeterSQL" - -#: windows/main/database/procedure.py:282 +#: windows/main/database/procedure.py:194 msgid "Procedure created successfully" -msgstr "" +msgstr "Prozedur erfolgreich erstellt" -#: windows/main/database/procedure.py:282 +#: windows/main/database/procedure.py:194 msgid "Procedure updated successfully" -msgstr "" +msgstr "Prozedur erfolgreich aktualisiert" -#: windows/main/database/procedure.py:294 +#: windows/main/database/procedure.py:206 #, python-brace-format msgid "Error saving procedure: {}" -msgstr "" +msgstr "Fehler beim Speichern der Prozedur: {}" -#: windows/main/database/procedure.py:305 -#, fuzzy, python-brace-format +#: windows/main/database/procedure.py:217 +#, python-brace-format msgid "Are you sure you want to delete procedure '{}'?" -msgstr "Möchten Sie die Datensätze löschen?" +msgstr "Sind Sie sicher, dass Sie die Prozedur '{}' löschen möchten?" -#: windows/main/database/procedure.py:306 windows/main/database/view.py:281 -#, fuzzy +#: windows/main/database/procedure.py:218 windows/main/database/view.py:282 msgid "Confirm Delete" msgstr "Löschen bestätigen" -#: windows/main/database/procedure.py:314 +#: windows/main/database/procedure.py:226 msgid "Procedure deleted successfully" -msgstr "" +msgstr "Prozedur erfolgreich gelöscht" -#: windows/main/database/procedure.py:320 +#: windows/main/database/procedure.py:232 #, python-brace-format msgid "Error deleting procedure: {}" -msgstr "" +msgstr "Fehler beim Löschen der Prozedur: {}" -#: windows/main/database/view.py:254 +#: windows/main/database/view.py:255 msgid "View created successfully" -msgstr "" +msgstr "Ansicht erfolgreich erstellt" -#: windows/main/database/view.py:254 +#: windows/main/database/view.py:255 msgid "View updated successfully" -msgstr "" +msgstr "Ansicht erfolgreich aktualisiert" -#: windows/main/database/view.py:267 +#: 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:280 +#: 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:290 +#: windows/main/database/view.py:291 msgid "View deleted successfully" -msgstr "" +msgstr "Ansicht erfolgreich gelöscht" -#: windows/main/database/view.py:296 +#: 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 #, python-brace-format msgid "{elapsed_ms:.0f} ms" -msgstr "" +msgstr "{elapsed_ms:.0f} ms" #: windows/main/query/controller.py:112 #, python-brace-format msgid "{elapsed_s:.2f} s" -msgstr "" +msgstr "{elapsed_s:.2f} s" #: windows/main/query/controller.py:115 -#, fuzzy msgid "none" -msgstr "Klonen" +msgstr "keine" #: windows/main/query/controller.py:121 #, python-brace-format @@ -1367,94 +1374,96 @@ 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 msgid "Query execution cancelled" -msgstr "" +msgstr "Abfrageausführung abgebrochen" #: windows/main/query/controller.py:176 -#, fuzzy msgid "No active database connection" -msgstr "Neue Verbindung" +msgstr "Keine aktive Datenbankverbindung" #: windows/main/query/history.py:55 -#, fuzzy msgid "(empty query)" -msgstr "Abfrage" +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 "Query {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" -#: windows/main/table/records.py:336 +#: windows/main/table/records.py:334 msgid "Error saving records" -msgstr "" +msgstr "Fehler beim Speichern der Datensätze" #~ msgid "Created at:" -#~ msgstr "" +#~ msgstr "Created at:" #~ msgid "Last connection:" -#~ msgstr "" +#~ msgstr "Last connection:" #~ msgid "Successful connections:" -#~ msgstr "" +#~ msgstr "Successful connections:" #~ msgid "Unsuccessful connections:" -#~ msgstr "" +#~ msgstr "Unsuccessful connections:" #~ msgid "Session Manager" -#~ msgstr "" +#~ msgstr "Session Manager" #~ msgid "Session name" -#~ msgstr "" +#~ msgstr "Session name" #~ msgid "Connection type" -#~ msgstr "" +#~ msgstr "Connection type" #~ msgid "Open" -#~ msgstr "" +#~ msgstr "Open" #~ msgid "Open session manager" -#~ msgstr "" +#~ msgstr "Open session manager" #~ msgid "Foreign Key" -#~ msgstr "" +#~ msgstr "Foreign Key" #~ msgid "New Session" #~ msgstr "Neue Sitzung" @@ -1464,32 +1473,31 @@ msgstr "" #~ msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" #~ msgstr "" -#~ "Tabelle `%(database_name)s`.`%(table_name)s`: " -#~ "%(total_rows) Zeilen insgesamt" +#~ "Tabelle `%(database_name)s`.`%(table_name)s`: %(total_rows) Zeilen insgesamt" #~ msgid "Next" #~ msgstr "Weiter" #~ msgid "{} rows affected" -#~ msgstr "" +#~ msgstr "{} rows affected" #~ msgid "Query {}" #~ msgstr "Abfrage" #~ msgid "Query {} (Error)" -#~ msgstr "" +#~ msgstr "Query {} (Error)" #~ msgid "Query {} ({} rows × {} cols)" -#~ msgstr "" +#~ msgstr "Query {} ({} rows × {} cols)" #~ msgid "{} rows" #~ msgstr "Zeilen" #~ msgid "{:.1f} ms" -#~ msgstr "" +#~ msgstr "{:.1f} ms" #~ msgid "{} warnings" -#~ msgstr "" +#~ msgstr "{} warnings" #~ msgid "Edit Value" #~ msgstr "Wert bearbeiten" @@ -1504,10 +1512,10 @@ msgstr "" #~ msgstr "Importieren" #~ msgid "Read only" -#~ msgstr "" +#~ msgstr "Read only" #~ msgid "CASCADED" -#~ msgstr "" +#~ msgstr "CASCADED" #~ msgid "CHECK OPTION" #~ msgstr "Verbindung" @@ -1550,7 +1558,7 @@ msgstr "" #~ msgstr "Optionen" #~ msgid "RadioBtn" -#~ msgstr "" +#~ msgstr "RadioBtn" #~ msgid "Edit Column" #~ msgstr "Spalte bearbeiten" @@ -1563,4 +1571,3 @@ msgstr "" #~ msgid "Refrsh" #~ msgstr "Aktualisieren" - diff --git a/locale/en_US/LC_MESSAGES/petersql.mo b/locale/en_US/LC_MESSAGES/petersql.mo index a950b1ad439600a78124ebd4dcdaa02abc194cc5..9152610b04044bae59aaa5bc5dd88e38da9c5fa4 100644 GIT binary patch literal 18212 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 literal 2349 zcmeH`&2Jk;7{;et!J&-aneYj zO7s>XB#wUh_oofzLp`4Px{pMm#)Ux4?4 zUxN>T--Fcu8Jq%t1Mdfa2f6Q0@Nw`M7HQ`+I1SE$W$-*me{X`ca~-7qyCCg<46cJ; zg7<=dr2Gq{{o{9x?VbRsHv`@UX4Cm|;2OqjAVLw>K>E85a^FG9_fvkD@>6gNHT)c; z-rrDS-Ht$sc}#eZIJuk16j9E zK-&2RWZk|4S+}1+`a6k5+L-~VzXa0$d2k)9fvnp>$`3%=zX{?KU!kMkw;)W4AJX|B zLDuaTkoljw8@Yos;8Ack<(ZW8DVIR*1Cln2{;~9*U~ErAX!F+gXgV2|b47NM+^rBk-Aom}vBPmMx{ z#fa0)VY!L@i&BnL_9y@U3_&99WTbv%N1--=f zt_nw*6^M~C{N;20RC$a`DPK<6NZBrn zrj6b}h+fV2Uez|nH7dy_u8D2EQkS`GKAX#IX`Y1Fi2H%>l}G*9YeywnZCt3;*Dq8m zTh&%8)4bws>D_?(UIiDfBn!F1IWPCTm(R(3u~f*P&7oy@rM$LPCJq&@Nl9YH^hc@> z4Q*cAZv8j*+FA|LN%>;Ez8(hI$o5~#G{ccqp|=s)L0pn%m<;39!ipUKt}YlkyDIaG zE15>EQ9VqHx!`3uTt~0HKZHl8ue#-7r~Oo^{S>D`8d3i vEOr8yxulJ@d{|z2za-15qeEH9=8JG4ZokdjZ}b1(W?A4x;xXbK;tBo(!#o~d diff --git a/locale/en_US/LC_MESSAGES/petersql.po b/locale/en_US/LC_MESSAGES/petersql.po index afeb887..b0ac6af 100644 --- a/locale/en_US/LC_MESSAGES/petersql.po +++ b/locale/en_US/LC_MESSAGES/petersql.po @@ -2,20 +2,19 @@ # 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-05-02 16:08+0200\n" +"POT-Creation-Date: 2026-05-31 12:36+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" -"Language: en_US\n" "Language-Team: en_US \n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: en_US\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.18.0\n" #: helpers/__init__.py:16 @@ -47,68 +46,68 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "OpenSSH client not found." -#: structures/engines/context.py:535 +#: structures/engines/context.py:544 msgid "This connection is read-only." -msgstr "" +msgstr "This connection is read-only." -#: structures/engines/mariadb/context.py:616 -#: structures/engines/mysql/context.py:627 -#: structures/engines/postgresql/context.py:685 -#: structures/engines/sqlite/context.py:552 +#: structures/engines/mariadb/context.py:632 +#: structures/engines/mysql/context.py:643 +#: 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:644 -#: structures/engines/mysql/context.py:655 -#: structures/engines/postgresql/context.py:710 -#: structures/engines/sqlite/context.py:576 +#: structures/engines/mariadb/context.py:660 +#: structures/engines/mysql/context.py:671 +#: 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:662 -#: structures/engines/mysql/context.py:673 -#: structures/engines/postgresql/context.py:728 -#: structures/engines/sqlite/context.py:594 +#: structures/engines/mariadb/context.py:678 +#: structures/engines/mysql/context.py:689 +#: 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:702 -#: structures/engines/mysql/context.py:711 -#: structures/engines/postgresql/context.py:768 -#: structures/engines/sqlite/context.py:634 +#: structures/engines/mariadb/context.py:718 +#: structures/engines/mysql/context.py:727 +#: 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:735 -#: structures/engines/mysql/context.py:742 -#: structures/engines/postgresql/context.py:798 -#: structures/engines/sqlite/context.py:662 +#: structures/engines/mariadb/context.py:751 +#: structures/engines/mysql/context.py:758 +#: 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:788 +#: structures/engines/mariadb/context.py:804 #, python-brace-format msgid "Trigger{trigger_index:03}" -msgstr "" +msgstr "Trigger{trigger_index:03}" #: windows/dialogs/connections/view.py:417 -#: windows/dialogs/connections/view.py:764 windows/main/controller.py:1212 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1290 #: windows/views.py:33 msgid "Connection" msgstr "Connection" #: windows/components/dataview.py:115 windows/components/dataview.py:240 #: windows/components/dataview.py:253 windows/components/dataview.py:268 -#: windows/main/database/procedure.py:129 windows/views.py:47 -#: windows/views.py:97 windows/views.py:1016 windows/views.py:1328 -#: windows/views.py:1361 windows/views.py:1385 windows/views.py:1409 -#: windows/views.py:1433 windows/views.py:1457 windows/views.py:1574 -#: windows/views.py:1954 windows/views.py:3079 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1024 +#: windows/views.py:1336 windows/views.py:1369 windows/views.py:1393 +#: windows/views.py:1417 windows/views.py:1441 windows/views.py:1465 +#: windows/views.py:1582 windows/views.py:1928 windows/views.py:2199 +#: windows/views.py:3293 windows/views.py:3515 msgid "Name" msgstr "Name" @@ -116,11 +115,11 @@ msgstr "Name" msgid "Last connection" msgstr "Last connection" -#: windows/dialogs/connections/view.py:655 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:61 msgid "New directory" msgstr "New directory" -#: windows/dialogs/connections/model.py:211 +#: windows/dialogs/connections/model.py:212 #: windows/dialogs/connections/view.py:615 windows/views.py:65 msgid "New connection" msgstr "New connection" @@ -133,15 +132,15 @@ msgstr "Rename" msgid "Clone connection" msgstr "Clone connection" -#: windows/main/database/procedure.py:183 windows/views.py:81 -#: windows/views.py:613 windows/views.py:1488 windows/views.py:1896 -#: windows/views.py:2155 windows/views.py:2932 windows/views.py:2981 -#: windows/views.py:3215 windows/views.py:3247 +#: windows/views.py:81 windows/views.py:613 windows/views.py:1496 +#: windows/views.py:1864 windows/views.py:2154 windows/views.py:2378 +#: windows/views.py:3203 windows/views.py:3252 windows/views.py:3651 +#: windows/views.py:3683 msgid "Delete" msgstr "Delete" -#: windows/views.py:111 windows/views.py:1333 windows/views.py:1629 -#: windows/views.py:2844 windows/views.py:3134 +#: windows/views.py:111 windows/views.py:1341 windows/views.py:1637 +#: windows/views.py:3115 windows/views.py:3570 msgid "Engine" msgstr "Engine" @@ -153,12 +152,11 @@ msgstr "Host + port" msgid "Username" msgstr "Username" -#: windows/views.py:161 windows/views.py:1142 +#: windows/views.py:161 windows/views.py:1150 msgid "Password" msgstr "Password" #: windows/views.py:174 -#, fuzzy msgid "Connection timeout" msgstr "Connection" @@ -168,7 +166,7 @@ msgstr "Use TLS" #: windows/views.py:203 msgid "Mark read only" -msgstr "" +msgstr "Mark read only" #: windows/views.py:214 msgid "Use SSH tunnel" @@ -176,13 +174,13 @@ msgstr "Use SSH tunnel" #: windows/views.py:225 msgid "Compressed client/server protocol" -msgstr "" +msgstr "Compressed client/server protocol" #: windows/views.py:244 msgid "Filename" msgstr "Filename" -#: windows/views.py:249 windows/views.py:368 windows/views.py:3373 +#: windows/views.py:249 windows/views.py:368 windows/views.py:3809 msgid "Select a file" msgstr "Select a file" @@ -191,12 +189,12 @@ msgid "*.*" msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:266 windows/views.py:1335 windows/views.py:1587 -#: windows/views.py:2842 windows/views.py:3092 +#: windows/views.py:266 windows/views.py:1343 windows/views.py:1595 +#: windows/views.py:3113 windows/views.py:3528 msgid "Comments" msgstr "Comments" -#: windows/main/controller.py:766 windows/views.py:280 windows/views.py:740 +#: windows/main/controller.py:769 windows/views.py:280 windows/views.py:740 #: windows/views.py:894 msgid "Settings" msgstr "Settings" @@ -247,919 +245,977 @@ msgstr "Remote host/port is the real DB target (defaults to DB Host/Port)." #: windows/views.py:400 msgid "SSH extra args" -msgstr "" +msgstr "SSH extra args" #: windows/views.py:415 msgid "SSH Tunnel" msgstr "SSH Tunnel" -#: windows/views.py:421 windows/views.py:1331 windows/views.py:2846 +#: windows/views.py:421 windows/views.py:1339 windows/views.py:3117 msgid "Created at" msgstr "Created at" #: windows/views.py:455 msgid "Successful connections" -msgstr "" +msgstr "Successful connections" #: windows/views.py:472 msgid "Last successful connection" -msgstr "" +msgstr "Last successful connection" #: windows/views.py:489 msgid "Unsuccessful connections" -msgstr "" +msgstr "Unsuccessful connections" #: windows/views.py:506 msgid "Last failure reason" -msgstr "" +msgstr "Last failure reason" #: windows/views.py:523 msgid "Total connection attempts" -msgstr "" +msgstr "Total connection attempts" #: windows/views.py:540 -#, fuzzy msgid "Average connection time (ms)" msgstr "Connection" #: windows/views.py:557 msgid "Most recent connection duration" -msgstr "" +msgstr "Most recent connection duration" #: windows/views.py:576 msgid "Statistics" -msgstr "" +msgstr "Statistics" -#: windows/views.py:594 windows/views.py:1853 +#: windows/views.py:594 windows/views.py:1821 msgid "Create" -msgstr "" +msgstr "Create" #: windows/views.py:598 msgid "Create connection" -msgstr "" +msgstr "Create connection" #: windows/views.py:601 msgid "Create directory" -msgstr "" +msgstr "Create directory" -#: windows/main/database/procedure.py:184 windows/views.py:630 -#: windows/views.py:854 windows/views.py:1483 windows/views.py:1899 -#: windows/views.py:2160 windows/views.py:2216 windows/views.py:2908 -#: windows/views.py:3250 windows/views.py:3387 +#: windows/views.py:630 windows/views.py:854 windows/views.py:1491 +#: windows/views.py:1867 windows/views.py:2159 windows/views.py:2383 +#: windows/views.py:2439 windows/views.py:3179 windows/views.py:3686 +#: windows/views.py:3823 msgid "Cancel" -msgstr "" +msgstr "Cancel" -#: windows/main/controller.py:323 windows/main/database/procedure.py:185 -#: windows/views.py:635 windows/views.py:2165 windows/views.py:2358 -#: windows/views.py:3255 windows/views.py:3393 +#: windows/main/controller.py:323 windows/views.py:635 windows/views.py:2164 +#: windows/views.py:2388 windows/views.py:2603 windows/views.py:3691 +#: windows/views.py:3829 msgid "Save" -msgstr "" +msgstr "Save" #: windows/views.py:642 msgid "Test" -msgstr "" +msgstr "Test" #: windows/views.py:649 msgid "Connect" -msgstr "" +msgstr "Connect" -#: windows/main/database/procedure.py:162 windows/views.py:752 +#: windows/views.py:752 msgid "Language" -msgstr "" +msgstr "Language" #: windows/views.py:757 msgid "English" -msgstr "" +msgstr "English" #: windows/views.py:757 msgid "Italian" -msgstr "" +msgstr "Italian" #: windows/views.py:757 msgid "French" -msgstr "" +msgstr "French" #: windows/views.py:769 msgid "Locale" -msgstr "" +msgstr "Locale" #: windows/views.py:790 -#, fuzzy msgid "Column content" msgstr "Clone connection" #: windows/views.py:800 msgid "Syntax" -msgstr "" +msgstr "Syntax" #: windows/views.py:857 msgid "Ok" -msgstr "" +msgstr "Ok" #: windows/views.py:888 msgid "PeterSQL" -msgstr "" +msgstr "PeterSQL" #: windows/views.py:897 msgid "File" -msgstr "" +msgstr "File" #: windows/views.py:900 msgid "About" -msgstr "" +msgstr "About" #: windows/views.py:903 msgid "Help" -msgstr "" +msgstr "Help" #: windows/views.py:908 msgid "Open connection manager" -msgstr "" +msgstr "Open connection manager" #: windows/views.py:912 msgid "Disconnect from server" -msgstr "" +msgstr "Disconnect from server" #: windows/views.py:914 msgid "tool" -msgstr "" +msgstr "tool" -#: windows/views.py:914 windows/views.py:2196 +#: windows/views.py:914 windows/views.py:2419 msgid "Refresh" -msgstr "" +msgstr "Refresh" -#: windows/views.py:918 windows/views.py:920 windows/views.py:1877 -#: windows/views.py:2200 windows/views.py:2344 +#: windows/views.py:918 windows/views.py:920 windows/views.py:1845 +#: windows/views.py:2423 windows/views.py:2589 msgid "Add" -msgstr "" +msgstr "Add" + +#: windows/views.py:925 +#, python-brace-format +msgid "{mode}" +msgstr "{mode}" -#: windows/views.py:954 windows/views.py:958 windows/views.py:2322 +#: windows/views.py:962 windows/views.py:966 windows/views.py:2556 +#: windows/views.py:2561 msgid "MyMenuItem" -msgstr "" +msgstr "MyMenuItem" -#: windows/views.py:961 windows/views.py:1927 windows/views.py:3278 +#: windows/views.py:969 windows/views.py:1895 windows/views.py:2568 +#: windows/views.py:3714 msgid "MyMenu" -msgstr "" +msgstr "MyMenu" -#: windows/views.py:976 windows/views.py:1511 windows/views.py:1518 -#: windows/views.py:1525 +#: windows/views.py:984 windows/views.py:1519 windows/views.py:1526 +#: windows/views.py:1533 msgid "MyLabel" -msgstr "" +msgstr "MyLabel" -#: windows/views.py:982 +#: windows/views.py:990 msgid "Databases" -msgstr "" +msgstr "Databases" -#: windows/views.py:983 windows/views.py:1330 windows/views.py:2847 +#: windows/views.py:991 windows/views.py:1338 windows/views.py:3118 msgid "Size" -msgstr "" +msgstr "Size" -#: windows/views.py:984 +#: windows/views.py:992 msgid "Elements" -msgstr "" +msgstr "Elements" -#: windows/views.py:985 +#: windows/views.py:993 msgid "Modified at" -msgstr "" +msgstr "Modified at" -#: windows/views.py:986 windows/views.py:1345 +#: windows/views.py:994 windows/views.py:1353 msgid "Tables" -msgstr "" +msgstr "Tables" -#: windows/views.py:993 +#: windows/views.py:1001 msgid "System" -msgstr "" +msgstr "System" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1039 -#: windows/views.py:1334 windows/views.py:2843 +#: windows/components/dataview.py:89 windows/views.py:1047 +#: windows/views.py:1342 windows/views.py:3114 msgid "Collation" -msgstr "" +msgstr "Collation" -#: windows/views.py:1068 +#: windows/views.py:1076 msgid "Encryption" -msgstr "" +msgstr "Encryption" -#: windows/views.py:1080 +#: windows/main/controller.py:1259 windows/main/controller.py:1281 +#: windows/main/controller.py:1285 windows/views.py:1088 msgid "Read Only" -msgstr "" +msgstr "Read Only" -#: windows/views.py:1097 +#: windows/views.py:1105 msgid "Tablespace" -msgstr "" +msgstr "Tablespace" -#: windows/views.py:1118 +#: windows/views.py:1126 msgid "Connection limit" -msgstr "" +msgstr "Connection limit" -#: windows/views.py:1161 +#: windows/views.py:1169 msgid "Profile" -msgstr "" +msgstr "Profile" -#: windows/views.py:1187 +#: windows/views.py:1195 msgid "Default tablespace" -msgstr "" +msgstr "Default tablespace" -#: windows/views.py:1208 +#: windows/views.py:1216 msgid "Temporary tablespace" -msgstr "" +msgstr "Temporary tablespace" -#: windows/views.py:1234 +#: windows/views.py:1242 msgid "Quota" -msgstr "" +msgstr "Quota" -#: windows/views.py:1253 +#: windows/views.py:1261 msgid "Unlimited quota" -msgstr "" +msgstr "Unlimited quota" -#: windows/views.py:1270 +#: windows/views.py:1278 msgid "Account status" -msgstr "" +msgstr "Account status" -#: windows/views.py:1291 +#: windows/views.py:1299 msgid "Password expire" -msgstr "" +msgstr "Password expire" -#: windows/views.py:1315 +#: windows/views.py:1323 msgid "Add new table" -msgstr "" +msgstr "Add new table" -#: windows/views.py:1317 +#: windows/views.py:1325 msgid "Clone table" -msgstr "" +msgstr "Clone table" -#: windows/main/controller.py:1633 windows/views.py:1319 +#: windows/main/controller.py:1770 windows/views.py:1327 msgid "Delete table" -msgstr "" +msgstr "Delete table" -#: windows/views.py:1329 +#: windows/views.py:1337 msgid "Rows" -msgstr "" +msgstr "Rows" -#: windows/views.py:1332 windows/views.py:2845 +#: windows/views.py:1340 windows/views.py:3116 msgid "Updated at" -msgstr "" +msgstr "Updated at" -#: windows/views.py:1350 +#: windows/views.py:1358 msgid "Add new view" -msgstr "" +msgstr "Add new view" -#: windows/views.py:1352 windows/views.py:1376 +#: windows/views.py:1360 windows/views.py:1384 msgid "Clone view" -msgstr "" +msgstr "Clone view" -#: windows/views.py:1354 -#, fuzzy +#: windows/views.py:1362 msgid "Delete view" msgstr "Delete" -#: windows/views.py:1362 windows/views.py:1386 windows/views.py:1410 -#: windows/views.py:1434 windows/views.py:1458 +#: windows/views.py:1370 windows/views.py:1394 windows/views.py:1418 +#: windows/views.py:1442 windows/views.py:1466 msgid "Definition" -msgstr "" +msgstr "Definition" -#: windows/views.py:1369 windows/views.py:2177 +#: windows/views.py:1377 msgid "Views" -msgstr "" +msgstr "Views" -#: windows/views.py:1374 +#: windows/views.py:1382 msgid "Add new procedure" -msgstr "" +msgstr "Add new procedure" -#: windows/views.py:1376 +#: windows/views.py:1384 msgid "Clone procedure" -msgstr "" +msgstr "Clone procedure" -#: windows/views.py:1378 +#: windows/views.py:1386 msgid "Delete procedure" -msgstr "" +msgstr "Delete procedure" -#: windows/views.py:1393 +#: windows/views.py:1401 msgid "Procedures" -msgstr "" +msgstr "Procedures" -#: windows/views.py:1398 -#, fuzzy +#: windows/views.py:1406 msgid "Add new function" msgstr "New connection" -#: windows/views.py:1400 -#, fuzzy +#: windows/views.py:1408 msgid "Clone function" msgstr "Clone connection" -#: windows/views.py:1402 -#, fuzzy +#: windows/views.py:1410 msgid "Delete function" msgstr "Last connection" -#: windows/views.py:1417 -#, fuzzy +#: windows/views.py:1425 msgid "Functions" msgstr "Connection" -#: windows/views.py:1422 +#: windows/views.py:1430 msgid "Add new trigger" -msgstr "" +msgstr "Add new trigger" -#: windows/views.py:1424 +#: windows/views.py:1432 msgid "Clone trigger" -msgstr "" +msgstr "Clone trigger" -#: windows/views.py:1426 -#, fuzzy +#: windows/views.py:1434 msgid "Delete trigger" msgstr "Delete" -#: windows/views.py:1441 windows/views.py:2185 +#: windows/views.py:1449 msgid "Triggers" -msgstr "" +msgstr "Triggers" -#: windows/views.py:1446 +#: windows/views.py:1454 msgid "Add new event" -msgstr "" +msgstr "Add new event" -#: windows/views.py:1448 +#: windows/views.py:1456 msgid "Clone event" -msgstr "" +msgstr "Clone event" -#: windows/views.py:1450 -#, fuzzy +#: windows/views.py:1458 msgid "Delete event" msgstr "Delete" -#: windows/views.py:1465 +#: windows/views.py:1473 msgid "Events" -msgstr "" +msgstr "Events" -#: windows/views.py:1493 windows/views.py:1904 windows/views.py:2214 -#: windows/views.py:2296 windows/views.py:2915 +#: windows/views.py:1501 windows/views.py:1872 windows/views.py:2437 +#: windows/views.py:2521 windows/views.py:3186 msgid "Apply" -msgstr "" +msgstr "Apply" -#: windows/main/database/procedure.py:122 windows/views.py:1505 -#: windows/views.py:1684 windows/views.py:2114 windows/views.py:3167 +#: windows/views.py:1513 windows/views.py:1692 windows/views.py:3603 msgid "Options" -msgstr "" +msgstr "Options" -#: windows/views.py:1536 +#: windows/views.py:1544 msgid "Diagram" -msgstr "" +msgstr "Diagram" -#: windows/views.py:1547 +#: windows/views.py:1555 msgid "Database" -msgstr "" +msgstr "Database" -#: windows/views.py:1602 windows/views.py:3107 +#: windows/views.py:1610 windows/views.py:3543 msgid "Base" -msgstr "" +msgstr "Base" -#: windows/views.py:1616 windows/views.py:3121 +#: windows/views.py:1624 windows/views.py:3557 msgid "Auto Increment" -msgstr "" +msgstr "Auto Increment" -#: windows/views.py:1644 windows/views.py:3149 +#: windows/views.py:1652 windows/views.py:3585 msgid "Default Collation" -msgstr "" +msgstr "Default Collation" -#: windows/views.py:1654 +#: windows/views.py:1662 msgid "Convert data" -msgstr "" +msgstr "Convert data" -#: windows/views.py:1662 +#: windows/views.py:1670 msgid "Row format" -msgstr "" +msgstr "Row format" -#: windows/views.py:1696 windows/views.py:1737 windows/views.py:1781 -#: windows/views.py:1879 windows/views.py:2204 +#: windows/views.py:1703 windows/views.py:1731 windows/views.py:1759 +#: windows/views.py:1847 windows/views.py:2259 windows/views.py:2427 +#: windows/views.py:3313 windows/views.py:3330 windows/views.py:3354 +#: windows/views.py:3381 msgid "Remove" -msgstr "" +msgstr "Remove" -#: windows/views.py:1703 windows/views.py:1744 windows/views.py:1788 +#: windows/views.py:1705 windows/views.py:1733 windows/views.py:1761 +#: windows/views.py:2261 windows/views.py:2529 windows/views.py:3320 +#: windows/views.py:3337 windows/views.py:3361 windows/views.py:3388 msgid "Clear" -msgstr "" +msgstr "Clear" -#: windows/views.py:1718 windows/views.py:3181 +#: windows/views.py:1718 windows/views.py:3617 msgid "Indexes" -msgstr "" +msgstr "Indexes" -#: windows/views.py:1732 windows/views.py:1776 windows/views.py:2927 -#: windows/views.py:2969 windows/views.py:3210 +#: windows/views.py:1729 windows/views.py:1757 windows/views.py:2257 +#: windows/views.py:3198 windows/views.py:3240 windows/views.py:3308 +#: windows/views.py:3349 windows/views.py:3376 windows/views.py:3646 msgid "Insert" -msgstr "" +msgstr "Insert" -#: windows/views.py:1762 +#: windows/views.py:1746 msgid "Foreign Keys" -msgstr "" +msgstr "Foreign Keys" -#: windows/views.py:1806 +#: windows/views.py:1774 msgid "Checks" -msgstr "" +msgstr "Checks" -#: windows/views.py:1873 windows/views.py:3202 +#: windows/views.py:1841 windows/views.py:3638 msgid "Columns:" -msgstr "" +msgstr "Columns:" -#: windows/views.py:1883 +#: windows/views.py:1851 msgid "Move Up" -msgstr "" +msgstr "Move Up" -#: windows/views.py:1885 +#: windows/views.py:1853 msgid "Move Down" -msgstr "" +msgstr "Move Down" -#: windows/views.py:1917 windows/views.py:1924 windows/views.py:3268 -#: windows/views.py:3275 +#: windows/views.py:1885 windows/views.py:1892 windows/views.py:3704 +#: windows/views.py:3711 msgid "Add Index" -msgstr "" +msgstr "Add Index" -#: windows/views.py:1921 windows/views.py:3272 +#: windows/views.py:1889 windows/views.py:3708 msgid "Add PrimaryKey" -msgstr "" +msgstr "Add PrimaryKey" -#: windows/views.py:1938 +#: windows/views.py:1906 msgid "Table" -msgstr "" +msgstr "Table" -#: windows/main/database/procedure.py:140 windows/views.py:1974 -msgid "Definer" -msgstr "" - -#: windows/views.py:1994 +#: windows/views.py:1948 windows/views.py:2219 msgid "Schema" -msgstr "" +msgstr "Schema" -#: windows/views.py:2020 -msgid "SQL security" -msgstr "" +#: windows/views.py:1976 windows/views.py:2247 +msgid "General" +msgstr "General" -#: windows/views.py:2027 -msgid "DEFINER" -msgstr "" - -#: windows/views.py:2027 -msgid "INVOKER" -msgstr "" - -#: windows/views.py:2039 +#: windows/views.py:1981 msgid "Algorithm" -msgstr "" +msgstr "Algorithm" -#: windows/views.py:2041 +#: windows/views.py:1983 msgid "UNDEFINED" -msgstr "" +msgstr "UNDEFINED" -#: windows/views.py:2044 +#: windows/views.py:1986 msgid "MERGE" -msgstr "" +msgstr "MERGE" -#: windows/views.py:2047 +#: windows/views.py:1989 msgid "TEMPTABLE" -msgstr "" +msgstr "TEMPTABLE" -#: windows/views.py:2057 +#: windows/views.py:1999 msgid "View constraint" -msgstr "" +msgstr "View constraint" -#: windows/views.py:2059 +#: windows/views.py:2001 msgid "None" -msgstr "" +msgstr "None" -#: windows/views.py:2062 +#: windows/views.py:2004 msgid "LOCAL" -msgstr "" +msgstr "LOCAL" -#: windows/views.py:2065 +#: windows/views.py:2007 msgid "CASCADE" -msgstr "" +msgstr "CASCADE" -#: windows/views.py:2068 +#: windows/views.py:2010 msgid "CHECK ONLY" -msgstr "" +msgstr "CHECK ONLY" -#: windows/views.py:2071 +#: windows/views.py:2013 msgid "READ ONLY" -msgstr "" +msgstr "READ ONLY" + +#: windows/views.py:2026 +msgid "Behavior" +msgstr "Behavior" + +#: windows/views.py:2033 windows/views.py:2281 +msgid "Definer" +msgstr "Definer" + +#: windows/views.py:2041 windows/views.py:2289 +msgid "*" +msgstr "*" + +#: windows/views.py:2053 windows/views.py:2301 +msgid "SQL security" +msgstr "SQL security" + +#: windows/views.py:2060 windows/views.py:2308 +msgid "DEFINER" +msgstr "DEFINER" -#: windows/views.py:2083 +#: windows/views.py:2060 windows/views.py:2308 +msgid "INVOKER" +msgstr "INVOKER" + +#: windows/views.py:2074 msgid "Force" -msgstr "" +msgstr "Force" -#: windows/views.py:2095 +#: windows/views.py:2086 msgid "Security barrier" -msgstr "" +msgstr "Security barrier" + +#: windows/views.py:2099 windows/views.py:2323 +msgid "Security" +msgstr "Security" + +#: windows/views.py:2176 +msgid "View" +msgstr "View" -#: windows/views.py:2202 +#: windows/views.py:2274 +msgid "Parameters" +msgstr "Parameters" + +#: windows/views.py:2400 +msgid "Procedure" +msgstr "Procedure" + +#: windows/views.py:2408 +msgid "Trigger" +msgstr "Delete" + +#: windows/views.py:2425 msgid "Duplicate" -msgstr "" +msgstr "Duplicate" -#: windows/views.py:2208 +#: windows/views.py:2431 msgid "Apply changes automatically" -msgstr "" +msgstr "Apply changes automatically" -#: windows/views.py:2210 windows/views.py:2211 +#: windows/views.py:2433 windows/views.py:2434 msgid "" -"If enabled, table edits are applied immediately without pressing Apply or" -" Cancel" +"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:2224 +#: windows/views.py:2447 #, python-brace-format -msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgid "" +"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" +"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -#: windows/views.py:2232 +#: windows/views.py:2455 msgid "First" -msgstr "" +msgstr "First" -#: windows/views.py:2250 +#: windows/views.py:2473 msgid "Last" -msgstr "" +msgstr "Last" -#: windows/views.py:2259 +#: windows/views.py:2482 msgid "Filters" +msgstr "Filters" + +#: windows/views.py:2524 +msgid "" +"Apply filters in data\n" +"CTRL+ENTER" msgstr "" +"Apply filters in data\n" +"CTRL+ENTER" -#: windows/views.py:2299 +#: windows/views.py:2525 msgid "CTRL+ENTER" -msgstr "" +msgstr "CTRL+ENTER" -#: windows/views.py:2319 +#: windows/views.py:2553 msgid "Insert row" -msgstr "" +msgstr "Insert row" + +#: windows/components/popup.py:31 windows/views.py:2565 +msgid "NULL" +msgstr "NULL" -#: windows/views.py:2327 +#: windows/views.py:2572 msgid "Data" -msgstr "" +msgstr "Data" -#: windows/main/controller.py:318 windows/views.py:2344 -#, fuzzy +#: windows/main/controller.py:318 windows/views.py:2589 msgid "New query" msgstr "New directory" -#: windows/views.py:2346 windows/views.py:2866 +#: windows/views.py:2591 windows/views.py:3137 msgid "Close" -msgstr "" +msgstr "Close" -#: windows/main/controller.py:319 windows/views.py:2346 +#: windows/main/controller.py:319 windows/views.py:2591 msgid "Close query" -msgstr "" +msgstr "Close query" -#: windows/views.py:2350 +#: windows/views.py:2595 msgid "Run" -msgstr "" +msgstr "Run" -#: windows/main/controller.py:320 windows/views.py:2350 -#, fuzzy +#: windows/main/controller.py:320 windows/views.py:2595 msgid "Execute" msgstr "SSH executable" -#: windows/views.py:2352 +#: windows/views.py:2597 msgid "Run all" -msgstr "" +msgstr "Run all" -#: windows/views.py:2352 +#: windows/views.py:2597 msgid "Execute all statements" -msgstr "" +msgstr "Execute all statements" -#: windows/main/controller.py:322 windows/views.py:2354 +#: windows/main/controller.py:322 windows/views.py:2599 msgid "Stop" -msgstr "" +msgstr "Stop" -#: windows/views.py:2419 +#: windows/views.py:2664 msgid "a page" -msgstr "" +msgstr "a page" -#: windows/views.py:2469 +#: windows/views.py:2714 msgid "Query" -msgstr "" +msgstr "Query" -#: windows/views.py:2820 +#: windows/views.py:3091 msgid "Character set" -msgstr "" +msgstr "Character set" -#: windows/views.py:2850 windows/views.py:2869 +#: windows/views.py:3121 windows/views.py:3140 msgid "New" -msgstr "" +msgstr "New" -#: windows/views.py:2889 +#: windows/views.py:3160 msgid "Insert record" -msgstr "" +msgstr "Insert record" -#: windows/views.py:2894 +#: windows/views.py:3165 msgid "Duplicate record" -msgstr "" +msgstr "Duplicate record" -#: windows/views.py:2901 +#: windows/views.py:3172 msgid "Delete record" -msgstr "" +msgstr "Delete record" -#: windows/views.py:2939 windows/views.py:3222 +#: windows/views.py:3210 windows/views.py:3658 msgid "Up" -msgstr "" +msgstr "Up" -#: windows/views.py:2946 windows/views.py:3229 +#: windows/views.py:3217 windows/views.py:3665 msgid "Down" -msgstr "" +msgstr "Down" -#: windows/views.py:2961 +#: windows/views.py:3232 msgid "Table:" -msgstr "" +msgstr "Table:" -#: windows/views.py:2974 +#: windows/views.py:3245 msgid "Clone" -msgstr "" +msgstr "Clone" + +#: windows/views.py:3270 +msgid "MyButton" +msgstr "MyButton" -#: windows/views.py:3358 +#: windows/views.py:3794 msgid "Save Starments" -msgstr "" +msgstr "Save Starments" -#: windows/views.py:3366 +#: windows/views.py:3802 msgid "Location" -msgstr "" +msgstr "Location" -#: windows/views.py:3373 +#: windows/views.py:3809 msgid "*.sql" -msgstr "" +msgstr "*.sql" #: 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: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:111 msgid "#" -msgstr "" +msgstr "#" #: windows/components/dataview.py:119 msgid "Data type" -msgstr "" +msgstr "Data type" #: windows/components/dataview.py:123 msgid "Length/Set" -msgstr "" +msgstr "Length/Set" #: windows/components/dataview.py:169 msgid "Add column\tCTRL+INS" -msgstr "" +msgstr "Add column\tCTRL+INS" #: windows/components/dataview.py:175 msgid "Remove column\tCTRL+DEL" -msgstr "" +msgstr "Remove column\tCTRL+DEL" #: windows/components/dataview.py:183 msgid "Move up\tCTRL+UP" -msgstr "" +msgstr "Move up\tCTRL+UP" #: windows/components/dataview.py:190 msgid "Move down\tCTRL+D" -msgstr "" +msgstr "Move down\tCTRL+D" #: windows/components/dataview.py:214 msgid "Create new index" -msgstr "" +msgstr "Create new index" #: windows/components/dataview.py:229 msgid "Append to index" -msgstr "" +msgstr "Append to index" #: windows/components/dataview.py:243 msgid "Column(s)/Expression" -msgstr "" +msgstr "Column(s)/Expression" #: windows/components/dataview.py:244 msgid "Condition" -msgstr "" +msgstr "Condition" #: windows/components/dataview.py:274 msgid "Column(s)" -msgstr "" +msgstr "Column(s)" #: windows/components/dataview.py:280 msgid "Reference table" -msgstr "" +msgstr "Reference table" #: windows/components/dataview.py:286 msgid "Reference column(s)" -msgstr "" +msgstr "Reference column(s)" #: windows/components/dataview.py:292 msgid "On UPDATE" -msgstr "" +msgstr "On UPDATE" #: windows/components/dataview.py:298 msgid "On DELETE" -msgstr "" +msgstr "On DELETE" #: windows/components/dataview.py:313 msgid "Add foreign key" -msgstr "" +msgstr "Add foreign key" #: 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:126 windows/main/query/renderer.py:192 msgid "Unknown error" -msgstr "" +msgstr "Unknown error" #: windows/dialogs/connections/view.py:416 msgid "Connection established successfully" -msgstr "" +msgstr "Connection established successfully" #: 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:431 msgid "Confirm save" -msgstr "" +msgstr "Confirm save" #: 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:485 msgid "Unsaved changes" -msgstr "" +msgstr "Unsaved changes" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:773 msgid "" -"This connection cannot work without TLS. TLS has been enabled " -"automatically." +"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:787 +#: windows/dialogs/connections/view.py:798 #, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "" +"Connection error:\n" +"{error}" -#: windows/dialogs/connections/view.py:788 +#: windows/dialogs/connections/view.py:799 msgid "Connection error" -msgstr "" +msgstr "Connection error" -#: windows/dialogs/connections/view.py:814 +#: 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:817 -#: windows/dialogs/connections/view.py:834 +#: windows/dialogs/connections/view.py:828 +#: windows/dialogs/connections/view.py:845 msgid "Confirm delete" -msgstr "" +msgstr "Confirm delete" -#: windows/dialogs/connections/view.py:831 +#: 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:315 #, python-brace-format msgid "{text} ({shortcut})" -msgstr "" +msgstr "{text} ({shortcut})" #: windows/main/controller.py:321 -#, fuzzy msgid "Execute all" msgstr "SSH executable" #: windows/main/controller.py:440 windows/main/controller.py:448 msgid "Query (1)" -msgstr "" +msgstr "Query (1)" #: windows/main/controller.py:467 #, python-brace-format msgid "Query ({query_number})" -msgstr "" +msgstr "Query ({query_number})" #: windows/main/controller.py:516 msgid "You have unsaved changes. Save before closing?" -msgstr "" +msgstr "You have unsaved changes. Save before closing?" #: windows/main/controller.py:517 msgid "Unsaved query" -msgstr "" +msgstr "Unsaved query" #: windows/main/controller.py:562 msgid "Save query" -msgstr "" +msgstr "Save query" #: windows/main/controller.py:565 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" -msgstr "" +msgstr "SQL files (*.sql)|*.sql|All files (*.*)|*.*" #: windows/main/controller.py:593 windows/main/controller.py:624 #: windows/main/controller.py:651 windows/main/database/list.py:119 -#: windows/main/database/procedure.py:294 -#: windows/main/database/procedure.py:320 windows/main/database/view.py:267 -#: windows/main/database/view.py:296 windows/main/query/controller.py:177 +#: windows/main/database/procedure.py:206 +#: windows/main/database/procedure.py:232 windows/main/database/view.py:268 +#: windows/main/database/view.py:297 windows/main/query/controller.py:177 msgid "Error" -msgstr "" +msgstr "Error" #: windows/main/controller.py:631 #, python-brace-format msgid "-- Saved query to {file_path}" -msgstr "" +msgstr "-- Saved query to {file_path}" #: windows/main/controller.py:657 #, python-brace-format msgid "-- Autosaved query to {file_path}" -msgstr "" +msgstr "-- Autosaved query to {file_path}" -#: windows/main/controller.py:719 +#: windows/main/controller.py:722 msgid "days" -msgstr "" +msgstr "days" -#: windows/main/controller.py:720 +#: windows/main/controller.py:723 msgid "hours" -msgstr "" +msgstr "hours" -#: windows/main/controller.py:721 +#: windows/main/controller.py:724 msgid "minutes" -msgstr "" +msgstr "minutes" -#: windows/main/controller.py:722 +#: windows/main/controller.py:725 msgid "seconds" -msgstr "" +msgstr "seconds" -#: windows/main/controller.py:730 +#: windows/main/controller.py:733 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" -msgstr "" +msgstr "Memory used: {used} ({percentage:.2%})" -#: windows/main/controller.py:766 +#: windows/main/controller.py:769 msgid "Settings saved successfully" -msgstr "" +msgstr "Settings saved successfully" -#: windows/main/controller.py:990 +#: windows/main/controller.py:993 #, python-brace-format msgid "~{estimated} (Loading...)" -msgstr "" +msgstr "~{estimated} (Loading...)" -#: windows/main/controller.py:992 +#: windows/main/controller.py:995 msgid "~ (Loading...)" -msgstr "" +msgstr "~ (Loading...)" + +#: windows/main/controller.py:1230 +msgid "Write Mode (2:00)" +msgstr "Write Mode (2:00)" + +#: windows/main/controller.py:1281 +msgid "Write Mode" +msgstr "Write Mode" -#: windows/main/controller.py:1214 +#: windows/main/controller.py:1292 msgid "Version" -msgstr "" +msgstr "Version" -#: windows/main/controller.py:1216 +#: windows/main/controller.py:1294 msgid "Uptime" -msgstr "" +msgstr "Uptime" -#: windows/main/controller.py:1299 +#: windows/main/controller.py:1429 #, 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:1332 +#: windows/main/controller.py:1462 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1168,146 +1224,142 @@ 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:1337 windows/main/controller.py:1358 +#: windows/main/controller.py:1467 windows/main/controller.py:1488 msgid "Delete database" -msgstr "" +msgstr "Delete database" -#: windows/main/controller.py:1343 +#: windows/main/controller.py:1473 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:1344 +#: windows/main/controller.py:1474 msgid "Dump not available" -msgstr "" +msgstr "Dump not available" -#: windows/main/controller.py:1357 +#: windows/main/controller.py:1487 msgid "Database deletion is not supported by this engine." -msgstr "" +msgstr "Database deletion is not supported by this engine." -#: windows/main/controller.py:1372 +#: windows/main/controller.py:1502 msgid "Database deleted successfully" -msgstr "" +msgstr "Database deleted successfully" -#: windows/main/controller.py:1373 windows/main/database/procedure.py:283 -#: windows/main/database/procedure.py:314 windows/main/database/view.py:255 -#: windows/main/database/view.py:290 +#: windows/main/controller.py:1503 windows/main/database/procedure.py:195 +#: windows/main/database/procedure.py:226 windows/main/database/view.py:256 +#: windows/main/database/view.py:291 msgid "Success" -msgstr "" +msgstr "Success" -#: windows/main/controller.py:1604 +#: windows/main/controller.py:1741 #, 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:1630 +#: windows/main/controller.py:1767 #, python-brace-format msgid "Do you want delete the table {table_name}?" -msgstr "" +msgstr "Do you want delete the table {table_name}?" -#: windows/main/controller.py:1652 +#: windows/main/controller.py:1789 #, python-brace-format msgid "{table_name} (COPY)" -msgstr "" +msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1797 +#: windows/main/controller.py:1948 msgid "Do you want delete the records?" -msgstr "" +msgstr "Do you want delete the records?" #: 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:106 msgid "Do you want to reconnect?" -msgstr "" +msgstr "Do you want to reconnect?" #: windows/main/database/list.py:108 msgid "Connection lost" -msgstr "" +msgstr "Connection lost" #: windows/main/database/list.py:118 msgid "Reconnection failed:" -msgstr "" - -#: windows/main/database/procedure.py:114 -msgid "Procedure" -msgstr "" +msgstr "Reconnection failed:" -#: windows/main/database/procedure.py:151 -msgid "Parameters" -msgstr "" - -#: windows/main/database/procedure.py:282 +#: windows/main/database/procedure.py:194 msgid "Procedure created successfully" -msgstr "" +msgstr "Procedure created successfully" -#: windows/main/database/procedure.py:282 +#: windows/main/database/procedure.py:194 msgid "Procedure updated successfully" -msgstr "" +msgstr "Procedure updated successfully" -#: windows/main/database/procedure.py:294 +#: windows/main/database/procedure.py:206 #, python-brace-format msgid "Error saving procedure: {}" -msgstr "" +msgstr "Error saving procedure: {}" -#: windows/main/database/procedure.py:305 +#: windows/main/database/procedure.py:217 #, python-brace-format msgid "Are you sure you want to delete procedure '{}'?" -msgstr "" +msgstr "Are you sure you want to delete procedure '{}'?" -#: windows/main/database/procedure.py:306 windows/main/database/view.py:281 +#: windows/main/database/procedure.py:218 windows/main/database/view.py:282 msgid "Confirm Delete" -msgstr "" +msgstr "Confirm Delete" -#: windows/main/database/procedure.py:314 +#: windows/main/database/procedure.py:226 msgid "Procedure deleted successfully" -msgstr "" +msgstr "Procedure deleted successfully" -#: windows/main/database/procedure.py:320 +#: windows/main/database/procedure.py:232 #, python-brace-format msgid "Error deleting procedure: {}" -msgstr "" +msgstr "Error deleting procedure: {}" -#: windows/main/database/view.py:254 +#: windows/main/database/view.py:255 msgid "View created successfully" -msgstr "" +msgstr "View created successfully" -#: windows/main/database/view.py:254 +#: windows/main/database/view.py:255 msgid "View updated successfully" -msgstr "" +msgstr "View updated successfully" -#: windows/main/database/view.py:267 +#: windows/main/database/view.py:268 #, python-brace-format msgid "Error saving view: {}" -msgstr "" +msgstr "Error saving view: {}" -#: windows/main/database/view.py:280 +#: 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:290 +#: windows/main/database/view.py:291 msgid "View deleted successfully" -msgstr "" +msgstr "View deleted successfully" -#: windows/main/database/view.py:296 +#: windows/main/database/view.py:297 #, python-brace-format msgid "Error deleting view: {}" -msgstr "" +msgstr "Error deleting view: {}" #: windows/main/query/controller.py:110 #, python-brace-format msgid "{elapsed_ms:.0f} ms" -msgstr "" +msgstr "{elapsed_ms:.0f} ms" #: windows/main/query/controller.py:112 #, python-brace-format msgid "{elapsed_s:.2f} s" -msgstr "" +msgstr "{elapsed_s:.2f} s" #: windows/main/query/controller.py:115 -#, fuzzy msgid "none" msgstr "Engine" @@ -1320,203 +1372,208 @@ 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 msgid "Query execution cancelled" -msgstr "" +msgstr "Query execution cancelled" #: windows/main/query/controller.py:176 msgid "No active database connection" -msgstr "" +msgstr "No active database connection" #: windows/main/query/history.py:55 -#, fuzzy 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 "" +msgstr "Error:" -#: windows/main/table/records.py:336 +#: windows/main/table/records.py:334 msgid "Error saving records" -msgstr "" +msgstr "Error saving records" #~ msgid "Created at:" -#~ msgstr "" +#~ msgstr "Created at:" #~ msgid "Last connection:" -#~ msgstr "" +#~ msgstr "Last connection:" #~ msgid "Successful connections:" -#~ msgstr "" +#~ msgstr "Successful connections:" #~ msgid "Unsuccessful connections:" -#~ msgstr "" +#~ msgstr "Unsuccessful connections:" #~ msgid "Session Manager" -#~ msgstr "" +#~ msgstr "Session Manager" #~ msgid "Session name" -#~ msgstr "" +#~ msgstr "Session name" #~ msgid "Connection type" -#~ msgstr "" +#~ msgstr "Connection type" #~ msgid "Open" -#~ msgstr "" +#~ msgstr "Open" #~ msgid "Open session manager" -#~ msgstr "" +#~ msgstr "Open session manager" #~ msgid "Foreign Key" -#~ msgstr "" +#~ msgstr "Foreign Key" #~ msgid "New Session" -#~ msgstr "" +#~ msgstr "New Session" #~ msgid "connection" -#~ msgstr "" +#~ msgstr "connection" #~ msgid "directory" -#~ msgstr "" +#~ msgstr "directory" #~ msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" -#~ msgstr "" +#~ msgstr "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" #~ msgid "Next" -#~ msgstr "" +#~ msgstr "Next" #~ msgid "{} rows affected" -#~ msgstr "" +#~ msgstr "{} rows affected" #~ msgid "Query {}" -#~ msgstr "" +#~ msgstr "Query {}" #~ msgid "Query {} (Error)" -#~ msgstr "" +#~ msgstr "Query {} (Error)" #~ msgid "Query {} ({} rows × {} cols)" -#~ msgstr "" +#~ msgstr "Query {} ({} rows × {} cols)" #~ msgid "{} rows" -#~ msgstr "" +#~ msgstr "{} rows" #~ msgid "{:.1f} ms" -#~ msgstr "" +#~ msgstr "{:.1f} ms" #~ msgid "{} warnings" -#~ msgstr "" +#~ msgstr "{} warnings" #~ msgid " Average connection time (ms)" -#~ msgstr "" +#~ msgstr " Average connection time (ms)" #~ msgid " Most recent connection duration" -#~ msgstr "" +#~ msgstr " Most recent connection duration" #~ msgid "Edit Value" -#~ msgstr "" +#~ msgstr "Edit Value" #~ msgid "Query #2" -#~ msgstr "" +#~ msgstr "Query #2" #~ msgid "Column5" -#~ msgstr "" +#~ msgstr "Column5" #~ msgid "Import" -#~ msgstr "" +#~ msgstr "Import" #~ msgid "Read only" -#~ msgstr "" +#~ msgstr "Read only" #~ msgid "CASCADED" -#~ msgstr "" +#~ msgstr "CASCADED" #~ msgid "CHECK OPTION" -#~ msgstr "" +#~ msgstr "CHECK OPTION" #~ msgid "collapsible" -#~ msgstr "" +#~ msgstr "collapsible" #~ msgid "Column3" -#~ msgstr "" +#~ msgstr "Column3" #~ msgid "Column4" -#~ msgstr "" +#~ msgstr "Column4" #~ 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 "" +#~ msgstr "Port" #~ msgid "Usage" -#~ msgstr "" +#~ msgstr "Usage" #~ msgid "%(total_rows)s" -#~ msgstr "" +#~ msgstr "%(total_rows)s" #~ msgid "rows total" -#~ msgstr "" +#~ msgstr "rows total" #~ msgid "Lines" -#~ msgstr "" +#~ msgstr "Lines" #~ msgid "Temporary" -#~ msgstr "" +#~ msgstr "Temporary" #~ msgid "Engine options" -#~ msgstr "" +#~ msgstr "Engine options" #~ msgid "RadioBtn" -#~ msgstr "" +#~ msgstr "RadioBtn" #~ msgid "Edit Column" -#~ msgstr "" +#~ msgstr "Edit Column" #~ msgid "Datatype" -#~ msgstr "" +#~ msgstr "Datatype" #~ msgid "Zero Fill" -#~ msgstr "" +#~ msgstr "Zero Fill" #~ msgid "Refrsh" -#~ msgstr "" - +#~ msgstr "Refrsh" diff --git a/locale/es_ES/LC_MESSAGES/petersql.mo b/locale/es_ES/LC_MESSAGES/petersql.mo index 3da8b8a92215f161305347ca44870e7dd5ee79a7..7a260644af32456b174a745d6ed8bc022a83d1e9 100644 GIT binary patch literal 18627 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 diff --git a/locale/es_ES/LC_MESSAGES/petersql.po b/locale/es_ES/LC_MESSAGES/petersql.po index 482a56c..2467f75 100644 --- a/locale/es_ES/LC_MESSAGES/petersql.po +++ b/locale/es_ES/LC_MESSAGES/petersql.po @@ -2,20 +2,19 @@ # 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-05-02 16:08+0200\n" +"POT-Creation-Date: 2026-05-31 12:36+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" -"Language: es_ES\n" "Language-Team: es_ES \n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: es_ES\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.18.0\n" #: helpers/__init__.py:16 @@ -47,68 +46,68 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "Cliente OpenSSH no encontrado." -#: structures/engines/context.py:535 +#: structures/engines/context.py:544 msgid "This connection is read-only." -msgstr "" +msgstr "Esta conexión es de solo lectura." -#: structures/engines/mariadb/context.py:616 -#: structures/engines/mysql/context.py:627 -#: structures/engines/postgresql/context.py:685 -#: structures/engines/sqlite/context.py:552 +#: structures/engines/mariadb/context.py:632 +#: structures/engines/mysql/context.py:643 +#: 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:644 -#: structures/engines/mysql/context.py:655 -#: structures/engines/postgresql/context.py:710 -#: structures/engines/sqlite/context.py:576 +#: structures/engines/mariadb/context.py:660 +#: structures/engines/mysql/context.py:671 +#: 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:662 -#: structures/engines/mysql/context.py:673 -#: structures/engines/postgresql/context.py:728 -#: structures/engines/sqlite/context.py:594 +#: structures/engines/mariadb/context.py:678 +#: structures/engines/mysql/context.py:689 +#: 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:702 -#: structures/engines/mysql/context.py:711 -#: structures/engines/postgresql/context.py:768 -#: structures/engines/sqlite/context.py:634 +#: structures/engines/mariadb/context.py:718 +#: structures/engines/mysql/context.py:727 +#: 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:735 -#: structures/engines/mysql/context.py:742 -#: structures/engines/postgresql/context.py:798 -#: structures/engines/sqlite/context.py:662 +#: structures/engines/mariadb/context.py:751 +#: structures/engines/mysql/context.py:758 +#: 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:788 +#: structures/engines/mariadb/context.py:804 #, python-brace-format msgid "Trigger{trigger_index:03}" -msgstr "" +msgstr "Trigger{trigger_index:03}" #: windows/dialogs/connections/view.py:417 -#: windows/dialogs/connections/view.py:764 windows/main/controller.py:1212 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1290 #: windows/views.py:33 msgid "Connection" msgstr "Conexión" #: windows/components/dataview.py:115 windows/components/dataview.py:240 #: windows/components/dataview.py:253 windows/components/dataview.py:268 -#: windows/main/database/procedure.py:129 windows/views.py:47 -#: windows/views.py:97 windows/views.py:1016 windows/views.py:1328 -#: windows/views.py:1361 windows/views.py:1385 windows/views.py:1409 -#: windows/views.py:1433 windows/views.py:1457 windows/views.py:1574 -#: windows/views.py:1954 windows/views.py:3079 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1024 +#: windows/views.py:1336 windows/views.py:1369 windows/views.py:1393 +#: windows/views.py:1417 windows/views.py:1441 windows/views.py:1465 +#: windows/views.py:1582 windows/views.py:1928 windows/views.py:2199 +#: windows/views.py:3293 windows/views.py:3515 msgid "Name" msgstr "Nombre" @@ -116,34 +115,32 @@ msgstr "Nombre" msgid "Last connection" msgstr "Última conexión" -#: windows/dialogs/connections/view.py:655 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:61 msgid "New directory" msgstr "Nuevo directorio" -#: windows/dialogs/connections/model.py:211 +#: windows/dialogs/connections/model.py:212 #: windows/dialogs/connections/view.py:615 windows/views.py:65 msgid "New connection" msgstr "Nueva conexión" #: windows/views.py:71 -#, fuzzy msgid "Rename" -msgstr "Nombre" +msgstr "Renombrar" #: windows/views.py:76 -#, fuzzy msgid "Clone connection" -msgstr "Nueva conexión" +msgstr "Clonar conexión" -#: windows/main/database/procedure.py:183 windows/views.py:81 -#: windows/views.py:613 windows/views.py:1488 windows/views.py:1896 -#: windows/views.py:2155 windows/views.py:2932 windows/views.py:2981 -#: windows/views.py:3215 windows/views.py:3247 +#: windows/views.py:81 windows/views.py:613 windows/views.py:1496 +#: windows/views.py:1864 windows/views.py:2154 windows/views.py:2378 +#: windows/views.py:3203 windows/views.py:3252 windows/views.py:3651 +#: windows/views.py:3683 msgid "Delete" msgstr "Eliminar" -#: windows/views.py:111 windows/views.py:1333 windows/views.py:1629 -#: windows/views.py:2844 windows/views.py:3134 +#: windows/views.py:111 windows/views.py:1341 windows/views.py:1637 +#: windows/views.py:3115 windows/views.py:3570 msgid "Engine" msgstr "Motor" @@ -155,12 +152,11 @@ msgstr "Host + puerto" msgid "Username" msgstr "Nombre de usuario" -#: windows/views.py:161 windows/views.py:1142 +#: windows/views.py:161 windows/views.py:1150 msgid "Password" msgstr "Contraseña" #: windows/views.py:174 -#, fuzzy msgid "Connection timeout" msgstr "Conexión perdida" @@ -170,7 +166,7 @@ msgstr "Usar TLS" #: windows/views.py:203 msgid "Mark read only" -msgstr "" +msgstr "Marcar como solo lectura" #: windows/views.py:214 msgid "Use SSH tunnel" @@ -178,28 +174,27 @@ msgstr "Usar túnel SSH" #: windows/views.py:225 msgid "Compressed client/server protocol" -msgstr "" +msgstr "Protocolo cliente/servidor comprimido" #: windows/views.py:244 msgid "Filename" msgstr "Nombre de archivo" -#: windows/views.py:249 windows/views.py:368 windows/views.py:3373 +#: windows/views.py:249 windows/views.py:368 windows/views.py:3809 msgid "Select a file" msgstr "Seleccionar un archivo" #: windows/views.py:249 windows/views.py:368 -#, fuzzy msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:266 windows/views.py:1335 windows/views.py:1587 -#: windows/views.py:2842 windows/views.py:3092 +#: windows/views.py:266 windows/views.py:1343 windows/views.py:1595 +#: windows/views.py:3113 windows/views.py:3528 msgid "Comments" msgstr "Comentarios" -#: windows/main/controller.py:766 windows/views.py:280 windows/views.py:740 +#: windows/main/controller.py:769 windows/views.py:280 windows/views.py:740 #: windows/views.py:894 msgid "Settings" msgstr "Configuraciones" @@ -234,16 +229,16 @@ msgstr "Puerto local" #: windows/views.py:354 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" +msgstr "" +"si el valor se establece en 0, se utilizará el primer puerto disponible" #: windows/views.py:363 msgid "Identity file" msgstr "Archivo de identidad" #: windows/views.py:379 -#, fuzzy msgid "Remote host + port" -msgstr "Host + puerto" +msgstr "Host remoto + puerto" #: windows/views.py:391 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." @@ -253,13 +248,13 @@ msgstr "" #: windows/views.py:400 msgid "SSH extra args" -msgstr "" +msgstr "Argumentos extra SSH" #: windows/views.py:415 msgid "SSH Tunnel" msgstr "Túnel SSH" -#: windows/views.py:421 windows/views.py:1331 windows/views.py:2846 +#: windows/views.py:421 windows/views.py:1339 windows/views.py:3117 msgid "Created at" msgstr "Creado en" @@ -268,7 +263,6 @@ msgid "Successful connections" msgstr "Conexiones exitosas" #: windows/views.py:472 -#, fuzzy msgid "Last successful connection" msgstr "Conexiones exitosas" @@ -278,51 +272,46 @@ msgstr "Conexiones fallidas" #: windows/views.py:506 msgid "Last failure reason" -msgstr "" +msgstr "Último motivo de fallo" #: windows/views.py:523 -#, fuzzy msgid "Total connection attempts" msgstr "Última conexión" #: windows/views.py:540 -#, fuzzy msgid "Average connection time (ms)" msgstr "Reconexión fallida:" #: windows/views.py:557 -#, fuzzy msgid "Most recent connection duration" -msgstr "Abrir administrador de conexiones" +msgstr "Duración de la conexión más reciente" #: windows/views.py:576 msgid "Statistics" msgstr "Estadísticas" -#: windows/views.py:594 windows/views.py:1853 +#: windows/views.py:594 windows/views.py:1821 msgid "Create" msgstr "Crear" #: windows/views.py:598 -#, fuzzy msgid "Create connection" -msgstr "Última conexión" +msgstr "Crear conexión" #: windows/views.py:601 -#, fuzzy msgid "Create directory" msgstr "Nuevo directorio" -#: windows/main/database/procedure.py:184 windows/views.py:630 -#: windows/views.py:854 windows/views.py:1483 windows/views.py:1899 -#: windows/views.py:2160 windows/views.py:2216 windows/views.py:2908 -#: windows/views.py:3250 windows/views.py:3387 +#: windows/views.py:630 windows/views.py:854 windows/views.py:1491 +#: windows/views.py:1867 windows/views.py:2159 windows/views.py:2383 +#: windows/views.py:2439 windows/views.py:3179 windows/views.py:3686 +#: windows/views.py:3823 msgid "Cancel" msgstr "Cancelar" -#: windows/main/controller.py:323 windows/main/database/procedure.py:185 -#: windows/views.py:635 windows/views.py:2165 windows/views.py:2358 -#: windows/views.py:3255 windows/views.py:3393 +#: windows/main/controller.py:323 windows/views.py:635 windows/views.py:2164 +#: windows/views.py:2388 windows/views.py:2603 windows/views.py:3691 +#: windows/views.py:3829 msgid "Save" msgstr "Guardar" @@ -334,7 +323,7 @@ msgstr "Probar" msgid "Connect" msgstr "Conectar" -#: windows/main/database/procedure.py:162 windows/views.py:752 +#: windows/views.py:752 msgid "Language" msgstr "Idioma" @@ -355,7 +344,6 @@ msgid "Locale" msgstr "Localización" #: windows/views.py:790 -#, fuzzy msgid "Column content" msgstr "Nueva conexión" @@ -395,536 +383,558 @@ msgstr "Desconectar del servidor" msgid "tool" msgstr "herramienta" -#: windows/views.py:914 windows/views.py:2196 +#: windows/views.py:914 windows/views.py:2419 msgid "Refresh" msgstr "Actualizar" -#: windows/views.py:918 windows/views.py:920 windows/views.py:1877 -#: windows/views.py:2200 windows/views.py:2344 +#: windows/views.py:918 windows/views.py:920 windows/views.py:1845 +#: windows/views.py:2423 windows/views.py:2589 msgid "Add" msgstr "Agregar" -#: windows/views.py:954 windows/views.py:958 windows/views.py:2322 +#: windows/views.py:925 +#, python-brace-format +msgid "{mode}" +msgstr "{mode}" + +#: windows/views.py:962 windows/views.py:966 windows/views.py:2556 +#: windows/views.py:2561 msgid "MyMenuItem" msgstr "MiElementoMenu" -#: windows/views.py:961 windows/views.py:1927 windows/views.py:3278 +#: windows/views.py:969 windows/views.py:1895 windows/views.py:2568 +#: windows/views.py:3714 msgid "MyMenu" msgstr "MiMenu" -#: windows/views.py:976 windows/views.py:1511 windows/views.py:1518 -#: windows/views.py:1525 +#: windows/views.py:984 windows/views.py:1519 windows/views.py:1526 +#: windows/views.py:1533 msgid "MyLabel" msgstr "MiEtiqueta" -#: windows/views.py:982 +#: windows/views.py:990 msgid "Databases" msgstr "Bases de datos" -#: windows/views.py:983 windows/views.py:1330 windows/views.py:2847 +#: windows/views.py:991 windows/views.py:1338 windows/views.py:3118 msgid "Size" msgstr "Tamaño" -#: windows/views.py:984 +#: windows/views.py:992 msgid "Elements" msgstr "Elementos" -#: windows/views.py:985 +#: windows/views.py:993 msgid "Modified at" msgstr "Modificado en" -#: windows/views.py:986 windows/views.py:1345 +#: windows/views.py:994 windows/views.py:1353 msgid "Tables" msgstr "Tablas" -#: windows/views.py:993 +#: windows/views.py:1001 msgid "System" msgstr "Sistema" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1039 -#: windows/views.py:1334 windows/views.py:2843 +#: windows/components/dataview.py:89 windows/views.py:1047 +#: windows/views.py:1342 windows/views.py:3114 msgid "Collation" msgstr "Intercalación" -#: windows/views.py:1068 +#: windows/views.py:1076 msgid "Encryption" -msgstr "" +msgstr "Cifrado" -#: windows/views.py:1080 +#: windows/main/controller.py:1259 windows/main/controller.py:1281 +#: windows/main/controller.py:1285 windows/views.py:1088 msgid "Read Only" -msgstr "" +msgstr "Solo lectura" -#: windows/views.py:1097 -#, fuzzy +#: windows/views.py:1105 msgid "Tablespace" -msgstr "Tablas" +msgstr "Tablespace" -#: windows/views.py:1118 -#, fuzzy +#: windows/views.py:1126 msgid "Connection limit" -msgstr "Conexión perdida" +msgstr "Límite de conexión" -#: windows/views.py:1161 -#, fuzzy +#: windows/views.py:1169 msgid "Profile" -msgstr "Archivo" +msgstr "Perfil" -#: windows/views.py:1187 -#, fuzzy +#: windows/views.py:1195 msgid "Default tablespace" -msgstr "Eliminar tabla" +msgstr "Tablespace predeterminado" -#: windows/views.py:1208 -#, fuzzy +#: windows/views.py:1216 msgid "Temporary tablespace" -msgstr "Temporal" +msgstr "Tablespace temporal" -#: windows/views.py:1234 +#: windows/views.py:1242 msgid "Quota" -msgstr "" +msgstr "Quota" -#: windows/views.py:1253 +#: windows/views.py:1261 msgid "Unlimited quota" -msgstr "" +msgstr "Cuota ilimitada" -#: windows/views.py:1270 +#: windows/views.py:1278 msgid "Account status" -msgstr "" +msgstr "Estado de cuenta" -#: windows/views.py:1291 -#, fuzzy +#: windows/views.py:1299 msgid "Password expire" -msgstr "Contraseña" +msgstr "Expiración de contraseña" -#: windows/views.py:1315 -#, fuzzy +#: windows/views.py:1323 msgid "Add new table" -msgstr "Eliminar tabla" +msgstr "Agregar nueva tabla" -#: windows/views.py:1317 -#, fuzzy +#: windows/views.py:1325 msgid "Clone table" -msgstr "Eliminar tabla" +msgstr "Clonar tabla" -#: windows/main/controller.py:1633 windows/views.py:1319 +#: windows/main/controller.py:1770 windows/views.py:1327 msgid "Delete table" msgstr "Eliminar tabla" -#: windows/views.py:1329 +#: windows/views.py:1337 msgid "Rows" msgstr "Filas" -#: windows/views.py:1332 windows/views.py:2845 +#: windows/views.py:1340 windows/views.py:3116 msgid "Updated at" msgstr "Actualizado en" -#: windows/views.py:1350 +#: windows/views.py:1358 msgid "Add new view" -msgstr "" +msgstr "Agregar nueva vista" -#: windows/views.py:1352 windows/views.py:1376 -#, fuzzy +#: windows/views.py:1360 windows/views.py:1384 msgid "Clone view" msgstr "Clonar" -#: windows/views.py:1354 -#, fuzzy +#: windows/views.py:1362 msgid "Delete view" msgstr "Eliminar" -#: windows/views.py:1362 windows/views.py:1386 windows/views.py:1410 -#: windows/views.py:1434 windows/views.py:1458 -#, fuzzy +#: windows/views.py:1370 windows/views.py:1394 windows/views.py:1418 +#: windows/views.py:1442 windows/views.py:1466 msgid "Definition" -msgstr "Condición" +msgstr "Definición" -#: windows/views.py:1369 windows/views.py:2177 +#: windows/views.py:1377 msgid "Views" msgstr "Vistas" -#: windows/views.py:1374 +#: windows/views.py:1382 msgid "Add new procedure" -msgstr "" +msgstr "Agregar nuevo procedimiento" -#: windows/views.py:1376 +#: windows/views.py:1384 msgid "Clone procedure" -msgstr "" +msgstr "Clonar procedimiento" -#: windows/views.py:1378 -#, fuzzy +#: windows/views.py:1386 msgid "Delete procedure" -msgstr "Eliminar registro" +msgstr "Eliminar procedimiento" -#: windows/views.py:1393 +#: windows/views.py:1401 msgid "Procedures" -msgstr "" +msgstr "Procedimientos" -#: windows/views.py:1398 -#, fuzzy +#: windows/views.py:1406 msgid "Add new function" -msgstr "Nueva conexión" +msgstr "Agregar nueva función" -#: windows/views.py:1400 -#, fuzzy +#: windows/views.py:1408 msgid "Clone function" -msgstr "Nueva conexión" +msgstr "Clonar función" -#: windows/views.py:1402 -#, fuzzy +#: windows/views.py:1410 msgid "Delete function" -msgstr "Eliminar registro" +msgstr "Eliminar función" -#: windows/views.py:1417 -#, fuzzy +#: windows/views.py:1425 msgid "Functions" -msgstr "Conexión" +msgstr "Funciones" -#: windows/views.py:1422 -#, fuzzy +#: windows/views.py:1430 msgid "Add new trigger" -msgstr "Disparadores" +msgstr "Agregar nuevo disparador" -#: windows/views.py:1424 -#, fuzzy +#: windows/views.py:1432 msgid "Clone trigger" -msgstr "Disparadores" +msgstr "Clonar disparador" -#: windows/views.py:1426 -#, fuzzy +#: windows/views.py:1434 msgid "Delete trigger" -msgstr "Eliminar registro" +msgstr "Eliminar disparador" -#: windows/views.py:1441 windows/views.py:2185 +#: windows/views.py:1449 msgid "Triggers" msgstr "Disparadores" -#: windows/views.py:1446 +#: windows/views.py:1454 msgid "Add new event" -msgstr "" +msgstr "Agregar nuevo evento" -#: windows/views.py:1448 -#, fuzzy +#: windows/views.py:1456 msgid "Clone event" msgstr "Clonar" -#: windows/views.py:1450 -#, fuzzy +#: windows/views.py:1458 msgid "Delete event" -msgstr "Eliminar tabla" +msgstr "Eliminar evento" -#: windows/views.py:1465 -#, fuzzy +#: windows/views.py:1473 msgid "Events" -msgstr "Elementos" +msgstr "Eventos" -#: windows/views.py:1493 windows/views.py:1904 windows/views.py:2214 -#: windows/views.py:2296 windows/views.py:2915 +#: windows/views.py:1501 windows/views.py:1872 windows/views.py:2437 +#: windows/views.py:2521 windows/views.py:3186 msgid "Apply" msgstr "Aplicar" -#: windows/main/database/procedure.py:122 windows/views.py:1505 -#: windows/views.py:1684 windows/views.py:2114 windows/views.py:3167 +#: windows/views.py:1513 windows/views.py:1692 windows/views.py:3603 msgid "Options" msgstr "Opciones" -#: windows/views.py:1536 +#: windows/views.py:1544 msgid "Diagram" msgstr "Diagrama" -#: windows/views.py:1547 +#: windows/views.py:1555 msgid "Database" msgstr "Base de datos" -#: windows/views.py:1602 windows/views.py:3107 +#: windows/views.py:1610 windows/views.py:3543 msgid "Base" msgstr "Base" -#: windows/views.py:1616 windows/views.py:3121 +#: windows/views.py:1624 windows/views.py:3557 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1644 windows/views.py:3149 +#: windows/views.py:1652 windows/views.py:3585 msgid "Default Collation" msgstr "Intercalación predeterminada" -#: windows/views.py:1654 +#: windows/views.py:1662 msgid "Convert data" -msgstr "" +msgstr "Convertir datos" -#: windows/views.py:1662 +#: windows/views.py:1670 msgid "Row format" -msgstr "" +msgstr "Formato de fila" -#: windows/views.py:1696 windows/views.py:1737 windows/views.py:1781 -#: windows/views.py:1879 windows/views.py:2204 +#: windows/views.py:1703 windows/views.py:1731 windows/views.py:1759 +#: windows/views.py:1847 windows/views.py:2259 windows/views.py:2427 +#: windows/views.py:3313 windows/views.py:3330 windows/views.py:3354 +#: windows/views.py:3381 msgid "Remove" msgstr "Eliminar" -#: windows/views.py:1703 windows/views.py:1744 windows/views.py:1788 +#: windows/views.py:1705 windows/views.py:1733 windows/views.py:1761 +#: windows/views.py:2261 windows/views.py:2529 windows/views.py:3320 +#: windows/views.py:3337 windows/views.py:3361 windows/views.py:3388 msgid "Clear" msgstr "Limpiar" -#: windows/views.py:1718 windows/views.py:3181 +#: windows/views.py:1718 windows/views.py:3617 msgid "Indexes" msgstr "Índices" -#: windows/views.py:1732 windows/views.py:1776 windows/views.py:2927 -#: windows/views.py:2969 windows/views.py:3210 +#: windows/views.py:1729 windows/views.py:1757 windows/views.py:2257 +#: windows/views.py:3198 windows/views.py:3240 windows/views.py:3308 +#: windows/views.py:3349 windows/views.py:3376 windows/views.py:3646 msgid "Insert" msgstr "Insertar" -#: windows/views.py:1762 +#: windows/views.py:1746 msgid "Foreign Keys" msgstr "Claves foráneas" -#: windows/views.py:1806 +#: windows/views.py:1774 msgid "Checks" msgstr "Comprobaciones" -#: windows/views.py:1873 windows/views.py:3202 +#: windows/views.py:1841 windows/views.py:3638 msgid "Columns:" msgstr "Columnas:" -#: windows/views.py:1883 -#, fuzzy +#: windows/views.py:1851 msgid "Move Up" msgstr "Mover arriba\tCTRL+UP" -#: windows/views.py:1885 -#, fuzzy +#: windows/views.py:1853 msgid "Move Down" msgstr "Mover abajo\tCTRL+D" -#: windows/views.py:1917 windows/views.py:1924 windows/views.py:3268 -#: windows/views.py:3275 +#: windows/views.py:1885 windows/views.py:1892 windows/views.py:3704 +#: windows/views.py:3711 msgid "Add Index" msgstr "Agregar índice" -#: windows/views.py:1921 windows/views.py:3272 +#: windows/views.py:1889 windows/views.py:3708 msgid "Add PrimaryKey" msgstr "Agregar clave primaria" -#: windows/views.py:1938 +#: windows/views.py:1906 msgid "Table" msgstr "Tabla" -#: windows/main/database/procedure.py:140 windows/views.py:1974 -#, fuzzy -msgid "Definer" -msgstr "Insertar" - -#: windows/views.py:1994 +#: windows/views.py:1948 windows/views.py:2219 msgid "Schema" -msgstr "" +msgstr "Schema" -#: windows/views.py:2020 -msgid "SQL security" -msgstr "" +#: windows/views.py:1976 windows/views.py:2247 +msgid "General" +msgstr "General" -#: windows/views.py:2027 -#, fuzzy -msgid "DEFINER" -msgstr "Insertar" - -#: windows/views.py:2027 -#, fuzzy -msgid "INVOKER" -msgstr "Insertar" - -#: windows/views.py:2039 +#: windows/views.py:1981 msgid "Algorithm" -msgstr "" +msgstr "Algoritmo" -#: windows/views.py:2041 -#, fuzzy +#: windows/views.py:1983 msgid "UNDEFINED" msgstr "Sin signo" -#: windows/views.py:2044 +#: windows/views.py:1986 msgid "MERGE" -msgstr "" +msgstr "MERGE" -#: windows/views.py:2047 -#, fuzzy +#: windows/views.py:1989 msgid "TEMPTABLE" -msgstr "Tabla" +msgstr "TEMPTABLE" -#: windows/views.py:2057 +#: windows/views.py:1999 msgid "View constraint" -msgstr "" +msgstr "Restricción de vista" -#: windows/views.py:2059 -#, fuzzy +#: windows/views.py:2001 msgid "None" -msgstr "Clonar" +msgstr "Ninguno" -#: windows/views.py:2062 -#, fuzzy +#: windows/views.py:2004 msgid "LOCAL" -msgstr "Localización" +msgstr "LOCAL" -#: windows/views.py:2065 -#, fuzzy +#: windows/views.py:2007 msgid "CASCADE" -msgstr "Cancelar" +msgstr "CASCADA" -#: windows/views.py:2068 -#, fuzzy +#: windows/views.py:2010 msgid "CHECK ONLY" -msgstr "Verificar" +msgstr "SOLO VERIFICAR" -#: windows/views.py:2071 +#: windows/views.py:2013 msgid "READ ONLY" -msgstr "" +msgstr "SOLO LECTURA" + +#: windows/views.py:2026 +msgid "Behavior" +msgstr "Comportamiento" + +#: windows/views.py:2033 windows/views.py:2281 +msgid "Definer" +msgstr "Definidor" -#: windows/views.py:2083 +#: windows/views.py:2041 windows/views.py:2289 +msgid "*" +msgstr "*" + +#: windows/views.py:2053 windows/views.py:2301 +msgid "SQL security" +msgstr "Seguridad SQL" + +#: windows/views.py:2060 windows/views.py:2308 +msgid "DEFINER" +msgstr "DEFINIDOR" + +#: windows/views.py:2060 windows/views.py:2308 +msgid "INVOKER" +msgstr "INVOCADOR" + +#: windows/views.py:2074 msgid "Force" -msgstr "" +msgstr "Forzar" -#: windows/views.py:2095 +#: windows/views.py:2086 msgid "Security barrier" -msgstr "" +msgstr "Barrera de seguridad" + +#: windows/views.py:2099 windows/views.py:2323 +msgid "Security" +msgstr "Seguridad" + +#: windows/views.py:2176 +msgid "View" +msgstr "Vistas" + +#: windows/views.py:2274 +msgid "Parameters" +msgstr "Parámetros" -#: windows/views.py:2202 -#, fuzzy +#: windows/views.py:2400 +msgid "Procedure" +msgstr "Procedure" + +#: windows/views.py:2408 +msgid "Trigger" +msgstr "Disparadores" + +#: windows/views.py:2425 msgid "Duplicate" -msgstr "Duplicar registro" +msgstr "Duplicar" -#: windows/views.py:2208 +#: windows/views.py:2431 msgid "Apply changes automatically" msgstr "Aplicar cambios automáticamente" -#: windows/views.py:2210 windows/views.py:2211 +#: windows/views.py:2433 windows/views.py:2434 msgid "" -"If enabled, table edits are applied immediately without pressing Apply or" -" Cancel" +"If enabled, table edits are applied immediately without pressing Apply or " +"Cancel" msgstr "" -"Si está habilitado, las ediciones de la tabla se aplican inmediatamente " -"sin presionar Aplicar o Cancelar" +"Si está habilitado, las ediciones de la tabla se aplican inmediatamente sin " +"presionar Aplicar o Cancelar" -#: windows/views.py:2224 +#: windows/views.py:2447 #, python-brace-format -msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgid "" +"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" +"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -#: windows/views.py:2232 -#, fuzzy +#: windows/views.py:2455 msgid "First" -msgstr "Filtros" +msgstr "Primero" -#: windows/views.py:2250 +#: windows/views.py:2473 msgid "Last" -msgstr "" +msgstr "Último" -#: windows/views.py:2259 +#: windows/views.py:2482 msgid "Filters" msgstr "Filtros" -#: windows/views.py:2299 +#: windows/views.py:2524 +msgid "" +"Apply filters in data\n" +"CTRL+ENTER" +msgstr "" +"Aplicar filtros en datos\n" +"CTRL+ENTER" + +#: windows/views.py:2525 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2319 +#: windows/views.py:2553 msgid "Insert row" msgstr "Insertar fila" -#: windows/views.py:2327 +#: windows/components/popup.py:31 windows/views.py:2565 +msgid "NULL" +msgstr "NULL" + +#: windows/views.py:2572 msgid "Data" msgstr "Datos" -#: windows/main/controller.py:318 windows/views.py:2344 -#, fuzzy +#: windows/main/controller.py:318 windows/views.py:2589 msgid "New query" msgstr "Consulta" -#: windows/views.py:2346 windows/views.py:2866 +#: windows/views.py:2591 windows/views.py:3137 msgid "Close" msgstr "Cerrar" -#: windows/main/controller.py:319 windows/views.py:2346 -#, fuzzy +#: windows/main/controller.py:319 windows/views.py:2591 msgid "Close query" msgstr "Consulta" -#: windows/views.py:2350 +#: windows/views.py:2595 msgid "Run" -msgstr "" +msgstr "Ejecutar" -#: windows/main/controller.py:320 windows/views.py:2350 -#, fuzzy +#: windows/main/controller.py:320 windows/views.py:2595 msgid "Execute" -msgstr "Ejecutable SSH" +msgstr "Ejecutar" -#: windows/views.py:2352 +#: windows/views.py:2597 msgid "Run all" -msgstr "" +msgstr "Ejecutar todo" -#: windows/views.py:2352 +#: windows/views.py:2597 msgid "Execute all statements" -msgstr "" +msgstr "Ejecutar todas las declaraciones" -#: windows/main/controller.py:322 windows/views.py:2354 +#: windows/main/controller.py:322 windows/views.py:2599 msgid "Stop" -msgstr "" +msgstr "Detener" -#: windows/views.py:2419 +#: windows/views.py:2664 msgid "a page" -msgstr "" +msgstr "una página" -#: windows/views.py:2469 +#: windows/views.py:2714 msgid "Query" msgstr "Consulta" -#: windows/views.py:2820 -#, fuzzy +#: windows/views.py:3091 msgid "Character set" -msgstr "Creado en" +msgstr "Conjunto de caracteres" -#: windows/views.py:2850 windows/views.py:2869 +#: windows/views.py:3121 windows/views.py:3140 msgid "New" msgstr "Nuevo" -#: windows/views.py:2889 +#: windows/views.py:3160 msgid "Insert record" msgstr "Insertar registro" -#: windows/views.py:2894 +#: windows/views.py:3165 msgid "Duplicate record" msgstr "Duplicar registro" -#: windows/views.py:2901 +#: windows/views.py:3172 msgid "Delete record" msgstr "Eliminar registro" -#: windows/views.py:2939 windows/views.py:3222 +#: windows/views.py:3210 windows/views.py:3658 msgid "Up" msgstr "Arriba" -#: windows/views.py:2946 windows/views.py:3229 +#: windows/views.py:3217 windows/views.py:3665 msgid "Down" msgstr "Abajo" -#: windows/views.py:2961 +#: windows/views.py:3232 msgid "Table:" msgstr "Tabla:" -#: windows/views.py:2974 +#: windows/views.py:3245 msgid "Clone" msgstr "Clonar" -#: windows/views.py:3358 +#: windows/views.py:3270 +msgid "MyButton" +msgstr "MiBotón" + +#: windows/views.py:3794 msgid "Save Starments" -msgstr "" +msgstr "Guardar declaraciones" -#: windows/views.py:3366 -#, fuzzy +#: windows/views.py:3802 msgid "Location" -msgstr "Intercalación" +msgstr "Ubicación" -#: windows/views.py:3373 +#: windows/views.py:3809 msgid "*.sql" -msgstr "" +msgstr "*.sql" #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 @@ -1034,10 +1044,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" @@ -1048,16 +1054,16 @@ msgstr "Texto/Expresión" #: windows/dialogs/connections/view.py:126 windows/main/query/renderer.py:192 msgid "Unknown error" -msgstr "" +msgstr "Error desconocido" #: windows/dialogs/connections/view.py:416 msgid "Connection established successfully" -msgstr "" +msgstr "Conexión establecida exitosamente" #: 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:431 msgid "Confirm save" @@ -1065,147 +1071,154 @@ msgstr "Confirmar guardar" #: 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:485 msgid "Unsaved changes" -msgstr "" +msgstr "Cambios sin guardar" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:773 msgid "" -"This connection cannot work without TLS. TLS has been enabled " -"automatically." +"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:787 -#, 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:788 +#: windows/dialogs/connections/view.py:799 msgid "Connection error" msgstr "Error de conexión" -#: windows/dialogs/connections/view.py:814 -#, 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:817 -#: windows/dialogs/connections/view.py:834 +#: windows/dialogs/connections/view.py:828 +#: windows/dialogs/connections/view.py:845 msgid "Confirm delete" msgstr "Confirmar eliminar" -#: windows/dialogs/connections/view.py:831 -#, 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:315 #, python-brace-format msgid "{text} ({shortcut})" -msgstr "" +msgstr "{text} ({shortcut})" #: windows/main/controller.py:321 -#, fuzzy msgid "Execute all" -msgstr "Ejecutable SSH" +msgstr "Ejecutar todo" #: windows/main/controller.py:440 windows/main/controller.py:448 -#, fuzzy msgid "Query (1)" msgstr "Consulta" #: windows/main/controller.py:467 #, python-brace-format msgid "Query ({query_number})" -msgstr "" +msgstr "Query ({query_number})" #: windows/main/controller.py:516 msgid "You have unsaved changes. Save before closing?" -msgstr "" +msgstr "Tiene cambios sin guardar. ¿Guardar antes de cerrar?" #: windows/main/controller.py:517 msgid "Unsaved query" -msgstr "" +msgstr "Consulta sin guardar" #: windows/main/controller.py:562 -#, fuzzy msgid "Save query" -msgstr "Consulta" +msgstr "Guardar consulta" #: windows/main/controller.py:565 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" -msgstr "" +msgstr "Archivos SQL (*.sql)|*.sql|Todos los archivos (*.*)|*.*" #: windows/main/controller.py:593 windows/main/controller.py:624 #: windows/main/controller.py:651 windows/main/database/list.py:119 -#: windows/main/database/procedure.py:294 -#: windows/main/database/procedure.py:320 windows/main/database/view.py:267 -#: windows/main/database/view.py:296 windows/main/query/controller.py:177 +#: windows/main/database/procedure.py:206 +#: windows/main/database/procedure.py:232 windows/main/database/view.py:268 +#: windows/main/database/view.py:297 windows/main/query/controller.py:177 msgid "Error" msgstr "Error" #: windows/main/controller.py:631 #, python-brace-format msgid "-- Saved query to {file_path}" -msgstr "" +msgstr "-- Consulta guardada en {file_path}" #: windows/main/controller.py:657 #, python-brace-format msgid "-- Autosaved query to {file_path}" -msgstr "" +msgstr "-- Consulta autoguardada en {file_path}" -#: windows/main/controller.py:719 +#: windows/main/controller.py:722 msgid "days" msgstr "días" -#: windows/main/controller.py:720 +#: windows/main/controller.py:723 msgid "hours" msgstr "horas" -#: windows/main/controller.py:721 +#: windows/main/controller.py:724 msgid "minutes" msgstr "minutos" -#: windows/main/controller.py:722 +#: windows/main/controller.py:725 msgid "seconds" msgstr "segundos" -#: windows/main/controller.py:730 +#: windows/main/controller.py:733 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizada: {used} ({percentage:.2%})" -#: windows/main/controller.py:766 +#: windows/main/controller.py:769 msgid "Settings saved successfully" -msgstr "" +msgstr "Configuraciones guardadas exitosamente" -#: windows/main/controller.py:990 +#: windows/main/controller.py:993 #, python-brace-format msgid "~{estimated} (Loading...)" -msgstr "" +msgstr "~{estimated} (Cargando...)" -#: windows/main/controller.py:992 +#: windows/main/controller.py:995 msgid "~ (Loading...)" -msgstr "" +msgstr "~ (Cargando...)" + +#: windows/main/controller.py:1230 +msgid "Write Mode (2:00)" +msgstr "Modo escritura (2:00)" -#: windows/main/controller.py:1214 +#: windows/main/controller.py:1281 +msgid "Write Mode" +msgstr "Modo escritura" + +#: windows/main/controller.py:1292 msgid "Version" msgstr "Versión" -#: windows/main/controller.py:1216 +#: windows/main/controller.py:1294 msgid "Uptime" msgstr "Tiempo de actividad" -#: windows/main/controller.py:1299 +#: windows/main/controller.py:1429 #, 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:1332 +#: windows/main/controller.py:1462 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1214,50 +1227,54 @@ 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:1337 windows/main/controller.py:1358 -#, fuzzy +#: windows/main/controller.py:1467 windows/main/controller.py:1488 msgid "Delete database" -msgstr "Eliminar tabla" +msgstr "Eliminar base de datos" -#: windows/main/controller.py:1343 +#: windows/main/controller.py:1473 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:1344 +#: windows/main/controller.py:1474 msgid "Dump not available" -msgstr "" +msgstr "Volcado no disponible" -#: windows/main/controller.py:1357 +#: windows/main/controller.py:1487 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:1372 +#: windows/main/controller.py:1502 msgid "Database deleted successfully" -msgstr "" +msgstr "Base de datos eliminada exitosamente" -#: windows/main/controller.py:1373 windows/main/database/procedure.py:283 -#: windows/main/database/procedure.py:314 windows/main/database/view.py:255 -#: windows/main/database/view.py:290 +#: windows/main/controller.py:1503 windows/main/database/procedure.py:195 +#: windows/main/database/procedure.py:226 windows/main/database/view.py:256 +#: windows/main/database/view.py:291 msgid "Success" -msgstr "" +msgstr "Éxito" -#: windows/main/controller.py:1604 +#: windows/main/controller.py:1741 #, 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:1630 -#, fuzzy, python-brace-format +#: windows/main/controller.py:1767 +#, 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:1652 +#: windows/main/controller.py:1789 #, python-brace-format msgid "{table_name} (COPY)" -msgstr "" +msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1797 +#: windows/main/controller.py:1948 msgid "Do you want delete the records?" msgstr "¿Quieres eliminar los registros?" @@ -1277,88 +1294,77 @@ msgstr "Conexión perdida" msgid "Reconnection failed:" msgstr "Reconexión fallida:" -#: windows/main/database/procedure.py:114 -msgid "Procedure" -msgstr "" - -#: windows/main/database/procedure.py:151 -#, fuzzy -msgid "Parameters" -msgstr "PeterSQL" - -#: windows/main/database/procedure.py:282 +#: windows/main/database/procedure.py:194 msgid "Procedure created successfully" -msgstr "" +msgstr "Procedimiento creado exitosamente" -#: windows/main/database/procedure.py:282 +#: windows/main/database/procedure.py:194 msgid "Procedure updated successfully" -msgstr "" +msgstr "Procedimiento actualizado exitosamente" -#: windows/main/database/procedure.py:294 +#: windows/main/database/procedure.py:206 #, python-brace-format msgid "Error saving procedure: {}" -msgstr "" +msgstr "Error al guardar procedimiento: {}" -#: windows/main/database/procedure.py:305 -#, fuzzy, python-brace-format +#: windows/main/database/procedure.py:217 +#, python-brace-format msgid "Are you sure you want to delete procedure '{}'?" -msgstr "¿Quieres eliminar los registros?" +msgstr "¿Está seguro de que desea eliminar el procedimiento '{}'?" -#: windows/main/database/procedure.py:306 windows/main/database/view.py:281 -#, fuzzy +#: windows/main/database/procedure.py:218 windows/main/database/view.py:282 msgid "Confirm Delete" msgstr "Confirmar eliminar" -#: windows/main/database/procedure.py:314 +#: windows/main/database/procedure.py:226 msgid "Procedure deleted successfully" -msgstr "" +msgstr "Procedimiento eliminado exitosamente" -#: windows/main/database/procedure.py:320 +#: windows/main/database/procedure.py:232 #, python-brace-format msgid "Error deleting procedure: {}" -msgstr "" +msgstr "Error al eliminar procedimiento: {}" -#: windows/main/database/view.py:254 +#: windows/main/database/view.py:255 msgid "View created successfully" -msgstr "" +msgstr "Vista creada exitosamente" -#: windows/main/database/view.py:254 +#: windows/main/database/view.py:255 msgid "View updated successfully" -msgstr "" +msgstr "Vista actualizada exitosamente" -#: windows/main/database/view.py:267 +#: windows/main/database/view.py:268 #, python-brace-format msgid "Error saving view: {}" -msgstr "" +msgstr "Error al guardar vista: {}" -#: windows/main/database/view.py:280 +#: windows/main/database/view.py:281 #, python-brace-format msgid "Are you sure you want to delete view '{}'?" -msgstr "" +msgstr "¿Está seguro de que desea eliminar la vista '{}'?" -#: windows/main/database/view.py:290 +#: windows/main/database/view.py:291 msgid "View deleted successfully" -msgstr "" +msgstr "Vista eliminada exitosamente" -#: windows/main/database/view.py:296 +#: 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 #, python-brace-format msgid "{elapsed_ms:.0f} ms" -msgstr "" +msgstr "{elapsed_ms:.0f} ms" #: windows/main/query/controller.py:112 #, python-brace-format msgid "{elapsed_s:.2f} s" -msgstr "" +msgstr "{elapsed_s:.2f} s" #: windows/main/query/controller.py:115 -#, fuzzy msgid "none" -msgstr "Clonar" +msgstr "ninguno" #: windows/main/query/controller.py:121 #, python-brace-format @@ -1369,94 +1375,96 @@ 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 msgid "Query execution cancelled" -msgstr "" +msgstr "Ejecución de consulta cancelada" #: windows/main/query/controller.py:176 -#, fuzzy msgid "No active database connection" -msgstr "Nueva conexión" +msgstr "Sin conexión de base de datos activa" #: windows/main/query/history.py:55 -#, fuzzy msgid "(empty query)" -msgstr "Consulta" +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 "Query {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" -#: windows/main/table/records.py:336 +#: windows/main/table/records.py:334 msgid "Error saving records" -msgstr "" +msgstr "Error al guardar registros" #~ msgid "Created at:" -#~ msgstr "" +#~ msgstr "Created at:" #~ msgid "Last connection:" -#~ msgstr "" +#~ msgstr "Last connection:" #~ msgid "Successful connections:" -#~ msgstr "" +#~ msgstr "Successful connections:" #~ msgid "Unsuccessful connections:" -#~ msgstr "" +#~ msgstr "Unsuccessful connections:" #~ msgid "Session Manager" -#~ msgstr "" +#~ msgstr "Session Manager" #~ msgid "Session name" -#~ msgstr "" +#~ msgstr "Session name" #~ msgid "Connection type" -#~ msgstr "" +#~ msgstr "Connection type" #~ msgid "Open" -#~ msgstr "" +#~ msgstr "Open" #~ msgid "Open session manager" -#~ msgstr "" +#~ msgstr "Open session manager" #~ msgid "Foreign Key" -#~ msgstr "" +#~ msgstr "Foreign Key" #~ msgid "New Session" #~ msgstr "Nueva sesión" @@ -1466,32 +1474,31 @@ msgstr "" #~ 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" +#~ "Tabla `%(database_name)s`.`%(table_name)s`: %(total_rows) filas en total" #~ msgid "Next" #~ msgstr "Siguiente" #~ msgid "{} rows affected" -#~ msgstr "" +#~ msgstr "{} rows affected" #~ msgid "Query {}" #~ msgstr "Consulta" #~ msgid "Query {} (Error)" -#~ msgstr "" +#~ msgstr "Query {} (Error)" #~ msgid "Query {} ({} rows × {} cols)" -#~ msgstr "" +#~ msgstr "Query {} ({} rows × {} cols)" #~ msgid "{} rows" #~ msgstr "Filas" #~ msgid "{:.1f} ms" -#~ msgstr "" +#~ msgstr "{:.1f} ms" #~ msgid "{} warnings" -#~ msgstr "" +#~ msgstr "{} warnings" #~ msgid "Edit Value" #~ msgstr "Editar valor" @@ -1506,10 +1513,10 @@ msgstr "" #~ msgstr "Importar" #~ msgid "Read only" -#~ msgstr "" +#~ msgstr "Read only" #~ msgid "CASCADED" -#~ msgstr "" +#~ msgstr "CASCADED" #~ msgid "CHECK OPTION" #~ msgstr "conexión" @@ -1552,7 +1559,7 @@ msgstr "" #~ msgstr "Opciones" #~ msgid "RadioBtn" -#~ msgstr "" +#~ msgstr "RadioBtn" #~ msgid "Edit Column" #~ msgstr "Editar columna" @@ -1565,4 +1572,3 @@ msgstr "" #~ msgid "Refrsh" #~ msgstr "Actualizar" - diff --git a/locale/fr_FR/LC_MESSAGES/petersql.mo b/locale/fr_FR/LC_MESSAGES/petersql.mo index b40cd26c0c3477da3b84ce1a6715949848ff38be..b8b07a439fd8cc3b25cb88b88d4364b1f2b1c851 100644 GIT binary patch literal 18905 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 ZBFR6zFUxo}NT~v0fDu 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 diff --git a/locale/fr_FR/LC_MESSAGES/petersql.po b/locale/fr_FR/LC_MESSAGES/petersql.po index e9d0ec5..4f1e58f 100644 --- a/locale/fr_FR/LC_MESSAGES/petersql.po +++ b/locale/fr_FR/LC_MESSAGES/petersql.po @@ -2,20 +2,19 @@ # 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-05-02 16:08+0200\n" +"POT-Creation-Date: 2026-05-31 12:36+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" -"Language: fr_FR\n" "Language-Team: fr_FR \n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Generated-By: Babel 2.18.0\n" #: helpers/__init__.py:16 @@ -47,68 +46,68 @@ msgstr "To" msgid "OpenSSH client not found." msgstr "Client OpenSSH introuvable." -#: structures/engines/context.py:535 +#: structures/engines/context.py:544 msgid "This connection is read-only." -msgstr "" +msgstr "Cette connexion est en lecture seule." -#: structures/engines/mariadb/context.py:616 -#: structures/engines/mysql/context.py:627 -#: structures/engines/postgresql/context.py:685 -#: structures/engines/sqlite/context.py:552 +#: structures/engines/mariadb/context.py:632 +#: structures/engines/mysql/context.py:643 +#: 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:644 -#: structures/engines/mysql/context.py:655 -#: structures/engines/postgresql/context.py:710 -#: structures/engines/sqlite/context.py:576 +#: structures/engines/mariadb/context.py:660 +#: structures/engines/mysql/context.py:671 +#: 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:662 -#: structures/engines/mysql/context.py:673 -#: structures/engines/postgresql/context.py:728 -#: structures/engines/sqlite/context.py:594 +#: structures/engines/mariadb/context.py:678 +#: structures/engines/mysql/context.py:689 +#: 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:702 -#: structures/engines/mysql/context.py:711 -#: structures/engines/postgresql/context.py:768 -#: structures/engines/sqlite/context.py:634 +#: structures/engines/mariadb/context.py:718 +#: structures/engines/mysql/context.py:727 +#: 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:735 -#: structures/engines/mysql/context.py:742 -#: structures/engines/postgresql/context.py:798 -#: structures/engines/sqlite/context.py:662 +#: structures/engines/mariadb/context.py:751 +#: structures/engines/mysql/context.py:758 +#: 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:788 +#: structures/engines/mariadb/context.py:804 #, python-brace-format msgid "Trigger{trigger_index:03}" -msgstr "" +msgstr "Trigger{trigger_index:03}" #: windows/dialogs/connections/view.py:417 -#: windows/dialogs/connections/view.py:764 windows/main/controller.py:1212 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1290 #: windows/views.py:33 msgid "Connection" msgstr "Connexion" #: windows/components/dataview.py:115 windows/components/dataview.py:240 #: windows/components/dataview.py:253 windows/components/dataview.py:268 -#: windows/main/database/procedure.py:129 windows/views.py:47 -#: windows/views.py:97 windows/views.py:1016 windows/views.py:1328 -#: windows/views.py:1361 windows/views.py:1385 windows/views.py:1409 -#: windows/views.py:1433 windows/views.py:1457 windows/views.py:1574 -#: windows/views.py:1954 windows/views.py:3079 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1024 +#: windows/views.py:1336 windows/views.py:1369 windows/views.py:1393 +#: windows/views.py:1417 windows/views.py:1441 windows/views.py:1465 +#: windows/views.py:1582 windows/views.py:1928 windows/views.py:2199 +#: windows/views.py:3293 windows/views.py:3515 msgid "Name" msgstr "Nom" @@ -116,34 +115,32 @@ msgstr "Nom" msgid "Last connection" msgstr "Dernière connexion" -#: windows/dialogs/connections/view.py:655 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:61 msgid "New directory" msgstr "Nouveau répertoire" -#: windows/dialogs/connections/model.py:211 +#: windows/dialogs/connections/model.py:212 #: windows/dialogs/connections/view.py:615 windows/views.py:65 msgid "New connection" msgstr "Nouvelle connexion" #: windows/views.py:71 -#, fuzzy msgid "Rename" -msgstr "Nom" +msgstr "Renommer" #: windows/views.py:76 -#, fuzzy msgid "Clone connection" -msgstr "Nouvelle connexion" +msgstr "Cloner la connexion" -#: windows/main/database/procedure.py:183 windows/views.py:81 -#: windows/views.py:613 windows/views.py:1488 windows/views.py:1896 -#: windows/views.py:2155 windows/views.py:2932 windows/views.py:2981 -#: windows/views.py:3215 windows/views.py:3247 +#: windows/views.py:81 windows/views.py:613 windows/views.py:1496 +#: windows/views.py:1864 windows/views.py:2154 windows/views.py:2378 +#: windows/views.py:3203 windows/views.py:3252 windows/views.py:3651 +#: windows/views.py:3683 msgid "Delete" msgstr "Supprimer" -#: windows/views.py:111 windows/views.py:1333 windows/views.py:1629 -#: windows/views.py:2844 windows/views.py:3134 +#: windows/views.py:111 windows/views.py:1341 windows/views.py:1637 +#: windows/views.py:3115 windows/views.py:3570 msgid "Engine" msgstr "Moteur" @@ -155,22 +152,21 @@ msgstr "Hôte + port" msgid "Username" msgstr "Nom d'utilisateur" -#: windows/views.py:161 windows/views.py:1142 +#: windows/views.py:161 windows/views.py:1150 msgid "Password" msgstr "Mot de passe" #: windows/views.py:174 -#, fuzzy msgid "Connection timeout" msgstr "Connexion perdue" #: windows/views.py:192 msgid "Use TLS" -msgstr "" +msgstr "Use TLS" #: windows/views.py:203 msgid "Mark read only" -msgstr "" +msgstr "Marquer en lecture seule" #: windows/views.py:214 msgid "Use SSH tunnel" @@ -178,28 +174,27 @@ msgstr "Utiliser un tunnel SSH" #: windows/views.py:225 msgid "Compressed client/server protocol" -msgstr "" +msgstr "Protocole client/serveur compressé" #: windows/views.py:244 msgid "Filename" msgstr "Nom de fichier" -#: windows/views.py:249 windows/views.py:368 windows/views.py:3373 +#: windows/views.py:249 windows/views.py:368 windows/views.py:3809 msgid "Select a file" msgstr "Sélectionner un fichier" #: windows/views.py:249 windows/views.py:368 -#, fuzzy msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:266 windows/views.py:1335 windows/views.py:1587 -#: windows/views.py:2842 windows/views.py:3092 +#: windows/views.py:266 windows/views.py:1343 windows/views.py:1595 +#: windows/views.py:3113 windows/views.py:3528 msgid "Comments" msgstr "Commentaires" -#: windows/main/controller.py:766 windows/views.py:280 windows/views.py:740 +#: windows/main/controller.py:769 windows/views.py:280 windows/views.py:740 #: windows/views.py:894 msgid "Settings" msgstr "Paramètres" @@ -218,7 +213,7 @@ msgstr "Hôte SSH + port" #: windows/views.py:313 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:322 msgid "SSH username" @@ -238,26 +233,25 @@ msgstr "si la valeur est définie à 0, le premier port disponible sera utilisé #: windows/views.py:363 msgid "Identity file" -msgstr "" +msgstr "Identity file" #: windows/views.py:379 -#, fuzzy msgid "Remote host + port" -msgstr "Hôte + port" +msgstr "Hôte distant + port" #: windows/views.py:391 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:400 msgid "SSH extra args" -msgstr "" +msgstr "Arguments SSH supplémentaires" #: windows/views.py:415 msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:421 windows/views.py:1331 windows/views.py:2846 +#: windows/views.py:421 windows/views.py:1339 windows/views.py:3117 msgid "Created at" msgstr "Créé le" @@ -266,7 +260,6 @@ msgid "Successful connections" msgstr "Connexions réussies" #: windows/views.py:472 -#, fuzzy msgid "Last successful connection" msgstr "Connexions réussies" @@ -276,51 +269,46 @@ msgstr "Connexions échouées" #: windows/views.py:506 msgid "Last failure reason" -msgstr "" +msgstr "Dernière raison de l'échec" #: windows/views.py:523 -#, fuzzy msgid "Total connection attempts" msgstr "Dernière connexion" #: windows/views.py:540 -#, fuzzy msgid "Average connection time (ms)" msgstr "Échec de la reconnexion :" #: windows/views.py:557 -#, fuzzy msgid "Most recent connection duration" -msgstr "Ouvrir le gestionnaire de connexions" +msgstr "Durée de la connexion la plus récente" #: windows/views.py:576 msgid "Statistics" msgstr "Statistiques" -#: windows/views.py:594 windows/views.py:1853 +#: windows/views.py:594 windows/views.py:1821 msgid "Create" msgstr "Créer" #: windows/views.py:598 -#, fuzzy msgid "Create connection" -msgstr "Dernière connexion" +msgstr "Créer une connexion" #: windows/views.py:601 -#, fuzzy msgid "Create directory" msgstr "Nouveau répertoire" -#: windows/main/database/procedure.py:184 windows/views.py:630 -#: windows/views.py:854 windows/views.py:1483 windows/views.py:1899 -#: windows/views.py:2160 windows/views.py:2216 windows/views.py:2908 -#: windows/views.py:3250 windows/views.py:3387 +#: windows/views.py:630 windows/views.py:854 windows/views.py:1491 +#: windows/views.py:1867 windows/views.py:2159 windows/views.py:2383 +#: windows/views.py:2439 windows/views.py:3179 windows/views.py:3686 +#: windows/views.py:3823 msgid "Cancel" msgstr "Annuler" -#: windows/main/controller.py:323 windows/main/database/procedure.py:185 -#: windows/views.py:635 windows/views.py:2165 windows/views.py:2358 -#: windows/views.py:3255 windows/views.py:3393 +#: windows/main/controller.py:323 windows/views.py:635 windows/views.py:2164 +#: windows/views.py:2388 windows/views.py:2603 windows/views.py:3691 +#: windows/views.py:3829 msgid "Save" msgstr "Enregistrer" @@ -332,7 +320,7 @@ msgstr "Tester" msgid "Connect" msgstr "Connecter" -#: windows/main/database/procedure.py:162 windows/views.py:752 +#: windows/views.py:752 msgid "Language" msgstr "Langue" @@ -353,9 +341,8 @@ msgid "Locale" msgstr "Localisation" #: windows/views.py:790 -#, fuzzy msgid "Column content" -msgstr "Nouvelle connexion" +msgstr "Contenu de colonne" #: windows/views.py:800 msgid "Syntax" @@ -393,536 +380,558 @@ msgstr "Se déconnecter du serveur" msgid "tool" msgstr "outil" -#: windows/views.py:914 windows/views.py:2196 +#: windows/views.py:914 windows/views.py:2419 msgid "Refresh" msgstr "Actualiser" -#: windows/views.py:918 windows/views.py:920 windows/views.py:1877 -#: windows/views.py:2200 windows/views.py:2344 +#: windows/views.py:918 windows/views.py:920 windows/views.py:1845 +#: windows/views.py:2423 windows/views.py:2589 msgid "Add" msgstr "Ajouter" -#: windows/views.py:954 windows/views.py:958 windows/views.py:2322 +#: windows/views.py:925 +#, python-brace-format +msgid "{mode}" +msgstr "{mode}" + +#: windows/views.py:962 windows/views.py:966 windows/views.py:2556 +#: windows/views.py:2561 msgid "MyMenuItem" msgstr "MonÉlémentMenu" -#: windows/views.py:961 windows/views.py:1927 windows/views.py:3278 +#: windows/views.py:969 windows/views.py:1895 windows/views.py:2568 +#: windows/views.py:3714 msgid "MyMenu" msgstr "MonMenu" -#: windows/views.py:976 windows/views.py:1511 windows/views.py:1518 -#: windows/views.py:1525 +#: windows/views.py:984 windows/views.py:1519 windows/views.py:1526 +#: windows/views.py:1533 msgid "MyLabel" msgstr "MonÉtiquette" -#: windows/views.py:982 +#: windows/views.py:990 msgid "Databases" msgstr "Bases de données" -#: windows/views.py:983 windows/views.py:1330 windows/views.py:2847 +#: windows/views.py:991 windows/views.py:1338 windows/views.py:3118 msgid "Size" msgstr "Taille" -#: windows/views.py:984 +#: windows/views.py:992 msgid "Elements" msgstr "Éléments" -#: windows/views.py:985 +#: windows/views.py:993 msgid "Modified at" msgstr "Modifié le" -#: windows/views.py:986 windows/views.py:1345 +#: windows/views.py:994 windows/views.py:1353 msgid "Tables" msgstr "Tables" -#: windows/views.py:993 +#: windows/views.py:1001 msgid "System" msgstr "Système" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1039 -#: windows/views.py:1334 windows/views.py:2843 +#: windows/components/dataview.py:89 windows/views.py:1047 +#: windows/views.py:1342 windows/views.py:3114 msgid "Collation" msgstr "Classement" -#: windows/views.py:1068 +#: windows/views.py:1076 msgid "Encryption" -msgstr "" +msgstr "Chiffrement" -#: windows/views.py:1080 +#: windows/main/controller.py:1259 windows/main/controller.py:1281 +#: windows/main/controller.py:1285 windows/views.py:1088 msgid "Read Only" -msgstr "" +msgstr "Lecture seule" -#: windows/views.py:1097 -#, fuzzy +#: windows/views.py:1105 msgid "Tablespace" -msgstr "Tables" +msgstr "Tablespace" -#: windows/views.py:1118 -#, fuzzy +#: windows/views.py:1126 msgid "Connection limit" -msgstr "Connexion perdue" +msgstr "Limite de connexion" -#: windows/views.py:1161 -#, fuzzy +#: windows/views.py:1169 msgid "Profile" -msgstr "Fichier" +msgstr "Profil" -#: windows/views.py:1187 -#, fuzzy +#: windows/views.py:1195 msgid "Default tablespace" -msgstr "Supprimer la table" +msgstr "Tablespace par défaut" -#: windows/views.py:1208 -#, fuzzy +#: windows/views.py:1216 msgid "Temporary tablespace" -msgstr "Temporaire" +msgstr "Tablespace temporaire" -#: windows/views.py:1234 +#: windows/views.py:1242 msgid "Quota" -msgstr "" +msgstr "Quota" -#: windows/views.py:1253 +#: windows/views.py:1261 msgid "Unlimited quota" -msgstr "" +msgstr "Quota illimitée" -#: windows/views.py:1270 +#: windows/views.py:1278 msgid "Account status" -msgstr "" +msgstr "Statut du compte" -#: windows/views.py:1291 -#, fuzzy +#: windows/views.py:1299 msgid "Password expire" msgstr "Mot de passe" -#: windows/views.py:1315 -#, fuzzy +#: windows/views.py:1323 msgid "Add new table" -msgstr "Supprimer la table" +msgstr "Ajouter une nouvelle table" -#: windows/views.py:1317 -#, fuzzy +#: windows/views.py:1325 msgid "Clone table" -msgstr "Supprimer la table" +msgstr "Cloner la table" -#: windows/main/controller.py:1633 windows/views.py:1319 +#: windows/main/controller.py:1770 windows/views.py:1327 msgid "Delete table" msgstr "Supprimer la table" -#: windows/views.py:1329 +#: windows/views.py:1337 msgid "Rows" msgstr "Lignes" -#: windows/views.py:1332 windows/views.py:2845 +#: windows/views.py:1340 windows/views.py:3116 msgid "Updated at" msgstr "Mis à jour le" -#: windows/views.py:1350 +#: windows/views.py:1358 msgid "Add new view" -msgstr "" +msgstr "Ajouter une nouvelle vue" -#: windows/views.py:1352 windows/views.py:1376 -#, fuzzy +#: windows/views.py:1360 windows/views.py:1384 msgid "Clone view" msgstr "Cloner" -#: windows/views.py:1354 -#, fuzzy +#: windows/views.py:1362 msgid "Delete view" msgstr "Supprimer" -#: windows/views.py:1362 windows/views.py:1386 windows/views.py:1410 -#: windows/views.py:1434 windows/views.py:1458 -#, fuzzy +#: windows/views.py:1370 windows/views.py:1394 windows/views.py:1418 +#: windows/views.py:1442 windows/views.py:1466 msgid "Definition" -msgstr "Condition" +msgstr "Définition" -#: windows/views.py:1369 windows/views.py:2177 +#: windows/views.py:1377 msgid "Views" msgstr "Vues" -#: windows/views.py:1374 +#: windows/views.py:1382 msgid "Add new procedure" -msgstr "" +msgstr "Ajouter une nouvelle procédure" -#: windows/views.py:1376 +#: windows/views.py:1384 msgid "Clone procedure" -msgstr "" +msgstr "Cloner la procédure" -#: windows/views.py:1378 -#, fuzzy +#: windows/views.py:1386 msgid "Delete procedure" -msgstr "Supprimer un enregistrement" +msgstr "Supprimer la procédure" -#: windows/views.py:1393 +#: windows/views.py:1401 msgid "Procedures" -msgstr "" +msgstr "Procédures" -#: windows/views.py:1398 -#, fuzzy +#: windows/views.py:1406 msgid "Add new function" -msgstr "Nouvelle connexion" +msgstr "Ajouter une nouvelle fonction" -#: windows/views.py:1400 -#, fuzzy +#: windows/views.py:1408 msgid "Clone function" -msgstr "Nouvelle connexion" +msgstr "Cloner la fonction" -#: windows/views.py:1402 -#, fuzzy +#: windows/views.py:1410 msgid "Delete function" -msgstr "Supprimer un enregistrement" +msgstr "Supprimer la fonction" -#: windows/views.py:1417 -#, fuzzy +#: windows/views.py:1425 msgid "Functions" -msgstr "Connexion" +msgstr "Fonctions" -#: windows/views.py:1422 -#, fuzzy +#: windows/views.py:1430 msgid "Add new trigger" -msgstr "Déclencheurs" +msgstr "Ajouter un nouveau déclencheur" -#: windows/views.py:1424 -#, fuzzy +#: windows/views.py:1432 msgid "Clone trigger" -msgstr "Déclencheurs" +msgstr "Cloner le déclencheur" -#: windows/views.py:1426 -#, fuzzy +#: windows/views.py:1434 msgid "Delete trigger" -msgstr "Supprimer un enregistrement" +msgstr "Supprimer le déclencheur" -#: windows/views.py:1441 windows/views.py:2185 +#: windows/views.py:1449 msgid "Triggers" msgstr "Déclencheurs" -#: windows/views.py:1446 +#: windows/views.py:1454 msgid "Add new event" -msgstr "" +msgstr "Ajouter un nouvel événement" -#: windows/views.py:1448 -#, fuzzy +#: windows/views.py:1456 msgid "Clone event" msgstr "Cloner" -#: windows/views.py:1450 -#, fuzzy +#: windows/views.py:1458 msgid "Delete event" -msgstr "Supprimer la table" +msgstr "Supprimer l'événement" -#: windows/views.py:1465 -#, fuzzy +#: windows/views.py:1473 msgid "Events" -msgstr "Éléments" +msgstr "Événements" -#: windows/views.py:1493 windows/views.py:1904 windows/views.py:2214 -#: windows/views.py:2296 windows/views.py:2915 +#: windows/views.py:1501 windows/views.py:1872 windows/views.py:2437 +#: windows/views.py:2521 windows/views.py:3186 msgid "Apply" msgstr "Appliquer" -#: windows/main/database/procedure.py:122 windows/views.py:1505 -#: windows/views.py:1684 windows/views.py:2114 windows/views.py:3167 +#: windows/views.py:1513 windows/views.py:1692 windows/views.py:3603 msgid "Options" msgstr "Options" -#: windows/views.py:1536 +#: windows/views.py:1544 msgid "Diagram" msgstr "Diagramme" -#: windows/views.py:1547 +#: windows/views.py:1555 msgid "Database" msgstr "Base de données" -#: windows/views.py:1602 windows/views.py:3107 +#: windows/views.py:1610 windows/views.py:3543 msgid "Base" msgstr "Base" -#: windows/views.py:1616 windows/views.py:3121 +#: windows/views.py:1624 windows/views.py:3557 msgid "Auto Increment" msgstr "Auto incrément" -#: windows/views.py:1644 windows/views.py:3149 +#: windows/views.py:1652 windows/views.py:3585 msgid "Default Collation" msgstr "Classement par défaut" -#: windows/views.py:1654 +#: windows/views.py:1662 msgid "Convert data" -msgstr "" +msgstr "Convertir les données" -#: windows/views.py:1662 +#: windows/views.py:1670 msgid "Row format" -msgstr "" +msgstr "Format de ligne" -#: windows/views.py:1696 windows/views.py:1737 windows/views.py:1781 -#: windows/views.py:1879 windows/views.py:2204 +#: windows/views.py:1703 windows/views.py:1731 windows/views.py:1759 +#: windows/views.py:1847 windows/views.py:2259 windows/views.py:2427 +#: windows/views.py:3313 windows/views.py:3330 windows/views.py:3354 +#: windows/views.py:3381 msgid "Remove" msgstr "Supprimer" -#: windows/views.py:1703 windows/views.py:1744 windows/views.py:1788 +#: windows/views.py:1705 windows/views.py:1733 windows/views.py:1761 +#: windows/views.py:2261 windows/views.py:2529 windows/views.py:3320 +#: windows/views.py:3337 windows/views.py:3361 windows/views.py:3388 msgid "Clear" msgstr "Effacer" -#: windows/views.py:1718 windows/views.py:3181 +#: windows/views.py:1718 windows/views.py:3617 msgid "Indexes" msgstr "Index" -#: windows/views.py:1732 windows/views.py:1776 windows/views.py:2927 -#: windows/views.py:2969 windows/views.py:3210 +#: windows/views.py:1729 windows/views.py:1757 windows/views.py:2257 +#: windows/views.py:3198 windows/views.py:3240 windows/views.py:3308 +#: windows/views.py:3349 windows/views.py:3376 windows/views.py:3646 msgid "Insert" msgstr "Insérer" -#: windows/views.py:1762 +#: windows/views.py:1746 msgid "Foreign Keys" msgstr "Clés étrangères" -#: windows/views.py:1806 +#: windows/views.py:1774 msgid "Checks" msgstr "Contrôles" -#: windows/views.py:1873 windows/views.py:3202 +#: windows/views.py:1841 windows/views.py:3638 msgid "Columns:" msgstr "Colonnes :" -#: windows/views.py:1883 -#, fuzzy +#: windows/views.py:1851 msgid "Move Up" msgstr "Déplacer vers le haut\tCTRL+UP" -#: windows/views.py:1885 -#, fuzzy +#: windows/views.py:1853 msgid "Move Down" msgstr "Déplacer vers le bas\tCTRL+D" -#: windows/views.py:1917 windows/views.py:1924 windows/views.py:3268 -#: windows/views.py:3275 +#: windows/views.py:1885 windows/views.py:1892 windows/views.py:3704 +#: windows/views.py:3711 msgid "Add Index" msgstr "Ajouter un index" -#: windows/views.py:1921 windows/views.py:3272 +#: windows/views.py:1889 windows/views.py:3708 msgid "Add PrimaryKey" msgstr "Ajouter une clé primaire" -#: windows/views.py:1938 +#: windows/views.py:1906 msgid "Table" msgstr "Table" -#: windows/main/database/procedure.py:140 windows/views.py:1974 -#, fuzzy -msgid "Definer" -msgstr "Insérer" - -#: windows/views.py:1994 +#: windows/views.py:1948 windows/views.py:2219 msgid "Schema" -msgstr "" - -#: windows/views.py:2020 -msgid "SQL security" -msgstr "" - -#: windows/views.py:2027 -#, fuzzy -msgid "DEFINER" -msgstr "Insérer" +msgstr "Schema" -#: windows/views.py:2027 -#, fuzzy -msgid "INVOKER" -msgstr "Insérer" +#: windows/views.py:1976 windows/views.py:2247 +msgid "General" +msgstr "Général" -#: windows/views.py:2039 +#: windows/views.py:1981 msgid "Algorithm" -msgstr "" +msgstr "Algorithme" -#: windows/views.py:2041 -#, fuzzy +#: windows/views.py:1983 msgid "UNDEFINED" msgstr "Non signé" -#: windows/views.py:2044 +#: windows/views.py:1986 msgid "MERGE" -msgstr "" +msgstr "MERGE" -#: windows/views.py:2047 -#, fuzzy +#: windows/views.py:1989 msgid "TEMPTABLE" -msgstr "Table" +msgstr "TEMPTABLE" -#: windows/views.py:2057 +#: windows/views.py:1999 msgid "View constraint" -msgstr "" +msgstr "Contrainte de vue" -#: windows/views.py:2059 -#, fuzzy +#: windows/views.py:2001 msgid "None" -msgstr "Cloner" +msgstr "Aucun" -#: windows/views.py:2062 -#, fuzzy +#: windows/views.py:2004 msgid "LOCAL" -msgstr "Localisation" +msgstr "LOCAL" -#: windows/views.py:2065 -#, fuzzy +#: windows/views.py:2007 msgid "CASCADE" -msgstr "Annuler" +msgstr "CASCADE" -#: windows/views.py:2068 -#, fuzzy +#: windows/views.py:2010 msgid "CHECK ONLY" -msgstr "Vérifier" +msgstr "VÉRIFIER SEULEMENT" -#: windows/views.py:2071 +#: windows/views.py:2013 msgid "READ ONLY" -msgstr "" +msgstr "LECTURE SEULE" + +#: windows/views.py:2026 +msgid "Behavior" +msgstr "Comportement" -#: windows/views.py:2083 +#: windows/views.py:2033 windows/views.py:2281 +msgid "Definer" +msgstr "Définisseur" + +#: windows/views.py:2041 windows/views.py:2289 +msgid "*" +msgstr "*" + +#: windows/views.py:2053 windows/views.py:2301 +msgid "SQL security" +msgstr "Sécurité SQL" + +#: windows/views.py:2060 windows/views.py:2308 +msgid "DEFINER" +msgstr "DÉFINISSEUR" + +#: windows/views.py:2060 windows/views.py:2308 +msgid "INVOKER" +msgstr "APPELANT" + +#: windows/views.py:2074 msgid "Force" -msgstr "" +msgstr "Forcer" -#: windows/views.py:2095 +#: windows/views.py:2086 msgid "Security barrier" -msgstr "" +msgstr "Barrière de sécurité" + +#: windows/views.py:2099 windows/views.py:2323 +msgid "Security" +msgstr "Sécurité" -#: windows/views.py:2202 -#, fuzzy +#: windows/views.py:2176 +msgid "View" +msgstr "Vues" + +#: windows/views.py:2274 +msgid "Parameters" +msgstr "Paramètres" + +#: windows/views.py:2400 +msgid "Procedure" +msgstr "Procedure" + +#: windows/views.py:2408 +msgid "Trigger" +msgstr "Déclencheurs" + +#: windows/views.py:2425 msgid "Duplicate" -msgstr "Dupliquer un enregistrement" +msgstr "Dupliquer" -#: windows/views.py:2208 +#: windows/views.py:2431 msgid "Apply changes automatically" msgstr "Appliquer les modifications automatiquement" -#: windows/views.py:2210 windows/views.py:2211 +#: windows/views.py:2433 windows/views.py:2434 msgid "" -"If enabled, table edits are applied immediately without pressing Apply or" -" Cancel" +"If enabled, table edits are applied immediately without pressing Apply or " +"Cancel" msgstr "" -"Si activé, les modifications de la table sont appliquées immédiatement " -"sans appuyer sur Appliquer ou Annuler" +"Si activé, les modifications de la table sont appliquées immédiatement sans " +"appuyer sur Appliquer ou Annuler" -#: windows/views.py:2224 +#: windows/views.py:2447 #, python-brace-format -msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgid "" +"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" +"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -#: windows/views.py:2232 -#, fuzzy +#: windows/views.py:2455 msgid "First" -msgstr "Filtres" +msgstr "Premier" -#: windows/views.py:2250 +#: windows/views.py:2473 msgid "Last" -msgstr "" +msgstr "Dernier" -#: windows/views.py:2259 +#: windows/views.py:2482 msgid "Filters" msgstr "Filtres" -#: windows/views.py:2299 +#: windows/views.py:2524 +msgid "" +"Apply filters in data\n" +"CTRL+ENTER" +msgstr "" +"Appliquer les filtres dans les données\n" +"CTRL+ENTRÉE" + +#: windows/views.py:2525 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2319 +#: windows/views.py:2553 msgid "Insert row" msgstr "Insérer une ligne" -#: windows/views.py:2327 +#: windows/components/popup.py:31 windows/views.py:2565 +msgid "NULL" +msgstr "NULL" + +#: windows/views.py:2572 msgid "Data" msgstr "Données" -#: windows/main/controller.py:318 windows/views.py:2344 -#, fuzzy +#: windows/main/controller.py:318 windows/views.py:2589 msgid "New query" msgstr "Requête" -#: windows/views.py:2346 windows/views.py:2866 +#: windows/views.py:2591 windows/views.py:3137 msgid "Close" msgstr "Fermer" -#: windows/main/controller.py:319 windows/views.py:2346 -#, fuzzy +#: windows/main/controller.py:319 windows/views.py:2591 msgid "Close query" msgstr "Requête" -#: windows/views.py:2350 +#: windows/views.py:2595 msgid "Run" -msgstr "" +msgstr "Exécuter" -#: windows/main/controller.py:320 windows/views.py:2350 -#, fuzzy +#: windows/main/controller.py:320 windows/views.py:2595 msgid "Execute" -msgstr "Exécutable SSH" +msgstr "Exécuter" -#: windows/views.py:2352 +#: windows/views.py:2597 msgid "Run all" -msgstr "" +msgstr "Tout exécuter" -#: windows/views.py:2352 +#: windows/views.py:2597 msgid "Execute all statements" -msgstr "" +msgstr "Exécuter toutes les instructions" -#: windows/main/controller.py:322 windows/views.py:2354 +#: windows/main/controller.py:322 windows/views.py:2599 msgid "Stop" -msgstr "" +msgstr "Arrêter" -#: windows/views.py:2419 +#: windows/views.py:2664 msgid "a page" -msgstr "" +msgstr "une page" -#: windows/views.py:2469 +#: windows/views.py:2714 msgid "Query" msgstr "Requête" -#: windows/views.py:2820 -#, fuzzy +#: windows/views.py:3091 msgid "Character set" -msgstr "Créé le" +msgstr "Jeu de caractères" -#: windows/views.py:2850 windows/views.py:2869 +#: windows/views.py:3121 windows/views.py:3140 msgid "New" msgstr "Nouveau" -#: windows/views.py:2889 +#: windows/views.py:3160 msgid "Insert record" msgstr "Insérer un enregistrement" -#: windows/views.py:2894 +#: windows/views.py:3165 msgid "Duplicate record" msgstr "Dupliquer un enregistrement" -#: windows/views.py:2901 +#: windows/views.py:3172 msgid "Delete record" msgstr "Supprimer un enregistrement" -#: windows/views.py:2939 windows/views.py:3222 +#: windows/views.py:3210 windows/views.py:3658 msgid "Up" msgstr "Haut" -#: windows/views.py:2946 windows/views.py:3229 +#: windows/views.py:3217 windows/views.py:3665 msgid "Down" msgstr "Bas" -#: windows/views.py:2961 +#: windows/views.py:3232 msgid "Table:" msgstr "Table :" -#: windows/views.py:2974 +#: windows/views.py:3245 msgid "Clone" msgstr "Cloner" -#: windows/views.py:3358 +#: windows/views.py:3270 +msgid "MyButton" +msgstr "MonBouton" + +#: windows/views.py:3794 msgid "Save Starments" -msgstr "" +msgstr "Enregistrer les instructions" -#: windows/views.py:3366 -#, fuzzy +#: windows/views.py:3802 msgid "Location" -msgstr "Classement" +msgstr "Emplacement" -#: windows/views.py:3373 +#: windows/views.py:3809 msgid "*.sql" -msgstr "" +msgstr "*.sql" #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 @@ -1032,10 +1041,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" @@ -1046,16 +1051,16 @@ msgstr "Texte/Expression" #: windows/dialogs/connections/view.py:126 windows/main/query/renderer.py:192 msgid "Unknown error" -msgstr "" +msgstr "Erreur inconnue" #: windows/dialogs/connections/view.py:416 msgid "Connection established successfully" -msgstr "" +msgstr "Connexion établie avec succès" #: 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:431 msgid "Confirm save" @@ -1063,147 +1068,154 @@ msgstr "Confirmer la sauvegarde" #: windows/dialogs/connections/view.py:483 msgid "You have unsaved changes. Do you want to save them before continuing?" -msgstr "" +msgstr "Vous avez des modifications non enregistrées. Voulez-vous les enregistrer avant de continuer?" #: windows/dialogs/connections/view.py:485 msgid "Unsaved changes" -msgstr "" +msgstr "Modifications non enregistrées" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:773 msgid "" -"This connection cannot work without TLS. TLS has been enabled " -"automatically." +"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:787 -#, 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:788 +#: windows/dialogs/connections/view.py:799 msgid "Connection error" msgstr "Erreur de connexion" -#: windows/dialogs/connections/view.py:814 -#, 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:817 -#: windows/dialogs/connections/view.py:834 +#: windows/dialogs/connections/view.py:828 +#: windows/dialogs/connections/view.py:845 msgid "Confirm delete" msgstr "Confirmer la suppression" -#: windows/dialogs/connections/view.py:831 -#, 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:315 #, python-brace-format msgid "{text} ({shortcut})" -msgstr "" +msgstr "{text} ({shortcut})" #: windows/main/controller.py:321 -#, fuzzy msgid "Execute all" -msgstr "Exécutable SSH" +msgstr "Tout exécuter" #: windows/main/controller.py:440 windows/main/controller.py:448 -#, fuzzy msgid "Query (1)" msgstr "Requête" #: windows/main/controller.py:467 #, python-brace-format msgid "Query ({query_number})" -msgstr "" +msgstr "Query ({query_number})" #: windows/main/controller.py:516 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:517 msgid "Unsaved query" -msgstr "" +msgstr "Requête non enregistrée" #: windows/main/controller.py:562 -#, fuzzy msgid "Save query" -msgstr "Requête" +msgstr "Enregistrer la requête" #: windows/main/controller.py:565 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" -msgstr "" +msgstr "Fichiers SQL (*.sql)|*.sql|Tous les fichiers (*.*)|*.*" #: windows/main/controller.py:593 windows/main/controller.py:624 #: windows/main/controller.py:651 windows/main/database/list.py:119 -#: windows/main/database/procedure.py:294 -#: windows/main/database/procedure.py:320 windows/main/database/view.py:267 -#: windows/main/database/view.py:296 windows/main/query/controller.py:177 +#: windows/main/database/procedure.py:206 +#: windows/main/database/procedure.py:232 windows/main/database/view.py:268 +#: windows/main/database/view.py:297 windows/main/query/controller.py:177 msgid "Error" msgstr "Erreur" #: windows/main/controller.py:631 #, python-brace-format msgid "-- Saved query to {file_path}" -msgstr "" +msgstr "-- Requête enregistrée dans {file_path}" #: windows/main/controller.py:657 #, python-brace-format msgid "-- Autosaved query to {file_path}" -msgstr "" +msgstr "-- Requête auto-enregistrée dans {file_path}" -#: windows/main/controller.py:719 +#: windows/main/controller.py:722 msgid "days" msgstr "jours" -#: windows/main/controller.py:720 +#: windows/main/controller.py:723 msgid "hours" msgstr "heures" -#: windows/main/controller.py:721 +#: windows/main/controller.py:724 msgid "minutes" msgstr "minutes" -#: windows/main/controller.py:722 +#: windows/main/controller.py:725 msgid "seconds" msgstr "secondes" -#: windows/main/controller.py:730 +#: windows/main/controller.py:733 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Mémoire utilisée : {used} ({percentage:.2%})" -#: windows/main/controller.py:766 +#: windows/main/controller.py:769 msgid "Settings saved successfully" -msgstr "" +msgstr "Paramètres enregistrés avec succès" -#: windows/main/controller.py:990 +#: windows/main/controller.py:993 #, python-brace-format msgid "~{estimated} (Loading...)" -msgstr "" +msgstr "~{estimated} (Chargement...)" -#: windows/main/controller.py:992 +#: windows/main/controller.py:995 msgid "~ (Loading...)" -msgstr "" +msgstr "~ (Chargement...)" + +#: windows/main/controller.py:1230 +msgid "Write Mode (2:00)" +msgstr "Mode écriture (2:00)" + +#: windows/main/controller.py:1281 +msgid "Write Mode" +msgstr "Mode écriture" -#: windows/main/controller.py:1214 +#: windows/main/controller.py:1292 msgid "Version" msgstr "Version" -#: windows/main/controller.py:1216 +#: windows/main/controller.py:1294 msgid "Uptime" msgstr "Temps de fonctionnement" -#: windows/main/controller.py:1299 +#: windows/main/controller.py:1429 #, 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:1332 +#: windows/main/controller.py:1462 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1212,50 +1224,54 @@ 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:1337 windows/main/controller.py:1358 -#, fuzzy +#: windows/main/controller.py:1467 windows/main/controller.py:1488 msgid "Delete database" -msgstr "Supprimer la table" +msgstr "Supprimer la base de données" -#: windows/main/controller.py:1343 +#: windows/main/controller.py:1473 msgid "Dump is not implemented yet. No action has been performed." -msgstr "" +msgstr "La sauvegarde n'est pas encore implémentée. Aucune action n'a été effectuée." -#: windows/main/controller.py:1344 +#: windows/main/controller.py:1474 msgid "Dump not available" -msgstr "" +msgstr "Sauvegarde non disponible" -#: windows/main/controller.py:1357 +#: windows/main/controller.py:1487 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:1372 +#: windows/main/controller.py:1502 msgid "Database deleted successfully" -msgstr "" +msgstr "Base de données supprimée avec succès" -#: windows/main/controller.py:1373 windows/main/database/procedure.py:283 -#: windows/main/database/procedure.py:314 windows/main/database/view.py:255 -#: windows/main/database/view.py:290 +#: windows/main/controller.py:1503 windows/main/database/procedure.py:195 +#: windows/main/database/procedure.py:226 windows/main/database/view.py:256 +#: windows/main/database/view.py:291 msgid "Success" -msgstr "" +msgstr "Succès" -#: windows/main/controller.py:1604 +#: windows/main/controller.py:1741 #, 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:1630 -#, fuzzy, python-brace-format +#: windows/main/controller.py:1767 +#, python-brace-format msgid "Do you want delete the table {table_name}?" -msgstr "Voulez-vous supprimer les enregistrements ?" +msgstr "Voulez-vous supprimer la table {table_name}?" -#: windows/main/controller.py:1652 +#: windows/main/controller.py:1789 #, python-brace-format msgid "{table_name} (COPY)" -msgstr "" +msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1797 +#: windows/main/controller.py:1948 msgid "Do you want delete the records?" msgstr "Voulez-vous supprimer les enregistrements ?" @@ -1275,88 +1291,77 @@ msgstr "Connexion perdue" msgid "Reconnection failed:" msgstr "Échec de la reconnexion :" -#: windows/main/database/procedure.py:114 -msgid "Procedure" -msgstr "" - -#: windows/main/database/procedure.py:151 -#, fuzzy -msgid "Parameters" -msgstr "PeterSQL" - -#: windows/main/database/procedure.py:282 +#: windows/main/database/procedure.py:194 msgid "Procedure created successfully" -msgstr "" +msgstr "Procédure créée avec succès" -#: windows/main/database/procedure.py:282 +#: windows/main/database/procedure.py:194 msgid "Procedure updated successfully" -msgstr "" +msgstr "Procédure mise à jour avec succès" -#: windows/main/database/procedure.py:294 +#: windows/main/database/procedure.py:206 #, python-brace-format msgid "Error saving procedure: {}" -msgstr "" +msgstr "Erreur lors de l'enregistrement de la procédure: {}" -#: windows/main/database/procedure.py:305 -#, fuzzy, python-brace-format +#: windows/main/database/procedure.py:217 +#, python-brace-format msgid "Are you sure you want to delete procedure '{}'?" -msgstr "Voulez-vous supprimer les enregistrements ?" +msgstr "Êtes-vous sûr de vouloir supprimer la procédure '{}'?" -#: windows/main/database/procedure.py:306 windows/main/database/view.py:281 -#, fuzzy +#: windows/main/database/procedure.py:218 windows/main/database/view.py:282 msgid "Confirm Delete" msgstr "Confirmer la suppression" -#: windows/main/database/procedure.py:314 +#: windows/main/database/procedure.py:226 msgid "Procedure deleted successfully" -msgstr "" +msgstr "Procédure supprimée avec succès" -#: windows/main/database/procedure.py:320 +#: windows/main/database/procedure.py:232 #, python-brace-format msgid "Error deleting procedure: {}" -msgstr "" +msgstr "Erreur lors de la suppression de la procédure: {}" -#: windows/main/database/view.py:254 +#: windows/main/database/view.py:255 msgid "View created successfully" -msgstr "" +msgstr "Vue créée avec succès" -#: windows/main/database/view.py:254 +#: windows/main/database/view.py:255 msgid "View updated successfully" -msgstr "" +msgstr "Vue mise à jour avec succès" -#: windows/main/database/view.py:267 +#: 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:280 +#: windows/main/database/view.py:281 #, python-brace-format msgid "Are you sure you want to delete view '{}'?" -msgstr "" +msgstr "Êtes-vous sûr de vouloir supprimer la vue '{}'?" -#: windows/main/database/view.py:290 +#: windows/main/database/view.py:291 msgid "View deleted successfully" -msgstr "" +msgstr "Vue supprimée avec succès" -#: windows/main/database/view.py:296 +#: 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 #, python-brace-format msgid "{elapsed_ms:.0f} ms" -msgstr "" +msgstr "{elapsed_ms:.0f} ms" #: windows/main/query/controller.py:112 #, python-brace-format msgid "{elapsed_s:.2f} s" -msgstr "" +msgstr "{elapsed_s:.2f} s" #: windows/main/query/controller.py:115 -#, fuzzy msgid "none" -msgstr "Cloner" +msgstr "aucun" #: windows/main/query/controller.py:121 #, python-brace-format @@ -1367,94 +1372,96 @@ 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 msgid "Query execution cancelled" -msgstr "" +msgstr "Exécution de la requête annulée" #: windows/main/query/controller.py:176 -#, fuzzy msgid "No active database connection" -msgstr "Nouvelle connexion" +msgstr "Aucune connexion de base de données active" #: windows/main/query/history.py:55 -#, fuzzy msgid "(empty query)" -msgstr "Requête" +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 "Query {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" -#: windows/main/table/records.py:336 +#: windows/main/table/records.py:334 msgid "Error saving records" -msgstr "" +msgstr "Erreur lors de l'enregistrement des enregistrements" #~ msgid "Created at:" -#~ msgstr "" +#~ msgstr "Created at:" #~ msgid "Last connection:" -#~ msgstr "" +#~ msgstr "Last connection:" #~ msgid "Successful connections:" -#~ msgstr "" +#~ msgstr "Successful connections:" #~ msgid "Unsuccessful connections:" -#~ msgstr "" +#~ msgstr "Unsuccessful connections:" #~ msgid "Session Manager" -#~ msgstr "" +#~ msgstr "Session Manager" #~ msgid "Session name" -#~ msgstr "" +#~ msgstr "Session name" #~ msgid "Connection type" -#~ msgstr "" +#~ msgstr "Connection type" #~ msgid "Open" -#~ msgstr "" +#~ msgstr "Open" #~ msgid "Open session manager" -#~ msgstr "" +#~ msgstr "Open session manager" #~ msgid "Foreign Key" -#~ msgstr "" +#~ msgstr "Foreign Key" #~ msgid "New Session" #~ msgstr "Nouvelle session" @@ -1464,32 +1471,31 @@ msgstr "" #~ 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" +#~ "Table `%(database_name)s`.`%(table_name)s` : %(total_rows) lignes au total" #~ msgid "Next" #~ msgstr "Suivant" #~ msgid "{} rows affected" -#~ msgstr "" +#~ msgstr "{} rows affected" #~ msgid "Query {}" #~ msgstr "Requête" #~ msgid "Query {} (Error)" -#~ msgstr "" +#~ msgstr "Query {} (Error)" #~ msgid "Query {} ({} rows × {} cols)" -#~ msgstr "" +#~ msgstr "Query {} ({} rows × {} cols)" #~ msgid "{} rows" #~ msgstr "Lignes" #~ msgid "{:.1f} ms" -#~ msgstr "" +#~ msgstr "{:.1f} ms" #~ msgid "{} warnings" -#~ msgstr "" +#~ msgstr "{} warnings" #~ msgid "Edit Value" #~ msgstr "Modifier la valeur" @@ -1504,10 +1510,10 @@ msgstr "" #~ msgstr "Importer" #~ msgid "Read only" -#~ msgstr "" +#~ msgstr "Read only" #~ msgid "CASCADED" -#~ msgstr "" +#~ msgstr "CASCADED" #~ msgid "CHECK OPTION" #~ msgstr "connexion" @@ -1550,7 +1556,7 @@ msgstr "" #~ msgstr "Options" #~ msgid "RadioBtn" -#~ msgstr "" +#~ msgstr "RadioBtn" #~ msgid "Edit Column" #~ msgstr "Modifier la colonne" @@ -1563,4 +1569,3 @@ msgstr "" #~ msgid "Refrsh" #~ msgstr "Actualiser" - diff --git a/locale/it_IT/LC_MESSAGES/petersql.mo b/locale/it_IT/LC_MESSAGES/petersql.mo index bbf4054a5b2fae8b7d8f42efbb3a75dfa6fd8409..5dc158827da054ff643d3d361704654b702e89b3 100644 GIT binary patch literal 18518 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 XfOZe-KZbv7{{JxdPc!Oo-`xK{j;54J literal 7957 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 diff --git a/locale/it_IT/LC_MESSAGES/petersql.po b/locale/it_IT/LC_MESSAGES/petersql.po index 32db9ee..c6f0fda 100644 --- a/locale/it_IT/LC_MESSAGES/petersql.po +++ b/locale/it_IT/LC_MESSAGES/petersql.po @@ -2,20 +2,19 @@ # 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-05-02 16:08+0200\n" +"POT-Creation-Date: 2026-05-31 12:36+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" -"Language: it_IT\n" "Language-Team: it_IT \n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: it_IT\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.18.0\n" #: helpers/__init__.py:16 @@ -47,68 +46,68 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "Client OpenSSH non trovato." -#: structures/engines/context.py:535 +#: structures/engines/context.py:544 msgid "This connection is read-only." -msgstr "" +msgstr "Questa connessione è in sola lettura." -#: structures/engines/mariadb/context.py:616 -#: structures/engines/mysql/context.py:627 -#: structures/engines/postgresql/context.py:685 -#: structures/engines/sqlite/context.py:552 +#: structures/engines/mariadb/context.py:632 +#: structures/engines/mysql/context.py:643 +#: 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:644 -#: structures/engines/mysql/context.py:655 -#: structures/engines/postgresql/context.py:710 -#: structures/engines/sqlite/context.py:576 +#: structures/engines/mariadb/context.py:660 +#: structures/engines/mysql/context.py:671 +#: 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:662 -#: structures/engines/mysql/context.py:673 -#: structures/engines/postgresql/context.py:728 -#: structures/engines/sqlite/context.py:594 +#: structures/engines/mariadb/context.py:678 +#: structures/engines/mysql/context.py:689 +#: 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:702 -#: structures/engines/mysql/context.py:711 -#: structures/engines/postgresql/context.py:768 -#: structures/engines/sqlite/context.py:634 +#: structures/engines/mariadb/context.py:718 +#: structures/engines/mysql/context.py:727 +#: 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:735 -#: structures/engines/mysql/context.py:742 -#: structures/engines/postgresql/context.py:798 -#: structures/engines/sqlite/context.py:662 +#: structures/engines/mariadb/context.py:751 +#: structures/engines/mysql/context.py:758 +#: 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:788 +#: structures/engines/mariadb/context.py:804 #, python-brace-format msgid "Trigger{trigger_index:03}" -msgstr "" +msgstr "Trigger{trigger_index:03}" #: windows/dialogs/connections/view.py:417 -#: windows/dialogs/connections/view.py:764 windows/main/controller.py:1212 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1290 #: windows/views.py:33 msgid "Connection" msgstr "Connessione" #: windows/components/dataview.py:115 windows/components/dataview.py:240 #: windows/components/dataview.py:253 windows/components/dataview.py:268 -#: windows/main/database/procedure.py:129 windows/views.py:47 -#: windows/views.py:97 windows/views.py:1016 windows/views.py:1328 -#: windows/views.py:1361 windows/views.py:1385 windows/views.py:1409 -#: windows/views.py:1433 windows/views.py:1457 windows/views.py:1574 -#: windows/views.py:1954 windows/views.py:3079 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1024 +#: windows/views.py:1336 windows/views.py:1369 windows/views.py:1393 +#: windows/views.py:1417 windows/views.py:1441 windows/views.py:1465 +#: windows/views.py:1582 windows/views.py:1928 windows/views.py:2199 +#: windows/views.py:3293 windows/views.py:3515 msgid "Name" msgstr "Nome" @@ -116,11 +115,11 @@ msgstr "Nome" msgid "Last connection" msgstr "Ultima connessione" -#: windows/dialogs/connections/view.py:655 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:61 msgid "New directory" msgstr "Nuova directory" -#: windows/dialogs/connections/model.py:211 +#: windows/dialogs/connections/model.py:212 #: windows/dialogs/connections/view.py:615 windows/views.py:65 msgid "New connection" msgstr "Nuova connessione" @@ -131,17 +130,17 @@ msgstr "Rinomina" #: windows/views.py:76 msgid "Clone connection" -msgstr "Chiudi connessione" +msgstr "Clona connessione" -#: windows/main/database/procedure.py:183 windows/views.py:81 -#: windows/views.py:613 windows/views.py:1488 windows/views.py:1896 -#: windows/views.py:2155 windows/views.py:2932 windows/views.py:2981 -#: windows/views.py:3215 windows/views.py:3247 +#: windows/views.py:81 windows/views.py:613 windows/views.py:1496 +#: windows/views.py:1864 windows/views.py:2154 windows/views.py:2378 +#: windows/views.py:3203 windows/views.py:3252 windows/views.py:3651 +#: windows/views.py:3683 msgid "Delete" msgstr "Elimina" -#: windows/views.py:111 windows/views.py:1333 windows/views.py:1629 -#: windows/views.py:2844 windows/views.py:3134 +#: windows/views.py:111 windows/views.py:1341 windows/views.py:1637 +#: windows/views.py:3115 windows/views.py:3570 msgid "Engine" msgstr "Motore" @@ -153,7 +152,7 @@ msgstr "Host + porta" msgid "Username" msgstr "Nome utente" -#: windows/views.py:161 windows/views.py:1142 +#: windows/views.py:161 windows/views.py:1150 msgid "Password" msgstr "Password" @@ -163,11 +162,11 @@ msgstr "Timeout connessione" #: windows/views.py:192 msgid "Use TLS" -msgstr "" +msgstr "Usa TLS" #: windows/views.py:203 msgid "Mark read only" -msgstr "" +msgstr "Segna come sola lettura" #: windows/views.py:214 msgid "Use SSH tunnel" @@ -175,13 +174,13 @@ msgstr "Usa tunnel SSH" #: windows/views.py:225 msgid "Compressed client/server protocol" -msgstr "" +msgstr "Protocollo client/server compresso" #: windows/views.py:244 msgid "Filename" msgstr "Nome file" -#: windows/views.py:249 windows/views.py:368 windows/views.py:3373 +#: windows/views.py:249 windows/views.py:368 windows/views.py:3809 msgid "Select a file" msgstr "Seleziona un file" @@ -190,12 +189,12 @@ msgid "*.*" msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:266 windows/views.py:1335 windows/views.py:1587 -#: windows/views.py:2842 windows/views.py:3092 +#: windows/views.py:266 windows/views.py:1343 windows/views.py:1595 +#: windows/views.py:3113 windows/views.py:3528 msgid "Comments" msgstr "Commenti" -#: windows/main/controller.py:766 windows/views.py:280 windows/views.py:740 +#: windows/main/controller.py:769 windows/views.py:280 windows/views.py:740 #: windows/views.py:894 msgid "Settings" msgstr "Impostazioni" @@ -214,7 +213,7 @@ msgstr "Host SSH + porta" #: windows/views.py:313 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:322 msgid "SSH username" @@ -230,29 +229,30 @@ msgstr "Porta locale" #: windows/views.py:354 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" +msgstr "" +"se il valore è impostato a 0, verrà utilizzata la prima porta disponibile" #: windows/views.py:363 msgid "Identity file" -msgstr "" +msgstr "File identità" #: windows/views.py:379 msgid "Remote host + port" -msgstr "Remoto host + porta" +msgstr "Host remoto + porta" #: windows/views.py:391 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:400 msgid "SSH extra args" -msgstr "" +msgstr "Argomenti extra SSH" #: windows/views.py:415 msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:421 windows/views.py:1331 windows/views.py:2846 +#: windows/views.py:421 windows/views.py:1339 windows/views.py:3117 msgid "Created at" msgstr "Creato il" @@ -270,7 +270,7 @@ msgstr "Connessioni non riuscite" #: windows/views.py:506 msgid "Last failure reason" -msgstr "" +msgstr "Ultimo motivo di fallimento" #: windows/views.py:523 msgid "Total connection attempts" @@ -282,34 +282,34 @@ msgstr "Media in ms del tempo di connessione" #: windows/views.py:557 msgid "Most recent connection duration" -msgstr "" +msgstr "Durata connessione più recente" #: windows/views.py:576 msgid "Statistics" msgstr "Statistiche" -#: windows/views.py:594 windows/views.py:1853 +#: windows/views.py:594 windows/views.py:1821 msgid "Create" msgstr "Crea" #: windows/views.py:598 msgid "Create connection" -msgstr "Nuova connessione" +msgstr "Crea connessione" #: windows/views.py:601 msgid "Create directory" -msgstr "Nuova directory" +msgstr "Crea directory" -#: windows/main/database/procedure.py:184 windows/views.py:630 -#: windows/views.py:854 windows/views.py:1483 windows/views.py:1899 -#: windows/views.py:2160 windows/views.py:2216 windows/views.py:2908 -#: windows/views.py:3250 windows/views.py:3387 +#: windows/views.py:630 windows/views.py:854 windows/views.py:1491 +#: windows/views.py:1867 windows/views.py:2159 windows/views.py:2383 +#: windows/views.py:2439 windows/views.py:3179 windows/views.py:3686 +#: windows/views.py:3823 msgid "Cancel" msgstr "Annulla" -#: windows/main/controller.py:323 windows/main/database/procedure.py:185 -#: windows/views.py:635 windows/views.py:2165 windows/views.py:2358 -#: windows/views.py:3255 windows/views.py:3393 +#: windows/main/controller.py:323 windows/views.py:635 windows/views.py:2164 +#: windows/views.py:2388 windows/views.py:2603 windows/views.py:3691 +#: windows/views.py:3829 msgid "Save" msgstr "Salva" @@ -321,7 +321,7 @@ msgstr "Testa" msgid "Connect" msgstr "Connetti" -#: windows/main/database/procedure.py:162 windows/views.py:752 +#: windows/views.py:752 msgid "Language" msgstr "Lingua" @@ -342,9 +342,8 @@ msgid "Locale" msgstr "Localizzazione" #: windows/views.py:790 -#, fuzzy msgid "Column content" -msgstr "Chiudi connessione" +msgstr "Contenuto colonna" #: windows/views.py:800 msgid "Syntax" @@ -382,536 +381,558 @@ msgstr "Disconnetti dal server" msgid "tool" msgstr "strumento" -#: windows/views.py:914 windows/views.py:2196 +#: windows/views.py:914 windows/views.py:2419 msgid "Refresh" msgstr "Aggiorna" -#: windows/views.py:918 windows/views.py:920 windows/views.py:1877 -#: windows/views.py:2200 windows/views.py:2344 +#: windows/views.py:918 windows/views.py:920 windows/views.py:1845 +#: windows/views.py:2423 windows/views.py:2589 msgid "Add" msgstr "Aggiungi" -#: windows/views.py:954 windows/views.py:958 windows/views.py:2322 +#: windows/views.py:925 +#, python-brace-format +msgid "{mode}" +msgstr "{mode}" + +#: windows/views.py:962 windows/views.py:966 windows/views.py:2556 +#: windows/views.py:2561 msgid "MyMenuItem" msgstr "IlMioElementoMenu" -#: windows/views.py:961 windows/views.py:1927 windows/views.py:3278 +#: windows/views.py:969 windows/views.py:1895 windows/views.py:2568 +#: windows/views.py:3714 msgid "MyMenu" msgstr "IlMioMenu" -#: windows/views.py:976 windows/views.py:1511 windows/views.py:1518 -#: windows/views.py:1525 +#: windows/views.py:984 windows/views.py:1519 windows/views.py:1526 +#: windows/views.py:1533 msgid "MyLabel" msgstr "LaMiaEtichetta" -#: windows/views.py:982 +#: windows/views.py:990 msgid "Databases" msgstr "Database" -#: windows/views.py:983 windows/views.py:1330 windows/views.py:2847 +#: windows/views.py:991 windows/views.py:1338 windows/views.py:3118 msgid "Size" msgstr "Dimensione" -#: windows/views.py:984 +#: windows/views.py:992 msgid "Elements" msgstr "Elementi" -#: windows/views.py:985 +#: windows/views.py:993 msgid "Modified at" msgstr "Modificato il" -#: windows/views.py:986 windows/views.py:1345 +#: windows/views.py:994 windows/views.py:1353 msgid "Tables" msgstr "Tabelle" -#: windows/views.py:993 +#: windows/views.py:1001 msgid "System" msgstr "Sistema" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1039 -#: windows/views.py:1334 windows/views.py:2843 +#: windows/components/dataview.py:89 windows/views.py:1047 +#: windows/views.py:1342 windows/views.py:3114 msgid "Collation" msgstr "Ordinamento" -#: windows/views.py:1068 +#: windows/views.py:1076 msgid "Encryption" -msgstr "" +msgstr "Crittografia" -#: windows/views.py:1080 +#: windows/main/controller.py:1259 windows/main/controller.py:1281 +#: windows/main/controller.py:1285 windows/views.py:1088 msgid "Read Only" -msgstr "" +msgstr "Sola lettura" -#: windows/views.py:1097 -#, fuzzy +#: windows/views.py:1105 msgid "Tablespace" -msgstr "Tabelle" +msgstr "Tablespace" -#: windows/views.py:1118 -#, fuzzy +#: windows/views.py:1126 msgid "Connection limit" -msgstr "Connessione persa" +msgstr "Limite connessioni" -#: windows/views.py:1161 -#, fuzzy +#: windows/views.py:1169 msgid "Profile" -msgstr "File" +msgstr "Profilo" -#: windows/views.py:1187 -#, fuzzy +#: windows/views.py:1195 msgid "Default tablespace" -msgstr "Elimina tabella" +msgstr "Tablespace predefinito" -#: windows/views.py:1208 -#, fuzzy +#: windows/views.py:1216 msgid "Temporary tablespace" -msgstr "Temporaneo" +msgstr "Tablespace temporaneo" -#: windows/views.py:1234 +#: windows/views.py:1242 msgid "Quota" -msgstr "" +msgstr "Quota" -#: windows/views.py:1253 +#: windows/views.py:1261 msgid "Unlimited quota" -msgstr "" +msgstr "Quota illimitata" -#: windows/views.py:1270 +#: windows/views.py:1278 msgid "Account status" -msgstr "" +msgstr "Stato account" -#: windows/views.py:1291 -#, fuzzy +#: windows/views.py:1299 msgid "Password expire" -msgstr "Password" +msgstr "Scadenza password" -#: windows/views.py:1315 -#, fuzzy +#: windows/views.py:1323 msgid "Add new table" -msgstr "Elimina tabella" +msgstr "Aggiungi nuova tabella" -#: windows/views.py:1317 -#, fuzzy +#: windows/views.py:1325 msgid "Clone table" -msgstr "Elimina tabella" +msgstr "Clona tabella" -#: windows/main/controller.py:1633 windows/views.py:1319 +#: windows/main/controller.py:1770 windows/views.py:1327 msgid "Delete table" msgstr "Elimina tabella" -#: windows/views.py:1329 +#: windows/views.py:1337 msgid "Rows" msgstr "Righe" -#: windows/views.py:1332 windows/views.py:2845 +#: windows/views.py:1340 windows/views.py:3116 msgid "Updated at" msgstr "Aggiornato il" -#: windows/views.py:1350 +#: windows/views.py:1358 msgid "Add new view" -msgstr "" +msgstr "Aggiungi nuova vista" -#: windows/views.py:1352 windows/views.py:1376 -#, fuzzy +#: windows/views.py:1360 windows/views.py:1384 msgid "Clone view" msgstr "Clona" -#: windows/views.py:1354 -#, fuzzy +#: windows/views.py:1362 msgid "Delete view" msgstr "Elimina" -#: windows/views.py:1362 windows/views.py:1386 windows/views.py:1410 -#: windows/views.py:1434 windows/views.py:1458 -#, fuzzy +#: windows/views.py:1370 windows/views.py:1394 windows/views.py:1418 +#: windows/views.py:1442 windows/views.py:1466 msgid "Definition" -msgstr "Condizione" +msgstr "Definizione" -#: windows/views.py:1369 windows/views.py:2177 +#: windows/views.py:1377 msgid "Views" msgstr "Viste" -#: windows/views.py:1374 +#: windows/views.py:1382 msgid "Add new procedure" -msgstr "" +msgstr "Aggiungi nuova procedura" -#: windows/views.py:1376 +#: windows/views.py:1384 msgid "Clone procedure" -msgstr "" +msgstr "Clona procedura" -#: windows/views.py:1378 -#, fuzzy +#: windows/views.py:1386 msgid "Delete procedure" -msgstr "Elimina record" +msgstr "Elimina procedura" -#: windows/views.py:1393 +#: windows/views.py:1401 msgid "Procedures" -msgstr "" +msgstr "Procedure" -#: windows/views.py:1398 -#, fuzzy +#: windows/views.py:1406 msgid "Add new function" -msgstr "Nuova connessione" +msgstr "Aggiungi nuova funzione" -#: windows/views.py:1400 -#, fuzzy +#: windows/views.py:1408 msgid "Clone function" -msgstr "Chiudi connessione" +msgstr "Clona funzione" -#: windows/views.py:1402 -#, fuzzy +#: windows/views.py:1410 msgid "Delete function" -msgstr "Elimina record" +msgstr "Elimina funzione" -#: windows/views.py:1417 -#, fuzzy +#: windows/views.py:1425 msgid "Functions" -msgstr "Connessione" +msgstr "Funzioni" -#: windows/views.py:1422 -#, fuzzy +#: windows/views.py:1430 msgid "Add new trigger" -msgstr "Trigger" +msgstr "Aggiungi nuovo trigger" -#: windows/views.py:1424 -#, fuzzy +#: windows/views.py:1432 msgid "Clone trigger" -msgstr "Trigger" +msgstr "Clona trigger" -#: windows/views.py:1426 -#, fuzzy +#: windows/views.py:1434 msgid "Delete trigger" -msgstr "Elimina record" +msgstr "Elimina trigger" -#: windows/views.py:1441 windows/views.py:2185 +#: windows/views.py:1449 msgid "Triggers" msgstr "Trigger" -#: windows/views.py:1446 +#: windows/views.py:1454 msgid "Add new event" -msgstr "" +msgstr "Aggiungi nuovo evento" -#: windows/views.py:1448 -#, fuzzy +#: windows/views.py:1456 msgid "Clone event" msgstr "Clona" -#: windows/views.py:1450 -#, fuzzy +#: windows/views.py:1458 msgid "Delete event" -msgstr "Elimina tabella" +msgstr "Elimina evento" -#: windows/views.py:1465 -#, fuzzy +#: windows/views.py:1473 msgid "Events" -msgstr "Elementi" +msgstr "Eventi" -#: windows/views.py:1493 windows/views.py:1904 windows/views.py:2214 -#: windows/views.py:2296 windows/views.py:2915 +#: windows/views.py:1501 windows/views.py:1872 windows/views.py:2437 +#: windows/views.py:2521 windows/views.py:3186 msgid "Apply" msgstr "Applica" -#: windows/main/database/procedure.py:122 windows/views.py:1505 -#: windows/views.py:1684 windows/views.py:2114 windows/views.py:3167 +#: windows/views.py:1513 windows/views.py:1692 windows/views.py:3603 msgid "Options" msgstr "Opzioni" -#: windows/views.py:1536 +#: windows/views.py:1544 msgid "Diagram" msgstr "Diagramma" -#: windows/views.py:1547 +#: windows/views.py:1555 msgid "Database" msgstr "Database" -#: windows/views.py:1602 windows/views.py:3107 +#: windows/views.py:1610 windows/views.py:3543 msgid "Base" msgstr "Base" -#: windows/views.py:1616 windows/views.py:3121 +#: windows/views.py:1624 windows/views.py:3557 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1644 windows/views.py:3149 +#: windows/views.py:1652 windows/views.py:3585 msgid "Default Collation" msgstr "Ordinamento predefinito" -#: windows/views.py:1654 +#: windows/views.py:1662 msgid "Convert data" -msgstr "" +msgstr "Converti dati" -#: windows/views.py:1662 +#: windows/views.py:1670 msgid "Row format" -msgstr "" +msgstr "Formato riga" -#: windows/views.py:1696 windows/views.py:1737 windows/views.py:1781 -#: windows/views.py:1879 windows/views.py:2204 +#: windows/views.py:1703 windows/views.py:1731 windows/views.py:1759 +#: windows/views.py:1847 windows/views.py:2259 windows/views.py:2427 +#: windows/views.py:3313 windows/views.py:3330 windows/views.py:3354 +#: windows/views.py:3381 msgid "Remove" msgstr "Rimuovi" -#: windows/views.py:1703 windows/views.py:1744 windows/views.py:1788 +#: windows/views.py:1705 windows/views.py:1733 windows/views.py:1761 +#: windows/views.py:2261 windows/views.py:2529 windows/views.py:3320 +#: windows/views.py:3337 windows/views.py:3361 windows/views.py:3388 msgid "Clear" msgstr "Pulisci" -#: windows/views.py:1718 windows/views.py:3181 +#: windows/views.py:1718 windows/views.py:3617 msgid "Indexes" msgstr "Indici" -#: windows/views.py:1732 windows/views.py:1776 windows/views.py:2927 -#: windows/views.py:2969 windows/views.py:3210 +#: windows/views.py:1729 windows/views.py:1757 windows/views.py:2257 +#: windows/views.py:3198 windows/views.py:3240 windows/views.py:3308 +#: windows/views.py:3349 windows/views.py:3376 windows/views.py:3646 msgid "Insert" msgstr "Inserisci" -#: windows/views.py:1762 +#: windows/views.py:1746 msgid "Foreign Keys" msgstr "Chiavi esterne" -#: windows/views.py:1806 +#: windows/views.py:1774 msgid "Checks" msgstr "Vincoli" -#: windows/views.py:1873 windows/views.py:3202 +#: windows/views.py:1841 windows/views.py:3638 msgid "Columns:" msgstr "Colonne:" -#: windows/views.py:1883 -#, fuzzy +#: windows/views.py:1851 msgid "Move Up" msgstr "Sposta su\tCTRL+UP" -#: windows/views.py:1885 -#, fuzzy +#: windows/views.py:1853 msgid "Move Down" msgstr "Sposta giù\tCTRL+D" -#: windows/views.py:1917 windows/views.py:1924 windows/views.py:3268 -#: windows/views.py:3275 +#: windows/views.py:1885 windows/views.py:1892 windows/views.py:3704 +#: windows/views.py:3711 msgid "Add Index" msgstr "Aggiungi indice" -#: windows/views.py:1921 windows/views.py:3272 +#: windows/views.py:1889 windows/views.py:3708 msgid "Add PrimaryKey" msgstr "Aggiungi chiave primaria" -#: windows/views.py:1938 +#: windows/views.py:1906 msgid "Table" msgstr "Tabella" -#: windows/main/database/procedure.py:140 windows/views.py:1974 -#, fuzzy -msgid "Definer" -msgstr "Inserisci" - -#: windows/views.py:1994 +#: windows/views.py:1948 windows/views.py:2219 msgid "Schema" -msgstr "" +msgstr "Schema" -#: windows/views.py:2020 -msgid "SQL security" -msgstr "" +#: windows/views.py:1976 windows/views.py:2247 +msgid "General" +msgstr "Generale" -#: windows/views.py:2027 -#, fuzzy -msgid "DEFINER" -msgstr "Inserisci" - -#: windows/views.py:2027 -#, fuzzy -msgid "INVOKER" -msgstr "Inserisci" - -#: windows/views.py:2039 +#: windows/views.py:1981 msgid "Algorithm" -msgstr "" +msgstr "Algoritmo" -#: windows/views.py:2041 -#, fuzzy +#: windows/views.py:1983 msgid "UNDEFINED" msgstr "Senza segno" -#: windows/views.py:2044 +#: windows/views.py:1986 msgid "MERGE" -msgstr "" +msgstr "MERGE" -#: windows/views.py:2047 -#, fuzzy +#: windows/views.py:1989 msgid "TEMPTABLE" -msgstr "Tabella" +msgstr "TEMPTABLE" -#: windows/views.py:2057 +#: windows/views.py:1999 msgid "View constraint" -msgstr "" +msgstr "Vincolo vista" -#: windows/views.py:2059 -#, fuzzy +#: windows/views.py:2001 msgid "None" -msgstr "Clona" +msgstr "Nessuno" -#: windows/views.py:2062 -#, fuzzy +#: windows/views.py:2004 msgid "LOCAL" -msgstr "Localizzazione" +msgstr "LOCALE" -#: windows/views.py:2065 -#, fuzzy +#: windows/views.py:2007 msgid "CASCADE" -msgstr "Annulla" +msgstr "A CASCATA" -#: windows/views.py:2068 -#, fuzzy +#: windows/views.py:2010 msgid "CHECK ONLY" -msgstr "Verifica" +msgstr "SOLO VERIFICA" -#: windows/views.py:2071 +#: windows/views.py:2013 msgid "READ ONLY" -msgstr "" +msgstr "SOLA LETTURA" + +#: windows/views.py:2026 +msgid "Behavior" +msgstr "Comportamento" -#: windows/views.py:2083 +#: windows/views.py:2033 windows/views.py:2281 +msgid "Definer" +msgstr "Definitore" + +#: windows/views.py:2041 windows/views.py:2289 +msgid "*" +msgstr "*" + +#: windows/views.py:2053 windows/views.py:2301 +msgid "SQL security" +msgstr "Sicurezza SQL" + +#: windows/views.py:2060 windows/views.py:2308 +msgid "DEFINER" +msgstr "DEFINITORE" + +#: windows/views.py:2060 windows/views.py:2308 +msgid "INVOKER" +msgstr "INVOCATORE" + +#: windows/views.py:2074 msgid "Force" -msgstr "" +msgstr "Forza" -#: windows/views.py:2095 +#: windows/views.py:2086 msgid "Security barrier" -msgstr "" +msgstr "Barriera di sicurezza" + +#: windows/views.py:2099 windows/views.py:2323 +msgid "Security" +msgstr "Sicurezza" + +#: windows/views.py:2176 +msgid "View" +msgstr "Viste" -#: windows/views.py:2202 -#, fuzzy +#: windows/views.py:2274 +msgid "Parameters" +msgstr "Parametri" + +#: windows/views.py:2400 +msgid "Procedure" +msgstr "Procedure" + +#: windows/views.py:2408 +msgid "Trigger" +msgstr "Trigger" + +#: windows/views.py:2425 msgid "Duplicate" -msgstr "Duplica record" +msgstr "Duplica" -#: windows/views.py:2208 +#: windows/views.py:2431 msgid "Apply changes automatically" msgstr "Applica modifiche automaticamente" -#: windows/views.py:2210 windows/views.py:2211 +#: windows/views.py:2433 windows/views.py:2434 msgid "" -"If enabled, table edits are applied immediately without pressing Apply or" -" Cancel" +"If enabled, table edits are applied immediately without pressing Apply or " +"Cancel" msgstr "" "Se abilitato, le modifiche alla tabella vengono applicate immediatamente " "senza premere Applica o Annulla" -#: windows/views.py:2224 +#: windows/views.py:2447 #, python-brace-format -msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgid "" +"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" +"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -#: windows/views.py:2232 -#, fuzzy +#: windows/views.py:2455 msgid "First" -msgstr "Filtri" +msgstr "Primo" -#: windows/views.py:2250 +#: windows/views.py:2473 msgid "Last" -msgstr "" +msgstr "Ultimo" -#: windows/views.py:2259 +#: windows/views.py:2482 msgid "Filters" msgstr "Filtri" -#: windows/views.py:2299 +#: windows/views.py:2524 +msgid "" +"Apply filters in data\n" +"CTRL+ENTER" +msgstr "" +"Applica filtri ai dati\n" +"CTRL+ENTER" + +#: windows/views.py:2525 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2319 +#: windows/views.py:2553 msgid "Insert row" msgstr "Inserisci riga" -#: windows/views.py:2327 +#: windows/components/popup.py:31 windows/views.py:2565 +msgid "NULL" +msgstr "NULL" + +#: windows/views.py:2572 msgid "Data" msgstr "Dati" -#: windows/main/controller.py:318 windows/views.py:2344 -#, fuzzy +#: windows/main/controller.py:318 windows/views.py:2589 msgid "New query" msgstr "Query" -#: windows/views.py:2346 windows/views.py:2866 +#: windows/views.py:2591 windows/views.py:3137 msgid "Close" msgstr "Chiudi" -#: windows/main/controller.py:319 windows/views.py:2346 -#, fuzzy +#: windows/main/controller.py:319 windows/views.py:2591 msgid "Close query" msgstr "Query" -#: windows/views.py:2350 +#: windows/views.py:2595 msgid "Run" -msgstr "" +msgstr "Esegui" -#: windows/main/controller.py:320 windows/views.py:2350 -#, fuzzy +#: windows/main/controller.py:320 windows/views.py:2595 msgid "Execute" -msgstr "Eseguibile SSH" +msgstr "Esegui" -#: windows/views.py:2352 +#: windows/views.py:2597 msgid "Run all" -msgstr "" +msgstr "Esegui tutto" -#: windows/views.py:2352 +#: windows/views.py:2597 msgid "Execute all statements" -msgstr "" +msgstr "Esegui tutte le istruzioni" -#: windows/main/controller.py:322 windows/views.py:2354 +#: windows/main/controller.py:322 windows/views.py:2599 msgid "Stop" -msgstr "" +msgstr "Ferma" -#: windows/views.py:2419 +#: windows/views.py:2664 msgid "a page" -msgstr "" +msgstr "una pagina" -#: windows/views.py:2469 +#: windows/views.py:2714 msgid "Query" msgstr "Query" -#: windows/views.py:2820 -#, fuzzy +#: windows/views.py:3091 msgid "Character set" -msgstr "Creato il" +msgstr "Set di caratteri" -#: windows/views.py:2850 windows/views.py:2869 +#: windows/views.py:3121 windows/views.py:3140 msgid "New" msgstr "Nuovo" -#: windows/views.py:2889 +#: windows/views.py:3160 msgid "Insert record" msgstr "Inserisci record" -#: windows/views.py:2894 +#: windows/views.py:3165 msgid "Duplicate record" msgstr "Duplica record" -#: windows/views.py:2901 +#: windows/views.py:3172 msgid "Delete record" msgstr "Elimina record" -#: windows/views.py:2939 windows/views.py:3222 +#: windows/views.py:3210 windows/views.py:3658 msgid "Up" msgstr "Su" -#: windows/views.py:2946 windows/views.py:3229 +#: windows/views.py:3217 windows/views.py:3665 msgid "Down" msgstr "Giù" -#: windows/views.py:2961 +#: windows/views.py:3232 msgid "Table:" msgstr "Tabella:" -#: windows/views.py:2974 +#: windows/views.py:3245 msgid "Clone" msgstr "Clona" -#: windows/views.py:3358 +#: windows/views.py:3270 +msgid "MyButton" +msgstr "IlMioPulsante" + +#: windows/views.py:3794 msgid "Save Starments" -msgstr "" +msgstr "Salva istruzioni" -#: windows/views.py:3366 -#, fuzzy +#: windows/views.py:3802 msgid "Location" -msgstr "Ordinamento" +msgstr "Posizione" -#: windows/views.py:3373 +#: windows/views.py:3809 msgid "*.sql" -msgstr "" +msgstr "*.sql" #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 @@ -1021,10 +1042,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" @@ -1035,16 +1052,16 @@ msgstr "Testo/Espressione" #: windows/dialogs/connections/view.py:126 windows/main/query/renderer.py:192 msgid "Unknown error" -msgstr "" +msgstr "Errore sconosciuto" #: windows/dialogs/connections/view.py:416 msgid "Connection established successfully" -msgstr "" +msgstr "Connessione stabilita con successo" #: 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:431 msgid "Confirm save" @@ -1052,147 +1069,154 @@ msgstr "Conferma salvataggio" #: 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:485 msgid "Unsaved changes" -msgstr "" +msgstr "Modifiche non salvate" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:773 msgid "" -"This connection cannot work without TLS. TLS has been enabled " -"automatically." +"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:787 -#, 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:788 +#: windows/dialogs/connections/view.py:799 msgid "Connection error" msgstr "Errore di connessione" -#: windows/dialogs/connections/view.py:814 -#, 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:817 -#: windows/dialogs/connections/view.py:834 +#: windows/dialogs/connections/view.py:828 +#: windows/dialogs/connections/view.py:845 msgid "Confirm delete" msgstr "Conferma eliminazione" -#: windows/dialogs/connections/view.py:831 -#, 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:315 #, python-brace-format msgid "{text} ({shortcut})" -msgstr "" +msgstr "{text} ({shortcut})" #: windows/main/controller.py:321 -#, fuzzy msgid "Execute all" -msgstr "Eseguibile SSH" +msgstr "Esegui tutto" #: windows/main/controller.py:440 windows/main/controller.py:448 -#, fuzzy msgid "Query (1)" msgstr "Query" #: windows/main/controller.py:467 #, python-brace-format msgid "Query ({query_number})" -msgstr "" +msgstr "Query ({query_number})" #: windows/main/controller.py:516 msgid "You have unsaved changes. Save before closing?" -msgstr "" +msgstr "Hai modifiche non salvate. Salvare prima di chiudere?" #: windows/main/controller.py:517 msgid "Unsaved query" -msgstr "" +msgstr "Query non salvata" #: windows/main/controller.py:562 -#, fuzzy msgid "Save query" -msgstr "Query" +msgstr "Salva query" #: windows/main/controller.py:565 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" -msgstr "" +msgstr "File SQL (*.sql)|*.sql|Tutti i file (*.*)|*.*" #: windows/main/controller.py:593 windows/main/controller.py:624 #: windows/main/controller.py:651 windows/main/database/list.py:119 -#: windows/main/database/procedure.py:294 -#: windows/main/database/procedure.py:320 windows/main/database/view.py:267 -#: windows/main/database/view.py:296 windows/main/query/controller.py:177 +#: windows/main/database/procedure.py:206 +#: windows/main/database/procedure.py:232 windows/main/database/view.py:268 +#: windows/main/database/view.py:297 windows/main/query/controller.py:177 msgid "Error" msgstr "Errore" #: windows/main/controller.py:631 #, python-brace-format msgid "-- Saved query to {file_path}" -msgstr "" +msgstr "-- Query salvata in {file_path}" #: windows/main/controller.py:657 #, python-brace-format msgid "-- Autosaved query to {file_path}" -msgstr "" +msgstr "-- Query salvata automaticamente in {file_path}" -#: windows/main/controller.py:719 +#: windows/main/controller.py:722 msgid "days" msgstr "giorni" -#: windows/main/controller.py:720 +#: windows/main/controller.py:723 msgid "hours" msgstr "ore" -#: windows/main/controller.py:721 +#: windows/main/controller.py:724 msgid "minutes" msgstr "minuti" -#: windows/main/controller.py:722 +#: windows/main/controller.py:725 msgid "seconds" msgstr "secondi" -#: windows/main/controller.py:730 +#: windows/main/controller.py:733 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizzata: {used} ({percentage:.2%})" -#: windows/main/controller.py:766 +#: windows/main/controller.py:769 msgid "Settings saved successfully" -msgstr "" +msgstr "Impostazioni salvate con successo" -#: windows/main/controller.py:990 +#: windows/main/controller.py:993 #, python-brace-format msgid "~{estimated} (Loading...)" -msgstr "" +msgstr "~{estimated} (Caricamento...)" -#: windows/main/controller.py:992 +#: windows/main/controller.py:995 msgid "~ (Loading...)" -msgstr "" +msgstr "~ (Caricamento...)" + +#: windows/main/controller.py:1230 +msgid "Write Mode (2:00)" +msgstr "Modalità scrittura (2:00)" -#: windows/main/controller.py:1214 +#: windows/main/controller.py:1281 +msgid "Write Mode" +msgstr "Modalità scrittura" + +#: windows/main/controller.py:1292 msgid "Version" msgstr "Versione" -#: windows/main/controller.py:1216 +#: windows/main/controller.py:1294 msgid "Uptime" msgstr "Tempo di attività" -#: windows/main/controller.py:1299 +#: windows/main/controller.py:1429 #, 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:1332 +#: windows/main/controller.py:1462 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1201,50 +1225,54 @@ 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:1337 windows/main/controller.py:1358 -#, fuzzy +#: windows/main/controller.py:1467 windows/main/controller.py:1488 msgid "Delete database" -msgstr "Elimina tabella" +msgstr "Elimina database" -#: windows/main/controller.py:1343 +#: windows/main/controller.py:1473 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:1344 +#: windows/main/controller.py:1474 msgid "Dump not available" -msgstr "" +msgstr "Dump non disponibile" -#: windows/main/controller.py:1357 +#: windows/main/controller.py:1487 msgid "Database deletion is not supported by this engine." -msgstr "" +msgstr "L'eliminazione del database non è supportata da questo motore." -#: windows/main/controller.py:1372 +#: windows/main/controller.py:1502 msgid "Database deleted successfully" -msgstr "" +msgstr "Database eliminato con successo" -#: windows/main/controller.py:1373 windows/main/database/procedure.py:283 -#: windows/main/database/procedure.py:314 windows/main/database/view.py:255 -#: windows/main/database/view.py:290 +#: windows/main/controller.py:1503 windows/main/database/procedure.py:195 +#: windows/main/database/procedure.py:226 windows/main/database/view.py:256 +#: windows/main/database/view.py:291 msgid "Success" -msgstr "" +msgstr "Successo" -#: windows/main/controller.py:1604 +#: windows/main/controller.py:1741 #, 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:1630 -#, fuzzy, python-brace-format +#: windows/main/controller.py:1767 +#, python-brace-format msgid "Do you want delete the table {table_name}?" -msgstr "Vuoi eliminare i record?" +msgstr "Vuoi eliminare la tabella {table_name}?" -#: windows/main/controller.py:1652 +#: windows/main/controller.py:1789 #, python-brace-format msgid "{table_name} (COPY)" -msgstr "" +msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1797 +#: windows/main/controller.py:1948 msgid "Do you want delete the records?" msgstr "Vuoi eliminare i record?" @@ -1264,88 +1292,77 @@ msgstr "Connessione persa" msgid "Reconnection failed:" msgstr "Riconnessione fallita:" -#: windows/main/database/procedure.py:114 -msgid "Procedure" -msgstr "" - -#: windows/main/database/procedure.py:151 -#, fuzzy -msgid "Parameters" -msgstr "PeterSQL" - -#: windows/main/database/procedure.py:282 +#: windows/main/database/procedure.py:194 msgid "Procedure created successfully" -msgstr "" +msgstr "Procedura creata con successo" -#: windows/main/database/procedure.py:282 +#: windows/main/database/procedure.py:194 msgid "Procedure updated successfully" -msgstr "" +msgstr "Procedura aggiornata con successo" -#: windows/main/database/procedure.py:294 +#: windows/main/database/procedure.py:206 #, python-brace-format msgid "Error saving procedure: {}" -msgstr "" +msgstr "Errore nel salvataggio della procedura: {}" -#: windows/main/database/procedure.py:305 -#, fuzzy, python-brace-format +#: windows/main/database/procedure.py:217 +#, python-brace-format msgid "Are you sure you want to delete procedure '{}'?" -msgstr "Vuoi eliminare i record?" +msgstr "Sei sicuro di voler eliminare la procedura '{}'?" -#: windows/main/database/procedure.py:306 windows/main/database/view.py:281 -#, fuzzy +#: windows/main/database/procedure.py:218 windows/main/database/view.py:282 msgid "Confirm Delete" msgstr "Conferma eliminazione" -#: windows/main/database/procedure.py:314 +#: windows/main/database/procedure.py:226 msgid "Procedure deleted successfully" -msgstr "" +msgstr "Procedura eliminata con successo" -#: windows/main/database/procedure.py:320 +#: windows/main/database/procedure.py:232 #, python-brace-format msgid "Error deleting procedure: {}" -msgstr "" +msgstr "Errore nell'eliminazione della procedura: {}" -#: windows/main/database/view.py:254 +#: windows/main/database/view.py:255 msgid "View created successfully" -msgstr "" +msgstr "Vista creata con successo" -#: windows/main/database/view.py:254 +#: windows/main/database/view.py:255 msgid "View updated successfully" -msgstr "" +msgstr "Vista aggiornata con successo" -#: windows/main/database/view.py:267 +#: 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:280 +#: windows/main/database/view.py:281 #, python-brace-format msgid "Are you sure you want to delete view '{}'?" -msgstr "" +msgstr "Sei sicuro di voler eliminare la vista '{}'?" -#: windows/main/database/view.py:290 +#: windows/main/database/view.py:291 msgid "View deleted successfully" -msgstr "" +msgstr "Vista eliminata con successo" -#: windows/main/database/view.py:296 +#: 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 #, python-brace-format msgid "{elapsed_ms:.0f} ms" -msgstr "" +msgstr "{elapsed_ms:.0f} ms" #: windows/main/query/controller.py:112 #, python-brace-format msgid "{elapsed_s:.2f} s" -msgstr "" +msgstr "{elapsed_s:.2f} s" #: windows/main/query/controller.py:115 -#, fuzzy msgid "none" -msgstr "Clona" +msgstr "nessuno" #: windows/main/query/controller.py:121 #, python-brace-format @@ -1356,88 +1373,90 @@ 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 msgid "Query execution cancelled" -msgstr "" +msgstr "Esecuzione query annullata" #: windows/main/query/controller.py:176 -#, fuzzy msgid "No active database connection" -msgstr "Nuova connessione" +msgstr "Nessuna connessione database attiva" #: windows/main/query/history.py:55 -#, fuzzy msgid "(empty query)" -msgstr "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" -#: windows/main/table/records.py:336 +#: windows/main/table/records.py:334 msgid "Error saving records" -msgstr "" +msgstr "Errore nel salvataggio dei record" #~ msgid "Created at:" -#~ msgstr "" +#~ msgstr "Created at:" #~ msgid "Last connection:" -#~ msgstr "" +#~ msgstr "Last connection:" #~ msgid "Successful connections:" -#~ msgstr "" +#~ msgstr "Successful connections:" #~ msgid "Unsuccessful connections:" -#~ msgstr "" +#~ msgstr "Unsuccessful connections:" #~ msgid "Session name" #~ msgstr "Nome sessione" #~ msgid "Open" -#~ msgstr "" +#~ msgstr "Open" #~ msgid "Open session manager" -#~ msgstr "" +#~ msgstr "Open session manager" #~ msgid "Foreign Key" -#~ msgstr "" +#~ msgstr "Foreign Key" #~ msgid "New Session" #~ msgstr "Nuova sessione" @@ -1447,32 +1466,31 @@ msgstr "" #~ msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" #~ msgstr "" -#~ "Tabella `%(database_name)s`.`%(table_name)s`: " -#~ "%(total_rows) righe totali" +#~ "Tabella `%(database_name)s`.`%(table_name)s`: %(total_rows) righe totali" #~ msgid "Next" #~ msgstr "Avanti" #~ msgid "{} rows affected" -#~ msgstr "" +#~ msgstr "{} rows affected" #~ msgid "Query {}" #~ msgstr "Query" #~ msgid "Query {} (Error)" -#~ msgstr "" +#~ msgstr "Query {} (Error)" #~ msgid "Query {} ({} rows × {} cols)" -#~ msgstr "" +#~ msgstr "Query {} ({} rows × {} cols)" #~ msgid "{} rows" #~ msgstr "Righe" #~ msgid "{:.1f} ms" -#~ msgstr "" +#~ msgstr "{:.1f} ms" #~ msgid "{} warnings" -#~ msgstr "" +#~ msgstr "{} warnings" #~ msgid "Edit Value" #~ msgstr "Modifica valore" @@ -1487,10 +1505,10 @@ msgstr "" #~ msgstr "Importa" #~ msgid "Read only" -#~ msgstr "" +#~ msgstr "Read only" #~ msgid "CASCADED" -#~ msgstr "" +#~ msgstr "CASCADED" #~ msgid "CHECK OPTION" #~ msgstr "connessione" @@ -1533,7 +1551,7 @@ msgstr "" #~ msgstr "Opzioni" #~ msgid "RadioBtn" -#~ msgstr "" +#~ msgstr "RadioBtn" #~ msgid "Edit Column" #~ msgstr "Modifica colonna" @@ -1546,4 +1564,3 @@ msgstr "" #~ msgid "Refrsh" #~ msgstr "Aggiorna" - diff --git a/locale/petersql.pot b/locale/petersql.pot index f155e7d..0042d82 100644 --- a/locale/petersql.pot +++ b/locale/petersql.pot @@ -27,68 +27,68 @@ msgstr "" msgid "OpenSSH client not found." msgstr "" -#: structures/engines/context.py:535 +#: structures/engines/context.py:544 msgid "This connection is read-only." msgstr "" -#: structures/engines/mariadb/context.py:616 -#: structures/engines/mysql/context.py:627 -#: structures/engines/postgresql/context.py:685 -#: structures/engines/sqlite/context.py:552 +#: structures/engines/mariadb/context.py:632 +#: structures/engines/mysql/context.py:643 +#: 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:644 -#: structures/engines/mysql/context.py:655 -#: structures/engines/postgresql/context.py:710 -#: structures/engines/sqlite/context.py:576 +#: structures/engines/mariadb/context.py:660 +#: structures/engines/mysql/context.py:671 +#: 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:662 -#: structures/engines/mysql/context.py:673 -#: structures/engines/postgresql/context.py:728 -#: structures/engines/sqlite/context.py:594 +#: structures/engines/mariadb/context.py:678 +#: structures/engines/mysql/context.py:689 +#: 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:702 -#: structures/engines/mysql/context.py:711 -#: structures/engines/postgresql/context.py:768 -#: structures/engines/sqlite/context.py:634 +#: structures/engines/mariadb/context.py:718 +#: structures/engines/mysql/context.py:727 +#: 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:735 -#: structures/engines/mysql/context.py:742 -#: structures/engines/postgresql/context.py:798 -#: structures/engines/sqlite/context.py:662 +#: structures/engines/mariadb/context.py:751 +#: structures/engines/mysql/context.py:758 +#: 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:788 +#: structures/engines/mariadb/context.py:804 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "" #: windows/dialogs/connections/view.py:417 -#: windows/dialogs/connections/view.py:764 windows/main/controller.py:1212 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1290 #: windows/views.py:33 msgid "Connection" msgstr "" #: windows/components/dataview.py:115 windows/components/dataview.py:240 #: windows/components/dataview.py:253 windows/components/dataview.py:268 -#: windows/main/database/procedure.py:129 windows/views.py:47 -#: windows/views.py:97 windows/views.py:1016 windows/views.py:1328 -#: windows/views.py:1361 windows/views.py:1385 windows/views.py:1409 -#: windows/views.py:1433 windows/views.py:1457 windows/views.py:1574 -#: windows/views.py:1954 windows/views.py:3079 +#: windows/views.py:47 windows/views.py:97 windows/views.py:1024 +#: windows/views.py:1336 windows/views.py:1369 windows/views.py:1393 +#: windows/views.py:1417 windows/views.py:1441 windows/views.py:1465 +#: windows/views.py:1582 windows/views.py:1928 windows/views.py:2199 +#: windows/views.py:3293 windows/views.py:3515 msgid "Name" msgstr "" @@ -96,11 +96,11 @@ msgstr "" msgid "Last connection" msgstr "" -#: windows/dialogs/connections/view.py:655 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:61 msgid "New directory" msgstr "" -#: windows/dialogs/connections/model.py:211 +#: windows/dialogs/connections/model.py:212 #: windows/dialogs/connections/view.py:615 windows/views.py:65 msgid "New connection" msgstr "" @@ -113,15 +113,15 @@ msgstr "" msgid "Clone connection" msgstr "" -#: windows/main/database/procedure.py:183 windows/views.py:81 -#: windows/views.py:613 windows/views.py:1488 windows/views.py:1896 -#: windows/views.py:2155 windows/views.py:2932 windows/views.py:2981 -#: windows/views.py:3215 windows/views.py:3247 +#: windows/views.py:81 windows/views.py:613 windows/views.py:1496 +#: windows/views.py:1864 windows/views.py:2154 windows/views.py:2378 +#: windows/views.py:3203 windows/views.py:3252 windows/views.py:3651 +#: windows/views.py:3683 msgid "Delete" msgstr "" -#: windows/views.py:111 windows/views.py:1333 windows/views.py:1629 -#: windows/views.py:2844 windows/views.py:3134 +#: windows/views.py:111 windows/views.py:1341 windows/views.py:1637 +#: windows/views.py:3115 windows/views.py:3570 msgid "Engine" msgstr "" @@ -133,7 +133,7 @@ msgstr "" msgid "Username" msgstr "" -#: windows/views.py:161 windows/views.py:1142 +#: windows/views.py:161 windows/views.py:1150 msgid "Password" msgstr "" @@ -161,7 +161,7 @@ msgstr "" msgid "Filename" msgstr "" -#: windows/views.py:249 windows/views.py:368 windows/views.py:3373 +#: windows/views.py:249 windows/views.py:368 windows/views.py:3809 msgid "Select a file" msgstr "" @@ -170,12 +170,12 @@ msgid "*.*" msgstr "" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:266 windows/views.py:1335 windows/views.py:1587 -#: windows/views.py:2842 windows/views.py:3092 +#: windows/views.py:266 windows/views.py:1343 windows/views.py:1595 +#: windows/views.py:3113 windows/views.py:3528 msgid "Comments" msgstr "" -#: windows/main/controller.py:766 windows/views.py:280 windows/views.py:740 +#: windows/main/controller.py:769 windows/views.py:280 windows/views.py:740 #: windows/views.py:894 msgid "Settings" msgstr "" @@ -232,7 +232,7 @@ msgstr "" msgid "SSH Tunnel" msgstr "" -#: windows/views.py:421 windows/views.py:1331 windows/views.py:2846 +#: windows/views.py:421 windows/views.py:1339 windows/views.py:3117 msgid "Created at" msgstr "" @@ -268,7 +268,7 @@ msgstr "" msgid "Statistics" msgstr "" -#: windows/views.py:594 windows/views.py:1853 +#: windows/views.py:594 windows/views.py:1821 msgid "Create" msgstr "" @@ -280,16 +280,16 @@ msgstr "" msgid "Create directory" msgstr "" -#: windows/main/database/procedure.py:184 windows/views.py:630 -#: windows/views.py:854 windows/views.py:1483 windows/views.py:1899 -#: windows/views.py:2160 windows/views.py:2216 windows/views.py:2908 -#: windows/views.py:3250 windows/views.py:3387 +#: windows/views.py:630 windows/views.py:854 windows/views.py:1491 +#: windows/views.py:1867 windows/views.py:2159 windows/views.py:2383 +#: windows/views.py:2439 windows/views.py:3179 windows/views.py:3686 +#: windows/views.py:3823 msgid "Cancel" msgstr "" -#: windows/main/controller.py:323 windows/main/database/procedure.py:185 -#: windows/views.py:635 windows/views.py:2165 windows/views.py:2358 -#: windows/views.py:3255 windows/views.py:3393 +#: windows/main/controller.py:323 windows/views.py:635 windows/views.py:2164 +#: windows/views.py:2388 windows/views.py:2603 windows/views.py:3691 +#: windows/views.py:3829 msgid "Save" msgstr "" @@ -301,7 +301,7 @@ msgstr "" msgid "Connect" msgstr "" -#: windows/main/database/procedure.py:162 windows/views.py:752 +#: windows/views.py:752 msgid "Language" msgstr "" @@ -361,492 +361,550 @@ msgstr "" msgid "tool" msgstr "" -#: windows/views.py:914 windows/views.py:2196 +#: windows/views.py:914 windows/views.py:2419 msgid "Refresh" msgstr "" -#: windows/views.py:918 windows/views.py:920 windows/views.py:1877 -#: windows/views.py:2200 windows/views.py:2344 +#: windows/views.py:918 windows/views.py:920 windows/views.py:1845 +#: windows/views.py:2423 windows/views.py:2589 msgid "Add" msgstr "" -#: windows/views.py:954 windows/views.py:958 windows/views.py:2322 +#: windows/views.py:925 +#, python-brace-format +msgid "{mode}" +msgstr "" + +#: windows/views.py:962 windows/views.py:966 windows/views.py:2556 +#: windows/views.py:2561 msgid "MyMenuItem" msgstr "" -#: windows/views.py:961 windows/views.py:1927 windows/views.py:3278 +#: windows/views.py:969 windows/views.py:1895 windows/views.py:2568 +#: windows/views.py:3714 msgid "MyMenu" msgstr "" -#: windows/views.py:976 windows/views.py:1511 windows/views.py:1518 -#: windows/views.py:1525 +#: windows/views.py:984 windows/views.py:1519 windows/views.py:1526 +#: windows/views.py:1533 msgid "MyLabel" msgstr "" -#: windows/views.py:982 +#: windows/views.py:990 msgid "Databases" msgstr "" -#: windows/views.py:983 windows/views.py:1330 windows/views.py:2847 +#: windows/views.py:991 windows/views.py:1338 windows/views.py:3118 msgid "Size" msgstr "" -#: windows/views.py:984 +#: windows/views.py:992 msgid "Elements" msgstr "" -#: windows/views.py:985 +#: windows/views.py:993 msgid "Modified at" msgstr "" -#: windows/views.py:986 windows/views.py:1345 +#: windows/views.py:994 windows/views.py:1353 msgid "Tables" msgstr "" -#: windows/views.py:993 +#: windows/views.py:1001 msgid "System" msgstr "" #: windows/components/dataview.py:43 windows/components/dataview.py:67 -#: windows/components/dataview.py:89 windows/views.py:1039 -#: windows/views.py:1334 windows/views.py:2843 +#: windows/components/dataview.py:89 windows/views.py:1047 +#: windows/views.py:1342 windows/views.py:3114 msgid "Collation" msgstr "" -#: windows/views.py:1068 +#: windows/views.py:1076 msgid "Encryption" msgstr "" -#: windows/views.py:1080 +#: windows/main/controller.py:1259 windows/main/controller.py:1281 +#: windows/main/controller.py:1285 windows/views.py:1088 msgid "Read Only" msgstr "" -#: windows/views.py:1097 +#: windows/views.py:1105 msgid "Tablespace" msgstr "" -#: windows/views.py:1118 +#: windows/views.py:1126 msgid "Connection limit" msgstr "" -#: windows/views.py:1161 +#: windows/views.py:1169 msgid "Profile" msgstr "" -#: windows/views.py:1187 +#: windows/views.py:1195 msgid "Default tablespace" msgstr "" -#: windows/views.py:1208 +#: windows/views.py:1216 msgid "Temporary tablespace" msgstr "" -#: windows/views.py:1234 +#: windows/views.py:1242 msgid "Quota" msgstr "" -#: windows/views.py:1253 +#: windows/views.py:1261 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1270 +#: windows/views.py:1278 msgid "Account status" msgstr "" -#: windows/views.py:1291 +#: windows/views.py:1299 msgid "Password expire" msgstr "" -#: windows/views.py:1315 +#: windows/views.py:1323 msgid "Add new table" msgstr "" -#: windows/views.py:1317 +#: windows/views.py:1325 msgid "Clone table" msgstr "" -#: windows/main/controller.py:1633 windows/views.py:1319 +#: windows/main/controller.py:1770 windows/views.py:1327 msgid "Delete table" msgstr "" -#: windows/views.py:1329 +#: windows/views.py:1337 msgid "Rows" msgstr "" -#: windows/views.py:1332 windows/views.py:2845 +#: windows/views.py:1340 windows/views.py:3116 msgid "Updated at" msgstr "" -#: windows/views.py:1350 +#: windows/views.py:1358 msgid "Add new view" msgstr "" -#: windows/views.py:1352 windows/views.py:1376 +#: windows/views.py:1360 windows/views.py:1384 msgid "Clone view" msgstr "" -#: windows/views.py:1354 +#: windows/views.py:1362 msgid "Delete view" msgstr "" -#: windows/views.py:1362 windows/views.py:1386 windows/views.py:1410 -#: windows/views.py:1434 windows/views.py:1458 +#: windows/views.py:1370 windows/views.py:1394 windows/views.py:1418 +#: windows/views.py:1442 windows/views.py:1466 msgid "Definition" msgstr "" -#: windows/views.py:1369 windows/views.py:2177 +#: windows/views.py:1377 msgid "Views" msgstr "" -#: windows/views.py:1374 +#: windows/views.py:1382 msgid "Add new procedure" msgstr "" -#: windows/views.py:1376 +#: windows/views.py:1384 msgid "Clone procedure" msgstr "" -#: windows/views.py:1378 +#: windows/views.py:1386 msgid "Delete procedure" msgstr "" -#: windows/views.py:1393 +#: windows/views.py:1401 msgid "Procedures" msgstr "" -#: windows/views.py:1398 +#: windows/views.py:1406 msgid "Add new function" msgstr "" -#: windows/views.py:1400 +#: windows/views.py:1408 msgid "Clone function" msgstr "" -#: windows/views.py:1402 +#: windows/views.py:1410 msgid "Delete function" msgstr "" -#: windows/views.py:1417 +#: windows/views.py:1425 msgid "Functions" msgstr "" -#: windows/views.py:1422 +#: windows/views.py:1430 msgid "Add new trigger" msgstr "" -#: windows/views.py:1424 +#: windows/views.py:1432 msgid "Clone trigger" msgstr "" -#: windows/views.py:1426 +#: windows/views.py:1434 msgid "Delete trigger" msgstr "" -#: windows/views.py:1441 windows/views.py:2185 +#: windows/views.py:1449 msgid "Triggers" msgstr "" -#: windows/views.py:1446 +#: windows/views.py:1454 msgid "Add new event" msgstr "" -#: windows/views.py:1448 +#: windows/views.py:1456 msgid "Clone event" msgstr "" -#: windows/views.py:1450 +#: windows/views.py:1458 msgid "Delete event" msgstr "" -#: windows/views.py:1465 +#: windows/views.py:1473 msgid "Events" msgstr "" -#: windows/views.py:1493 windows/views.py:1904 windows/views.py:2214 -#: windows/views.py:2296 windows/views.py:2915 +#: windows/views.py:1501 windows/views.py:1872 windows/views.py:2437 +#: windows/views.py:2521 windows/views.py:3186 msgid "Apply" msgstr "" -#: windows/main/database/procedure.py:122 windows/views.py:1505 -#: windows/views.py:1684 windows/views.py:2114 windows/views.py:3167 +#: windows/views.py:1513 windows/views.py:1692 windows/views.py:3603 msgid "Options" msgstr "" -#: windows/views.py:1536 +#: windows/views.py:1544 msgid "Diagram" msgstr "" -#: windows/views.py:1547 +#: windows/views.py:1555 msgid "Database" msgstr "" -#: windows/views.py:1602 windows/views.py:3107 +#: windows/views.py:1610 windows/views.py:3543 msgid "Base" msgstr "" -#: windows/views.py:1616 windows/views.py:3121 +#: windows/views.py:1624 windows/views.py:3557 msgid "Auto Increment" msgstr "" -#: windows/views.py:1644 windows/views.py:3149 +#: windows/views.py:1652 windows/views.py:3585 msgid "Default Collation" msgstr "" -#: windows/views.py:1654 +#: windows/views.py:1662 msgid "Convert data" msgstr "" -#: windows/views.py:1662 +#: windows/views.py:1670 msgid "Row format" msgstr "" -#: windows/views.py:1696 windows/views.py:1737 windows/views.py:1781 -#: windows/views.py:1879 windows/views.py:2204 +#: windows/views.py:1703 windows/views.py:1731 windows/views.py:1759 +#: windows/views.py:1847 windows/views.py:2259 windows/views.py:2427 +#: windows/views.py:3313 windows/views.py:3330 windows/views.py:3354 +#: windows/views.py:3381 msgid "Remove" msgstr "" -#: windows/views.py:1703 windows/views.py:1744 windows/views.py:1788 +#: windows/views.py:1705 windows/views.py:1733 windows/views.py:1761 +#: windows/views.py:2261 windows/views.py:2529 windows/views.py:3320 +#: windows/views.py:3337 windows/views.py:3361 windows/views.py:3388 msgid "Clear" msgstr "" -#: windows/views.py:1718 windows/views.py:3181 +#: windows/views.py:1718 windows/views.py:3617 msgid "Indexes" msgstr "" -#: windows/views.py:1732 windows/views.py:1776 windows/views.py:2927 -#: windows/views.py:2969 windows/views.py:3210 +#: windows/views.py:1729 windows/views.py:1757 windows/views.py:2257 +#: windows/views.py:3198 windows/views.py:3240 windows/views.py:3308 +#: windows/views.py:3349 windows/views.py:3376 windows/views.py:3646 msgid "Insert" msgstr "" -#: windows/views.py:1762 +#: windows/views.py:1746 msgid "Foreign Keys" msgstr "" -#: windows/views.py:1806 +#: windows/views.py:1774 msgid "Checks" msgstr "" -#: windows/views.py:1873 windows/views.py:3202 +#: windows/views.py:1841 windows/views.py:3638 msgid "Columns:" msgstr "" -#: windows/views.py:1883 +#: windows/views.py:1851 msgid "Move Up" msgstr "" -#: windows/views.py:1885 +#: windows/views.py:1853 msgid "Move Down" msgstr "" -#: windows/views.py:1917 windows/views.py:1924 windows/views.py:3268 -#: windows/views.py:3275 +#: windows/views.py:1885 windows/views.py:1892 windows/views.py:3704 +#: windows/views.py:3711 msgid "Add Index" msgstr "" -#: windows/views.py:1921 windows/views.py:3272 +#: windows/views.py:1889 windows/views.py:3708 msgid "Add PrimaryKey" msgstr "" -#: windows/views.py:1938 +#: windows/views.py:1906 msgid "Table" msgstr "" -#: windows/main/database/procedure.py:140 windows/views.py:1974 -msgid "Definer" -msgstr "" - -#: windows/views.py:1994 +#: windows/views.py:1948 windows/views.py:2219 msgid "Schema" msgstr "" -#: windows/views.py:2020 -msgid "SQL security" -msgstr "" - -#: windows/views.py:2027 -msgid "DEFINER" -msgstr "" - -#: windows/views.py:2027 -msgid "INVOKER" +#: windows/views.py:1976 windows/views.py:2247 +msgid "General" msgstr "" -#: windows/views.py:2039 +#: windows/views.py:1981 msgid "Algorithm" msgstr "" -#: windows/views.py:2041 +#: windows/views.py:1983 msgid "UNDEFINED" msgstr "" -#: windows/views.py:2044 +#: windows/views.py:1986 msgid "MERGE" msgstr "" -#: windows/views.py:2047 +#: windows/views.py:1989 msgid "TEMPTABLE" msgstr "" -#: windows/views.py:2057 +#: windows/views.py:1999 msgid "View constraint" msgstr "" -#: windows/views.py:2059 +#: windows/views.py:2001 msgid "None" msgstr "" -#: windows/views.py:2062 +#: windows/views.py:2004 msgid "LOCAL" msgstr "" -#: windows/views.py:2065 +#: windows/views.py:2007 msgid "CASCADE" msgstr "" -#: windows/views.py:2068 +#: windows/views.py:2010 msgid "CHECK ONLY" msgstr "" -#: windows/views.py:2071 +#: windows/views.py:2013 msgid "READ ONLY" msgstr "" -#: windows/views.py:2083 +#: windows/views.py:2026 +msgid "Behavior" +msgstr "" + +#: windows/views.py:2033 windows/views.py:2281 +msgid "Definer" +msgstr "" + +#: windows/views.py:2041 windows/views.py:2289 +msgid "*" +msgstr "" + +#: windows/views.py:2053 windows/views.py:2301 +msgid "SQL security" +msgstr "" + +#: windows/views.py:2060 windows/views.py:2308 +msgid "DEFINER" +msgstr "" + +#: windows/views.py:2060 windows/views.py:2308 +msgid "INVOKER" +msgstr "" + +#: windows/views.py:2074 msgid "Force" msgstr "" -#: windows/views.py:2095 +#: windows/views.py:2086 msgid "Security barrier" msgstr "" -#: windows/views.py:2202 +#: windows/views.py:2099 windows/views.py:2323 +msgid "Security" +msgstr "" + +#: windows/views.py:2176 +msgid "View" +msgstr "" + +#: windows/views.py:2274 +msgid "Parameters" +msgstr "" + +#: windows/views.py:2400 +msgid "Procedure" +msgstr "" + +#: windows/views.py:2408 +msgid "Trigger" +msgstr "" + +#: windows/views.py:2425 msgid "Duplicate" msgstr "" -#: windows/views.py:2208 +#: windows/views.py:2431 msgid "Apply changes automatically" msgstr "" -#: windows/views.py:2210 windows/views.py:2211 +#: windows/views.py:2433 windows/views.py:2434 msgid "" "If enabled, table edits are applied immediately without pressing Apply or" " Cancel" msgstr "" -#: windows/views.py:2224 +#: windows/views.py:2447 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" -#: windows/views.py:2232 +#: windows/views.py:2455 msgid "First" msgstr "" -#: windows/views.py:2250 +#: windows/views.py:2473 msgid "Last" msgstr "" -#: windows/views.py:2259 +#: windows/views.py:2482 msgid "Filters" msgstr "" -#: windows/views.py:2299 +#: windows/views.py:2524 +msgid "" +"Apply filters in data\n" +"CTRL+ENTER" +msgstr "" + +#: windows/views.py:2525 msgid "CTRL+ENTER" msgstr "" -#: windows/views.py:2319 +#: windows/views.py:2553 msgid "Insert row" msgstr "" -#: windows/views.py:2327 +#: windows/components/popup.py:31 windows/views.py:2565 +msgid "NULL" +msgstr "" + +#: windows/views.py:2572 msgid "Data" msgstr "" -#: windows/main/controller.py:318 windows/views.py:2344 +#: windows/main/controller.py:318 windows/views.py:2589 msgid "New query" msgstr "" -#: windows/views.py:2346 windows/views.py:2866 +#: windows/views.py:2591 windows/views.py:3137 msgid "Close" msgstr "" -#: windows/main/controller.py:319 windows/views.py:2346 +#: windows/main/controller.py:319 windows/views.py:2591 msgid "Close query" msgstr "" -#: windows/views.py:2350 +#: windows/views.py:2595 msgid "Run" msgstr "" -#: windows/main/controller.py:320 windows/views.py:2350 +#: windows/main/controller.py:320 windows/views.py:2595 msgid "Execute" msgstr "" -#: windows/views.py:2352 +#: windows/views.py:2597 msgid "Run all" msgstr "" -#: windows/views.py:2352 +#: windows/views.py:2597 msgid "Execute all statements" msgstr "" -#: windows/main/controller.py:322 windows/views.py:2354 +#: windows/main/controller.py:322 windows/views.py:2599 msgid "Stop" msgstr "" -#: windows/views.py:2419 +#: windows/views.py:2664 msgid "a page" msgstr "" -#: windows/views.py:2469 +#: windows/views.py:2714 msgid "Query" msgstr "" -#: windows/views.py:2820 +#: windows/views.py:3091 msgid "Character set" msgstr "" -#: windows/views.py:2850 windows/views.py:2869 +#: windows/views.py:3121 windows/views.py:3140 msgid "New" msgstr "" -#: windows/views.py:2889 +#: windows/views.py:3160 msgid "Insert record" msgstr "" -#: windows/views.py:2894 +#: windows/views.py:3165 msgid "Duplicate record" msgstr "" -#: windows/views.py:2901 +#: windows/views.py:3172 msgid "Delete record" msgstr "" -#: windows/views.py:2939 windows/views.py:3222 +#: windows/views.py:3210 windows/views.py:3658 msgid "Up" msgstr "" -#: windows/views.py:2946 windows/views.py:3229 +#: windows/views.py:3217 windows/views.py:3665 msgid "Down" msgstr "" -#: windows/views.py:2961 +#: windows/views.py:3232 msgid "Table:" msgstr "" -#: windows/views.py:2974 +#: windows/views.py:3245 msgid "Clone" msgstr "" -#: windows/views.py:3358 +#: windows/views.py:3270 +msgid "MyButton" +msgstr "" + +#: windows/views.py:3794 msgid "Save Starments" msgstr "" -#: windows/views.py:3366 +#: windows/views.py:3802 msgid "Location" msgstr "" -#: windows/views.py:3373 +#: windows/views.py:3809 msgid "*.sql" msgstr "" @@ -958,10 +1016,6 @@ msgstr "" msgid "No default value" msgstr "" -#: windows/components/popup.py:31 -msgid "NULL" -msgstr "" - #: windows/components/popup.py:35 msgid "AUTO INCREMENT" msgstr "" @@ -995,34 +1049,34 @@ msgstr "" msgid "Unsaved changes" msgstr "" -#: windows/dialogs/connections/view.py:762 +#: windows/dialogs/connections/view.py:773 msgid "" "This connection cannot work without TLS. TLS has been enabled " "automatically." msgstr "" -#: windows/dialogs/connections/view.py:787 +#: windows/dialogs/connections/view.py:798 #, python-brace-format msgid "" "Connection error:\n" "{error}" msgstr "" -#: windows/dialogs/connections/view.py:788 +#: windows/dialogs/connections/view.py:799 msgid "Connection error" msgstr "" -#: windows/dialogs/connections/view.py:814 +#: 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:817 -#: windows/dialogs/connections/view.py:834 +#: windows/dialogs/connections/view.py:828 +#: windows/dialogs/connections/view.py:845 msgid "Confirm delete" msgstr "" -#: windows/dialogs/connections/view.py:831 +#: windows/dialogs/connections/view.py:842 #, python-brace-format msgid "Do you want to delete the directory '{directory_name}'?" msgstr "" @@ -1063,9 +1117,9 @@ msgstr "" #: windows/main/controller.py:593 windows/main/controller.py:624 #: windows/main/controller.py:651 windows/main/database/list.py:119 -#: windows/main/database/procedure.py:294 -#: windows/main/database/procedure.py:320 windows/main/database/view.py:267 -#: windows/main/database/view.py:296 windows/main/query/controller.py:177 +#: windows/main/database/procedure.py:206 +#: windows/main/database/procedure.py:232 windows/main/database/view.py:268 +#: windows/main/database/view.py:297 windows/main/query/controller.py:177 msgid "Error" msgstr "" @@ -1079,54 +1133,62 @@ msgstr "" msgid "-- Autosaved query to {file_path}" msgstr "" -#: windows/main/controller.py:719 +#: windows/main/controller.py:722 msgid "days" msgstr "" -#: windows/main/controller.py:720 +#: windows/main/controller.py:723 msgid "hours" msgstr "" -#: windows/main/controller.py:721 +#: windows/main/controller.py:724 msgid "minutes" msgstr "" -#: windows/main/controller.py:722 +#: windows/main/controller.py:725 msgid "seconds" msgstr "" -#: windows/main/controller.py:730 +#: windows/main/controller.py:733 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "" -#: windows/main/controller.py:766 +#: windows/main/controller.py:769 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:990 +#: windows/main/controller.py:993 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:992 +#: windows/main/controller.py:995 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:1214 +#: windows/main/controller.py:1230 +msgid "Write Mode (2:00)" +msgstr "" + +#: windows/main/controller.py:1281 +msgid "Write Mode" +msgstr "" + +#: windows/main/controller.py:1292 msgid "Version" msgstr "" -#: windows/main/controller.py:1216 +#: windows/main/controller.py:1294 msgid "Uptime" msgstr "" -#: windows/main/controller.py:1299 +#: windows/main/controller.py:1429 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:1332 +#: windows/main/controller.py:1462 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1136,48 +1198,48 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:1337 windows/main/controller.py:1358 +#: windows/main/controller.py:1467 windows/main/controller.py:1488 msgid "Delete database" msgstr "" -#: windows/main/controller.py:1343 +#: windows/main/controller.py:1473 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:1344 +#: windows/main/controller.py:1474 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:1357 +#: windows/main/controller.py:1487 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:1372 +#: windows/main/controller.py:1502 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:1373 windows/main/database/procedure.py:283 -#: windows/main/database/procedure.py:314 windows/main/database/view.py:255 -#: windows/main/database/view.py:290 +#: windows/main/controller.py:1503 windows/main/database/procedure.py:195 +#: windows/main/database/procedure.py:226 windows/main/database/view.py:256 +#: windows/main/database/view.py:291 msgid "Success" msgstr "" -#: windows/main/controller.py:1604 +#: windows/main/controller.py:1741 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:1630 +#: windows/main/controller.py:1767 #, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "" -#: windows/main/controller.py:1652 +#: windows/main/controller.py:1789 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1797 +#: windows/main/controller.py:1948 msgid "Do you want delete the records?" msgstr "" @@ -1197,68 +1259,60 @@ msgstr "" msgid "Reconnection failed:" msgstr "" -#: windows/main/database/procedure.py:114 -msgid "Procedure" -msgstr "" - -#: windows/main/database/procedure.py:151 -msgid "Parameters" -msgstr "" - -#: windows/main/database/procedure.py:282 +#: windows/main/database/procedure.py:194 msgid "Procedure created successfully" msgstr "" -#: windows/main/database/procedure.py:282 +#: windows/main/database/procedure.py:194 msgid "Procedure updated successfully" msgstr "" -#: windows/main/database/procedure.py:294 +#: windows/main/database/procedure.py:206 #, python-brace-format msgid "Error saving procedure: {}" msgstr "" -#: windows/main/database/procedure.py:305 +#: windows/main/database/procedure.py:217 #, python-brace-format msgid "Are you sure you want to delete procedure '{}'?" msgstr "" -#: windows/main/database/procedure.py:306 windows/main/database/view.py:281 +#: windows/main/database/procedure.py:218 windows/main/database/view.py:282 msgid "Confirm Delete" msgstr "" -#: windows/main/database/procedure.py:314 +#: windows/main/database/procedure.py:226 msgid "Procedure deleted successfully" msgstr "" -#: windows/main/database/procedure.py:320 +#: windows/main/database/procedure.py:232 #, python-brace-format msgid "Error deleting procedure: {}" msgstr "" -#: windows/main/database/view.py:254 +#: windows/main/database/view.py:255 msgid "View created successfully" msgstr "" -#: windows/main/database/view.py:254 +#: windows/main/database/view.py:255 msgid "View updated successfully" msgstr "" -#: windows/main/database/view.py:267 +#: windows/main/database/view.py:268 #, python-brace-format msgid "Error saving view: {}" msgstr "" -#: windows/main/database/view.py:280 +#: windows/main/database/view.py:281 #, python-brace-format msgid "Are you sure you want to delete view '{}'?" msgstr "" -#: windows/main/database/view.py:290 +#: windows/main/database/view.py:291 msgid "View deleted successfully" msgstr "" -#: windows/main/database/view.py:296 +#: windows/main/database/view.py:297 #, python-brace-format msgid "Error deleting view: {}" msgstr "" @@ -1338,7 +1392,7 @@ msgstr "" msgid "Error:" msgstr "" -#: windows/main/table/records.py:336 +#: windows/main/table/records.py:334 msgid "Error saving records" msgstr "" diff --git a/pyproject.toml b/pyproject.toml index 64b996d..75450a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,6 @@ readme = "README.md" requires-python = ">=3.14" dependencies = [ "babel>=2.18.0", - "memray>=1.19.3", "oracledb>=3.4.2", "psutil>=7.2.2", "psycopg2-binary>=2.9.11", @@ -18,6 +17,7 @@ dependencies = [ [project.optional-dependencies] dev = [ + "memray>=1.19.3", "mypy>=1.19.1", "pillow>=12.2.0", "pre-commit>=4.5.1", diff --git a/uv.lock b/uv.lock index 0e87750..1fc389d 100644 --- a/uv.lock +++ b/uv.lock @@ -498,7 +498,6 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "babel" }, - { name = "memray" }, { name = "oracledb" }, { name = "psutil" }, { name = "psycopg2-binary" }, @@ -510,6 +509,7 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "memray" }, { name = "mypy" }, { name = "pillow" }, { name = "pre-commit" }, @@ -529,7 +529,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "babel", specifier = ">=2.18.0" }, - { name = "memray", specifier = ">=1.19.3" }, + { 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" }, diff --git a/windows/main/controller.py b/windows/main/controller.py index aec1ec4..9a6e9a3 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -24,7 +24,7 @@ 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, SQLProcedure +from structures.engines.database import SQLTable, SQLColumn, SQLIndex, SQLForeignKey, SQLRecord, SQLView, SQLTrigger, SQLDatabase, SQLProcedure, SQLFunction from windows.views import MainFrameView @@ -33,13 +33,13 @@ 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, CURRENT_PROCEDURE, WRITE_OVERRIDE +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.main.explorer import TreeExplorerController from windows.main.database.list import ListDatabaseTable, ListDatabaseView, ListDatabaseProcedure, ListDatabaseFunction, ListDatabaseTrigger, ListDatabaseEvent from windows.main.database.view import ViewEditorController -from windows.main.database.procedure import ProcedureEditorController +from windows.main.database.routine import RoutineController from windows.main.database.options import DatabaseOptionsController from windows.main.table.check import TableCheckController @@ -98,7 +98,7 @@ def __init__(self): self._setup_query_pages() self.controller_view_editor = ViewEditorController(self) - self.controller_procedure_editor = ProcedureEditorController(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) @@ -671,6 +671,7 @@ 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) @@ -768,7 +769,7 @@ 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, SQLProcedure]] = 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", @@ -781,7 +782,8 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S current_view = CURRENT_VIEW.get_value() current_trigger = CURRENT_TRIGGER.get_value() current_procedure = CURRENT_PROCEDURE.get_value() - procedure_page_index = self.MainFrameNotebook.FindPage(self.panel_procedures) + current_function = CURRENT_FUNCTION.get_value() + routine_page_index = self.MainFrameNotebook.FindPage(self.panel_routine) total_pages = self.MainFrameNotebook.GetPageCount() @@ -806,7 +808,10 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S self.MainFrameNotebook.GetPage(5).Hide() if not current_procedure: - self.MainFrameNotebook.GetPage(procedure_page_index).Hide() + self.MainFrameNotebook.GetPage(routine_page_index).Hide() + + if not current_function: + self.MainFrameNotebook.GetPage(routine_page_index).Hide() return @@ -841,10 +846,16 @@ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, S self.MainFrameNotebook.SetSelection(3) elif isinstance(current, SQLProcedure): - self.MainFrameNotebook.GetPage(procedure_page_index).Show() + 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() != procedure_page_index: - self.MainFrameNotebook.SetSelection(procedure_page_index) + 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() @@ -1284,7 +1295,7 @@ def _on_current_session(self, session: Session): self.m_toggleBtn1.SetValue(False) self.m_toggleBtn1.SetLabel(_("Read Only")) - self.toggle_panel(session.connection if session else None) + self.toggle_panel(session if session else None) if session: wx.CallAfter(self.status_bar.SetStatusText, f"{_('Connection')}: {session.name}", 1) @@ -1586,9 +1597,48 @@ def on_insert_procedure(self): CURRENT_PROCEDURE.set_value(None) new_proc = session.context.build_empty_procedure(database) CURRENT_PROCEDURE.set_value(new_proc) - procedure_page_index = self.MainFrameNotebook.FindPage(self.panel_procedures) - self._toggle_panel(procedure_page_index, True) - self.MainFrameNotebook.SetSelection(procedure_page_index) + 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() @@ -1604,9 +1654,9 @@ def on_clone_procedure(self): ) CURRENT_PROCEDURE.set_value(None) CURRENT_PROCEDURE.set_value(clone) - procedure_page_index = self.MainFrameNotebook.FindPage(self.panel_procedures) - self._toggle_panel(procedure_page_index, True) - self.MainFrameNotebook.SetSelection(procedure_page_index) + 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): diff --git a/windows/main/database/procedure.py b/windows/main/database/procedure.py deleted file mode 100644 index 1d65477..0000000 --- a/windows/main/database/procedure.py +++ /dev/null @@ -1,325 +0,0 @@ -from typing import Optional - -import wx - -from gettext import gettext as _ - -from helpers.bindings import AbstractModel, wx_call_after_debounce -from helpers.logger import logger -from helpers.observables import Observable - -from structures.connection import ConnectionEngine -from structures.engines.database import SQLProcedure - -from windows.main import CURRENT_SESSION, CURRENT_DATABASE, CURRENT_PROCEDURE - - -class EditViewModel(AbstractModel): - def __init__(self): - super().__init__() - - self.name = Observable() - self.parameters = Observable() - self.language = Observable() - self.definer = Observable() - self.body = Observable() - - wx_call_after_debounce( - self.name, self.parameters, self.language, self.definer, self.body, - callback=self.update_procedure - ) - - CURRENT_PROCEDURE.subscribe(self._load_procedure) - - def _load_procedure(self, procedure: Optional[SQLProcedure]): - if procedure is None: - return - - self.name.set_initial(procedure.name) - self.parameters.set_initial(getattr(procedure, "parameters", "")) - self.body.set_initial(getattr(procedure, "statement", "")) - - session = CURRENT_SESSION.get_value() - if not session: - return - - engine = session.engine - if engine in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): - self._load_mysql_fields(procedure) - elif engine == ConnectionEngine.POSTGRESQL: - self._load_postgresql_fields(procedure) - - def update_procedure(self, *args): - if not any(args): - return - - procedure = CURRENT_PROCEDURE.get_value() - if not procedure: - return - - procedure.name = self.name.get_value() or procedure.name - if hasattr(procedure, "parameters"): - procedure.parameters = self.parameters.get_value() or "" - if hasattr(procedure, "statement"): - procedure.statement = self.body.get_value() or "" - - session = CURRENT_SESSION.get_value() - if not session: - return - - engine = session.engine - if engine in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): - self._update_mysql_fields(procedure) - elif engine == ConnectionEngine.POSTGRESQL: - self._update_postgresql_fields(procedure) - - def _load_mysql_fields(self, procedure: SQLProcedure): - if hasattr(procedure, "definer"): - self.definer.set_initial(procedure.definer) - - def _load_postgresql_fields(self, procedure: SQLProcedure): - if hasattr(procedure, "language"): - self.language.set_initial(procedure.language) - - def _update_mysql_fields(self, procedure: SQLProcedure): - if hasattr(procedure, "definer"): - procedure.definer = self.definer.get_value() or "" - - def _update_postgresql_fields(self, procedure: SQLProcedure): - if hasattr(procedure, "language"): - procedure.language = self.language.get_value() or "plpgsql" - - -class ProcedureEditorController: - def __init__(self, parent): - 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.stc_procedure) - SQL.apply(self.parent.stc_procedure) - except Exception: - pass - - self.model = EditViewModel() - self._bind_controls() - self._bind_buttons() - - wx_call_after_debounce( - self.model.name, self.model.parameters, - self.model.language, self.model.definer, self.model.body, - callback=self.update_button_states - ) - - CURRENT_PROCEDURE.subscribe(self.on_current_procedure_changed) - - # ------------------------------------------------------------------ - # Bindings - # ------------------------------------------------------------------ - - def _bind_controls(self): - self.model.bind_controls( - name=self.parent.txt_name_procedure, - body=self.parent.stc_procedure, - ) - - def _bind_buttons(self): - self.parent.btn_save_procedure.Bind(wx.EVT_BUTTON, self.on_save_procedure) - self.parent.btn_delete_procedure.Bind(wx.EVT_BUTTON, self.on_delete_procedure) - self.parent.btn_cancel_procedure.Bind(wx.EVT_BUTTON, self.on_cancel_procedure) - - # ------------------------------------------------------------------ - # Button state - # ------------------------------------------------------------------ - - def _get_original_procedure(self, procedure: SQLProcedure) -> Optional[SQLProcedure]: - if procedure.is_new: - return None - database = CURRENT_DATABASE.get_value() - if not database: - return None - return next((p for p in database.procedures if p.id == procedure.id), None) - - def _has_changes(self, procedure: SQLProcedure) -> bool: - if procedure.is_new: - return True - original = self._get_original_procedure(procedure) - if original is None: - return True - self.model.update_procedure(procedure) - return procedure != original - - def update_button_states(self, *args, **kwargs): - procedure = CURRENT_PROCEDURE.get_value() - logger.debug( - "ui trace: procedure.update_button_states procedure=%s is_new=%s", - getattr(procedure, "name", None) if procedure is not None else None, - getattr(procedure, "is_new", None) if procedure is not None else None, - ) - if procedure is None: - self.parent.btn_save_procedure.Enable(False) - self.parent.btn_cancel_procedure.Enable(False) - self.parent.btn_delete_procedure.Enable(False) - else: - has_changes = self._has_changes(procedure) - self.parent.btn_save_procedure.Enable(has_changes) - self.parent.btn_cancel_procedure.Enable(has_changes) - self.parent.btn_delete_procedure.Enable(not procedure.is_new) - - # ------------------------------------------------------------------ - # Actions - # ------------------------------------------------------------------ - - def on_save_procedure(self, event): - self.do_save_procedure() - - def on_delete_procedure(self, event): - self.do_delete_procedure() - - def on_cancel_procedure(self, event): - self.do_cancel_procedure() - - def do_save_procedure(self): - procedure = CURRENT_PROCEDURE.get_value() - if not procedure: - return - session = CURRENT_SESSION.get_value() - if not session: - return - - is_new = procedure.is_new - try: - procedure.save() - 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() - saved = next((p for p in database.procedures if p.name == procedure.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 procedure: {}").format(str(e)), _("Error"), wx.OK | wx.ICON_ERROR) - - def do_delete_procedure(self): - procedure = CURRENT_PROCEDURE.get_value() - if not procedure: - return - session = CURRENT_SESSION.get_value() - if not session: - return - - result = wx.MessageBox( - _("Are you sure you want to delete procedure '{}'?").format(procedure.name), - _("Confirm Delete"), - wx.YES_NO | wx.ICON_QUESTION, - ) - if result != wx.YES: - return - - try: - procedure.drop() - wx.MessageBox(_("Procedure deleted successfully"), _("Success"), wx.OK | wx.ICON_INFORMATION) - CURRENT_PROCEDURE.set_value(None) - database = CURRENT_DATABASE.get_value() - database.procedures.refresh() - self.parent.controller_tree_connections.refresh_current_database() - except Exception as e: - wx.MessageBox(_("Error deleting procedure: {}").format(str(e)), _("Error"), wx.OK | wx.ICON_ERROR) - - def do_cancel_procedure(self): - procedure = CURRENT_PROCEDURE.get_value() - if not procedure: - return - CURRENT_PROCEDURE.set_value(None) - CURRENT_PROCEDURE.set_value(procedure) - self.update_button_states() - - # ------------------------------------------------------------------ - # Current procedure changed - # ------------------------------------------------------------------ - - def on_current_procedure_changed(self, procedure: Optional[SQLProcedure]): - logger.debug( - "ui trace: procedure.on_current_procedure_changed procedure=%s is_new=%s", - getattr(procedure, "name", None) if procedure is not None else None, - getattr(procedure, "is_new", None) if procedure is not None else None, - ) - self.update_button_states() - - if procedure 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): - if engine not in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): - return - cmb = getattr(self.parent, 'cmb_procedure_definer', None) - if cmb is None: - return - try: - logger.debug("ui trace: procedure._populate_definers start engine=%s", engine.name) - definers = session.context.get_definers() - cmb.Clear() - for definer in definers: - cmb.Append(definer) - logger.debug("ui trace: procedure._populate_definers done count=%s", len(definers)) - except Exception: - pass - - def apply_engine_visibility(self, engine: ConnectionEngine): - logger.debug("ui trace: procedure.apply_engine_visibility engine=%s", engine.name) - if engine in (ConnectionEngine.MYSQL, ConnectionEngine.MARIADB): - self._apply_mysql_visibility() - elif engine == ConnectionEngine.POSTGRESQL: - self._apply_postgresql_visibility() - else: - self._apply_default_visibility() - - self.parent.m_panel73.GetSizer().Layout() - self.parent.panel_procedures.Layout() - - def _apply_mysql_visibility(self): - definer = getattr(self.parent, 'pnl_procedure_row_definer', None) - language = getattr(self.parent, 'pnl_procedure_row_language', None) - self._batch_show_hide( - show=[w for w in [definer] if w], - hide=[w for w in [language] if w], - ) - - def _apply_postgresql_visibility(self): - definer = getattr(self.parent, 'pnl_procedure_row_definer', None) - language = getattr(self.parent, 'pnl_procedure_row_language', None) - self._batch_show_hide( - show=[w for w in [language] if w], - hide=[w for w in [definer] if w], - ) - - def _apply_default_visibility(self): - definer = getattr(self.parent, 'pnl_procedure_row_definer', None) - language = getattr(self.parent, 'pnl_procedure_row_language', None) - self._batch_show_hide( - show=[], - hide=[w for w in [definer, language] if w], - ) - - def _batch_show_hide(self, show: list[wx.Window], hide: list[wx.Window]): - 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) 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/explorer.py b/windows/main/explorer.py index 2ddaef4..74ef4e5 100755 --- a/windows/main/explorer.py +++ b/windows/main/explorer.py @@ -208,7 +208,7 @@ def select_session(self, session: Session, event): 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(): diff --git a/windows/main/table/records.py b/windows/main/table/records.py index 465c409..57ac467 100644 --- a/windows/main/table/records.py +++ b/windows/main/table/records.py @@ -339,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/views.py b/windows/views.py index 809ad81..2e26a7a 100755 --- a/windows/views.py +++ b/windows/views.py @@ -18,7 +18,6 @@ import wx.dataview import wx.stc import wx.lib.agw.hypertreelist -import wx.adv import gettext _ = gettext.gettext @@ -1381,7 +1380,7 @@ def __init__( self, parent ): 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 ) - self.tool_clone_procedure = self.m_toolBar52.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 procedure"), _(u"Clone procedure"), None ) + 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 ) 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 ) @@ -1422,7 +1421,7 @@ def __init__( self, parent ): 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_notebook10.AddPage( self.m_panel6521, _(u"Functions"), True ) self.m_panel65211 = wx.Panel( self.m_notebook10, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer1482111 = wx.BoxSizer( wx.VERTICAL ) @@ -1699,22 +1698,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.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_delete_index = wx.Button( self.PanelTableIndex, wx.ID_ANY, _(u"Remove"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + 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.btn_delete_index.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_index.Enable( False ) + self.m_toolBar12.Realize() - 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.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 ) - - - 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 ) @@ -1731,37 +1722,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 ) - - bSizer78 = wx.BoxSizer( wx.HORIZONTAL ) - - bSizer79 = wx.BoxSizer( wx.VERTICAL ) - - self.btn_insert_foreign_key = wx.Button( self.PanelTableFK, wx.ID_ANY, _(u"Insert"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_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 ) + bSizer77 = wx.BoxSizer( wx.HORIZONTAL ) - self.btn_delete_foreign_key = wx.Button( self.PanelTableFK, wx.ID_ANY, _(u"Remove"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + 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 ) - self.btn_delete_foreign_key.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_foreign_key.Enable( False ) + 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 ) - bSizer79.Add( self.btn_delete_foreign_key, 0, wx.ALL|wx.EXPAND, 5 ) + 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_clear_foreign_key = wx.Button( self.PanelTableFK, wx.ID_ANY, _(u"Clear"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.m_toolBar121.Realize() - 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 ) @@ -1775,37 +1750,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 ) - - bSizer781 = wx.BoxSizer( wx.HORIZONTAL ) - - bSizer792 = wx.BoxSizer( wx.VERTICAL ) - - self.btn_insert_check = wx.Button( self.PanelTableCheck, wx.ID_ANY, _(u"Insert"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_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 ) + bSizer771 = wx.BoxSizer( wx.HORIZONTAL ) - self.btn_delete_check = wx.Button( self.PanelTableCheck, wx.ID_ANY, _(u"Remove"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + 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 ) - self.btn_delete_check.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_check.Enable( False ) + 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 ) - bSizer792.Add( self.btn_delete_check, 0, wx.ALL|wx.EXPAND, 5 ) + 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_clear_check = wx.Button( self.PanelTableCheck, wx.ID_ANY, _(u"Clear"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) + self.m_toolBar1211.Realize() - 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 ) @@ -1953,7 +1912,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 ) @@ -1976,26 +1941,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 ) @@ -2020,31 +1965,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 ) @@ -2059,9 +1992,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 ) @@ -2083,9 +2016,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_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.pnl_view_editor_root, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + 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 ) @@ -2095,9 +2077,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 ) @@ -2107,23 +2089,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 ) @@ -2156,7 +2139,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 ) @@ -2189,30 +2179,329 @@ def __init__( self, parent ): self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex ) MainFrameNotebookIndex += 1 - self.panel_procedures = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + self.panel_routine = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer160 = wx.BoxSizer( wx.VERTICAL ) - self.m_splitter9 = wx.SplitterWindow( self.panel_procedures, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_3D ) + 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_panel73 = wx.Panel( self.m_splitter9, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer166 = wx.BoxSizer( wx.VERTICAL ) + 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 ) + bSizer871 = wx.BoxSizer( wx.HORIZONTAL ) - self.m_staticText401 = wx.StaticText( self.m_panel73, wx.ID_ANY, _(u"Name"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText401 = wx.StaticText( self.m_panel81, wx.ID_ANY, _(u"Name"), wx.DefaultPosition, wx.DefaultSize, 0 ) self.m_staticText401.Wrap( -1 ) self.m_staticText401.SetMinSize( wx.Size( 150,-1 ) ) bSizer871.Add( self.m_staticText401, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - self.txt_name_procedure = wx.TextCtrl( self.m_panel73, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer871.Add( self.txt_name_procedure, 1, wx.ALIGN_CENTER|wx.ALL, 5 ) + 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 ) + + + bSizer1701.Add( bSizer871, 0, wx.EXPAND, 5 ) + + szr_view_schema1 = wx.BoxSizer( wx.HORIZONTAL ) + + 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 ) + + self.lbl_view_schema1.SetMinSize( wx.Size( 150,-1 ) ) + + szr_view_schema1.Add( self.lbl_view_schema1, 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 ) + + + bSizer1701.Add( szr_view_schema1, 0, wx.EXPAND, 5 ) + + bSizer181 = wx.BoxSizer( wx.HORIZONTAL ) + + bSizer891 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText77 = wx.StaticText( self.m_panel81, wx.ID_ANY, _(u"Type"), wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText77.Wrap( -1 ) + + self.m_staticText77.SetMinSize( wx.Size( 150,-1 ) ) + + bSizer891.Add( self.m_staticText77, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + 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 ) + + + bSizer181.Add( bSizer891, 1, 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 ) + + bSizer1161.Add( self.m_staticText78, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) + + 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 ) + + bSizer1161.Add( self.routine_return_type, 1, wx.ALL, 5 ) - bSizer166.Add( bSizer871, 0, wx.EXPAND, 5 ) + 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 ) @@ -2221,40 +2510,40 @@ def __init__( self, parent ): self.m_panel74 = wx.Panel( self.m_splitter9, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) bSizer161 = wx.BoxSizer( wx.VERTICAL ) - self.stc_procedure = wx.stc.StyledTextCtrl( self.m_panel74, wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,-1 ), 0) - self.stc_procedure.SetUseTabs ( True ) - self.stc_procedure.SetTabWidth ( 4 ) - self.stc_procedure.SetIndent ( 4 ) - self.stc_procedure.SetTabIndents( True ) - self.stc_procedure.SetBackSpaceUnIndents( True ) - self.stc_procedure.SetViewEOL( False ) - self.stc_procedure.SetViewWhiteSpace( False ) - self.stc_procedure.SetMarginWidth( 2, 0 ) - self.stc_procedure.SetIndentationGuides( True ) - self.stc_procedure.SetReadOnly( False ) - self.stc_procedure.SetMarginWidth( 1, 0 ) - self.stc_procedure.SetMarginType( 0, wx.stc.STC_MARGIN_NUMBER ) - self.stc_procedure.SetMarginWidth( 0, self.stc_procedure.TextWidth( wx.stc.STC_STYLE_LINENUMBER, "_99999" ) ) - self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS ) - self.stc_procedure.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK) - self.stc_procedure.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE) - self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS ) - self.stc_procedure.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK ) - self.stc_procedure.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE ) - self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY ) - self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS ) - self.stc_procedure.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK ) - self.stc_procedure.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE ) - self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUS ) - self.stc_procedure.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.BLACK) - self.stc_procedure.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.WHITE) - self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY ) - self.stc_procedure.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY ) - self.stc_procedure.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) ) - self.stc_procedure.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) ) - self.stc_procedure.SetMinSize( wx.Size( -1,200 ) ) - - bSizer161.Add( self.stc_procedure, 1, wx.EXPAND | wx.ALL, 5 ) + 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 ) @@ -2265,29 +2554,29 @@ def __init__( self, parent ): bSizer911 = wx.BoxSizer( wx.HORIZONTAL ) - self.btn_delete_procedure = wx.Button( self.panel_procedures, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.btn_delete_procedure.Enable( False ) + 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_delete_procedure, 0, wx.ALL, 5 ) + bSizer911.Add( self.btn_routine_delete, 0, wx.ALL, 5 ) - self.btn_cancel_procedure = wx.Button( self.panel_procedures, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.btn_cancel_procedure.Enable( False ) + 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_cancel_procedure, 0, wx.ALL, 5 ) + bSizer911.Add( self.btn_routine_cancel, 0, wx.ALL, 5 ) - self.btn_save_procedure = wx.Button( self.panel_procedures, wx.ID_ANY, _(u"Save"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.btn_save_procedure.Enable( False ) + 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_save_procedure, 0, wx.ALL, 5 ) + bSizer911.Add( self.btn_routine_save, 0, wx.ALL, 5 ) bSizer160.Add( bSizer911, 0, wx.EXPAND, 5 ) - self.panel_procedures.SetSizer( bSizer160 ) - self.panel_procedures.Layout() - bSizer160.Fit( self.panel_procedures ) - self.MainFrameNotebook.AddPage( self.panel_procedures, _(u"Procedure"), False ) + self.panel_routine.SetSizer( bSizer160 ) + self.panel_routine.Layout() + bSizer160.Fit( self.panel_routine ) + self.MainFrameNotebook.AddPage( self.panel_routine, _(u"Routine"), True ) MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/code-folding.png", wx.BITMAP_TYPE_ANY ) if ( MainFrameNotebookBitmap.IsOk() ): MainFrameNotebookImages.Add( MainFrameNotebookBitmap ) @@ -2459,7 +2748,7 @@ def __init__( self, parent ): 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 ) @@ -2609,7 +2898,7 @@ def __init__( self, parent ): MainFrameNotebookIndex += 1 - bSizer25.Add( self.MainFrameNotebook, 1, wx.ALL|wx.EXPAND, 5 ) + bSizer25.Add( self.MainFrameNotebook, 0, wx.ALL|wx.EXPAND, 5 ) self.m_panel15.SetSizer( bSizer25 ) @@ -2694,12 +2983,12 @@ def __init__( self, parent ): 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_view, id = self.tool_insert_procedure.GetId() ) - self.Bind( wx.EVT_TOOL, self.on_clone_view, id = self.tool_clone_procedure.GetId() ) - self.Bind( wx.EVT_TOOL, self.on_delete_view, id = self.tool_delete_procedure.GetId() ) - self.Bind( wx.EVT_TOOL, self.on_insert_view, id = self.tool_insert_function.GetId() ) - self.Bind( wx.EVT_TOOL, self.on_clone_view, id = self.tool_clone_function.GetId() ) - self.Bind( wx.EVT_TOOL, self.on_delete_view, id = self.tool_delete_function.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() ) @@ -2709,14 +2998,14 @@ def __init__( self, parent ): 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() ) @@ -2724,6 +3013,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() ) @@ -2795,11 +3090,23 @@ def on_clone_view( self, event ): def on_delete_view( self, event ): event.Skip() + def on_insert_procedure( self, event ): + event.Skip() + def on_clone_procedure( self, event ): + event.Skip() + def on_delete_procedure( self, event ): + event.Skip() + def on_insert_function( self, event ): + event.Skip() + def on_clone_function( self, event ): + event.Skip() + def on_delete_function( self, event ): + event.Skip() @@ -2831,8 +3138,14 @@ def on_delete_foreign_key( self, event ): 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() @@ -2853,6 +3166,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() @@ -2938,6 +3269,10 @@ 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 ) @@ -2954,610 +3289,3 @@ def m_splitter8OnIdle( self, event ): self.m_splitter8.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_dataViewListCtrl2 = wx.dataview.DataViewListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.DV_ROW_LINES ) - bSizer144.Add( self.m_dataViewListCtrl2, 0, wx.ALL, 5 ) - - self.m_dataViewCtrl10 = wx.dataview.DataViewCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_dataViewColumn181 = self.m_dataViewCtrl10.AppendTextColumn( _(u"Comments"), 7, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) - self.m_dataViewColumn191 = self.m_dataViewCtrl10.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_dataViewColumn171 = self.m_dataViewCtrl10.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_dataViewColumn161 = self.m_dataViewCtrl10.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_dataViewColumn151 = self.m_dataViewCtrl10.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_dataViewColumn141 = self.m_dataViewCtrl10.AppendTextColumn( _(u"Size"), 2, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_RIGHT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE ) - bSizer144.Add( self.m_dataViewCtrl10, 0, wx.ALL, 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 ) - - - 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 ) - - bSizer531 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText391 = wx.StaticText( self, wx.ID_ANY, _(u"Table:"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText391.Wrap( -1 ) - - bSizer531.Add( self.m_staticText391, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) - - - bSizer531.Add( ( 100, 0), 0, wx.EXPAND, 5 ) - - self.btn_insert_table = wx.Button( self, 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.btn_clone_table = wx.Button( self, wx.ID_ANY, _(u"Clone"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_clone_table.SetBitmap( wx.Bitmap( u"icons/16x16/table_multiple.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_clone_table.Enable( False ) - - bSizer531.Add( self.btn_clone_table, 0, wx.ALL|wx.EXPAND, 5 ) - - self.btn_delete_table1 = wx.Button( self, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE ) - - self.btn_delete_table1.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) ) - self.btn_delete_table1.Enable( False ) - - bSizer531.Add( self.btn_delete_table1, 0, wx.ALL|wx.EXPAND, 2 ) - - - bSizer531.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - - bSizer144.Add( bSizer531, 0, wx.EXPAND, 5 ) - - bSizer156 = wx.BoxSizer( wx.VERTICAL ) - - - bSizer144.Add( bSizer156, 1, wx.EXPAND, 5 ) - - self.m_button57 = wx.Button( self, wx.ID_ANY, _(u"MyButton"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer144.Add( self.m_button57, 0, wx.ALL, 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 ) - 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 ) - - 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() - - def on_insert_table( self, event ): - event.Skip() - - def on_clone_table( self, event ): - event.Skip() - - def on_delete_table( 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 MyWizard1 ( wx.adv.Wizard ): - - def __init__( self, parent ): - wx.adv.Wizard.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, bitmap = wx.NullBitmap, pos = wx.DefaultPosition, style = wx.DEFAULT_DIALOG_STYLE ) - - self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) - self.m_pages = [] - - self.Centre( wx.BOTH ) - - - def __del__( self ): - pass - - -########################################################################### -## Class SaveStatments -########################################################################### - -class SaveStatments ( wx.Dialog ): - - def __init__( self, parent ): - wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Save Starments"), pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.DEFAULT_DIALOG_STYLE ) - - self.SetSizeHints( wx.Size( 320,200 ), wx.DefaultSize ) - - bSizer163 = wx.BoxSizer( wx.VERTICAL ) - - bSizer164 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_staticText86 = wx.StaticText( self, wx.ID_ANY, _(u"Location"), wx.DefaultPosition, wx.DefaultSize, 0 ) - self.m_staticText86.Wrap( -1 ) - - self.m_staticText86.SetMinSize( wx.Size( 150,-1 ) ) - - bSizer164.Add( self.m_staticText86, 0, wx.ALIGN_CENTER|wx.ALL, 5 ) - - self.m_filePicker5 = wx.FilePickerCtrl( self, wx.ID_ANY, wx.EmptyString, _(u"Select a file"), _(u"*.sql"), wx.DefaultPosition, wx.DefaultSize, wx.FLP_DEFAULT_STYLE|wx.FLP_SAVE|wx.FLP_SMALL|wx.FLP_USE_TEXTCTRL ) - bSizer164.Add( self.m_filePicker5, 1, wx.ALL, 5 ) - - - bSizer163.Add( bSizer164, 0, wx.EXPAND, 5 ) - - - bSizer163.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.m_staticline7 = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) - bSizer163.Add( self.m_staticline7, 0, wx.EXPAND | wx.ALL, 5 ) - - bSizer165 = wx.BoxSizer( wx.HORIZONTAL ) - - self.m_button57 = wx.Button( self, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer165.Add( self.m_button57, 0, wx.ALL, 5 ) - - - bSizer165.Add( ( 0, 0), 1, wx.EXPAND, 5 ) - - self.m_button58 = wx.Button( self, wx.ID_ANY, _(u"Save"), wx.DefaultPosition, wx.DefaultSize, 0 ) - bSizer165.Add( self.m_button58, 0, wx.ALL, 5 ) - - - bSizer163.Add( bSizer165, 0, wx.EXPAND, 5 ) - - - self.SetSizer( bSizer163 ) - self.Layout() - bSizer163.Fit( self ) - - self.Centre( wx.BOTH ) - - def __del__( self ): - pass - - From de9606bbc62278128edfaaac97941e8a6dc85f31 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 1 Jun 2026 09:05:17 +0200 Subject: [PATCH 61/93] Update project dependencies and ignore assets/screenshots --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) 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/ From 96b7f2cc0e0333809d0f94f68394f7207fc36c52 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 2 Jun 2026 10:15:05 +0200 Subject: [PATCH 62/93] Add stored function support for MySQL and MariaDB engines, including deterministic flag handling and related tests --- PeterSQL.fbp | 10 +++---- structures/engines/mariadb/context.py | 26 +++++++++++++++++ structures/engines/mysql/context.py | 29 ++++++++++++++++++- structures/engines/mysql/database.py | 6 ++-- tests/engines/base_function_tests.py | 9 ++++-- .../engines/mariadb/test_integration_suite.py | 21 ++++++++++++++ tests/engines/mysql/test_integration_suite.py | 21 ++++++++++++++ 7 files changed, 111 insertions(+), 11 deletions(-) diff --git a/PeterSQL.fbp b/PeterSQL.fbp index fc395c7..847cf95 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -19604,7 +19604,7 @@ Security 0 - + 1 1 1 @@ -19656,16 +19656,16 @@ wxTAB_TRAVERSAL - + Security wxVERTICAL none - + 5 wxEXPAND | wxALL 0 - + 1 1 1 @@ -19717,7 +19717,7 @@ wxTAB_TRAVERSAL - + szr_view_definer1 wxHORIZONTAL diff --git a/structures/engines/mariadb/context.py b/structures/engines/mariadb/context.py index 570961e..e30415a 100755 --- a/structures/engines/mariadb/context.py +++ b/structures/engines/mariadb/context.py @@ -301,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, ) @@ -330,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( diff --git a/structures/engines/mysql/context.py b/structures/engines/mysql/context.py index 0c0cab6..1c5c3af 100644 --- a/structures/engines/mysql/context.py +++ b/structures/engines/mysql/context.py @@ -311,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, ) @@ -340,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( @@ -630,6 +656,7 @@ def build_empty_database(self, /, name: str = "") -> MySQLDatabase: 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, ) @@ -783,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 c47ad13..73cfe66 100644 --- a/structures/engines/mysql/database.py +++ b/structures/engines/mysql/database.py @@ -439,7 +439,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 @@ -451,7 +451,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" @@ -460,7 +460,7 @@ def raw_create(self) -> str: RETURNS {self.returns} {deterministic} BEGIN - {self.sql}; + {self.statement}; END """ 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/mariadb/test_integration_suite.py b/tests/engines/mariadb/test_integration_suite.py index fd87646..bf3094d 100644 --- a/tests/engines/mariadb/test_integration_suite.py +++ b/tests/engines/mariadb/test_integration_suite.py @@ -9,6 +9,7 @@ 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_readonly_tests import BaseReadOnlyTests @@ -68,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): diff --git a/tests/engines/mysql/test_integration_suite.py b/tests/engines/mysql/test_integration_suite.py index 3f0b53c..7453298 100644 --- a/tests/engines/mysql/test_integration_suite.py +++ b/tests/engines/mysql/test_integration_suite.py @@ -9,6 +9,7 @@ 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_readonly_tests import BaseReadOnlyTests @@ -68,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): From 3b6af5053beed48fcfc3279a48b6fc09b7673884 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 2 Jun 2026 10:17:39 +0200 Subject: [PATCH 63/93] docs: update function support status --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 70f9067..40b62a2 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,13 @@ 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. - + + - 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? From 24db2ba9840f58a7cd790eb1e443d35b7017e4c6 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 2 Jun 2026 10:20:41 +0200 Subject: [PATCH 64/93] docs: note stored function support in MySQL/MariaDB engines --- ENGINES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ENGINES.md b/ENGINES.md index e499f93..cc511f3 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. From 28d8ae3cf49c7a030c5d2aec14d82ffd65a215e0 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 4 Jun 2026 13:52:56 +0200 Subject: [PATCH 65/93] fix(postgresql): handle alter table diffs as pairs Refactor PostgreSQLTable.alter() to iterate over (original, current) pairs from merge_original_current() instead of using dict-based access. This fixes the critical audit issue where the previous implementation incorrectly accessed map_columns['added'], map_columns['removed'], etc. Also fix PostgreSQLDatabase.alter() to check all fields when _changed_fields is empty, matching the behavior of MySQL/MariaDB implementations. --- structures/engines/postgresql/database.py | 51 +++++++++++------------ 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/structures/engines/postgresql/database.py b/structures/engines/postgresql/database.py index d4587e3..ef758e7 100644 --- a/structures/engines/postgresql/database.py +++ b/structures/engines/postgresql/database.py @@ -44,12 +44,14 @@ def alter(self) -> bool: name = getattr(self, "_original_name", self.name) - if "tablespace" in self._changed_fields and self.tablespace: + 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(name)} SET TABLESPACE {self.context.quote_identifier(self.tablespace)}" ) - if "connection_limit" in self._changed_fields and 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(name)} CONNECTION LIMIT {int(self.connection_limit)}" ) @@ -148,38 +150,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) From 9952f73b0d9d492b44e814cea566938519d9b707 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 4 Jun 2026 13:53:18 +0200 Subject: [PATCH 66/93] fix(mysql/mariadb): handle empty changed_fields in database alter Pass None to _build_database_clauses() when _changed_fields is empty, allowing the method to check all fields instead of skipping them. This ensures consistent behavior across all database engines. --- structures/engines/mariadb/database.py | 3 ++- structures/engines/mysql/database.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/structures/engines/mariadb/database.py b/structures/engines/mariadb/database.py index 5f1b165..57bddcd 100644 --- a/structures/engines/mariadb/database.py +++ b/structures/engines/mariadb/database.py @@ -41,7 +41,8 @@ def create(self) -> bool: return self.context.execute(query) def alter(self) -> bool: - clauses = self._build_database_clauses(self._changed_fields) + fields = self._changed_fields or None + clauses = self._build_database_clauses(fields) if not clauses: return False diff --git a/structures/engines/mysql/database.py b/structures/engines/mysql/database.py index 73cfe66..7938470 100644 --- a/structures/engines/mysql/database.py +++ b/structures/engines/mysql/database.py @@ -55,7 +55,8 @@ def create(self) -> bool: return self.context.execute(query) def alter(self) -> bool: - clauses = self._build_database_clauses(self._changed_fields) + fields = self._changed_fields or None + clauses = self._build_database_clauses(fields) if not clauses: return False From 4e348a58501133652feb05c04377747134b75787 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 4 Jun 2026 13:53:44 +0200 Subject: [PATCH 67/93] refactor(tests): move unit tests to tests/core/ directory Relocate test files from tests/ to tests/core/ for better organization: - test_column_controller.py -> tests/core/ - test_configurations.py -> tests/core/ - test_connections.py -> tests/core/test_connections_repository.py - test_session.py -> tests/core/ (with additional tests) - test_engines_init.py -> tests/core/ - test_observables.py -> tests/core/ --- tests/core/test_column_controller.py | 244 +++++++++++++++++++ tests/core/test_configurations.py | 72 ++++++ tests/core/test_connections_repository.py | 270 ++++++++++++++++++++++ tests/core/test_session.py | 31 +++ 4 files changed, 617 insertions(+) create mode 100644 tests/core/test_column_controller.py create mode 100644 tests/core/test_configurations.py create mode 100644 tests/core/test_connections_repository.py diff --git a/tests/core/test_column_controller.py b/tests/core/test_column_controller.py new file mode 100644 index 0000000..7bfd050 --- /dev/null +++ b/tests/core/test_column_controller.py @@ -0,0 +1,244 @@ +import pytest +from unittest.mock import Mock, patch, call + +from structures.engines.sqlite.context import SQLiteContext +from structures.engines.sqlite.database import SQLiteDatabase, SQLiteTable, SQLiteIndex, SQLiteColumn +from windows.main.table.column import TableColumnsController + + +@pytest.fixture +def mock_session(): + connection = Mock() + session = Mock() + session.connection = connection + session.context = SQLiteContext(connection=connection) + return session + + +@pytest.fixture +def mock_table(mock_session): + database = SQLiteDatabase(id=1, name="test_db", context=mock_session.context) + table = SQLiteTable(id=1, name="test_table", database=database) + table.columns = Mock() + table.columns.get_value.return_value = [ + SQLiteColumn(id=1, name="col1", table=table, datatype=table.database.context.DATATYPE.VARCHAR), + SQLiteColumn(id=2, name="col2", table=table, datatype=table.database.context.DATATYPE.VARCHAR), + ] + table.indexes = Mock() + original_indexes = [ + SQLiteIndex(id=1, name="idx_test", type=table.database.context.INDEXTYPE.UNIQUE, columns=["col1"], table=table) + ] + table.indexes.get_value.return_value = original_indexes.copy() + table.indexes.__iter__ = Mock(return_value=iter(original_indexes.copy())) + table.indexes.append = Mock() + table.indexes.index = Mock(return_value=0) + table.copy = Mock(return_value=table) + return table + + +@patch('wx.GetApp') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') +def test_append_column_index(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): + # Setup mocks + mock_get_app.return_value = Mock() + mock_current_session.get_value.return_value = mock_session + mock_current_table.get_value.return_value = mock_table + mock_new_table.get_value.return_value = None + + # Mock the controller and its model + controller = TableColumnsController(Mock()) + controller.model = Mock() + controller.model.GetRow.return_value = 1 # second column + controller.model.data = mock_table.columns.get_value() + + # Mock selected item + selected = Mock() + selected.IsOk.return_value = True + + # Existing index + existing_index = mock_table.indexes.get_value()[0] + + # Call the method + result = controller.append_column_index(selected, existing_index) + + # Assertions + assert result is True + # Check that append was called + mock_table.indexes.append.assert_called_once_with(existing_index, replace_existing=True) + assert "col2" in existing_index.columns + mock_new_table.set_value.assert_called_once_with(mock_table) + + +@patch('wx.GetApp') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') +def test_on_column_insert(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): + # Setup mocks + mock_get_app.return_value = Mock() + mock_current_session.get_value.return_value = mock_session + mock_current_table.get_value.return_value = mock_table + mock_new_table.get_value.return_value = None + + # Mock the controller + list_ctrl = Mock() + controller = TableColumnsController(list_ctrl) + controller.model = Mock() + controller.model.GetItem.return_value = Mock() + controller._do_edit = Mock() + + # Mock selection + selected = Mock() + selected.IsOk.return_value = True + list_ctrl.GetSelection.return_value = selected + + # Mock get_data_by_item + current_column = mock_table.columns.get_value()[0] + controller.model.get_data_by_item.return_value = current_column + + # Mock build_empty_column + empty_column = SQLiteColumn(id=3, name="", table=mock_table, datatype=mock_table.database.context.DATATYPE.VARCHAR) + mock_session.context.build_empty_column = Mock(return_value=empty_column) + + # Mock table.columns + mock_table.columns.insert = Mock() + mock_table.columns.__len__ = Mock(return_value=2) + mock_table.columns.index = Mock(return_value=0) + + # Call the method + controller.on_column_insert(Mock()) + + # Assertions + mock_table.columns.insert.assert_called_once() + + +@patch('wx.GetApp') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') +def test_on_column_delete(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): + # Setup mocks + mock_get_app.return_value = Mock() + mock_current_session.get_value.return_value = mock_session + mock_current_table.get_value.return_value = mock_table + mock_new_table.get_value.return_value = None + + # Mock the controller + list_ctrl = Mock() + controller = TableColumnsController(list_ctrl) + controller.model = Mock() + + # Mock selection + selected = Mock() + selected.IsOk.return_value = True + list_ctrl.GetSelection.return_value = selected + + # Mock get_data_by_item + column_to_delete = mock_table.columns.get_value()[0] + controller.model.get_data_by_item.return_value = column_to_delete + + # Mock table.columns.remove + mock_table.columns.remove = Mock() + + # Mock indexes + index_with_column = Mock() + index_with_column.columns = Mock() + index_with_column.columns.__contains__ = Mock(return_value=True) + index_with_column.columns.remove = Mock() + index_with_column.columns.__len__ = Mock(return_value=0) # After remove, len=0 + index_without = Mock() + index_without.columns = Mock() + index_without.columns.__contains__ = Mock(return_value=False) + mock_table.indexes.get_value.return_value = [index_with_column, index_without] + mock_table.indexes.remove = Mock() + + # Call the method + controller.on_column_delete(Mock()) + + # Assertions + mock_table.columns.remove.assert_called_once() + mock_new_table.set_value.assert_called_once_with(mock_table) + + +@patch('wx.GetApp') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.CURRENT_COLUMN') +@patch('windows.main.table.column.NEW_TABLE') +def test_on_column_move_up(mock_new_table, mock_current_column, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): + # Setup mocks + mock_get_app.return_value = Mock() + mock_current_session.get_value.return_value = mock_session + mock_current_table.get_value.return_value = mock_table + mock_new_table.get_value.return_value = None + + # Mock the controller + list_ctrl = Mock() + controller = TableColumnsController(list_ctrl) + controller.model = Mock() + + # Mock selection + selected = Mock() + selected.IsOk.return_value = True + list_ctrl.GetSelection.return_value = selected + + # Mock model + selected_column = mock_table.columns.get_value()[1] # second column + controller.model.get_data_by_item.return_value = selected_column + controller.model.GetRow.return_value = 1 # selected row + controller.model.GetItem.return_value = Mock() # new item + + # Mock table.columns.move_up + mock_table.columns.move_up = Mock() + + # Mock list_ctrl.Select + list_ctrl.Select = Mock() + + # Call the method + controller.on_column_move_up(Mock()) + + # Assertions + mock_table.columns.move_up.assert_called_once_with(selected_column) + list_ctrl.Select.assert_called_once() + + +@patch('wx.GetApp') +@patch('windows.main.table.column.CURRENT_SESSION') +@patch('windows.main.table.column.CURRENT_TABLE') +@patch('windows.main.table.column.NEW_TABLE') +def test_insert_column_index(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): + # Setup mocks + mock_get_app.return_value = Mock() + mock_current_session.get_value.return_value = mock_session + mock_current_table.get_value.return_value = mock_table + mock_new_table.get_value.return_value = None + + # Mock the controller + list_ctrl = Mock() + controller = TableColumnsController(list_ctrl) + controller.model = Mock() + + # Mock selection + selected = Mock() + selected.IsOk.return_value = True + list_ctrl.GetSelection.return_value = selected + + # Mock model + selected_column = mock_table.columns.get_value()[0] + controller.model.get_data_by_item.return_value = selected_column + controller.model.GetRow.return_value = 0 + controller.model.data = mock_table.columns.get_value() + + # Mock table.indexes.append and build_empty_index + mock_table.indexes.append = Mock() + mock_table.indexes.__iter__ = Mock(return_value=iter([])) # No existing indexes + mock_session.context.build_empty_index = Mock(return_value=Mock()) + + # Call the method + controller.insert_column_index(selected, mock_session.context.INDEXTYPE.UNIQUE) + + # Assertions + mock_table.indexes.append.assert_called_once() + mock_new_table.set_value.assert_called_once_with(mock_table) diff --git a/tests/core/test_configurations.py b/tests/core/test_configurations.py new file mode 100644 index 0000000..d37f662 --- /dev/null +++ b/tests/core/test_configurations.py @@ -0,0 +1,72 @@ +import pytest + +from structures.configurations import ( + CredentialsConfiguration, + SourceConfiguration, + SSHTunnelConfiguration, +) + + +class TestConfigurations: + def test_credentials_configuration(self): + config = CredentialsConfiguration( + hostname="localhost", username="user", password="pass", port=3306 + ) + assert config.hostname == "localhost" + assert config.username == "user" + assert config.password == "pass" + assert config.port == 3306 + + def test_source_configuration(self): + config = SourceConfiguration(filename="/path/to/db.sqlite") + assert config.filename == "/path/to/db.sqlite" + + def test_ssh_tunnel_configuration(self): + config = SSHTunnelConfiguration( + enabled=True, + executable="ssh", + hostname="remote.host", + port=22, + username="sshuser", + password="sshpwd", + local_port=3307, + ) + assert config.enabled == True + assert config.executable == "ssh" + assert config.hostname == "remote.host" + assert config.port == 22 + assert config.username == "sshuser" + assert config.password == "sshpwd" + assert config.local_port == 3307 + assert config.is_enabled == True + + def test_ssh_tunnel_configuration_disabled(self): + config = SSHTunnelConfiguration( + enabled=False, + executable="ssh", + hostname="remote.host", + port=22, + username=None, + password=None, + local_port=3307, + ) + assert config.is_enabled == False + + def test_ssh_tunnel_configuration_supports_remote_target_and_identity(self): + config = SSHTunnelConfiguration( + enabled=True, + executable="ssh", + hostname="bastion.example.com", + port=22, + username="sshuser", + password=None, + local_port=0, + remote_host="db.internal", + remote_port=3306, + identity_file="/home/user/.ssh/id_ed25519", + ) + + assert config.is_enabled is True + assert config.remote_host == "db.internal" + assert config.remote_port == 3306 + assert config.identity_file == "/home/user/.ssh/id_ed25519" diff --git a/tests/core/test_connections_repository.py b/tests/core/test_connections_repository.py new file mode 100644 index 0000000..9289684 --- /dev/null +++ b/tests/core/test_connections_repository.py @@ -0,0 +1,270 @@ +import os +import tempfile +import pytest +import yaml + +from structures.connection import Connection, ConnectionEngine, ConnectionDirectory +from structures.configurations import ( + CredentialsConfiguration, + SourceConfiguration, + SSHTunnelConfiguration, +) +from windows.dialogs.connections.repository import ConnectionsRepository + + +class TestConnectionsRepository: + @pytest.fixture + def temp_yaml(self): + """Create a temporary YAML file path for testing.""" + import tempfile + import os + + with tempfile.NamedTemporaryFile(mode="w+", suffix=".yml", delete=False) as tmp: + tmp_path = tmp.name + yield tmp_path + os.unlink(tmp_path) + + @pytest.fixture + def repo(self, temp_yaml, monkeypatch): + """Create a ConnectionsRepository instance with a temporary YAML file.""" + return ConnectionsRepository(config_file=str(temp_yaml)) + + def test_load_empty_yaml(self, temp_yaml, repo): + """Test loading from an empty or non-existent YAML file.""" + # Ensure file doesn't exist or is empty + with open(temp_yaml, "w") as f: + f.write("") + + connections = repo.load() + assert connections == [] + + def test_load_connections_from_yaml(self, temp_yaml, repo): + """Test loading connections from YAML.""" + data = [ + { + "id": 1, + "name": "Test SQLite", + "engine": "SQLite", + "configuration": {"filename": ":memory:"}, + "comments": "Test connection", + }, + { + "id": 2, + "name": "Test MySQL", + "engine": "MySQL", + "configuration": { + "hostname": "localhost", + "port": 3306, + "username": "user", + "password": "pass", + }, + "ssh_tunnel": { + "enabled": True, + "hostname": "remote.host", + "port": 22, + "username": "sshuser", + "password": "sshpass", + "local_port": 3307, + }, + }, + ] + with open(temp_yaml, "w") as f: + yaml.dump(data, f) + + connections = repo.load() + assert len(connections) == 2 + + # Check first connection + conn1 = connections[0] + assert conn1.id == 1 + assert conn1.name == "Test SQLite" + assert conn1.engine == ConnectionEngine.SQLITE + assert isinstance(conn1.configuration, SourceConfiguration) + assert conn1.configuration.filename == ":memory:" + assert conn1.comments == "Test connection" + + # Check second connection + conn2 = connections[1] + assert conn2.id == 2 + assert conn2.name == "Test MySQL" + assert conn2.engine == ConnectionEngine.MYSQL + assert isinstance(conn2.configuration, CredentialsConfiguration) + assert conn2.configuration.hostname == "localhost" + assert conn2.configuration.port == 3306 + assert conn2.configuration.username == "user" + assert conn2.configuration.password == "pass" + assert conn2.ssh_tunnel.enabled is True + assert conn2.ssh_tunnel.hostname == "remote.host" + + def test_load_directories_from_yaml(self, temp_yaml, repo): + """Test loading directories with nested connections.""" + data = [ + { + "type": "directory", + "name": "Production", + "children": [ + { + "id": 1, + "name": "Prod DB", + "engine": "PostgreSQL", + "configuration": { + "hostname": "prod.example.com", + "port": 5432, + "username": "produser", + "password": "prodpass", + }, + } + ], + }, + { + "type": "directory", + "name": "Development", + "children": [ + { + "id": 2, + "name": "Dev DB", + "engine": "SQLite", + "configuration": {"filename": "dev.db"}, + } + ], + }, + ] + with open(temp_yaml, "w") as f: + yaml.dump(data, f) + + items = repo.load() + assert len(items) == 2 + + # Check first directory + dir1 = items[0] + assert isinstance(dir1, ConnectionDirectory) + assert dir1.name == "Production" + assert len(dir1.children) == 1 + + conn = dir1.children[0] + assert conn.name == "Prod DB" + assert conn.engine == ConnectionEngine.POSTGRESQL + + # Check second directory + dir2 = items[1] + assert isinstance(dir2, ConnectionDirectory) + assert dir2.name == "Development" + assert len(dir2.children) == 1 + + conn2 = dir2.children[0] + assert conn2.name == "Dev DB" + assert conn2.engine == ConnectionEngine.SQLITE + + def test_add_connection(self, temp_yaml, repo): + """Test adding a new connection.""" + config = SourceConfiguration(filename="test.db") + connection = Connection( + id=0, + name="New Connection", + engine=ConnectionEngine.SQLITE, + configuration=config, + comments="Added connection", + ) + conn_id = repo.add_connection(connection) + assert conn_id == 0 + + # Check YAML + with open(temp_yaml, "r") as f: + data = yaml.safe_load(f) + assert len(data) == 1 + assert data[0]["name"] == "New Connection" + assert data[0]["id"] == 0 + + def test_save_connection(self, temp_yaml, repo): + """Test saving/updating an existing connection.""" + # Start with a connection + data = [ + { + "id": 1, + "name": "Original Name", + "engine": "SQLite", + "configuration": {"filename": ":memory:"}, + } + ] + with open(temp_yaml, "w") as f: + yaml.dump(data, f) + + # Load and modify + connections = repo.load() + conn = connections[0] + conn.name = "Updated Name" + + # Save + repo.save_connection(conn) + + # Check YAML was updated + with open(temp_yaml, "r") as f: + updated_data = yaml.safe_load(f) + assert updated_data[0]["name"] == "Updated Name" + + def test_delete_connection(self, temp_yaml, repo): + """Test deleting a connection.""" + # Start with connections + data = [ + { + "id": 1, + "name": "Conn1", + "engine": "SQLite", + "configuration": {"filename": "db1.db"}, + }, + { + "id": 2, + "name": "Conn2", + "engine": "SQLite", + "configuration": {"filename": "db2.db"}, + }, + ] + with open(temp_yaml, "w") as f: + yaml.dump(data, f) + + # Load and delete first connection + connections = repo.load() + conn_to_delete = connections[0] + repo.delete_connection(conn_to_delete) + + # Check only one connection remains + with open(temp_yaml, "r") as f: + updated_data = yaml.safe_load(f) + assert len(updated_data) == 1 + assert updated_data[0]["name"] == "Conn2" + + def test_add_directory(self, temp_yaml, repo): + """Test adding a new directory.""" + with open(temp_yaml, "w") as f: + f.write("[]") + + directory = ConnectionDirectory(id=-1, name="New Directory") + + repo.add_directory(directory) + + # Check YAML + with open(temp_yaml, "r") as f: + data = yaml.safe_load(f) + assert len(data) == 1 + assert data[0]["type"] == "directory" + assert data[0]["name"] == "New Directory" + + def test_delete_directory(self, temp_yaml, repo): + """Test deleting a directory.""" + data = [ + {"type": "directory", "name": "Dir1", "children": []}, + {"type": "directory", "name": "Dir2", "children": []}, + ] + 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) + + # 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" 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 From f64bcfd0d1cdd4c8329df8b1d97d98a3e0672f95 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 4 Jun 2026 13:54:20 +0200 Subject: [PATCH 68/93] chore(tests): remove old test files after core/ migration Delete obsolete test files that were moved to tests/core/: - tests/test_column_controller.py - tests/test_configurations.py - tests/test_connections.py - tests/test_engines_init.py - tests/test_observables.py - tests/test_session.py Add tests/autocomplete/__init__.py for package structure. --- tests/autocomplete/__init__.py | 0 tests/test_column_controller.py | 244 ----------------------------- tests/test_configurations.py | 72 --------- tests/test_connections.py | 270 -------------------------------- tests/test_engines_init.py | 53 ------- tests/test_observables.py | 73 --------- tests/test_session.py | 77 --------- 7 files changed, 789 deletions(-) create mode 100644 tests/autocomplete/__init__.py delete mode 100644 tests/test_column_controller.py delete mode 100644 tests/test_configurations.py delete mode 100644 tests/test_connections.py delete mode 100644 tests/test_engines_init.py delete mode 100644 tests/test_observables.py delete mode 100644 tests/test_session.py diff --git a/tests/autocomplete/__init__.py b/tests/autocomplete/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_column_controller.py b/tests/test_column_controller.py deleted file mode 100644 index 7bfd050..0000000 --- a/tests/test_column_controller.py +++ /dev/null @@ -1,244 +0,0 @@ -import pytest -from unittest.mock import Mock, patch, call - -from structures.engines.sqlite.context import SQLiteContext -from structures.engines.sqlite.database import SQLiteDatabase, SQLiteTable, SQLiteIndex, SQLiteColumn -from windows.main.table.column import TableColumnsController - - -@pytest.fixture -def mock_session(): - connection = Mock() - session = Mock() - session.connection = connection - session.context = SQLiteContext(connection=connection) - return session - - -@pytest.fixture -def mock_table(mock_session): - database = SQLiteDatabase(id=1, name="test_db", context=mock_session.context) - table = SQLiteTable(id=1, name="test_table", database=database) - table.columns = Mock() - table.columns.get_value.return_value = [ - SQLiteColumn(id=1, name="col1", table=table, datatype=table.database.context.DATATYPE.VARCHAR), - SQLiteColumn(id=2, name="col2", table=table, datatype=table.database.context.DATATYPE.VARCHAR), - ] - table.indexes = Mock() - original_indexes = [ - SQLiteIndex(id=1, name="idx_test", type=table.database.context.INDEXTYPE.UNIQUE, columns=["col1"], table=table) - ] - table.indexes.get_value.return_value = original_indexes.copy() - table.indexes.__iter__ = Mock(return_value=iter(original_indexes.copy())) - table.indexes.append = Mock() - table.indexes.index = Mock(return_value=0) - table.copy = Mock(return_value=table) - return table - - -@patch('wx.GetApp') -@patch('windows.main.table.column.CURRENT_SESSION') -@patch('windows.main.table.column.CURRENT_TABLE') -@patch('windows.main.table.column.NEW_TABLE') -def test_append_column_index(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): - # Setup mocks - mock_get_app.return_value = Mock() - mock_current_session.get_value.return_value = mock_session - mock_current_table.get_value.return_value = mock_table - mock_new_table.get_value.return_value = None - - # Mock the controller and its model - controller = TableColumnsController(Mock()) - controller.model = Mock() - controller.model.GetRow.return_value = 1 # second column - controller.model.data = mock_table.columns.get_value() - - # Mock selected item - selected = Mock() - selected.IsOk.return_value = True - - # Existing index - existing_index = mock_table.indexes.get_value()[0] - - # Call the method - result = controller.append_column_index(selected, existing_index) - - # Assertions - assert result is True - # Check that append was called - mock_table.indexes.append.assert_called_once_with(existing_index, replace_existing=True) - assert "col2" in existing_index.columns - mock_new_table.set_value.assert_called_once_with(mock_table) - - -@patch('wx.GetApp') -@patch('windows.main.table.column.CURRENT_SESSION') -@patch('windows.main.table.column.CURRENT_TABLE') -@patch('windows.main.table.column.NEW_TABLE') -def test_on_column_insert(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): - # Setup mocks - mock_get_app.return_value = Mock() - mock_current_session.get_value.return_value = mock_session - mock_current_table.get_value.return_value = mock_table - mock_new_table.get_value.return_value = None - - # Mock the controller - list_ctrl = Mock() - controller = TableColumnsController(list_ctrl) - controller.model = Mock() - controller.model.GetItem.return_value = Mock() - controller._do_edit = Mock() - - # Mock selection - selected = Mock() - selected.IsOk.return_value = True - list_ctrl.GetSelection.return_value = selected - - # Mock get_data_by_item - current_column = mock_table.columns.get_value()[0] - controller.model.get_data_by_item.return_value = current_column - - # Mock build_empty_column - empty_column = SQLiteColumn(id=3, name="", table=mock_table, datatype=mock_table.database.context.DATATYPE.VARCHAR) - mock_session.context.build_empty_column = Mock(return_value=empty_column) - - # Mock table.columns - mock_table.columns.insert = Mock() - mock_table.columns.__len__ = Mock(return_value=2) - mock_table.columns.index = Mock(return_value=0) - - # Call the method - controller.on_column_insert(Mock()) - - # Assertions - mock_table.columns.insert.assert_called_once() - - -@patch('wx.GetApp') -@patch('windows.main.table.column.CURRENT_SESSION') -@patch('windows.main.table.column.CURRENT_TABLE') -@patch('windows.main.table.column.NEW_TABLE') -def test_on_column_delete(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): - # Setup mocks - mock_get_app.return_value = Mock() - mock_current_session.get_value.return_value = mock_session - mock_current_table.get_value.return_value = mock_table - mock_new_table.get_value.return_value = None - - # Mock the controller - list_ctrl = Mock() - controller = TableColumnsController(list_ctrl) - controller.model = Mock() - - # Mock selection - selected = Mock() - selected.IsOk.return_value = True - list_ctrl.GetSelection.return_value = selected - - # Mock get_data_by_item - column_to_delete = mock_table.columns.get_value()[0] - controller.model.get_data_by_item.return_value = column_to_delete - - # Mock table.columns.remove - mock_table.columns.remove = Mock() - - # Mock indexes - index_with_column = Mock() - index_with_column.columns = Mock() - index_with_column.columns.__contains__ = Mock(return_value=True) - index_with_column.columns.remove = Mock() - index_with_column.columns.__len__ = Mock(return_value=0) # After remove, len=0 - index_without = Mock() - index_without.columns = Mock() - index_without.columns.__contains__ = Mock(return_value=False) - mock_table.indexes.get_value.return_value = [index_with_column, index_without] - mock_table.indexes.remove = Mock() - - # Call the method - controller.on_column_delete(Mock()) - - # Assertions - mock_table.columns.remove.assert_called_once() - mock_new_table.set_value.assert_called_once_with(mock_table) - - -@patch('wx.GetApp') -@patch('windows.main.table.column.CURRENT_SESSION') -@patch('windows.main.table.column.CURRENT_TABLE') -@patch('windows.main.table.column.CURRENT_COLUMN') -@patch('windows.main.table.column.NEW_TABLE') -def test_on_column_move_up(mock_new_table, mock_current_column, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): - # Setup mocks - mock_get_app.return_value = Mock() - mock_current_session.get_value.return_value = mock_session - mock_current_table.get_value.return_value = mock_table - mock_new_table.get_value.return_value = None - - # Mock the controller - list_ctrl = Mock() - controller = TableColumnsController(list_ctrl) - controller.model = Mock() - - # Mock selection - selected = Mock() - selected.IsOk.return_value = True - list_ctrl.GetSelection.return_value = selected - - # Mock model - selected_column = mock_table.columns.get_value()[1] # second column - controller.model.get_data_by_item.return_value = selected_column - controller.model.GetRow.return_value = 1 # selected row - controller.model.GetItem.return_value = Mock() # new item - - # Mock table.columns.move_up - mock_table.columns.move_up = Mock() - - # Mock list_ctrl.Select - list_ctrl.Select = Mock() - - # Call the method - controller.on_column_move_up(Mock()) - - # Assertions - mock_table.columns.move_up.assert_called_once_with(selected_column) - list_ctrl.Select.assert_called_once() - - -@patch('wx.GetApp') -@patch('windows.main.table.column.CURRENT_SESSION') -@patch('windows.main.table.column.CURRENT_TABLE') -@patch('windows.main.table.column.NEW_TABLE') -def test_insert_column_index(mock_new_table, mock_current_table, mock_current_session, mock_get_app, mock_session, mock_table): - # Setup mocks - mock_get_app.return_value = Mock() - mock_current_session.get_value.return_value = mock_session - mock_current_table.get_value.return_value = mock_table - mock_new_table.get_value.return_value = None - - # Mock the controller - list_ctrl = Mock() - controller = TableColumnsController(list_ctrl) - controller.model = Mock() - - # Mock selection - selected = Mock() - selected.IsOk.return_value = True - list_ctrl.GetSelection.return_value = selected - - # Mock model - selected_column = mock_table.columns.get_value()[0] - controller.model.get_data_by_item.return_value = selected_column - controller.model.GetRow.return_value = 0 - controller.model.data = mock_table.columns.get_value() - - # Mock table.indexes.append and build_empty_index - mock_table.indexes.append = Mock() - mock_table.indexes.__iter__ = Mock(return_value=iter([])) # No existing indexes - mock_session.context.build_empty_index = Mock(return_value=Mock()) - - # Call the method - controller.insert_column_index(selected, mock_session.context.INDEXTYPE.UNIQUE) - - # Assertions - mock_table.indexes.append.assert_called_once() - mock_new_table.set_value.assert_called_once_with(mock_table) diff --git a/tests/test_configurations.py b/tests/test_configurations.py deleted file mode 100644 index d37f662..0000000 --- a/tests/test_configurations.py +++ /dev/null @@ -1,72 +0,0 @@ -import pytest - -from structures.configurations import ( - CredentialsConfiguration, - SourceConfiguration, - SSHTunnelConfiguration, -) - - -class TestConfigurations: - def test_credentials_configuration(self): - config = CredentialsConfiguration( - hostname="localhost", username="user", password="pass", port=3306 - ) - assert config.hostname == "localhost" - assert config.username == "user" - assert config.password == "pass" - assert config.port == 3306 - - def test_source_configuration(self): - config = SourceConfiguration(filename="/path/to/db.sqlite") - assert config.filename == "/path/to/db.sqlite" - - def test_ssh_tunnel_configuration(self): - config = SSHTunnelConfiguration( - enabled=True, - executable="ssh", - hostname="remote.host", - port=22, - username="sshuser", - password="sshpwd", - local_port=3307, - ) - assert config.enabled == True - assert config.executable == "ssh" - assert config.hostname == "remote.host" - assert config.port == 22 - assert config.username == "sshuser" - assert config.password == "sshpwd" - assert config.local_port == 3307 - assert config.is_enabled == True - - def test_ssh_tunnel_configuration_disabled(self): - config = SSHTunnelConfiguration( - enabled=False, - executable="ssh", - hostname="remote.host", - port=22, - username=None, - password=None, - local_port=3307, - ) - assert config.is_enabled == False - - def test_ssh_tunnel_configuration_supports_remote_target_and_identity(self): - config = SSHTunnelConfiguration( - enabled=True, - executable="ssh", - hostname="bastion.example.com", - port=22, - username="sshuser", - password=None, - local_port=0, - remote_host="db.internal", - remote_port=3306, - identity_file="/home/user/.ssh/id_ed25519", - ) - - assert config.is_enabled is True - assert config.remote_host == "db.internal" - assert config.remote_port == 3306 - assert config.identity_file == "/home/user/.ssh/id_ed25519" diff --git a/tests/test_connections.py b/tests/test_connections.py deleted file mode 100644 index 9289684..0000000 --- a/tests/test_connections.py +++ /dev/null @@ -1,270 +0,0 @@ -import os -import tempfile -import pytest -import yaml - -from structures.connection import Connection, ConnectionEngine, ConnectionDirectory -from structures.configurations import ( - CredentialsConfiguration, - SourceConfiguration, - SSHTunnelConfiguration, -) -from windows.dialogs.connections.repository import ConnectionsRepository - - -class TestConnectionsRepository: - @pytest.fixture - def temp_yaml(self): - """Create a temporary YAML file path for testing.""" - import tempfile - import os - - with tempfile.NamedTemporaryFile(mode="w+", suffix=".yml", delete=False) as tmp: - tmp_path = tmp.name - yield tmp_path - os.unlink(tmp_path) - - @pytest.fixture - def repo(self, temp_yaml, monkeypatch): - """Create a ConnectionsRepository instance with a temporary YAML file.""" - return ConnectionsRepository(config_file=str(temp_yaml)) - - def test_load_empty_yaml(self, temp_yaml, repo): - """Test loading from an empty or non-existent YAML file.""" - # Ensure file doesn't exist or is empty - with open(temp_yaml, "w") as f: - f.write("") - - connections = repo.load() - assert connections == [] - - def test_load_connections_from_yaml(self, temp_yaml, repo): - """Test loading connections from YAML.""" - data = [ - { - "id": 1, - "name": "Test SQLite", - "engine": "SQLite", - "configuration": {"filename": ":memory:"}, - "comments": "Test connection", - }, - { - "id": 2, - "name": "Test MySQL", - "engine": "MySQL", - "configuration": { - "hostname": "localhost", - "port": 3306, - "username": "user", - "password": "pass", - }, - "ssh_tunnel": { - "enabled": True, - "hostname": "remote.host", - "port": 22, - "username": "sshuser", - "password": "sshpass", - "local_port": 3307, - }, - }, - ] - with open(temp_yaml, "w") as f: - yaml.dump(data, f) - - connections = repo.load() - assert len(connections) == 2 - - # Check first connection - conn1 = connections[0] - assert conn1.id == 1 - assert conn1.name == "Test SQLite" - assert conn1.engine == ConnectionEngine.SQLITE - assert isinstance(conn1.configuration, SourceConfiguration) - assert conn1.configuration.filename == ":memory:" - assert conn1.comments == "Test connection" - - # Check second connection - conn2 = connections[1] - assert conn2.id == 2 - assert conn2.name == "Test MySQL" - assert conn2.engine == ConnectionEngine.MYSQL - assert isinstance(conn2.configuration, CredentialsConfiguration) - assert conn2.configuration.hostname == "localhost" - assert conn2.configuration.port == 3306 - assert conn2.configuration.username == "user" - assert conn2.configuration.password == "pass" - assert conn2.ssh_tunnel.enabled is True - assert conn2.ssh_tunnel.hostname == "remote.host" - - def test_load_directories_from_yaml(self, temp_yaml, repo): - """Test loading directories with nested connections.""" - data = [ - { - "type": "directory", - "name": "Production", - "children": [ - { - "id": 1, - "name": "Prod DB", - "engine": "PostgreSQL", - "configuration": { - "hostname": "prod.example.com", - "port": 5432, - "username": "produser", - "password": "prodpass", - }, - } - ], - }, - { - "type": "directory", - "name": "Development", - "children": [ - { - "id": 2, - "name": "Dev DB", - "engine": "SQLite", - "configuration": {"filename": "dev.db"}, - } - ], - }, - ] - with open(temp_yaml, "w") as f: - yaml.dump(data, f) - - items = repo.load() - assert len(items) == 2 - - # Check first directory - dir1 = items[0] - assert isinstance(dir1, ConnectionDirectory) - assert dir1.name == "Production" - assert len(dir1.children) == 1 - - conn = dir1.children[0] - assert conn.name == "Prod DB" - assert conn.engine == ConnectionEngine.POSTGRESQL - - # Check second directory - dir2 = items[1] - assert isinstance(dir2, ConnectionDirectory) - assert dir2.name == "Development" - assert len(dir2.children) == 1 - - conn2 = dir2.children[0] - assert conn2.name == "Dev DB" - assert conn2.engine == ConnectionEngine.SQLITE - - def test_add_connection(self, temp_yaml, repo): - """Test adding a new connection.""" - config = SourceConfiguration(filename="test.db") - connection = Connection( - id=0, - name="New Connection", - engine=ConnectionEngine.SQLITE, - configuration=config, - comments="Added connection", - ) - conn_id = repo.add_connection(connection) - assert conn_id == 0 - - # Check YAML - with open(temp_yaml, "r") as f: - data = yaml.safe_load(f) - assert len(data) == 1 - assert data[0]["name"] == "New Connection" - assert data[0]["id"] == 0 - - def test_save_connection(self, temp_yaml, repo): - """Test saving/updating an existing connection.""" - # Start with a connection - data = [ - { - "id": 1, - "name": "Original Name", - "engine": "SQLite", - "configuration": {"filename": ":memory:"}, - } - ] - with open(temp_yaml, "w") as f: - yaml.dump(data, f) - - # Load and modify - connections = repo.load() - conn = connections[0] - conn.name = "Updated Name" - - # Save - repo.save_connection(conn) - - # Check YAML was updated - with open(temp_yaml, "r") as f: - updated_data = yaml.safe_load(f) - assert updated_data[0]["name"] == "Updated Name" - - def test_delete_connection(self, temp_yaml, repo): - """Test deleting a connection.""" - # Start with connections - data = [ - { - "id": 1, - "name": "Conn1", - "engine": "SQLite", - "configuration": {"filename": "db1.db"}, - }, - { - "id": 2, - "name": "Conn2", - "engine": "SQLite", - "configuration": {"filename": "db2.db"}, - }, - ] - with open(temp_yaml, "w") as f: - yaml.dump(data, f) - - # Load and delete first connection - connections = repo.load() - conn_to_delete = connections[0] - repo.delete_connection(conn_to_delete) - - # Check only one connection remains - with open(temp_yaml, "r") as f: - updated_data = yaml.safe_load(f) - assert len(updated_data) == 1 - assert updated_data[0]["name"] == "Conn2" - - def test_add_directory(self, temp_yaml, repo): - """Test adding a new directory.""" - with open(temp_yaml, "w") as f: - f.write("[]") - - directory = ConnectionDirectory(id=-1, name="New Directory") - - repo.add_directory(directory) - - # Check YAML - with open(temp_yaml, "r") as f: - data = yaml.safe_load(f) - assert len(data) == 1 - assert data[0]["type"] == "directory" - assert data[0]["name"] == "New Directory" - - def test_delete_directory(self, temp_yaml, repo): - """Test deleting a directory.""" - data = [ - {"type": "directory", "name": "Dir1", "children": []}, - {"type": "directory", "name": "Dir2", "children": []}, - ] - 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) - - # 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" 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 From be2a88afeac3cba9a37cd349538df4ef0e427726 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 4 Jun 2026 13:54:57 +0200 Subject: [PATCH 69/93] refactor(tests): rewrite test runner with subcommand interface Replace --suite/--update/--refresh-screenshots flags with subcommands: - unit: run tests/core/ - autocomplete: run tests/autocomplete/ - integration: run integration tests - ui: run UI tests with screenshot refresh Default behavior (no args) now runs all tests and updates README badges. --- scripts/runtest.py | 410 ++++++++++++++++----------------------------- 1 file changed, 143 insertions(+), 267 deletions(-) diff --git a/scripts/runtest.py b/scripts/runtest.py index 09a3718..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,308 +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, - ) + with open(README, "w") as f: + f.write(content) + print(" README.md updated") + except IOError as e: + print(f" Error updating README: {e}") - print(f" Tests badge updated: {tests_total}") - content = _update_suite_badges_block(content, suite_stats) - print(" Suite matrix badges updated") - - # Update engine badges - - 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) - - print("\nREADME.md updated") - - except IOError as e: - print(f"Error updating README: {e}") - - # Cleanup - try: - os.remove(RESULTS_FILE) - except OSError: - pass +# --------------------------------------------------------------------------- +# Runner +# --------------------------------------------------------------------------- +def _run(cmd: list[str], capture: bool = False) -> int: + if not capture: + return subprocess.run(cmd).returncode try: - os.remove(JUNIT_FILE) - except OSError: - pass - - -def main(): - parser = argparse.ArgumentParser(description='Unified test runner') - parser.add_argument( - '--suite', - choices=['unit', 'integration', 'ui', 'all'], - default='unit', - help='Select test suite: unit, integration, ui, or all (default: unit)', - ) - parser.add_argument( - '--engine', - choices=[engine.value.name.lower() for engine in ConnectionEngine], - help='Filter tests by engine suite (tests/engines//)', + 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.", ) - parser.add_argument('--update', action='store_true', - help='Run all tests (unit + integration) and update README badges') - parser.add_argument( - '--refresh-screenshots', - action='store_true', - help='Refresh UI scenario screenshots (available with --suite ui)', - ) - - args = parser.parse_args() + sub = parser.add_subparsers(dest="suite", metavar="suite") - if args.suite == 'ui': - tests_target = 'tests/ui/test_scenarios.py' - else: - tests_target = f"tests/engines/{args.engine}/" if args.engine else 'tests/' + sub.add_parser("unit", help="Unit tests only (tests/core/)") - pytest_command = ['uv', 'run', 'pytest', tests_target] + ac = sub.add_parser("autocomplete", help="Autocomplete golden-case tests") + ac.add_argument("--engine", choices=engine_choices, help="Restrict to one engine") - if args.suite == 'unit': - pytest_command.extend(['--tb=short', '-m', 'not integration']) - elif args.suite == 'integration': - pytest_command.extend(['--tb=short', '-m', 'integration']) - elif args.suite == 'ui': - pytest_command.extend(['--tb=short', '-n', '1']) - else: - pytest_command.extend(['--tb=no']) - - if args.refresh_screenshots and args.suite != 'ui': - print("Error: --refresh-screenshots requires --suite ui") - return 2 - - if args.refresh_screenshots: - pytest_command.append('--refresh-screenshots') - - if args.update and args.suite != 'all': - print("Error: --update requires --suite all") - return 2 - - if 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: - update_pytest_command = [ - 'uv', - 'run', - 'pytest', - tests_target, - '--tb=no', - '--junitxml', - JUNIT_FILE, - ] - process = subprocess.Popen( - update_pytest_command, - 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 + it = sub.add_parser("integration", help="Integration tests only") + it.add_argument("--engine", choices=engine_choices, help="Restrict to one engine") - else: - suite_name = args.suite.upper() if args.suite == 'all' else args.suite - engine_info = f" for engine '{args.engine}'" if args.engine else "" - print(f"Running {suite_name} tests{engine_info}...") - result = subprocess.run(pytest_command) - exit_code = result.returncode + sub.add_parser("ui", help="UI tests (always refreshes screenshots)") - print(f"\nLocal tests completed") - print("\nNote: Use --suite integration or --suite all to include integration tests. Use --update --suite all to update badges.") + args = parser.parse_args() + suite = args.suite + engine = getattr(args, "engine", None) + + if suite == "unit": + print("Running unit tests...") + exit_code = _run([ + "uv", "run", "pytest", "tests/core/", + "--tb=short", + ]) + + 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", + ]) + + 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", + ]) - print(f"\nDone. Pytest exit code: {exit_code}") + 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()) From 086e14a75a8a88ad20b0390ed724591bebd3b641 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 4 Jun 2026 13:55:50 +0200 Subject: [PATCH 70/93] fix(ui): update controller toolbar enable logic and screenshot path - Fix _on_current_index to use m_toolBar12.EnableTool() instead of btn_delete_index.Enable() - Fix _on_current_foreign_key to use m_toolBar121.EnableTool() instead of btn_delete_foreign_key.Enable() - Remove unused btn_delete_procedure.Enable() call - Fix capture_window_screenshot to use str(target_path) for SaveFile() --- tests/ui/scenario_helpers.py | 2 +- windows/main/controller.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/ui/scenario_helpers.py b/tests/ui/scenario_helpers.py index b19388b..48df2ce 100644 --- a/tests/ui/scenario_helpers.py +++ b/tests/ui/scenario_helpers.py @@ -181,4 +181,4 @@ def capture_window_screenshot(window: wx.TopLevelWindow, target_path: Path) -> N finally: memory_dc.SelectObject(wx.NullBitmap) - bitmap.SaveFile(target_path, wx.BITMAP_TYPE_PNG) + bitmap.SaveFile(str(target_path), wx.BITMAP_TYPE_PNG) diff --git a/windows/main/controller.py b/windows/main/controller.py index 9a6e9a3..bdcd980 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -1586,9 +1586,6 @@ def _on_current_procedure(self, current: SQLProcedure): ) self.toggle_panel(current) - can_act = current is not None and not current.is_new - self.btn_delete_procedure.Enable(can_act) - def on_insert_procedure(self): session = CURRENT_SESSION.get_value() database = CURRENT_DATABASE.get_value() @@ -1889,7 +1886,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() @@ -1899,7 +1896,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) From 242c3ee5f67e0f21b3da32fa2a2271f4f90e2590 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 4 Jun 2026 16:41:56 +0200 Subject: [PATCH 71/93] fix(schema): correct equality comparisons in SQLDatabase and SQLForeignKey --- structures/engines/database.py | 14 +++- tests/engines/base_database_tests.py | 22 +++++- tests/engines/base_foreignkey_tests.py | 102 +++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/structures/engines/database.py b/structures/engines/database.py index 54c337e..84e39fd 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 @@ -548,7 +556,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 ]): diff --git a/tests/engines/base_database_tests.py b/tests/engines/base_database_tests.py index a703367..c1b1e9c 100644 --- a/tests/engines/base_database_tests.py +++ b/tests/engines/base_database_tests.py @@ -116,4 +116,24 @@ def test_database_create_returns_false(self, session, database): assert new_database.create() is False def test_database_alter_returns_false(self, database): - assert database.alter() is False \ No newline at end of file + assert database.alter() is False + + 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 From fde086fd9805ab046ba33043f280e7b8c466ea9a Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 4 Jun 2026 16:58:14 +0200 Subject: [PATCH 72/93] fix(sqlite): correct SQLiteColumn.drop signature and implementation --- structures/engines/sqlite/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structures/engines/sqlite/database.py b/structures/engines/sqlite/database.py index 756eb27..0e29ecd 100644 --- a/structures/engines/sqlite/database.py +++ b/structures/engines/sqlite/database.py @@ -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) From a2e3d1f08e15696bf285da84340f0ed7cf6d2011 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 4 Jun 2026 17:21:34 +0200 Subject: [PATCH 73/93] chore(release): bump VERSION to 0.1.0 to match pyproject.toml --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 885113911bf9a7b3e620a1bf89adb37f4644be5b Mon Sep 17 00:00:00 2001 From: gtripoli Date: Thu, 4 Jun 2026 17:34:00 +0200 Subject: [PATCH 74/93] fix(sqlite): correct SQLiteColumn.modify signature to accept current parameter --- structures/engines/sqlite/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structures/engines/sqlite/database.py b/structures/engines/sqlite/database.py index 0e29ecd..d8397ab 100644 --- a/structures/engines/sqlite/database.py +++ b/structures/engines/sqlite/database.py @@ -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: From 18c0a254212b4854e76fcfa69d716368e14a1587 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Fri, 5 Jun 2026 16:48:06 +0200 Subject: [PATCH 75/93] fix(sqlite): raise explicit errors for database file operations --- structures/engines/sqlite/database.py | 6 +++--- tests/engines/base_database_tests.py | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/structures/engines/sqlite/database.py b/structures/engines/sqlite/database.py index d8397ab..e72b805 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) diff --git a/tests/engines/base_database_tests.py b/tests/engines/base_database_tests.py index c1b1e9c..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,14 +110,26 @@ 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 + 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.""" From 3a921b59c6b7072eafb21d1da9a27f8a804c7567 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Fri, 5 Jun 2026 16:54:19 +0200 Subject: [PATCH 76/93] fix(sqlite): replace record asserts and bare excepts --- structures/engines/sqlite/database.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/structures/engines/sqlite/database.py b/structures/engines/sqlite/database.py index e72b805..f627e3f 100644 --- a/structures/engines/sqlite/database.py +++ b/structures/engines/sqlite/database.py @@ -404,7 +404,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())})""" @@ -418,7 +418,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 = [] @@ -454,7 +454,7 @@ def insert(self) -> bool: return transaction.execute(raw_insert_record) except PermissionError: raise - except: + except Exception: return False return False @@ -466,7 +466,7 @@ def update(self) -> bool: return transaction.execute(raw_update_record) except PermissionError: raise - except: + except Exception: return False return False @@ -478,7 +478,7 @@ def delete(self) -> bool: return transaction.execute(raw_delete_record) except PermissionError: raise - except: + except Exception: return False return False From c807bf27b020ed81559e61d5d321753bfe963cf1 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Fri, 5 Jun 2026 16:58:13 +0200 Subject: [PATCH 77/93] chore(postgresql): clean up trigger import and schema typing --- structures/engines/postgresql/database.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/structures/engines/postgresql/database.py b/structures/engines/postgresql/database.py index ef758e7..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 @@ -71,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): @@ -403,7 +404,7 @@ def delete(self) -> bool: @dataclasses.dataclass class PostgreSQLView(SQLView): - schema: str = "public" + schema: Optional[str] = "public" @property def fully_qualified_name(self): From 5b65660f50cc8399ab03fa141f3279f4f3a8d265 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Fri, 5 Jun 2026 17:20:46 +0200 Subject: [PATCH 78/93] chore(schema): enforce abstract column and index APIs --- structures/engines/database.py | 104 +++++++++++++++++++++++++++++++- tests/core/test_engines_init.py | 15 ++++- 2 files changed, 115 insertions(+), 4 deletions(-) diff --git a/structures/engines/database.py b/structures/engines/database.py index 84e39fd..f4c1656 100755 --- a/structures/engines/database.py +++ b/structures/engines/database.py @@ -248,8 +248,42 @@ def is_new(self): def generate_uuid(length: int = 8) -> str: return str(uuid.uuid4())[::-1][:length] + # Abstract API that concrete engine index classes must implement. + @abc.abstractmethod + def create(self) -> bool: + """Create the index in the underlying database. + + Concrete engine classes must execute the appropriate SQL statement and + return ``True`` on success. + """ + raise NotImplementedError + + @abc.abstractmethod + def drop(self) -> bool: + """Drop the index from the underlying database. + + Concrete engine classes must execute the appropriate DROP statement and + return ``True`` on success. + """ + raise NotImplementedError + + @abc.abstractmethod + def alter(self, original_index: Self) -> bool: + """Alter the index to match ``original_index``. + + Implementations should generate the necessary ALTER statements. + """ + 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 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 = [] @@ -310,6 +344,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)} @@ -529,9 +598,40 @@ def copy(self): cls = self.__class__ 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) 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 From 322f849416975a0aec285df17fa24be6a57b1d66 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Fri, 5 Jun 2026 17:31:46 +0200 Subject: [PATCH 79/93] docs: update engine parity and project status after audit fixes --- ENGINES.md | 30 ++++++++++++++++++++++++++++++ PROJECT_STATUS.md | 8 +++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/ENGINES.md b/ENGINES.md index cc511f3..d521690 100644 --- a/ENGINES.md +++ b/ENGINES.md @@ -20,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..56f2a12 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. @@ -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,7 @@ ## 6. Recently Added +- 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. From 70a9290ac9b93f34f693d862cd69ef7f1496a844 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Fri, 5 Jun 2026 17:52:27 +0200 Subject: [PATCH 80/93] fix(ui): prompt to reconnect dropped database connections --- structures/engines/context.py | 35 ++++++++++++++ tests/engines/test_connection_lost.py | 67 +++++++++++++++++++++++++++ windows/main/controller.py | 46 ++++++++++++++++++ windows/main/query/controller.py | 9 ++++ windows/main/query/executor.py | 7 ++- 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 tests/engines/test_connection_lost.py diff --git a/structures/engines/context.py b/structures/engines/context.py index ffd8d7a..750cdda 100755 --- a/structures/engines/context.py +++ b/structures/engines/context.py @@ -5,6 +5,9 @@ from gettext import gettext as _ from typing import Any, Optional +import pymysql +import psycopg2 +import sqlite3 import yaml from constants import WORKDIR @@ -32,6 +35,11 @@ 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, @@ -551,10 +559,37 @@ 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): + raise ConnectionLostError( + _("Database connection lost: {error}").format(error=str(ex)) + ) from ex raise return True + @staticmethod + def _is_connection_lost(exc: Exception) -> bool: + """Return True when the exception indicates a lost DB connection.""" + # PyMySQL / MySQL / MariaDB + if pymysql and isinstance(exc, pymysql.err.InterfaceError): + return True + if pymysql and isinstance(exc, pymysql.err.OperationalError): + return True + + # PostgreSQL + if psycopg2 and isinstance(exc, psycopg2.OperationalError): + return True + if psycopg2 and isinstance(exc, psycopg2.InterfaceError): + 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/tests/engines/test_connection_lost.py b/tests/engines/test_connection_lost.py new file mode 100644 index 0000000..9f0301f --- /dev/null +++ b/tests/engines/test_connection_lost.py @@ -0,0 +1,67 @@ +import sqlite3 +from unittest.mock import Mock + +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 + + +@pytest.mark.parametrize( + "exc, expected", + [ + (pymysql.err.InterfaceError(0, ""), True), + (pymysql.err.OperationalError(2006, "MySQL server has gone away"), True), + (psycopg2.OperationalError("server closed the connection unexpectedly"), True), + (psycopg2.InterfaceError("connection already closed"), True), + (sqlite3.OperationalError("database is locked"), True), + (sqlite3.OperationalError("disk I/O error"), True), + (sqlite3.OperationalError("unable to open database file"), True), + (sqlite3.OperationalError("no such table: foo"), False), + (ValueError("unexpected"), False), + ], +) +def test_is_connection_lost_detection(exc, expected): + assert AbstractContext._is_connection_lost(exc) is expected + + +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") + + with pytest.raises(ValueError, match="syntax error"): + AbstractContext.execute(context, "SELECT 1") diff --git a/windows/main/controller.py b/windows/main/controller.py index bdcd980..e113e71 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -34,6 +34,7 @@ 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, CURRENT_PROCEDURE, CURRENT_FUNCTION, WRITE_OVERRIDE +from windows.state import SESSIONS_LIST from windows.main.explorer import TreeExplorerController @@ -352,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] = { @@ -2087,5 +2089,49 @@ 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: + if not wx.IsMainThread(): + wx.CallAfter(self._on_query_connection_lost, session, error) + return + + choice = wx.MessageDialog( + None, + message=_("Database scollegato, vuoi ricollegarti?"), + caption=_("Connessione persa"), + style=wx.YES_NO | wx.ICON_QUESTION, + ).ShowModal() + + if choice == wx.ID_YES: + try: + session.connect() + wx.MessageBox( + _("Connessione ripristinata con successo."), + _("Connessione ripristinata"), + wx.OK | wx.ICON_INFORMATION, + ) + except Exception as ex: + logger.error("Reconnection failed: %s", ex, exc_info=True) + wx.MessageBox( + _("Impossibile ricollegarsi: {error}").format(error=str(ex)), + _("Riconnessione fallita"), + 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/query/controller.py b/windows/main/query/controller.py index 81376dc..68e6650 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,6 +40,7 @@ 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) @@ -216,6 +218,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 +263,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 +279,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..8ef84dc 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 @@ -176,12 +177,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: From 3e3258b4e7871c67b31d9ed77ef559ead3bff82d Mon Sep 17 00:00:00 2001 From: gtripoli Date: Fri, 5 Jun 2026 18:04:54 +0200 Subject: [PATCH 81/93] fix(ui): handle dropped connections globally --- structures/engines/context.py | 27 +++++++++++-- tests/engines/test_connection_lost.py | 58 +++++++++++++++++++++++++++ windows/main/controller.py | 7 +++- 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/structures/engines/context.py b/structures/engines/context.py index 750cdda..46092d1 100755 --- a/structures/engines/context.py +++ b/structures/engines/context.py @@ -1,9 +1,10 @@ import abc import contextlib import re +import threading from gettext import gettext as _ -from typing import Any, Optional +from typing import Any, Callable, Optional import pymysql import psycopg2 @@ -72,6 +73,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.""" @@ -560,13 +563,29 @@ def execute(self, query: str) -> bool: logger.error(query) QUERY_LOGS.append(f"/* {str(ex)} */") if self._is_connection_lost(ex): - raise ConnectionLostError( - _("Database connection lost: {error}").format(error=str(ex)) - ) from 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.""" diff --git a/tests/engines/test_connection_lost.py b/tests/engines/test_connection_lost.py index 9f0301f..0f1ea73 100644 --- a/tests/engines/test_connection_lost.py +++ b/tests/engines/test_connection_lost.py @@ -9,6 +9,7 @@ from structures.engines.context import ConnectionLostError, AbstractContext from structures.connection import Connection, ConnectionEngine from structures.configurations import CredentialsConfiguration, SourceConfiguration +from structures.session import Session @pytest.mark.parametrize( @@ -62,6 +63,63 @@ def test_execute_reraises_non_connection_error(): 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_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() diff --git a/windows/main/controller.py b/windows/main/controller.py index e113e71..2720373 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -1306,6 +1306,8 @@ def _on_current_session(self, session: Session): 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) colors_datatypes = defaultdict(list) @@ -2090,8 +2092,11 @@ def on_cancel_query_execution(self, event): 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_query_connection_lost, session, error) + wx.CallAfter(self._on_global_connection_lost, session, error) return choice = wx.MessageDialog( From 4b816e31aaec88307c317ea5e332a547236f9981 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 6 Jun 2026 10:59:20 +0200 Subject: [PATCH 82/93] fix(locales): fix locales.py crashing on duplicate obsolete/active msgids - Replace header-prepend + pybabel update with pybabel init for new catalogs - Add --ignore-obsolete to pybabel update so re-activated strings don't produce a duplicate entry alongside their stale #~ counterpart - Remove dead code: generate_header(), get_project_info(), shutil, toml, datetime imports msgfmt 0.23.1 now errors on msgid present in both active and #~ sections. --- scripts/locales.py | 63 +++------------------------------------------- 1 file changed, 3 insertions(+), 60 deletions(-) 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") From bf5cb8f39bb6f159d028ea91dff33938a85d5abd Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 6 Jun 2026 11:13:01 +0200 Subject: [PATCH 83/93] refine connection-loss detection: scope OperationalError by code/pgcode, update i18n strings, add tests - _is_connection_lost(): PyMySQL OperationalError now only returns True for known disconnect codes (2006/2013/2055) or disconnect message fragments; ordinary SQL errors (1045, 1049, 1064, etc.) propagate without triggering reconnect. - psycopg2 OperationalError: True only when pgcode is None (network-level); server-side SQL errors (pgcode set) are no longer treated as lost connections. - Add _PYMYSQL_DISCONNECT_CODES and _PYMYSQL_DISCONNECT_FRAGMENTS module-level constants for clarity and test coverage. - windows/main/controller.py: replace Italian hard-coded strings in the connection-lost dialog with proper English _() calls; update .pot/.po/.mo for all supported locales. - tests/engines/test_connection_lost.py: expand parametrize table with negative cases (non-disconnect codes, pgcode-bearing OperationalError), add standalone unit tests for the new detection rules, and add QueryExecutor concurrency-guard integration tests. --- locale/de_DE/LC_MESSAGES/petersql.mo | Bin 18654 -> 18369 bytes locale/de_DE/LC_MESSAGES/petersql.po | 983 +++++++++++++------------ locale/en_US/LC_MESSAGES/petersql.mo | Bin 18212 -> 17336 bytes locale/en_US/LC_MESSAGES/petersql.po | 982 ++++++++++++------------- locale/es_ES/LC_MESSAGES/petersql.mo | Bin 18627 -> 18232 bytes locale/es_ES/LC_MESSAGES/petersql.po | 980 +++++++++++++------------ locale/fr_FR/LC_MESSAGES/petersql.mo | Bin 18905 -> 18773 bytes locale/fr_FR/LC_MESSAGES/petersql.po | 985 +++++++++++++------------- locale/it_IT/LC_MESSAGES/petersql.mo | Bin 18518 -> 18024 bytes locale/it_IT/LC_MESSAGES/petersql.po | 967 ++++++++++++------------- locale/petersql.pot | 770 +++++++++++--------- structures/engines/context.py | 35 +- tests/engines/test_connection_lost.py | 127 +++- windows/main/controller.py | 12 +- 14 files changed, 2957 insertions(+), 2884 deletions(-) diff --git a/locale/de_DE/LC_MESSAGES/petersql.mo b/locale/de_DE/LC_MESSAGES/petersql.mo index 338eebd6468f60b5df504e48ece710a376e6125f..7823f6fb848e7c1342be03ce5f9d6c527f7b685a 100644 GIT binary patch literal 18369 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 Ezc$nsZvX%Q literal 18654 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 diff --git a/locale/de_DE/LC_MESSAGES/petersql.po b/locale/de_DE/LC_MESSAGES/petersql.po index 217b7b9..e48d29a 100644 --- a/locale/de_DE/LC_MESSAGES/petersql.po +++ b/locale/de_DE/LC_MESSAGES/petersql.po @@ -6,15 +6,15 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-05-31 12:36+0200\n" +"POT-Creation-Date: 2026-06-06 10:58+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" -"Language-Team: de_DE \n" "Language: de_DE\n" +"Language-Team: de_DE \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.18.0\n" #: helpers/__init__.py:16 @@ -46,780 +46,886 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "OpenSSH-Client nicht gefunden." -#: structures/engines/context.py:544 +#: structures/engines/context.py:567 msgid "This connection is read-only." msgstr "Diese Verbindung ist schreibgeschützt." -#: structures/engines/mariadb/context.py:632 -#: structures/engines/mysql/context.py:643 +#: 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 "Table{table_index:03}" -#: structures/engines/mariadb/context.py:660 -#: structures/engines/mysql/context.py:671 +#: 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 "Column{column_index:03}" -#: structures/engines/mariadb/context.py:678 -#: structures/engines/mysql/context.py:689 +#: 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 "Index{index_number:03}" -#: structures/engines/mariadb/context.py:718 -#: structures/engines/mysql/context.py:727 +#: 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 "ForeignKey{foreign_key_number:03}" -#: structures/engines/mariadb/context.py:751 -#: structures/engines/mysql/context.py:758 +#: 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 "View{view_index:03}" -#: structures/engines/mariadb/context.py:804 +#: structures/engines/mariadb/context.py:830 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "Trigger{trigger_index:03}" #: windows/dialogs/connections/view.py:417 -#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1290 -#: windows/views.py:33 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1303 +#: windows/views.py:62 msgid "Connection" msgstr "Verbindung" #: windows/components/dataview.py:115 windows/components/dataview.py:240 #: windows/components/dataview.py:253 windows/components/dataview.py:268 -#: windows/views.py:47 windows/views.py:97 windows/views.py:1024 -#: windows/views.py:1336 windows/views.py:1369 windows/views.py:1393 -#: windows/views.py:1417 windows/views.py:1441 windows/views.py:1465 -#: windows/views.py:1582 windows/views.py:1928 windows/views.py:2199 -#: windows/views.py:3293 windows/views.py:3515 +#: 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:438 +#: windows/views.py:77 windows/views.py:467 msgid "Last connection" msgstr "Letzte Verbindung" -#: windows/dialogs/connections/view.py:669 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:212 -#: windows/dialogs/connections/view.py:615 windows/views.py:65 +#: windows/dialogs/connections/view.py:615 windows/views.py:94 msgid "New connection" msgstr "Neue Verbindung" -#: windows/views.py:71 +#: windows/views.py:100 msgid "Rename" msgstr "Umbenennen" -#: windows/views.py:76 +#: windows/views.py:105 msgid "Clone connection" msgstr "Verbindung klonen" -#: windows/views.py:81 windows/views.py:613 windows/views.py:1496 -#: windows/views.py:1864 windows/views.py:2154 windows/views.py:2378 -#: windows/views.py:3203 windows/views.py:3252 windows/views.py:3651 -#: windows/views.py:3683 +#: 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:1341 windows/views.py:1637 -#: windows/views.py:3115 windows/views.py:3570 +#: 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:1150 +#: windows/views.py:190 windows/views.py:1179 msgid "Password" msgstr "Passwort" -#: windows/views.py:174 +#: windows/views.py:203 msgid "Connection timeout" msgstr "Verbindung verloren" -#: 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:214 +#: windows/views.py:243 msgid "Use SSH tunnel" msgstr "SSH-Tunnel verwenden" -#: windows/views.py:225 +#: windows/views.py:254 msgid "Compressed client/server protocol" msgstr "Komprimiertes Client/Server-Protokoll" -#: windows/views.py:244 +#: windows/views.py:273 msgid "Filename" msgstr "Dateiname" -#: windows/views.py:249 windows/views.py:368 windows/views.py:3809 +#: windows/views.py:278 windows/views.py:397 msgid "Select a file" msgstr "Datei auswählen" -#: windows/views.py:249 windows/views.py:368 +#: windows/views.py:278 windows/views.py:397 msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:266 windows/views.py:1343 windows/views.py:1595 -#: windows/views.py:3113 windows/views.py:3528 +#: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 msgid "Comments" msgstr "Kommentare" -#: windows/main/controller.py:769 windows/views.py:280 windows/views.py:740 -#: windows/views.py:894 +#: windows/main/controller.py:772 windows/views.py:309 windows/views.py:769 +#: windows/views.py:923 msgid "Settings" msgstr "Einstellungen" -#: windows/views.py:288 +#: windows/views.py:317 msgid "SSH executable" msgstr "SSH-Executable" -#: windows/views.py:293 +#: windows/views.py:322 msgid "ssh" msgstr "ssh" -#: windows/views.py:301 +#: windows/views.py:330 msgid "SSH host + port" msgstr "SSH-Host + Port" -#: windows/views.py:313 +#: 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:322 +#: windows/views.py:351 msgid "SSH username" msgstr "SSH-Benutzername" -#: windows/views.py:335 +#: windows/views.py:364 msgid "SSH password" msgstr "SSH-Passwort" -#: windows/views.py:348 +#: windows/views.py:377 msgid "Local port" msgstr "Lokaler Port" -#: windows/views.py:354 +#: 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" +msgstr "wenn der Wert auf 0 gesetzt ist, wird der erste verfügbare Port verwendet" -#: windows/views.py:363 +#: windows/views.py:392 msgid "Identity file" msgstr "Identitätsdatei" -#: windows/views.py:379 +#: windows/views.py:408 msgid "Remote host + port" msgstr "Remote-Host + Port" -#: windows/views.py:391 +#: 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)." +msgstr "Remote-Host/Port ist das eigentliche DB-Ziel (standardmäßig DB-Host/Port)." -#: windows/views.py:400 +#: windows/views.py:429 msgid "SSH extra args" msgstr "Zusätzliche SSH-Argumente" -#: windows/views.py:415 +#: windows/views.py:444 msgid "SSH Tunnel" msgstr "SSH-Tunnel" -#: windows/views.py:421 windows/views.py:1339 windows/views.py:3117 +#: windows/views.py:450 windows/views.py:1368 msgid "Created at" msgstr "Erstellt am" -#: windows/views.py:455 +#: windows/views.py:484 msgid "Successful connections" msgstr "Erfolgreiche Verbindungen" -#: windows/views.py:472 +#: windows/views.py:501 msgid "Last successful connection" msgstr "Erfolgreiche Verbindungen" -#: windows/views.py:489 +#: windows/views.py:518 msgid "Unsuccessful connections" msgstr "Erfolglose Verbindungen" -#: windows/views.py:506 +#: windows/views.py:535 msgid "Last failure reason" msgstr "Letzter Fehlergrund" -#: windows/views.py:523 +#: windows/views.py:552 msgid "Total connection attempts" msgstr "Letzte Verbindung" -#: windows/views.py:540 +#: windows/views.py:569 msgid "Average connection time (ms)" msgstr "Wiederverbindung fehlgeschlagen:" -#: windows/views.py:557 +#: windows/views.py:586 msgid "Most recent connection duration" msgstr "Dauer der letzten Verbindung" -#: windows/views.py:576 +#: windows/views.py:605 msgid "Statistics" msgstr "Statistiken" -#: windows/views.py:594 windows/views.py:1821 +#: windows/views.py:623 windows/views.py:1850 msgid "Create" msgstr "Erstellen" -#: windows/views.py:598 +#: windows/views.py:627 msgid "Create connection" msgstr "Verbindung erstellen" -#: windows/views.py:601 +#: windows/views.py:630 msgid "Create directory" msgstr "Neues Verzeichnis" -#: windows/views.py:630 windows/views.py:854 windows/views.py:1491 -#: windows/views.py:1867 windows/views.py:2159 windows/views.py:2383 -#: windows/views.py:2439 windows/views.py:3179 windows/views.py:3686 -#: windows/views.py:3823 +#: 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:323 windows/views.py:635 windows/views.py:2164 -#: windows/views.py:2388 windows/views.py:2603 windows/views.py:3691 -#: windows/views.py:3829 +#: 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:642 +#: windows/views.py:671 msgid "Test" msgstr "Testen" -#: windows/views.py:649 +#: windows/views.py:678 msgid "Connect" msgstr "Verbinden" -#: windows/views.py:752 +#: windows/views.py:781 windows/views.py:2388 msgid "Language" msgstr "Sprache" -#: windows/views.py:757 +#: windows/views.py:786 msgid "English" msgstr "Englisch" -#: windows/views.py:757 +#: windows/views.py:786 msgid "Italian" msgstr "Italienisch" -#: windows/views.py:757 +#: windows/views.py:786 msgid "French" msgstr "Französisch" -#: windows/views.py:769 +#: windows/views.py:798 msgid "Locale" msgstr "Lokale" -#: windows/views.py:790 +#: windows/views.py:819 msgid "Column content" msgstr "Spalteninhalt" -#: windows/views.py:800 +#: windows/views.py:829 msgid "Syntax" msgstr "Syntax" -#: windows/views.py:857 +#: windows/views.py:886 msgid "Ok" msgstr "Ok" -#: windows/views.py:888 +#: windows/views.py:917 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:897 +#: windows/views.py:926 msgid "File" msgstr "Datei" -#: windows/views.py:900 +#: windows/views.py:929 msgid "About" msgstr "Über" -#: windows/views.py:903 +#: windows/views.py:932 msgid "Help" msgstr "Hilfe" -#: windows/views.py:908 +#: windows/views.py:937 msgid "Open connection manager" msgstr "Verbindungsmanager öffnen" -#: windows/views.py:912 +#: windows/views.py:941 msgid "Disconnect from server" msgstr "Vom Server trennen" -#: windows/views.py:914 +#: windows/views.py:943 msgid "tool" msgstr "Werkzeug" -#: windows/views.py:914 windows/views.py:2419 +#: windows/views.py:943 windows/views.py:2628 msgid "Refresh" msgstr "Aktualisieren" -#: windows/views.py:918 windows/views.py:920 windows/views.py:1845 -#: windows/views.py:2423 windows/views.py:2589 +#: 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:925 +#: windows/views.py:954 #, python-brace-format msgid "{mode}" msgstr "{mode}" -#: windows/views.py:962 windows/views.py:966 windows/views.py:2556 -#: windows/views.py:2561 +#: windows/views.py:991 windows/views.py:995 windows/views.py:2765 +#: windows/views.py:2770 msgid "MyMenuItem" msgstr "MeinMenüElement" -#: windows/views.py:969 windows/views.py:1895 windows/views.py:2568 -#: windows/views.py:3714 +#: windows/views.py:998 windows/views.py:1924 windows/views.py:2777 msgid "MyMenu" msgstr "MeinMenü" -#: windows/views.py:984 windows/views.py:1519 windows/views.py:1526 -#: windows/views.py:1533 +#: windows/views.py:1013 windows/views.py:1548 windows/views.py:1555 +#: windows/views.py:1562 msgid "MyLabel" msgstr "MeinLabel" -#: windows/views.py:990 +#: windows/views.py:1019 msgid "Databases" msgstr "Datenbanken" -#: windows/views.py:991 windows/views.py:1338 windows/views.py:3118 +#: windows/views.py:1020 windows/views.py:1367 msgid "Size" msgstr "Größe" -#: windows/views.py:992 +#: windows/views.py:1021 msgid "Elements" msgstr "Elemente" -#: windows/views.py:993 +#: windows/views.py:1022 msgid "Modified at" msgstr "Geändert am" -#: windows/views.py:994 windows/views.py:1353 +#: windows/views.py:1023 windows/views.py:1382 msgid "Tables" msgstr "Tabellen" -#: windows/views.py:1001 +#: 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:1047 -#: windows/views.py:1342 windows/views.py:3114 +#: windows/components/dataview.py:89 windows/views.py:1076 +#: windows/views.py:1371 msgid "Collation" msgstr "Sortierung" -#: windows/views.py:1076 +#: windows/views.py:1105 msgid "Encryption" msgstr "Verschlüsselung" -#: windows/main/controller.py:1259 windows/main/controller.py:1281 -#: windows/main/controller.py:1285 windows/views.py:1088 +#: windows/main/controller.py:1272 windows/main/controller.py:1294 +#: windows/main/controller.py:1298 windows/views.py:1117 msgid "Read Only" msgstr "Schreibgeschützt" -#: windows/views.py:1105 +#: windows/views.py:1134 msgid "Tablespace" msgstr "Tablespace" -#: windows/views.py:1126 +#: windows/views.py:1155 msgid "Connection limit" msgstr "Verbindungslimit" -#: windows/views.py:1169 +#: windows/views.py:1198 msgid "Profile" msgstr "Profil" -#: windows/views.py:1195 +#: windows/views.py:1224 msgid "Default tablespace" msgstr "Standard-Tablespace" -#: windows/views.py:1216 +#: windows/views.py:1245 msgid "Temporary tablespace" msgstr "Temporärer Tablespace" -#: windows/views.py:1242 +#: windows/views.py:1271 msgid "Quota" msgstr "Quota" -#: windows/views.py:1261 +#: windows/views.py:1290 msgid "Unlimited quota" msgstr "Unbegrenzte Quota" -#: windows/views.py:1278 +#: windows/views.py:1307 msgid "Account status" msgstr "Kontostatus" -#: windows/views.py:1299 +#: windows/views.py:1328 msgid "Password expire" msgstr "Passwortablauf" -#: windows/views.py:1323 +#: windows/views.py:1352 msgid "Add new table" msgstr "Neue Tabelle hinzufügen" -#: windows/views.py:1325 +#: windows/views.py:1354 msgid "Clone table" msgstr "Tabelle klonen" -#: windows/main/controller.py:1770 windows/views.py:1327 +#: windows/main/controller.py:1821 windows/views.py:1356 msgid "Delete table" msgstr "Tabelle löschen" -#: windows/views.py:1337 +#: windows/views.py:1366 windows/views.py:2457 msgid "Rows" msgstr "Zeilen" -#: windows/views.py:1340 windows/views.py:3116 +#: windows/views.py:1369 msgid "Updated at" msgstr "Aktualisiert am" -#: windows/views.py:1358 +#: windows/views.py:1387 msgid "Add new view" msgstr "Neue Ansicht hinzufügen" -#: windows/views.py:1360 windows/views.py:1384 +#: windows/views.py:1389 msgid "Clone view" msgstr "Klonen" -#: windows/views.py:1362 +#: windows/views.py:1391 msgid "Delete view" msgstr "Löschen" -#: windows/views.py:1370 windows/views.py:1394 windows/views.py:1418 -#: windows/views.py:1442 windows/views.py:1466 +#: 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:1377 +#: windows/views.py:1406 msgid "Views" msgstr "Ansichten" -#: windows/views.py:1382 +#: windows/views.py:1411 msgid "Add new procedure" msgstr "Neue Prozedur hinzufügen" -#: windows/views.py:1384 +#: windows/views.py:1413 msgid "Clone procedure" msgstr "Prozedur klonen" -#: windows/views.py:1386 +#: windows/views.py:1415 msgid "Delete procedure" msgstr "Prozedur löschen" -#: windows/views.py:1401 +#: windows/views.py:1430 msgid "Procedures" msgstr "Prozeduren" -#: windows/views.py:1406 +#: windows/views.py:1435 msgid "Add new function" msgstr "Neue Funktion hinzufügen" -#: windows/views.py:1408 +#: windows/views.py:1437 msgid "Clone function" msgstr "Funktion klonen" -#: windows/views.py:1410 +#: windows/views.py:1439 msgid "Delete function" msgstr "Funktion löschen" -#: windows/views.py:1425 +#: windows/views.py:1454 msgid "Functions" msgstr "Funktionen" -#: windows/views.py:1430 +#: windows/views.py:1459 msgid "Add new trigger" msgstr "Neuen Trigger hinzufügen" -#: windows/views.py:1432 +#: windows/views.py:1461 msgid "Clone trigger" msgstr "Trigger klonen" -#: windows/views.py:1434 +#: windows/views.py:1463 msgid "Delete trigger" msgstr "Trigger löschen" -#: windows/views.py:1449 +#: windows/views.py:1478 msgid "Triggers" msgstr "Trigger" -#: windows/views.py:1454 +#: windows/views.py:1483 msgid "Add new event" msgstr "Neues Ereignis hinzufügen" -#: windows/views.py:1456 +#: windows/views.py:1485 msgid "Clone event" msgstr "Klonen" -#: windows/views.py:1458 +#: windows/views.py:1487 msgid "Delete event" msgstr "Ereignis löschen" -#: windows/views.py:1473 +#: windows/views.py:1502 msgid "Events" msgstr "Ereignisse" -#: windows/views.py:1501 windows/views.py:1872 windows/views.py:2437 -#: windows/views.py:2521 windows/views.py:3186 +#: windows/views.py:1530 windows/views.py:1901 windows/views.py:2646 +#: windows/views.py:2730 msgid "Apply" msgstr "Anwenden" -#: windows/views.py:1513 windows/views.py:1692 windows/views.py:3603 +#: windows/views.py:1542 windows/views.py:1721 msgid "Options" msgstr "Optionen" -#: windows/views.py:1544 +#: windows/views.py:1573 msgid "Diagram" msgstr "Diagramm" -#: windows/views.py:1555 +#: windows/views.py:1584 msgid "Database" msgstr "Datenbank" -#: windows/views.py:1610 windows/views.py:3543 +#: windows/views.py:1639 msgid "Base" msgstr "Basis" -#: windows/views.py:1624 windows/views.py:3557 +#: windows/views.py:1653 msgid "Auto Increment" msgstr "Auto Inkrement" -#: windows/views.py:1652 windows/views.py:3585 +#: windows/views.py:1681 msgid "Default Collation" msgstr "Standard-Sortierung" -#: windows/views.py:1662 +#: windows/views.py:1691 msgid "Convert data" msgstr "Daten konvertieren" -#: windows/views.py:1670 +#: windows/views.py:1699 msgid "Row format" msgstr "Zeilenformat" -#: windows/views.py:1703 windows/views.py:1731 windows/views.py:1759 -#: windows/views.py:1847 windows/views.py:2259 windows/views.py:2427 -#: windows/views.py:3313 windows/views.py:3330 windows/views.py:3354 -#: windows/views.py:3381 +#: 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:1705 windows/views.py:1733 windows/views.py:1761 -#: windows/views.py:2261 windows/views.py:2529 windows/views.py:3320 -#: windows/views.py:3337 windows/views.py:3361 windows/views.py:3388 +#: 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:1718 windows/views.py:3617 +#: windows/views.py:1747 msgid "Indexes" msgstr "Indizes" -#: windows/views.py:1729 windows/views.py:1757 windows/views.py:2257 -#: windows/views.py:3198 windows/views.py:3240 windows/views.py:3308 -#: windows/views.py:3349 windows/views.py:3376 windows/views.py:3646 +#: windows/views.py:1758 windows/views.py:1786 windows/views.py:2324 msgid "Insert" msgstr "Einfügen" -#: windows/views.py:1746 +#: windows/views.py:1775 msgid "Foreign Keys" msgstr "Fremdschlüssel" -#: windows/views.py:1774 +#: windows/views.py:1803 msgid "Checks" msgstr "Prüfungen" -#: windows/views.py:1841 windows/views.py:3638 +#: windows/views.py:1870 msgid "Columns:" msgstr "Spalten:" -#: windows/views.py:1851 +#: windows/views.py:1880 msgid "Move Up" msgstr "Nach oben bewegen\tCTRL+UP" -#: windows/views.py:1853 +#: windows/views.py:1882 msgid "Move Down" msgstr "Nach unten bewegen\tCTRL+D" -#: windows/views.py:1885 windows/views.py:1892 windows/views.py:3704 -#: windows/views.py:3711 +#: windows/views.py:1914 windows/views.py:1921 msgid "Add Index" msgstr "Index hinzufügen" -#: windows/views.py:1889 windows/views.py:3708 +#: windows/views.py:1918 msgid "Add PrimaryKey" msgstr "Primärschlüssel hinzufügen" -#: windows/views.py:1906 +#: windows/views.py:1935 msgid "Table" msgstr "Tabelle" -#: windows/views.py:1948 windows/views.py:2219 +#: windows/views.py:1977 windows/views.py:2243 msgid "Schema" msgstr "Schema" -#: windows/views.py:1976 windows/views.py:2247 +#: windows/views.py:2005 windows/views.py:2314 msgid "General" msgstr "Allgemein" -#: windows/views.py:1981 +#: windows/views.py:2010 msgid "Algorithm" msgstr "Algorithmus" -#: windows/views.py:1983 +#: windows/views.py:2012 msgid "UNDEFINED" msgstr "Ohne Vorzeichen" -#: windows/views.py:1986 +#: windows/views.py:2015 msgid "MERGE" msgstr "MERGE" -#: windows/views.py:1989 +#: windows/views.py:2018 msgid "TEMPTABLE" msgstr "TEMPTABLE" -#: windows/views.py:1999 +#: windows/views.py:2028 msgid "View constraint" msgstr "Ansichtseinschränkung" -#: windows/views.py:2001 +#: windows/views.py:2030 msgid "None" msgstr "Keiner" -#: windows/views.py:2004 +#: windows/views.py:2033 msgid "LOCAL" msgstr "LOKAL" -#: windows/views.py:2007 +#: windows/views.py:2036 msgid "CASCADE" msgstr "KASKADE" -#: windows/views.py:2010 +#: windows/views.py:2039 msgid "CHECK ONLY" msgstr "NUR PRÜFEN" -#: windows/views.py:2013 +#: windows/views.py:2042 msgid "READ ONLY" msgstr "SCHREIBGESCHÜTZT" -#: windows/views.py:2026 +#: windows/views.py:2055 windows/views.py:2483 msgid "Behavior" msgstr "Verhalten" -#: windows/views.py:2033 windows/views.py:2281 +#: windows/views.py:2062 windows/views.py:2490 msgid "Definer" msgstr "Definierer" -#: windows/views.py:2041 windows/views.py:2289 +#: windows/views.py:2070 windows/views.py:2498 msgid "*" msgstr "*" -#: windows/views.py:2053 windows/views.py:2301 +#: windows/views.py:2082 windows/views.py:2510 msgid "SQL security" msgstr "SQL-Sicherheit" -#: windows/views.py:2060 windows/views.py:2308 +#: windows/views.py:2089 windows/views.py:2517 msgid "DEFINER" msgstr "DEFINIERER" -#: windows/views.py:2060 windows/views.py:2308 +#: windows/views.py:2089 windows/views.py:2517 msgid "INVOKER" msgstr "AUFRUFER" -#: windows/views.py:2074 +#: windows/views.py:2103 msgid "Force" msgstr "Erzwingen" -#: windows/views.py:2086 +#: windows/views.py:2115 msgid "Security barrier" msgstr "Sicherheitsbarriere" -#: windows/views.py:2099 windows/views.py:2323 +#: windows/views.py:2128 windows/views.py:2532 msgid "Security" msgstr "Sicherheit" -#: windows/views.py:2176 +#: windows/views.py:2205 msgid "View" msgstr "Ansichten" -#: windows/views.py:2274 +#: windows/views.py:2262 +#, fuzzy +msgid "Type" +msgstr "Datentyp" + +#: 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 +#, fuzzy +msgid "Return type" +msgstr "Datentyp" + +#: windows/views.py:2299 +#, fuzzy +msgid "Comment" +msgstr "Kommentare" + +#: windows/components/dataview.py:111 windows/views.py:2335 +msgid "#" +msgstr "#" + +#: windows/views.py:2337 +#, fuzzy +msgid "Datatype" +msgstr "Datentyp" + +#: windows/views.py:2338 +#, fuzzy +msgid "Context" +msgstr "Verbinden" + +#: windows/views.py:2345 msgid "Parameters" msgstr "Parameter" -#: windows/views.py:2400 -msgid "Procedure" -msgstr "Procedure" +#: windows/views.py:2354 +#, fuzzy +msgid "Data access" +msgstr "Datenbanken" + +#: 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 +#, fuzzy +msgid "MODIFIES SQL DATA" +msgstr "Geändert am" + +#: windows/views.py:2371 +#, fuzzy +msgid "Deterministic" +msgstr "Definition" + +#: windows/views.py:2395 +#, fuzzy +msgid "SQL" +msgstr "*.sql" + +#: windows/views.py:2395 +msgid "PLPGSQL" +msgstr "" + +#: windows/views.py:2405 +#, fuzzy +msgid "Volatility" +msgstr "Virtualität" + +#: windows/views.py:2412 +msgid "VOLATILE" +msgstr "" + +#: windows/views.py:2412 +#, fuzzy +msgid "STABLE" +msgstr "Tabelle" + +#: windows/views.py:2412 +#, fuzzy +msgid "IMMUTABLE" +msgstr "Tabelle" + +#: windows/views.py:2422 +msgid "Parallel" +msgstr "" -#: windows/views.py:2408 +#: windows/views.py:2429 +#, fuzzy +msgid "UNSAFE" +msgstr "Speichern" + +#: windows/views.py:2429 +msgid "RESTRICTED" +msgstr "" + +#: windows/views.py:2429 +#, fuzzy +msgid "SAFE" +msgstr "Speichern" + +#: windows/views.py:2441 +#, fuzzy +msgid "Cost" +msgstr "Schließen" + +#: windows/views.py:2609 +#, fuzzy +msgid "Routine" +msgstr "Betriebszeit" + +#: windows/views.py:2617 msgid "Trigger" msgstr "Trigger" -#: windows/views.py:2425 +#: windows/views.py:2634 msgid "Duplicate" msgstr "Duplizieren" -#: windows/views.py:2431 +#: windows/views.py:2640 msgid "Apply changes automatically" msgstr "Änderungen automatisch anwenden" -#: windows/views.py:2433 windows/views.py:2434 +#: windows/views.py:2642 windows/views.py:2643 msgid "" -"If enabled, table edits are applied immediately without pressing Apply or " -"Cancel" +"If enabled, table edits are applied immediately without pressing Apply or" +" Cancel" msgstr "" "Wenn aktiviert, werden Tabellenbearbeitungen sofort angewendet, ohne auf " "Anwenden oder Abbrechen zu drücken" -#: windows/views.py:2447 +#: windows/views.py:2656 #, python-brace-format -msgid "" -"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" -"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -#: windows/views.py:2455 +#: windows/views.py:2664 msgid "First" msgstr "Erste" -#: windows/views.py:2473 +#: windows/views.py:2682 msgid "Last" msgstr "Letzte" -#: windows/views.py:2482 +#: windows/views.py:2691 msgid "Filters" msgstr "Filter" -#: windows/views.py:2524 +#: windows/views.py:2733 msgid "" "Apply filters in data\n" "CTRL+ENTER" @@ -827,114 +933,62 @@ msgstr "" "Filter in Daten anwenden\n" "STRG+EINGABE" -#: windows/views.py:2525 +#: windows/views.py:2734 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2553 +#: windows/views.py:2762 msgid "Insert row" msgstr "Zeile einfügen" -#: windows/components/popup.py:31 windows/views.py:2565 +#: windows/components/popup.py:31 windows/views.py:2774 msgid "NULL" msgstr "NULL" -#: windows/views.py:2572 +#: windows/views.py:2781 msgid "Data" msgstr "Daten" -#: windows/main/controller.py:318 windows/views.py:2589 +#: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" msgstr "Abfrage" -#: windows/views.py:2591 windows/views.py:3137 +#: windows/views.py:2800 msgid "Close" msgstr "Schließen" -#: windows/main/controller.py:319 windows/views.py:2591 +#: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" msgstr "Abfrage" -#: windows/views.py:2595 +#: windows/views.py:2804 msgid "Run" msgstr "Ausführen" -#: windows/main/controller.py:320 windows/views.py:2595 +#: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" msgstr "Ausführen" -#: windows/views.py:2597 +#: windows/views.py:2806 msgid "Run all" msgstr "Alle ausführen" -#: windows/views.py:2597 +#: windows/views.py:2806 msgid "Execute all statements" msgstr "Alle Anweisungen ausführen" -#: windows/main/controller.py:322 windows/views.py:2599 +#: windows/main/controller.py:323 windows/views.py:2808 msgid "Stop" msgstr "Stopp" -#: windows/views.py:2664 +#: windows/views.py:2873 msgid "a page" msgstr "eine Seite" -#: windows/views.py:2714 +#: windows/views.py:2923 msgid "Query" msgstr "Abfrage" -#: windows/views.py:3091 -msgid "Character set" -msgstr "Zeichensatz" - -#: windows/views.py:3121 windows/views.py:3140 -msgid "New" -msgstr "Neu" - -#: windows/views.py:3160 -msgid "Insert record" -msgstr "Datensatz einfügen" - -#: windows/views.py:3165 -msgid "Duplicate record" -msgstr "Datensatz duplizieren" - -#: windows/views.py:3172 -msgid "Delete record" -msgstr "Datensatz löschen" - -#: windows/views.py:3210 windows/views.py:3658 -msgid "Up" -msgstr "Hoch" - -#: windows/views.py:3217 windows/views.py:3665 -msgid "Down" -msgstr "Runter" - -#: windows/views.py:3232 -msgid "Table:" -msgstr "Tabelle:" - -#: windows/views.py:3245 -msgid "Clone" -msgstr "Klonen" - -#: windows/views.py:3270 -msgid "MyButton" -msgstr "MeinButton" - -#: windows/views.py:3794 -msgid "Save Starments" -msgstr "Anweisungen speichern" - -#: windows/views.py:3802 -msgid "Location" -msgstr "Speicherort" - -#: windows/views.py:3809 -msgid "*.sql" -msgstr "*.sql" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 msgid "Allow NULL" @@ -967,10 +1021,6 @@ msgstr "Ohne Vorzeichen" msgid "Zerofill" msgstr "Nullfüllung" -#: windows/components/dataview.py:111 -msgid "#" -msgstr "#" - #: windows/components/dataview.py:119 msgid "Data type" msgstr "Datentyp" @@ -1070,7 +1120,9 @@ msgstr "Speichern bestätigen" #: 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?" +msgstr "" +"Sie haben ungespeicherte Änderungen. Möchten Sie sie vor dem Fortfahren " +"speichern?" #: windows/dialogs/connections/view.py:485 msgid "Unsaved changes" @@ -1078,9 +1130,11 @@ msgstr "Ungespeicherte Änderungen" #: windows/dialogs/connections/view.py:773 msgid "" -"This connection cannot work without TLS. TLS has been enabled automatically." +"This connection cannot work without TLS. TLS has been enabled " +"automatically." msgstr "" -"Diese Verbindung kann ohne TLS nicht funktionieren. TLS wurde automatisch aktiviert." +"Diese Verbindung kann ohne TLS nicht funktionieren. TLS wurde automatisch" +" aktiviert." #: windows/dialogs/connections/view.py:798 #, python-brace-format @@ -1110,114 +1164,114 @@ msgstr "Löschen bestätigen" msgid "Do you want to delete the directory '{directory_name}'?" msgstr "Möchten Sie das Verzeichnis '{directory_name}' löschen?" -#: windows/main/controller.py:315 +#: windows/main/controller.py:316 #, python-brace-format msgid "{text} ({shortcut})" msgstr "{text} ({shortcut})" -#: windows/main/controller.py:321 +#: windows/main/controller.py:322 msgid "Execute all" msgstr "Alle ausführen" -#: windows/main/controller.py:440 windows/main/controller.py:448 +#: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" msgstr "Abfrage" -#: windows/main/controller.py:467 +#: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" msgstr "Query ({query_number})" -#: windows/main/controller.py:516 +#: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" msgstr "Sie haben ungespeicherte Änderungen. Vor dem Schließen speichern?" -#: windows/main/controller.py:517 +#: windows/main/controller.py:519 msgid "Unsaved query" msgstr "Ungespeicherte Abfrage" -#: windows/main/controller.py:562 +#: windows/main/controller.py:564 msgid "Save query" msgstr "Abfrage speichern" -#: windows/main/controller.py:565 +#: windows/main/controller.py:567 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" msgstr "SQL-Dateien (*.sql)|*.sql|Alle Dateien (*.*)|*.*" -#: windows/main/controller.py:593 windows/main/controller.py:624 -#: windows/main/controller.py:651 windows/main/database/list.py:119 -#: windows/main/database/procedure.py:206 -#: windows/main/database/procedure.py:232 windows/main/database/view.py:268 -#: windows/main/database/view.py:297 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:631 +#: windows/main/controller.py:633 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "-- Abfrage gespeichert in {file_path}" -#: windows/main/controller.py:657 +#: windows/main/controller.py:659 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "-- Abfrage automatisch gespeichert in {file_path}" -#: windows/main/controller.py:722 +#: windows/main/controller.py:725 msgid "days" msgstr "Tage" -#: windows/main/controller.py:723 +#: windows/main/controller.py:726 msgid "hours" msgstr "Stunden" -#: windows/main/controller.py:724 +#: windows/main/controller.py:727 msgid "minutes" msgstr "Minuten" -#: windows/main/controller.py:725 +#: windows/main/controller.py:728 msgid "seconds" msgstr "Sekunden" -#: windows/main/controller.py:733 +#: windows/main/controller.py:736 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Verwendeter Speicher: {used} ({percentage:.2%})" -#: windows/main/controller.py:769 +#: windows/main/controller.py:772 msgid "Settings saved successfully" msgstr "Einstellungen erfolgreich gespeichert" -#: windows/main/controller.py:993 +#: windows/main/controller.py:1006 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "~{estimated} (Wird geladen...)" -#: windows/main/controller.py:995 +#: windows/main/controller.py:1008 msgid "~ (Loading...)" msgstr "~ (Wird geladen...)" -#: windows/main/controller.py:1230 +#: windows/main/controller.py:1243 msgid "Write Mode (2:00)" msgstr "Schreibmodus (2:00)" -#: windows/main/controller.py:1281 +#: windows/main/controller.py:1294 msgid "Write Mode" msgstr "Schreibmodus" -#: windows/main/controller.py:1292 +#: windows/main/controller.py:1305 msgid "Version" msgstr "Version" -#: windows/main/controller.py:1294 +#: windows/main/controller.py:1307 msgid "Uptime" msgstr "Betriebszeit" -#: windows/main/controller.py:1429 +#: windows/main/controller.py:1444 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "Möchten Sie die Änderung an {database_name} verwerfen?" -#: windows/main/controller.py:1462 +#: windows/main/controller.py:1477 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1226,57 +1280,87 @@ 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" +"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:1467 windows/main/controller.py:1488 +#: windows/main/controller.py:1482 windows/main/controller.py:1503 msgid "Delete database" msgstr "Datenbank löschen" -#: windows/main/controller.py:1473 +#: windows/main/controller.py:1488 msgid "Dump is not implemented yet. No action has been performed." msgstr "Dump ist noch nicht implementiert. Es wurde keine Aktion ausgeführt." -#: windows/main/controller.py:1474 +#: windows/main/controller.py:1489 msgid "Dump not available" msgstr "Dump nicht verfügbar" -#: windows/main/controller.py:1487 +#: windows/main/controller.py:1502 msgid "Database deletion is not supported by this engine." msgstr "Das Löschen von Datenbanken wird von dieser Engine nicht unterstützt." -#: windows/main/controller.py:1502 +#: windows/main/controller.py:1517 msgid "Database deleted successfully" msgstr "Datenbank erfolgreich gelöscht" -#: windows/main/controller.py:1503 windows/main/database/procedure.py:195 -#: windows/main/database/procedure.py:226 windows/main/database/view.py:256 +#: 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 "Erfolg" -#: windows/main/controller.py:1741 +#: windows/main/controller.py:1792 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "Möchten Sie die Änderung an {table_name} verwerfen?" -#: windows/main/controller.py:1767 +#: windows/main/controller.py:1818 #, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "Möchten Sie die Tabelle {table_name} löschen?" -#: windows/main/controller.py:1789 +#: windows/main/controller.py:1840 #, python-brace-format msgid "{table_name} (COPY)" msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1948 +#: windows/main/controller.py:1999 msgid "Do you want delete the records?" msgstr "Möchten Sie die Datensätze löschen?" +#: windows/main/controller.py:2104 +#, fuzzy +msgid "Database connection lost. Do you want to reconnect?" +msgstr "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 +#, fuzzy +msgid "Connection restored successfully." +msgstr "Verbindung erfolgreich hergestellt" + +#: windows/main/controller.py:2114 +#, fuzzy +msgid "Connection restored" +msgstr "Verbindungsfehler" + +#: windows/main/controller.py:2120 +#, python-brace-format +msgid "Could not reconnect: {error}" +msgstr "" + +#: windows/main/controller.py:2121 +#, fuzzy +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." @@ -1285,44 +1369,60 @@ msgstr "Die Verbindung zur Datenbank wurde verloren." msgid "Do you want to reconnect?" msgstr "Möchten Sie erneut verbinden?" -#: windows/main/database/list.py:108 -msgid "Connection lost" -msgstr "Verbindung verloren" - #: windows/main/database/list.py:118 msgid "Reconnection failed:" msgstr "Wiederverbindung fehlgeschlagen:" -#: windows/main/database/procedure.py:194 +#: windows/main/database/routine.py:545 +#, fuzzy +msgid "Function created successfully" +msgstr "Ansicht erfolgreich erstellt" + +#: windows/main/database/routine.py:547 +#, fuzzy +msgid "Function updated successfully" +msgstr "Ansicht erfolgreich aktualisiert" + +#: windows/main/database/routine.py:551 msgid "Procedure created successfully" msgstr "Prozedur erfolgreich erstellt" -#: windows/main/database/procedure.py:194 +#: windows/main/database/routine.py:553 msgid "Procedure updated successfully" msgstr "Prozedur erfolgreich aktualisiert" -#: windows/main/database/procedure.py:206 -#, python-brace-format -msgid "Error saving procedure: {}" -msgstr "Fehler beim Speichern der Prozedur: {}" +#: windows/main/database/routine.py:590 +#, fuzzy, python-brace-format +msgid "Error saving routine: {}" +msgstr "Fehler beim Speichern der Ansicht: {}" -#: windows/main/database/procedure.py:217 -#, python-brace-format -msgid "Are you sure you want to delete procedure '{}'?" -msgstr "Sind Sie sicher, dass Sie die Prozedur '{}' löschen möchten?" +#: windows/main/database/routine.py:604 +#, fuzzy +msgid "Function" +msgstr "Funktionen" + +#: windows/main/database/routine.py:604 +msgid "Procedure" +msgstr "Procedure" -#: windows/main/database/procedure.py:218 windows/main/database/view.py:282 +#: windows/main/database/routine.py:607 +#, fuzzy, python-brace-format +msgid "Are you sure you want to delete {} '{}'?" +msgstr "Sind Sie sicher, dass Sie die Ansicht '{}' 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/procedure.py:226 -msgid "Procedure deleted successfully" -msgstr "Prozedur erfolgreich gelöscht" +#: windows/main/database/routine.py:619 +#, fuzzy, python-brace-format +msgid "{} deleted successfully" +msgstr "Ansicht erfolgreich gelöscht" -#: windows/main/database/procedure.py:232 -#, python-brace-format -msgid "Error deleting procedure: {}" -msgstr "Fehler beim Löschen der Prozedur: {}" +#: windows/main/database/routine.py:634 +#, fuzzy, python-brace-format +msgid "Error deleting routine: {}" +msgstr "Fehler beim Löschen der Ansicht: {}" #: windows/main/database/view.py:255 msgid "View created successfully" @@ -1351,21 +1451,21 @@ msgstr "Ansicht erfolgreich gelöscht" msgid "Error deleting view: {}" 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 "{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 "{elapsed_s:.2f} s" -#: windows/main/query/controller.py:115 +#: windows/main/query/controller.py:117 msgid "none" msgstr "keine" -#: windows/main/query/controller.py:121 +#: windows/main/query/controller.py:123 #, python-brace-format msgid "" "Query execution stopped after {elapsed}.\n" @@ -1380,14 +1480,19 @@ msgstr "" "Fehlgeschlagene: {failed}.\n" "Letzte Anweisung: #{last}." -#: windows/main/query/controller.py:134 +#: windows/main/query/controller.py:136 msgid "Query execution cancelled" msgstr "Abfrageausführung abgebrochen" -#: windows/main/query/controller.py:176 +#: windows/main/query/controller.py:178 msgid "No active database connection" msgstr "Keine aktive Datenbankverbindung" +#: windows/main/query/controller.py:227 +#, fuzzy +msgid "Database connection lost" +msgstr "Verbindung verloren" + #: windows/main/query/history.py:55 msgid "(empty query)" msgstr "(leere Abfrage)" @@ -1435,139 +1540,3 @@ msgstr "Fehler" msgid "Error saving records" msgstr "Fehler beim Speichern der Datensätze" -#~ msgid "Created at:" -#~ msgstr "Created at:" - -#~ msgid "Last connection:" -#~ msgstr "Last connection:" - -#~ msgid "Successful connections:" -#~ msgstr "Successful connections:" - -#~ msgid "Unsuccessful connections:" -#~ msgstr "Unsuccessful connections:" - -#~ msgid "Session Manager" -#~ msgstr "Session Manager" - -#~ msgid "Session name" -#~ msgstr "Session name" - -#~ msgid "Connection type" -#~ msgstr "Connection type" - -#~ msgid "Open" -#~ msgstr "Open" - -#~ msgid "Open session manager" -#~ msgstr "Open session manager" - -#~ msgid "Foreign Key" -#~ msgstr "Foreign Key" - -#~ 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 "{} rows affected" - -#~ msgid "Query {}" -#~ msgstr "Abfrage" - -#~ msgid "Query {} (Error)" -#~ msgstr "Query {} (Error)" - -#~ msgid "Query {} ({} rows × {} cols)" -#~ msgstr "Query {} ({} rows × {} cols)" - -#~ msgid "{} rows" -#~ msgstr "Zeilen" - -#~ msgid "{:.1f} ms" -#~ msgstr "{:.1f} ms" - -#~ msgid "{} warnings" -#~ msgstr "{} warnings" - -#~ msgid "Edit Value" -#~ msgstr "Wert bearbeiten" - -#~ msgid "Query #2" -#~ msgstr "Abfrage #2" - -#~ msgid "Column5" -#~ msgstr "Spalte5" - -#~ msgid "Import" -#~ msgstr "Importieren" - -#~ msgid "Read only" -#~ msgstr "Read only" - -#~ msgid "CASCADED" -#~ msgstr "CASCADED" - -#~ 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 "RadioBtn" - -#~ msgid "Edit Column" -#~ msgstr "Spalte bearbeiten" - -#~ msgid "Datatype" -#~ msgstr "Datentyp" - -#~ msgid "Zero Fill" -#~ msgstr "Nullfüllung" - -#~ msgid "Refrsh" -#~ msgstr "Aktualisieren" diff --git a/locale/en_US/LC_MESSAGES/petersql.mo b/locale/en_US/LC_MESSAGES/petersql.mo index 9152610b04044bae59aaa5bc5dd88e38da9c5fa4..55287fb186860f3ad628363f4797060c22e19f77 100644 GIT binary patch literal 17336 zcmeI2d6->AnTHEVAnAlHVGAK}NRV`Q=H>62-()0oejwYw?J>7kiyXBmF z(`nm|QCZvoi95<74j{@nM2B%i90W#D1YBT5T%sb1q9UV$paV1S?^NB}H;MR+|M)zP zIZwX()^@7Cs`~1yI@Q@dW72yKPvZf`oB-cB)tC1V~83b9tx8#z6Bmc zJc1{~5jY2a5FQ3U11G~Tx%h)n<-ZGQ!u-Ic{~XRF{sKH4PCwqZa{@eqcs)E4E_LZ$ zP~~0&r^C%q^=*SS@Xap&3aIw4g|p#Dp$G4V_3(#KdQKx#{XQN}f@i`h@EmvuOhDD! z<+u(WOgsry|0WkNxcCT^-s5l@yb`LNYoPSL)un$HYCOLPRsSPU_3d`~PeS$US*Z8_ z;NnwJ_9)^rq4cbU$HGM}-T^fqJy7o#q2Ax_cm>qBeHf~Lw?ehQ3rg?1AX8x;g7e^0 zE`1u4EB#M^GL6%q>RApo9-UD0)C*Nl;J6j4{>!1-+X*$!*FfoaJybt#h5R$0;%71Z zI#m8aC))Q8g{uE(sP^Z(^k%5~TcPymg3_}OO23qgZ-LV5O)h;W)VN#^HBX;}((g`q z47?Yf1Rsa>@I@%SP9d=gUIZ1t9PWa5!n0wQ&4(rhO&nUYJNwd=KX4TG`tzA{9TUsLB00?RDZq+)sNj!^*;wS?q;rScQRD@ z!=TzZ#_+a{-jz>!98nfU0j3RC{@sJ_cpiZ-L$LYM1^5ls->G)pr2K zCB3FV)i)DLkJ(V;axzr?^-%NK1m!1|Lzb@Tg?c~dI0{woB~bOe18RP+hNr?Cp!B>S zGF9dgsQ&yGs{NOs>|^pNw!LGZ(ocn|w-Ktn)8Qnz!Z88W&iPRNUJq685R^S{fvWc^ zI0@bW4}>>CmHU|Er=aHfE-1Ts6srAapxS*NYFrL@mDR5XDtkrA`p2R4-UTPYyWm0a9(V}+B3u9;g0j;Wp~@dz zYv!I}eIGh4^LCxEpQ1kj#m;N}!6`LoZ-hUbD z{e$Xk{4gjzXF~OFHdMWf9nXeSiLZbf&+}b+H&i_rL9OorsBzr})y@@A{n-g=(%c9; z;a8#T?6`V6k7q!&-wmh0buQh9Q;Cm3_2*Kk`rhsMQJ4N{sQ2!H$H50+4g4`wKc0cI z_vfJ6nZ#tM{B)@J(NOx%fvSHllzlCB>~tK2>i-TXySWUioogI#g7Wvb!vwq+Ho_^V z+4jza(q{!!d*?yb*9n)y9w>cwy8LTg{CX&Ry2Yh`0;-W3@oP}y{21(j z&%xLqHCXmS={p2f->8f4aQQnOZ-yF=JE6w$Q5XL)>^T5G1cB*pvh&mf72gP@_W+b% z9)!~8tx)yc4AuWTT>NuTCOUx0e=kVSUhW3x|?e;-tTZiK4;cBuM3=ko7?>eoY1^*!d|KZ9C_ zzk$;85H@}J=_8=xwNUf12WhHCc_sCK>w)y|WS zPeYB*vru;Q5>z`Uon`Ae73#f3Q1zV+W#{L*^z)(Yx(9Z{jV^r`ls=z@s_*MidOhay zpMcWiX(;`kg{uEWsQG*u&V+|C$U1M$g?fLP<0`0nd!g#N7-~E=!c$=$O3(K}&FhU& z^KdUze;$Ogk4K@}`-w|`4yxXlpxT?nWJ<5Y9gl))XBJeyYaP#kvgdZFdIzB1i=fV1 z+o0MXbG#HDOZ+N03*G{y*F8|}eg$e=9);5H`!4<~sCu4;t9Z=(P z0aW>mpytDeYHu5SFWd=bXSL_p^VTxBg80Qy?|%sD{f{_)0;>ESQ1kNulzzLR+IEhr z((|WK{d)$g-pL#S#OYAuHxp_+=eYEFQ1#S9t?$#J#&tDRI~$?;6Tn(nf}QX>I0ya$ zY96PquPJ0P`Vy#iyP*1W5sd3|EV=Ycq27BpJPv*sO0PSh^#20XINuM|&u>AM z-|ga0LFxNzsQP~g)&8Uw%M%^Xg6e+{l-;a@YA5430_E@D3={A@un~R-s=cWglk}MZ z)!r@76-uk)F?5n0|OU@^j>Ru9Sw;9Oh@M{{Z;M0rX9E~Ripam)B$I;wg&OMjXOQO*Jy#*J`FZFND1qlX1QUs$Pw@L)#3Nsi)>rvC zk+iQ7c^LT}@}_bI+=<+ZoJ^VHVILyj^c12;{^$}!>jN_~D-qe?y-4}FiNLFolc?hi z_(Md`7m#t}V~Cz7EzCoXWqgakOcyy3eh#@5c?)tFb)E$Oh>RfRXOj#351dSyH4fh_4n=;7>_*DZf0P68Hsq@=z7FP*BdXfbef>$iloHu0YOq@dI65uO)s9ays%+yo-l2$ldL3tzH;r%?az1j9OBa6Z!mAzs8zzx+Ts(Hngujf`xU~6BUqv5v zX^Y{}q}L$>F711cLJ#sU$RUdGgvjzZWK6-a1)hq08~Hu*D@4zike?twL*^j&BdTVMO(f^`@c0Ybh1;k-s%a*&MH! z_r?a(nP5Y~FAk5ZMDHu-w)FR{^*YwH^(49yYx+#fK)zHoEy-lQlq-5s(Jz)F(~?R_ zypCKd*cOMn!*tdUM^^`H#%Y83Fh~#Oyv?pmE*SBGtwE03 zsYjF^_pP(Dl-hqHt-`4MkT ze`lv@DHMWSN+XfBIu{BVRMoGS9QJcVLFD-iTh=e8lYXX>$FLQHFybvQ$xJQgj zqQ|s^fj62jc@g@>ff1h(qJ>nD35tOyWxV-gpID4+UN_pUVYt zkG*0#8+f(ZsLr(dQD9nwVSj5nADXt7-nN$ZglSuqXj|>AUDLVVRH@MB=aNCjv<(Nz z&Gt7kZJEI5y-Yq=-GEE5<83o5qiv%TV{PMhq+QsJvyH{0Ob|WeUu$KiEuYCSdQ2Z7 zW?$Vas=GE?OW!J?ro^^F7(~%TX}+dx?pQK@dqX@=%?p=|lb+SE(Y1Uwehb4+X3``z zMM20|dgxxv^ByH~skGA}H<%8yUVA(l%7~+OZYoi&5=Ar(Wm=soGZTcN=3!#0;)Ld! zv6#|sqA1H_Ad`-UQ7|eclXP&fBwMN~lSyaO)lGSf;~sHYf|eZb(6wT0cBU;1{34ph zsIr@KN-7=FgM3I+wuqLKwRD@1^8BJ{Pps@%gXOkk$?=a@94!QKXh2q{bOoFR@70Mt za~UNs9eKGtPN7sN!^2tW+48_F&L2Wl+J=W3HXClv84Riwb@c zJ%T}A4=wFnX$|_JC4m@TcAEu(^uaqA*>y5?Okn^*__+CY6%@6TW)MZ^#?6~E2 z)Yre@l~-&+=5FhsFk04I*?|946F9D)K>Kt9qk+K6e`S6}MEU(rfa~ zaU||Rxh+bJU}#lSGU0*ak*xF%1^4bMuKAVUtgI>Zth~PL5bgV(CO;lVYWy>OrVpNP zJ^rvCF_v__5QL0qHb~J8y{aYUZ}roec&lqK6*BBdEdGQYy~rfkNJa~>mrKM8jg%p| z9g~QCRs3t$um(~l-RzBx+guMDP^z=mJrnmV+H4Zqo@sDfkSs|9OOO4rIvk%0;+a*^ zieF#JiNokewyuCSQW)azR^~(2Ilu9VfKvk*HbQue;ZXVpP6``xrR+cu#{0m^Fvul` z%}RGlh|GBbUc%3qRY9g;R^dmz1)e;S=~#2&+SNF^juiT*IU%u)2c}~X$D~0`o#xra z7_eEe?_Xb7+#nkFllF6_b8TBor|IO_QQ{mxfFaROwP%mxgP4Ji7p%aDjWtf2*je@{ zj?XO)H}wY8l;;Fv2OAcc7d<6i9;)|h0_ zP^-Cd@vFw`OjkaY9@NmVfplq%u^b9iwawxU?Z)4g$G^1aM{<-T)L*bnkz&t+?KYuQ zu+jc*(>2=Z4{(O*8tn>lC5Mh;kTvnSZH;Va4V&4-(v72)1sTWU1F&&xi-s%Cq53Ty z%;HVR+v;aZ)XE9TtlezZ=DhYqXQD5mP=9xOi$d~A6EAyNKgWoKae8m>DtG*dm+Bw~ zW0vmPctJ*{+h^zC_=l-?`%yH)mQV?J!L|a9#B?ja_kvE-T{+)YLeAUnwf9bBmkM-_ z*_f~brm~VvqhUTa_6zh)VB%k|c2Qk9G!_q)vr=Z)76{^ntt~wnpD0-(`{YDL9KOt8 z5aqqGAmbNgGL1DlmFd8XPJA6$KY&5u#%=O=(^xT&;ZwA?awwvJ>jB9tvGUjmW$tmE z*3iqX}9O^9<89YT0uQ!r>~K-x;K+J7|2oS^sU--?wv@B{f^@Jlwp|7^QACc z<+`epn&iv0d)m_lYr4G^#}y6*MX%P`GMRZL%lkETQ@*Y-u9ai0YGN%|E!^@;be5By zkBz^^daR4?VfQ7OPas1uGCif7BzE^$YwScLueQE1+LEbzLyR}@i8Ya4uhe>@vJpE5 zA7P_LtEI2R)|iQd*3#mgO*z4~V(8<5hoU&QdLWct758d23QBZm{^GD-l)59FnHYu8 z9~?|4H5w|{-dYz|SST++y@-nop;HRZZF6Op?Cb1p^!V{A2OGE9?)hE4Z)H@iO{qbf61j2} z)ouAj2BW|!q|cqy9V-cAMVnFe#n`{be(JQF{@i8`h&eC*SR)pHelb^D;ziw`voq&z z0LZuT*hwnJDFjzYpyIfyREtc%bH=Qge*8aMRX<-(vbCZ$c|{o$C0o24% zLO-f6!g|wuDBC!Lv5$j~bL_XqUamjR`IDQ1a-z7-v8@EFhUda`SS)c8VR>G}A`LwD z_@FB2)h=#cxUkNw=XQg8h`=lDIZlo2@e`jwn2knF>n>aVI-pZ5krJz9`Jbt!SFgG6 zi6oX57r!`&cd(4{y@C!$DWAh3=P)+Z^kBTWg4~=mC3`;k$xkY-DoU`SDO(7DOh(4;S;?9h7qE;-prK^Bh+@b~*O7nlT&=&LBu_ z(6-?=k8+yX>l>)C`g6}~ush9|J|^|{I4SInHkcn&7~{9~n(_|2AsaO}E*u>9veBMt zi}p&R*kY0>m1R1|%|2V4*+ZGywzb{s>qx^X=-?L(vwLvN=TI<)PtD20l{d(8+-&!1 zJM(^uyvD}HI`sRn2A&0hJ~<1K7l)VQ#wC(!=Z&=n1( zQw^=9p{Svc54=QIOGoF5miG3ZL~n0R_u7V@U~5`SrlFl?n!Uvf7oX9va7n|Wh2EmG zninrxu#l%l=Y@v8(9cEOD&)gvj}&{jZsyFtp})5hZ|DpBtV{IH>FivQNjK)hp>u1x zIcWQthLvo9e3a%2aWq=Cc&TTD%W8Ap$;-S&bxUiyI=T`Q-BMrL>{VAoANMKEo`$rk zkYPbhV3T zE4OK{Y$wLn#_hWtQbJ%~a%k}R6 z;aYc}D-m}`J7LT&!CJG8aA~ZMQSNT{-{Eq6EB{Su{~hk%`5mr4O7{8q*0pZ@ZCQ1* zTXl=`*SO8C+8*~`=T2E!na2;%HMEqnv`|}kp%jv~S=u7BDW%O;8c5QR1;jGOn|t3R*WQiyy|--| zQrs9uWE|81VFm%0fq~J%1zBWulo4y!MMR;4RhB_OobT^F=ic13;0*uFeCA`` zPoMie&pGco=Q+<=-;?enEU4$gK8dtGvT+K{y01U z{Ruc3?t;g_r{DrO^=-x+1Q$W2pXl^@sC=z(I&63GXTw9$2jDEY9jd;I;lc0m7IwhJa0isVH$eWG+xVFTAA!^0 zkKt7KBvkp&I=%#_pzlYcW$!eo`sP5z&v*Jl$114y)WQSdI;eWvpxU#^#czfxZ!1)N z7ebYLsY|~cs=c3v%Ktg1-vQNi_rb_+I2Zj%r@so-Pm_+a`HzRnU+1_M%AXxj?cD@b ze;ms03_KEE02jh5UHn~8_J0>10(U}{^E)U%ybjfWlaIFL%yq1UDt{GJea%pQZ-Z*z znNaQ61Tozt;BxqWm;M!~eD^?=|4pd+A9wM)p~`;&%8oxl**V3t_RWFP7ecjf1(e^L zq3rL1@=qUBe`TQR+YaZz_d|*_SHi{cb}0LP32Wh8oGtw{_(ixKu7XRCHRfbE0OilC zq1t^LtcQ0&wc}+dKOBg&W#8dY{x}}0UA0i{s)w?x0cxDILWZW<2-W{7$ka3+gc={$ zz{BCqQ28Hpd>E>n$DrE#GpKg%hN|y?OIbJF_i!6pvK!8sCxRK z+A{!^F9#>VZBX@%y7&)4^}|PDC%neR?}4)CWvKFIEwb;;fwJ#7sB&xJWOy=^oonDE zxE`v%&wyHQHbMC%3zh$3$IGC~zZNS0^>8x$0@Qf81s)IYgR*-!)HrwbJ#E^{$30?+lmT1E-*$3srv{%Fc{q2oFHt237C*P=5I^R6SQh&5O@K)$=u|{NIQB z!$+as`?2FMp~k~=P~&dO3D!;zs{TqS`&K~Ndy3QBpvvimOl8yOxWn<2Q2FnKQ{gw^ zWcWR(e2+l&+b*bjUxM0r4z9H0@j9sSdIwwwAA`z&WR=Z-v||<2_+0_je;c6eJQq%d zDX4jqhmG(eI0ZfkRsX{-{nsx3RVcgnUu^w06UyI*K$SNKE`diujpx&01w0$7Kl&Zd zgRo9>ivfucS7~cvrzf=zzX;$DElg?WEz|e)sOR_`gw_qUj?;)HA0p1E~xx* zr)Qw-4WZgO0#)wE9Iu4a(XWN_>y0k{3y!x!m3KGPy6^y$pLapk_X1S={s8G3Gkqy* zG+Yif{w{#(*H1#(aVu1N?s4(=L)HHasCMm!D(@A?ifS7_4=P_JJOVC*8po$Y+1C!$ zu6IEBu@9=g3{<`?PQL)k?u(%6xdh6-s~m4}dA|=SHadz6e#`EpRQo6Ux5VT>7M1t51g-XS1R59S&8_JePiqW0m7F$5l}N zZh)<@56bQv9Pfg%_hG2=o^<*%F8wvfgX^q6j)MpB-lN49ecS7k8 zLfQRosCGUCwZ8ud%C488%A3Qcr}RZo`Vy#o^-$yBR2RPi%Ko$AEVvoYgd;BfLvS|w z6)yfpsP}GzGvNJDb$gVW84Hhu~`nDiO&K)Ar^RZ#7zck!n~m3J1D zJqf6Cvo5^=)!z3(mG@z%Uj+|CzYfaoJD}G2Z#exYQ2q22RQ{=Nx9y(o=t23j7OK6c zK-J$2RnJDK`SWhL5N>zzS3}u<6V&_nK$Y_#lz$$D>c5>(Uq^~{Cp z|M~DxxD?KV>!9*)avXpv|2(Mt+o0O_J}CcO1doT8L)m>7)Hrwms=s~%WykYS{q`zU zy))L>^5#ROFM_J48mj&iq3m4i*bKE^wnEk04%Oas9p3{Npl^e!=W?k0*F)`>pM$dJ zCdb>M#>3a)k?;v9J6?pU|Bq1Zop!3VcQ%xM4232cJKLlm>qfmbP1(d&^hAQt_xCA~AHJ*=duW`Bh*F)LS z3whP}Q14&o_+_Z_?}f^DA5^_RgtB87l%IbE)sMf0>Sr}V@dv@T!MRZ7tboeD(&W*w7&27NrBL=g1ohryPTvI&Kz|x4-?MN(xCibJfA9FB8e?S$H7EEB)8 zr_6KVk0XsU0{Yd2H#z-O^a}}}?CL%SRwKtDe@*;6cqZK9-v1Chl(_QeB^3GlZ;=y_ z5mZJhG-pH<>EFHJ_h+F@-akXw2HLV zE*;gJrym}z&rc!KNUKr=Pp3un`$vASM1F_NcKJsLpN;$tGKIK07yotit;n%R`7?+z zj5H%hBbuvsA(O~=7CaKEaAjz}EFi0p@@Fba3$hUTPb7vsj_7#?Swa4%;e7ayigeFW zu)>8;A?+iCH#q$OcpKs)%Srnd{0yRdc|FsR>BtJ?d}JN+81ijo2J$K7GUP<$R^-!2 z@wt;gqf^$v4G8s7s`2wP67qZBe^De>%xvl-y^(=r%J0j?@I~hIl+%$mG2+AIld)(~w9=yw*&@-xh^BgJe1u z44>f-M`}EqDx@>hn|iw1mb7+sM{xt$z)uclyv^>NjK9_MxA+-KrzmLyg-kr3%w|dv zxgZ<&6NSJp#pGlCsWL4{4i5T3DRoQI-)b6CgV`XNA4(HSWw&}Ay=`r#A(!(r33(!E zZO-LVn5thdJ`~Fg`k@!YZRuD(8IPq(X}B%#2O+t2!#us135}X?k{B-1>Eq)Li^u2gClkp=DdHH18 z_bSt2m1&HHzG?J_Vq22gz%(^N6>o=Or`8~OqMRd?mH|}mc~u^M9H(n^Xxfs8rsbod zYnlQ-mdDZv=n*sBraQ}L3Q482U2g{6?oWrIA8?jQE@L&W--!DPm-DHRx+W`7`7NMVAd$4GF4 zr5F>9w=frrV~0OL_Q2A1r|U+G$S2lfJ6-Ltc8R5x$mwGP+eP52Y>=SzXsX+BUR2yb zcWN}!o0GA@AeJ`G$uMsF+8YS6Y385Co@vgGEg%V3jYR0n#M1ug zUawMHEMV26EW5}>TY(lbwfrq)dDAL1_-*-PLzvlD9L{f41xs7p4zQS)D5P^Hh{`_ zHm!2QY&Nr$(H7NIrMEk>^^p>_pja1gY-QM$b;;P89(l6VIt<*qsjTyFp0hN=)Ux7i zvo6{;*L)g^g*cS<=llS-ru_tMkCVq0R#RJ4OO7jh{! zI@W_y$PG8sVtpE#7B-;aT(lClM3a%2K`l+DC0fs-Upvp`-qIYY_eMtTD`Sb81!eIJ zn>WkilHAyL<+#Z4rPP}i9Um}sn;$RG6i1K!yF477TB32HS4(@wdQL<}KbkoTXrqS# zYw7xIz#77DbYkIDLxNQZkKk9u_i>Wx%M{Z6eh}?3>jOU%A2RFRDJC?h`K&;(l-b~? za%KZ-w7105DrZ_d&f0hei)d>C`;(mGIA-{!b$~@ry^~n#*{SWb5pgbIa>O{PBpK#O zwy{K#e&gJ-l|v4zJdW2cNDtWKOg8Y`J|7*={Ln-kiI(zmds{OsKNu6C*!x8-yVbPj zW2s~;W7;-0HME&F4oL-0Ap~%ce#-ZdNFQKy=`h6X4%xz^xUoHK-0Q6F`JviwjvsAV z&RfFracyHR*?xWn_(Yp_tWfAULo`o3@BmLs~lmp$MGfh)z>Uv zG+Jfavx($@JjR~YF4r@lc%jS)R*1mWlVuT$CRw{ey*YBg1k=tQVViA4A!pUzPSZZz zSjgvTWczSite-Pg`*6FTDLAy|{j`bBogErm9h^#$vbrO+v;;(2bd)!4BCGb|Mmn~{ zC^g|Wzb&y;fs%Dj+ql_m%y`W$Z7n@53iWn2Hz=f4a4fTzj%9F95XEEw8r<_ODl=!}KoR_%1ffah<^v2>YErFWm%W;#n}^is&J?|be2W65@l zd(-4Xj<(ZtV?&2-FXhIjAREn>GxhOfqF=9aSyeGK5_!@MbVh=G!H?Dmt&s8P5X>yw zCne0Y18K+%=xjXVr(!t`ubK)S8nw}56CXf!DPi!juh{6(+L3&g*}|*cr865ZxE2t- zo~aQHSj~~BOglS@#npQYM^enG(Hdvf_*T4Oq*ocOz{M7h&%vAsvRlKx=(}yyQ`r0Z zCuFnE)unh;&eUfXu-560?fd0$SV2`&Bhjshql1 z5>v%m;ihU!TQS-#)6oZKmtEz%I1UiaSDqmlnyx}d673ia=4c%by~^lIy6XH0=kpag z7Qa}ri;dn5*>w2M95$LvJq7maR1~x$E!t(3;&00bG1l>=Rz(HoiZfIhSq_6}JL}d*iGI9p+-X03A~l*EraM05 zr(>qO)C3ps`eQ+mWRd9R6dLEiV0R|g={y?@EpP#k4SDYw+MV1^l?>=)$Tu|aGMkK> z4n^`QQ??}W&M<5CHV|sX>}hH5>}hChYcV~M`=Z}^tAq~G;XX=m=iX>ri}v3h?nJTy zw-9#swBG?eKD$*9olyR{*rNmS*fGv|w6x-GUMOXalB!HBv4w;%1jFlTV$_WrI|{- z>>F%zy?pg)5a#V%kcwLrZO7zUY$z-v<}BUP=to6{4C^Ex_*Tas`>gy?${TJOQOXrQ>~coMM2oa`n>!HZj8&5J%+DWOQO+>Ee!}^-Kfl)6Vj*nZmR~)Y`CyKYybmyqqXYVuPjHzte*m+JB zajXkEJ%&SUD||w5S{Pxm&1kJF?wm#4?C>hvvatkdH8nL=X2%FC3zwdFgCfdPIG?-+ zFIBfDs@;&U_ln0QudZfUO?=7!eeo}Sait1%`-m;bT%a<*w6Xt#YA1L zi-dr|x2Cseef7$*_f$Wpf@(e*IeBLW>%Eoz$$Uj89|W;f^?LSPKFl+@NDbF4U+vl8 zn#zoK%o=Z5)#?iSwS+sX8;9$?MxCm?8;#i>(gnUy8~TpN8iF_ciUx`Q2!hE*=5`D#D%v0Qd#_?Z29)x zHgTa{em)<6TA#So<|BOKQhV4Ne?>iUsXcM2J#nec-+SFnwf6bHm`iQ@(NVnHp19L4 n{x`3QJMFiAr|tfC%09Q+jfT5yuCcknp18aocSZf?m)HLVh^Y_2 diff --git a/locale/en_US/LC_MESSAGES/petersql.po b/locale/en_US/LC_MESSAGES/petersql.po index b0ac6af..3a81cb0 100644 --- a/locale/en_US/LC_MESSAGES/petersql.po +++ b/locale/en_US/LC_MESSAGES/petersql.po @@ -6,15 +6,15 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-05-31 12:36+0200\n" +"POT-Creation-Date: 2026-06-06 10:58+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" -"Language-Team: en_US \n" "Language: en_US\n" +"Language-Team: en_US \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.18.0\n" #: helpers/__init__.py:16 @@ -46,778 +46,886 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "OpenSSH client not found." -#: structures/engines/context.py:544 +#: structures/engines/context.py:567 msgid "This connection is read-only." msgstr "This connection is read-only." -#: structures/engines/mariadb/context.py:632 -#: structures/engines/mysql/context.py:643 +#: 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 "Table{table_index:03}" -#: structures/engines/mariadb/context.py:660 -#: structures/engines/mysql/context.py:671 +#: 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 "Column{column_index:03}" -#: structures/engines/mariadb/context.py:678 -#: structures/engines/mysql/context.py:689 +#: 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 "Index{index_number:03}" -#: structures/engines/mariadb/context.py:718 -#: structures/engines/mysql/context.py:727 +#: 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 "ForeignKey{foreign_key_number:03}" -#: structures/engines/mariadb/context.py:751 -#: structures/engines/mysql/context.py:758 +#: 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 "View{view_index:03}" -#: structures/engines/mariadb/context.py:804 +#: structures/engines/mariadb/context.py:830 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "Trigger{trigger_index:03}" #: windows/dialogs/connections/view.py:417 -#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1290 -#: windows/views.py:33 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1303 +#: windows/views.py:62 msgid "Connection" msgstr "Connection" #: windows/components/dataview.py:115 windows/components/dataview.py:240 #: windows/components/dataview.py:253 windows/components/dataview.py:268 -#: windows/views.py:47 windows/views.py:97 windows/views.py:1024 -#: windows/views.py:1336 windows/views.py:1369 windows/views.py:1393 -#: windows/views.py:1417 windows/views.py:1441 windows/views.py:1465 -#: windows/views.py:1582 windows/views.py:1928 windows/views.py:2199 -#: windows/views.py:3293 windows/views.py:3515 +#: 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:438 +#: windows/views.py:77 windows/views.py:467 msgid "Last connection" msgstr "Last connection" -#: windows/dialogs/connections/view.py:669 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:212 -#: windows/dialogs/connections/view.py:615 windows/views.py:65 +#: 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:613 windows/views.py:1496 -#: windows/views.py:1864 windows/views.py:2154 windows/views.py:2378 -#: windows/views.py:3203 windows/views.py:3252 windows/views.py:3651 -#: windows/views.py:3683 +#: 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:1341 windows/views.py:1637 -#: windows/views.py:3115 windows/views.py:3570 +#: 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:1150 +#: windows/views.py:190 windows/views.py:1179 msgid "Password" msgstr "Password" -#: windows/views.py:174 +#: windows/views.py:203 msgid "Connection timeout" msgstr "Connection" -#: 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:214 +#: windows/views.py:243 msgid "Use SSH tunnel" msgstr "Use SSH tunnel" -#: windows/views.py:225 +#: windows/views.py:254 msgid "Compressed client/server protocol" msgstr "Compressed client/server protocol" -#: windows/views.py:244 +#: windows/views.py:273 msgid "Filename" msgstr "Filename" -#: windows/views.py:249 windows/views.py:368 windows/views.py:3809 +#: windows/views.py:278 windows/views.py:397 msgid "Select a file" msgstr "Select a file" -#: windows/views.py:249 windows/views.py:368 +#: windows/views.py:278 windows/views.py:397 msgid "*.*" msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:266 windows/views.py:1343 windows/views.py:1595 -#: windows/views.py:3113 windows/views.py:3528 +#: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 msgid "Comments" msgstr "Comments" -#: windows/main/controller.py:769 windows/views.py:280 windows/views.py:740 -#: windows/views.py:894 +#: windows/main/controller.py:772 windows/views.py:309 windows/views.py:769 +#: windows/views.py:923 msgid "Settings" msgstr "Settings" -#: windows/views.py:288 +#: windows/views.py:317 msgid "SSH executable" msgstr "SSH executable" -#: windows/views.py:293 +#: windows/views.py:322 msgid "ssh" msgstr "ssh" -#: windows/views.py:301 +#: windows/views.py:330 msgid "SSH host + port" msgstr "SSH host + port" -#: windows/views.py:313 +#: 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:322 +#: windows/views.py:351 msgid "SSH username" msgstr "SSH username" -#: windows/views.py:335 +#: windows/views.py:364 msgid "SSH password" msgstr "SSH password" -#: windows/views.py:348 +#: windows/views.py:377 msgid "Local port" msgstr "Local port" -#: windows/views.py:354 +#: 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:363 +#: windows/views.py:392 msgid "Identity file" msgstr "Identity file" -#: windows/views.py:379 +#: windows/views.py:408 msgid "Remote host + port" msgstr "Remote host + port" -#: windows/views.py:391 +#: 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:400 +#: windows/views.py:429 msgid "SSH extra args" msgstr "SSH extra args" -#: windows/views.py:415 +#: windows/views.py:444 msgid "SSH Tunnel" msgstr "SSH Tunnel" -#: windows/views.py:421 windows/views.py:1339 windows/views.py:3117 +#: windows/views.py:450 windows/views.py:1368 msgid "Created at" msgstr "Created at" -#: windows/views.py:455 +#: windows/views.py:484 msgid "Successful connections" msgstr "Successful connections" -#: windows/views.py:472 +#: windows/views.py:501 msgid "Last successful connection" msgstr "Last successful connection" -#: windows/views.py:489 +#: windows/views.py:518 msgid "Unsuccessful connections" msgstr "Unsuccessful connections" -#: windows/views.py:506 +#: windows/views.py:535 msgid "Last failure reason" msgstr "Last failure reason" -#: windows/views.py:523 +#: windows/views.py:552 msgid "Total connection attempts" msgstr "Total connection attempts" -#: windows/views.py:540 +#: windows/views.py:569 msgid "Average connection time (ms)" msgstr "Connection" -#: windows/views.py:557 +#: windows/views.py:586 msgid "Most recent connection duration" msgstr "Most recent connection duration" -#: windows/views.py:576 +#: windows/views.py:605 msgid "Statistics" msgstr "Statistics" -#: windows/views.py:594 windows/views.py:1821 +#: windows/views.py:623 windows/views.py:1850 msgid "Create" msgstr "Create" -#: windows/views.py:598 +#: windows/views.py:627 msgid "Create connection" msgstr "Create connection" -#: windows/views.py:601 +#: windows/views.py:630 msgid "Create directory" msgstr "Create directory" -#: windows/views.py:630 windows/views.py:854 windows/views.py:1491 -#: windows/views.py:1867 windows/views.py:2159 windows/views.py:2383 -#: windows/views.py:2439 windows/views.py:3179 windows/views.py:3686 -#: windows/views.py:3823 +#: 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 "Cancel" -#: windows/main/controller.py:323 windows/views.py:635 windows/views.py:2164 -#: windows/views.py:2388 windows/views.py:2603 windows/views.py:3691 -#: windows/views.py:3829 +#: windows/main/controller.py:324 windows/views.py:664 windows/views.py:2193 +#: windows/views.py:2597 windows/views.py:2812 msgid "Save" msgstr "Save" -#: windows/views.py:642 +#: windows/views.py:671 msgid "Test" msgstr "Test" -#: windows/views.py:649 +#: windows/views.py:678 msgid "Connect" msgstr "Connect" -#: windows/views.py:752 +#: windows/views.py:781 windows/views.py:2388 msgid "Language" msgstr "Language" -#: windows/views.py:757 +#: windows/views.py:786 msgid "English" msgstr "English" -#: windows/views.py:757 +#: windows/views.py:786 msgid "Italian" msgstr "Italian" -#: windows/views.py:757 +#: windows/views.py:786 msgid "French" msgstr "French" -#: windows/views.py:769 +#: windows/views.py:798 msgid "Locale" msgstr "Locale" -#: windows/views.py:790 +#: windows/views.py:819 msgid "Column content" msgstr "Clone connection" -#: windows/views.py:800 +#: windows/views.py:829 msgid "Syntax" msgstr "Syntax" -#: windows/views.py:857 +#: windows/views.py:886 msgid "Ok" msgstr "Ok" -#: windows/views.py:888 +#: windows/views.py:917 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:897 +#: windows/views.py:926 msgid "File" msgstr "File" -#: windows/views.py:900 +#: windows/views.py:929 msgid "About" msgstr "About" -#: windows/views.py:903 +#: windows/views.py:932 msgid "Help" msgstr "Help" -#: windows/views.py:908 +#: windows/views.py:937 msgid "Open connection manager" msgstr "Open connection manager" -#: windows/views.py:912 +#: windows/views.py:941 msgid "Disconnect from server" msgstr "Disconnect from server" -#: windows/views.py:914 +#: windows/views.py:943 msgid "tool" msgstr "tool" -#: windows/views.py:914 windows/views.py:2419 +#: windows/views.py:943 windows/views.py:2628 msgid "Refresh" msgstr "Refresh" -#: windows/views.py:918 windows/views.py:920 windows/views.py:1845 -#: windows/views.py:2423 windows/views.py:2589 +#: windows/views.py:947 windows/views.py:949 windows/views.py:1874 +#: windows/views.py:2632 windows/views.py:2798 msgid "Add" msgstr "Add" -#: windows/views.py:925 +#: windows/views.py:954 #, python-brace-format msgid "{mode}" msgstr "{mode}" -#: windows/views.py:962 windows/views.py:966 windows/views.py:2556 -#: windows/views.py:2561 +#: windows/views.py:991 windows/views.py:995 windows/views.py:2765 +#: windows/views.py:2770 msgid "MyMenuItem" msgstr "MyMenuItem" -#: windows/views.py:969 windows/views.py:1895 windows/views.py:2568 -#: windows/views.py:3714 +#: windows/views.py:998 windows/views.py:1924 windows/views.py:2777 msgid "MyMenu" msgstr "MyMenu" -#: windows/views.py:984 windows/views.py:1519 windows/views.py:1526 -#: windows/views.py:1533 +#: windows/views.py:1013 windows/views.py:1548 windows/views.py:1555 +#: windows/views.py:1562 msgid "MyLabel" msgstr "MyLabel" -#: windows/views.py:990 +#: windows/views.py:1019 msgid "Databases" msgstr "Databases" -#: windows/views.py:991 windows/views.py:1338 windows/views.py:3118 +#: windows/views.py:1020 windows/views.py:1367 msgid "Size" msgstr "Size" -#: windows/views.py:992 +#: windows/views.py:1021 msgid "Elements" msgstr "Elements" -#: windows/views.py:993 +#: windows/views.py:1022 msgid "Modified at" msgstr "Modified at" -#: windows/views.py:994 windows/views.py:1353 +#: windows/views.py:1023 windows/views.py:1382 msgid "Tables" msgstr "Tables" -#: windows/views.py:1001 +#: 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:1047 -#: windows/views.py:1342 windows/views.py:3114 +#: windows/components/dataview.py:89 windows/views.py:1076 +#: windows/views.py:1371 msgid "Collation" msgstr "Collation" -#: windows/views.py:1076 +#: windows/views.py:1105 msgid "Encryption" msgstr "Encryption" -#: windows/main/controller.py:1259 windows/main/controller.py:1281 -#: windows/main/controller.py:1285 windows/views.py:1088 +#: windows/main/controller.py:1272 windows/main/controller.py:1294 +#: windows/main/controller.py:1298 windows/views.py:1117 msgid "Read Only" msgstr "Read Only" -#: windows/views.py:1105 +#: windows/views.py:1134 msgid "Tablespace" msgstr "Tablespace" -#: windows/views.py:1126 +#: windows/views.py:1155 msgid "Connection limit" msgstr "Connection limit" -#: windows/views.py:1169 +#: windows/views.py:1198 msgid "Profile" msgstr "Profile" -#: windows/views.py:1195 +#: windows/views.py:1224 msgid "Default tablespace" msgstr "Default tablespace" -#: windows/views.py:1216 +#: windows/views.py:1245 msgid "Temporary tablespace" msgstr "Temporary tablespace" -#: windows/views.py:1242 +#: windows/views.py:1271 msgid "Quota" msgstr "Quota" -#: windows/views.py:1261 +#: windows/views.py:1290 msgid "Unlimited quota" msgstr "Unlimited quota" -#: windows/views.py:1278 +#: windows/views.py:1307 msgid "Account status" msgstr "Account status" -#: windows/views.py:1299 +#: windows/views.py:1328 msgid "Password expire" msgstr "Password expire" -#: windows/views.py:1323 +#: windows/views.py:1352 msgid "Add new table" msgstr "Add new table" -#: windows/views.py:1325 +#: windows/views.py:1354 msgid "Clone table" msgstr "Clone table" -#: windows/main/controller.py:1770 windows/views.py:1327 +#: windows/main/controller.py:1821 windows/views.py:1356 msgid "Delete table" msgstr "Delete table" -#: windows/views.py:1337 +#: windows/views.py:1366 windows/views.py:2457 msgid "Rows" msgstr "Rows" -#: windows/views.py:1340 windows/views.py:3116 +#: windows/views.py:1369 msgid "Updated at" msgstr "Updated at" -#: windows/views.py:1358 +#: windows/views.py:1387 msgid "Add new view" msgstr "Add new view" -#: windows/views.py:1360 windows/views.py:1384 +#: windows/views.py:1389 msgid "Clone view" msgstr "Clone view" -#: windows/views.py:1362 +#: windows/views.py:1391 msgid "Delete view" msgstr "Delete" -#: windows/views.py:1370 windows/views.py:1394 windows/views.py:1418 -#: windows/views.py:1442 windows/views.py:1466 +#: 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:1377 +#: windows/views.py:1406 msgid "Views" msgstr "Views" -#: windows/views.py:1382 +#: windows/views.py:1411 msgid "Add new procedure" msgstr "Add new procedure" -#: windows/views.py:1384 +#: windows/views.py:1413 msgid "Clone procedure" msgstr "Clone procedure" -#: windows/views.py:1386 +#: windows/views.py:1415 msgid "Delete procedure" msgstr "Delete procedure" -#: windows/views.py:1401 +#: windows/views.py:1430 msgid "Procedures" msgstr "Procedures" -#: windows/views.py:1406 +#: windows/views.py:1435 msgid "Add new function" msgstr "New connection" -#: windows/views.py:1408 +#: windows/views.py:1437 msgid "Clone function" msgstr "Clone connection" -#: windows/views.py:1410 +#: windows/views.py:1439 msgid "Delete function" msgstr "Last connection" -#: windows/views.py:1425 +#: windows/views.py:1454 msgid "Functions" msgstr "Connection" -#: windows/views.py:1430 +#: windows/views.py:1459 msgid "Add new trigger" msgstr "Add new trigger" -#: windows/views.py:1432 +#: windows/views.py:1461 msgid "Clone trigger" msgstr "Clone trigger" -#: windows/views.py:1434 +#: windows/views.py:1463 msgid "Delete trigger" msgstr "Delete" -#: windows/views.py:1449 +#: windows/views.py:1478 msgid "Triggers" msgstr "Triggers" -#: windows/views.py:1454 +#: windows/views.py:1483 msgid "Add new event" msgstr "Add new event" -#: windows/views.py:1456 +#: windows/views.py:1485 msgid "Clone event" msgstr "Clone event" -#: windows/views.py:1458 +#: windows/views.py:1487 msgid "Delete event" msgstr "Delete" -#: windows/views.py:1473 +#: windows/views.py:1502 msgid "Events" msgstr "Events" -#: windows/views.py:1501 windows/views.py:1872 windows/views.py:2437 -#: windows/views.py:2521 windows/views.py:3186 +#: windows/views.py:1530 windows/views.py:1901 windows/views.py:2646 +#: windows/views.py:2730 msgid "Apply" msgstr "Apply" -#: windows/views.py:1513 windows/views.py:1692 windows/views.py:3603 +#: windows/views.py:1542 windows/views.py:1721 msgid "Options" msgstr "Options" -#: windows/views.py:1544 +#: windows/views.py:1573 msgid "Diagram" msgstr "Diagram" -#: windows/views.py:1555 +#: windows/views.py:1584 msgid "Database" msgstr "Database" -#: windows/views.py:1610 windows/views.py:3543 +#: windows/views.py:1639 msgid "Base" msgstr "Base" -#: windows/views.py:1624 windows/views.py:3557 +#: windows/views.py:1653 msgid "Auto Increment" msgstr "Auto Increment" -#: windows/views.py:1652 windows/views.py:3585 +#: windows/views.py:1681 msgid "Default Collation" msgstr "Default Collation" -#: windows/views.py:1662 +#: windows/views.py:1691 msgid "Convert data" msgstr "Convert data" -#: windows/views.py:1670 +#: windows/views.py:1699 msgid "Row format" msgstr "Row format" -#: windows/views.py:1703 windows/views.py:1731 windows/views.py:1759 -#: windows/views.py:1847 windows/views.py:2259 windows/views.py:2427 -#: windows/views.py:3313 windows/views.py:3330 windows/views.py:3354 -#: windows/views.py:3381 +#: 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 "Remove" -#: windows/views.py:1705 windows/views.py:1733 windows/views.py:1761 -#: windows/views.py:2261 windows/views.py:2529 windows/views.py:3320 -#: windows/views.py:3337 windows/views.py:3361 windows/views.py:3388 +#: windows/views.py:1734 windows/views.py:1762 windows/views.py:1790 +#: windows/views.py:2328 windows/views.py:2738 msgid "Clear" msgstr "Clear" -#: windows/views.py:1718 windows/views.py:3617 +#: windows/views.py:1747 msgid "Indexes" msgstr "Indexes" -#: windows/views.py:1729 windows/views.py:1757 windows/views.py:2257 -#: windows/views.py:3198 windows/views.py:3240 windows/views.py:3308 -#: windows/views.py:3349 windows/views.py:3376 windows/views.py:3646 +#: windows/views.py:1758 windows/views.py:1786 windows/views.py:2324 msgid "Insert" msgstr "Insert" -#: windows/views.py:1746 +#: windows/views.py:1775 msgid "Foreign Keys" msgstr "Foreign Keys" -#: windows/views.py:1774 +#: windows/views.py:1803 msgid "Checks" msgstr "Checks" -#: windows/views.py:1841 windows/views.py:3638 +#: windows/views.py:1870 msgid "Columns:" msgstr "Columns:" -#: windows/views.py:1851 +#: windows/views.py:1880 msgid "Move Up" msgstr "Move Up" -#: windows/views.py:1853 +#: windows/views.py:1882 msgid "Move Down" msgstr "Move Down" -#: windows/views.py:1885 windows/views.py:1892 windows/views.py:3704 -#: windows/views.py:3711 +#: windows/views.py:1914 windows/views.py:1921 msgid "Add Index" msgstr "Add Index" -#: windows/views.py:1889 windows/views.py:3708 +#: windows/views.py:1918 msgid "Add PrimaryKey" msgstr "Add PrimaryKey" -#: windows/views.py:1906 +#: windows/views.py:1935 msgid "Table" msgstr "Table" -#: windows/views.py:1948 windows/views.py:2219 +#: windows/views.py:1977 windows/views.py:2243 msgid "Schema" msgstr "Schema" -#: windows/views.py:1976 windows/views.py:2247 +#: windows/views.py:2005 windows/views.py:2314 msgid "General" msgstr "General" -#: windows/views.py:1981 +#: windows/views.py:2010 msgid "Algorithm" msgstr "Algorithm" -#: windows/views.py:1983 +#: windows/views.py:2012 msgid "UNDEFINED" msgstr "UNDEFINED" -#: windows/views.py:1986 +#: windows/views.py:2015 msgid "MERGE" msgstr "MERGE" -#: windows/views.py:1989 +#: windows/views.py:2018 msgid "TEMPTABLE" msgstr "TEMPTABLE" -#: windows/views.py:1999 +#: windows/views.py:2028 msgid "View constraint" msgstr "View constraint" -#: windows/views.py:2001 +#: windows/views.py:2030 msgid "None" msgstr "None" -#: windows/views.py:2004 +#: windows/views.py:2033 msgid "LOCAL" msgstr "LOCAL" -#: windows/views.py:2007 +#: windows/views.py:2036 msgid "CASCADE" msgstr "CASCADE" -#: windows/views.py:2010 +#: windows/views.py:2039 msgid "CHECK ONLY" msgstr "CHECK ONLY" -#: windows/views.py:2013 +#: windows/views.py:2042 msgid "READ ONLY" msgstr "READ ONLY" -#: windows/views.py:2026 +#: windows/views.py:2055 windows/views.py:2483 msgid "Behavior" msgstr "Behavior" -#: windows/views.py:2033 windows/views.py:2281 +#: windows/views.py:2062 windows/views.py:2490 msgid "Definer" msgstr "Definer" -#: windows/views.py:2041 windows/views.py:2289 +#: windows/views.py:2070 windows/views.py:2498 msgid "*" msgstr "*" -#: windows/views.py:2053 windows/views.py:2301 +#: windows/views.py:2082 windows/views.py:2510 msgid "SQL security" msgstr "SQL security" -#: windows/views.py:2060 windows/views.py:2308 +#: windows/views.py:2089 windows/views.py:2517 msgid "DEFINER" msgstr "DEFINER" -#: windows/views.py:2060 windows/views.py:2308 +#: windows/views.py:2089 windows/views.py:2517 msgid "INVOKER" msgstr "INVOKER" -#: windows/views.py:2074 +#: windows/views.py:2103 msgid "Force" msgstr "Force" -#: windows/views.py:2086 +#: windows/views.py:2115 msgid "Security barrier" msgstr "Security barrier" -#: windows/views.py:2099 windows/views.py:2323 +#: windows/views.py:2128 windows/views.py:2532 msgid "Security" msgstr "Security" -#: windows/views.py:2176 +#: windows/views.py:2205 msgid "View" msgstr "View" -#: windows/views.py:2274 +#: windows/views.py:2262 +#, fuzzy +msgid "Type" +msgstr "Data type" + +#: 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 +#, fuzzy +msgid "Return type" +msgstr "Data type" + +#: windows/views.py:2299 +#, fuzzy +msgid "Comment" +msgstr "Comments" + +#: windows/components/dataview.py:111 windows/views.py:2335 +msgid "#" +msgstr "#" + +#: windows/views.py:2337 +#, fuzzy +msgid "Datatype" +msgstr "Data type" + +#: windows/views.py:2338 +#, fuzzy +msgid "Context" +msgstr "Connect" + +#: windows/views.py:2345 msgid "Parameters" msgstr "Parameters" -#: windows/views.py:2400 -msgid "Procedure" -msgstr "Procedure" +#: windows/views.py:2354 +#, fuzzy +msgid "Data access" +msgstr "Databases" -#: windows/views.py:2408 +#: 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 +#, fuzzy +msgid "MODIFIES SQL DATA" +msgstr "Modified at" + +#: windows/views.py:2371 +#, fuzzy +msgid "Deterministic" +msgstr "Definition" + +#: windows/views.py:2395 +#, fuzzy +msgid "SQL" +msgstr "*.sql" + +#: windows/views.py:2395 +msgid "PLPGSQL" +msgstr "" + +#: windows/views.py:2405 +#, fuzzy +msgid "Volatility" +msgstr "Virtuality" + +#: windows/views.py:2412 +msgid "VOLATILE" +msgstr "" + +#: windows/views.py:2412 +#, fuzzy +msgid "STABLE" +msgstr "Table" + +#: windows/views.py:2412 +#, fuzzy +msgid "IMMUTABLE" +msgstr "Table" + +#: windows/views.py:2422 +msgid "Parallel" +msgstr "" + +#: windows/views.py:2429 +#, fuzzy +msgid "UNSAFE" +msgstr "Save" + +#: windows/views.py:2429 +msgid "RESTRICTED" +msgstr "" + +#: windows/views.py:2429 +#, fuzzy +msgid "SAFE" +msgstr "Save" + +#: windows/views.py:2441 +#, fuzzy +msgid "Cost" +msgstr "Close" + +#: windows/views.py:2609 +#, fuzzy +msgid "Routine" +msgstr "Uptime" + +#: windows/views.py:2617 msgid "Trigger" msgstr "Delete" -#: windows/views.py:2425 +#: windows/views.py:2634 msgid "Duplicate" msgstr "Duplicate" -#: windows/views.py:2431 +#: windows/views.py:2640 msgid "Apply changes automatically" msgstr "Apply changes automatically" -#: windows/views.py:2433 windows/views.py:2434 +#: windows/views.py:2642 windows/views.py:2643 msgid "" -"If enabled, table edits are applied immediately without pressing Apply or " -"Cancel" +"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" +"If enabled, table edits are applied immediately without pressing Apply or" +" Cancel" -#: windows/views.py:2447 +#: windows/views.py:2656 #, python-brace-format -msgid "" -"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" -"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -#: windows/views.py:2455 +#: windows/views.py:2664 msgid "First" msgstr "First" -#: windows/views.py:2473 +#: windows/views.py:2682 msgid "Last" msgstr "Last" -#: windows/views.py:2482 +#: windows/views.py:2691 msgid "Filters" msgstr "Filters" -#: windows/views.py:2524 +#: windows/views.py:2733 msgid "" "Apply filters in data\n" "CTRL+ENTER" @@ -825,114 +933,62 @@ msgstr "" "Apply filters in data\n" "CTRL+ENTER" -#: windows/views.py:2525 +#: windows/views.py:2734 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2553 +#: windows/views.py:2762 msgid "Insert row" msgstr "Insert row" -#: windows/components/popup.py:31 windows/views.py:2565 +#: windows/components/popup.py:31 windows/views.py:2774 msgid "NULL" msgstr "NULL" -#: windows/views.py:2572 +#: windows/views.py:2781 msgid "Data" msgstr "Data" -#: windows/main/controller.py:318 windows/views.py:2589 +#: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" msgstr "New directory" -#: windows/views.py:2591 windows/views.py:3137 +#: windows/views.py:2800 msgid "Close" msgstr "Close" -#: windows/main/controller.py:319 windows/views.py:2591 +#: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" msgstr "Close query" -#: windows/views.py:2595 +#: windows/views.py:2804 msgid "Run" msgstr "Run" -#: windows/main/controller.py:320 windows/views.py:2595 +#: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" msgstr "SSH executable" -#: windows/views.py:2597 +#: windows/views.py:2806 msgid "Run all" msgstr "Run all" -#: windows/views.py:2597 +#: windows/views.py:2806 msgid "Execute all statements" msgstr "Execute all statements" -#: windows/main/controller.py:322 windows/views.py:2599 +#: windows/main/controller.py:323 windows/views.py:2808 msgid "Stop" msgstr "Stop" -#: windows/views.py:2664 +#: windows/views.py:2873 msgid "a page" msgstr "a page" -#: windows/views.py:2714 +#: windows/views.py:2923 msgid "Query" msgstr "Query" -#: windows/views.py:3091 -msgid "Character set" -msgstr "Character set" - -#: windows/views.py:3121 windows/views.py:3140 -msgid "New" -msgstr "New" - -#: windows/views.py:3160 -msgid "Insert record" -msgstr "Insert record" - -#: windows/views.py:3165 -msgid "Duplicate record" -msgstr "Duplicate record" - -#: windows/views.py:3172 -msgid "Delete record" -msgstr "Delete record" - -#: windows/views.py:3210 windows/views.py:3658 -msgid "Up" -msgstr "Up" - -#: windows/views.py:3217 windows/views.py:3665 -msgid "Down" -msgstr "Down" - -#: windows/views.py:3232 -msgid "Table:" -msgstr "Table:" - -#: windows/views.py:3245 -msgid "Clone" -msgstr "Clone" - -#: windows/views.py:3270 -msgid "MyButton" -msgstr "MyButton" - -#: windows/views.py:3794 -msgid "Save Starments" -msgstr "Save Starments" - -#: windows/views.py:3802 -msgid "Location" -msgstr "Location" - -#: windows/views.py:3809 -msgid "*.sql" -msgstr "*.sql" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 msgid "Allow NULL" @@ -965,10 +1021,6 @@ msgstr "Unsigned" msgid "Zerofill" msgstr "Zerofill" -#: windows/components/dataview.py:111 -msgid "#" -msgstr "#" - #: windows/components/dataview.py:119 msgid "Data type" msgstr "Data type" @@ -1076,9 +1128,11 @@ msgstr "Unsaved changes" #: windows/dialogs/connections/view.py:773 msgid "" -"This connection cannot work without TLS. TLS has been enabled automatically." +"This connection cannot work without TLS. TLS has been enabled " +"automatically." msgstr "" -"This connection cannot work without TLS. TLS has been enabled automatically." +"This connection cannot work without TLS. TLS has been enabled " +"automatically." #: windows/dialogs/connections/view.py:798 #, python-brace-format @@ -1108,114 +1162,114 @@ msgstr "Confirm delete" msgid "Do you want to delete the directory '{directory_name}'?" msgstr "Do you want to delete the directory '{directory_name}'?" -#: windows/main/controller.py:315 +#: windows/main/controller.py:316 #, python-brace-format msgid "{text} ({shortcut})" msgstr "{text} ({shortcut})" -#: windows/main/controller.py:321 +#: windows/main/controller.py:322 msgid "Execute all" msgstr "SSH executable" -#: windows/main/controller.py:440 windows/main/controller.py:448 +#: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" msgstr "Query (1)" -#: windows/main/controller.py:467 +#: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" msgstr "Query ({query_number})" -#: windows/main/controller.py:516 +#: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" msgstr "You have unsaved changes. Save before closing?" -#: windows/main/controller.py:517 +#: windows/main/controller.py:519 msgid "Unsaved query" msgstr "Unsaved query" -#: windows/main/controller.py:562 +#: windows/main/controller.py:564 msgid "Save query" msgstr "Save query" -#: windows/main/controller.py:565 +#: windows/main/controller.py:567 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" msgstr "SQL files (*.sql)|*.sql|All files (*.*)|*.*" -#: windows/main/controller.py:593 windows/main/controller.py:624 -#: windows/main/controller.py:651 windows/main/database/list.py:119 -#: windows/main/database/procedure.py:206 -#: windows/main/database/procedure.py:232 windows/main/database/view.py:268 -#: windows/main/database/view.py:297 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:631 +#: windows/main/controller.py:633 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "-- Saved query to {file_path}" -#: windows/main/controller.py:657 +#: windows/main/controller.py:659 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "-- Autosaved query to {file_path}" -#: windows/main/controller.py:722 +#: windows/main/controller.py:725 msgid "days" msgstr "days" -#: windows/main/controller.py:723 +#: windows/main/controller.py:726 msgid "hours" msgstr "hours" -#: windows/main/controller.py:724 +#: windows/main/controller.py:727 msgid "minutes" msgstr "minutes" -#: windows/main/controller.py:725 +#: windows/main/controller.py:728 msgid "seconds" msgstr "seconds" -#: windows/main/controller.py:733 +#: windows/main/controller.py:736 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memory used: {used} ({percentage:.2%})" -#: windows/main/controller.py:769 +#: windows/main/controller.py:772 msgid "Settings saved successfully" msgstr "Settings saved successfully" -#: windows/main/controller.py:993 +#: windows/main/controller.py:1006 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "~{estimated} (Loading...)" -#: windows/main/controller.py:995 +#: windows/main/controller.py:1008 msgid "~ (Loading...)" msgstr "~ (Loading...)" -#: windows/main/controller.py:1230 +#: windows/main/controller.py:1243 msgid "Write Mode (2:00)" msgstr "Write Mode (2:00)" -#: windows/main/controller.py:1281 +#: windows/main/controller.py:1294 msgid "Write Mode" msgstr "Write Mode" -#: windows/main/controller.py:1292 +#: windows/main/controller.py:1305 msgid "Version" msgstr "Version" -#: windows/main/controller.py:1294 +#: windows/main/controller.py:1307 msgid "Uptime" msgstr "Uptime" -#: windows/main/controller.py:1429 +#: windows/main/controller.py:1444 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "Do you want discard the change to {database_name}?" -#: windows/main/controller.py:1462 +#: windows/main/controller.py:1477 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1230,51 +1284,80 @@ msgstr "" "- Yes: open dump flow (coming soon, no drop).\n" "- No: drop the database now." -#: windows/main/controller.py:1467 windows/main/controller.py:1488 +#: windows/main/controller.py:1482 windows/main/controller.py:1503 msgid "Delete database" msgstr "Delete database" -#: windows/main/controller.py:1473 +#: windows/main/controller.py:1488 msgid "Dump is not implemented yet. No action has been performed." msgstr "Dump is not implemented yet. No action has been performed." -#: windows/main/controller.py:1474 +#: windows/main/controller.py:1489 msgid "Dump not available" msgstr "Dump not available" -#: windows/main/controller.py:1487 +#: windows/main/controller.py:1502 msgid "Database deletion is not supported by this engine." msgstr "Database deletion is not supported by this engine." -#: windows/main/controller.py:1502 +#: windows/main/controller.py:1517 msgid "Database deleted successfully" msgstr "Database deleted successfully" -#: windows/main/controller.py:1503 windows/main/database/procedure.py:195 -#: windows/main/database/procedure.py:226 windows/main/database/view.py:256 +#: 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 "Success" -#: windows/main/controller.py:1741 +#: windows/main/controller.py:1792 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "Do you want discard the change to {table_name}?" -#: windows/main/controller.py:1767 +#: windows/main/controller.py:1818 #, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "Do you want delete the table {table_name}?" -#: windows/main/controller.py:1789 +#: windows/main/controller.py:1840 #, python-brace-format msgid "{table_name} (COPY)" msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1948 +#: 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/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 "The connection to the database was lost." @@ -1283,44 +1366,60 @@ msgstr "The connection to the database was lost." msgid "Do you want to reconnect?" msgstr "Do you want to reconnect?" -#: windows/main/database/list.py:108 -msgid "Connection lost" -msgstr "Connection lost" - #: windows/main/database/list.py:118 msgid "Reconnection failed:" msgstr "Reconnection failed:" -#: windows/main/database/procedure.py:194 +#: windows/main/database/routine.py:545 +#, fuzzy +msgid "Function created successfully" +msgstr "View created successfully" + +#: windows/main/database/routine.py:547 +#, fuzzy +msgid "Function updated successfully" +msgstr "View updated successfully" + +#: windows/main/database/routine.py:551 msgid "Procedure created successfully" msgstr "Procedure created successfully" -#: windows/main/database/procedure.py:194 +#: windows/main/database/routine.py:553 msgid "Procedure updated successfully" msgstr "Procedure updated successfully" -#: windows/main/database/procedure.py:206 -#, python-brace-format -msgid "Error saving procedure: {}" -msgstr "Error saving procedure: {}" +#: windows/main/database/routine.py:590 +#, fuzzy, python-brace-format +msgid "Error saving routine: {}" +msgstr "Error saving view: {}" -#: windows/main/database/procedure.py:217 -#, python-brace-format -msgid "Are you sure you want to delete procedure '{}'?" -msgstr "Are you sure you want to delete procedure '{}'?" +#: windows/main/database/routine.py:604 +#, fuzzy +msgid "Function" +msgstr "Connection" -#: windows/main/database/procedure.py:218 windows/main/database/view.py:282 +#: windows/main/database/routine.py:604 +msgid "Procedure" +msgstr "Procedure" + +#: windows/main/database/routine.py:607 +#, fuzzy, python-brace-format +msgid "Are you sure you want to delete {} '{}'?" +msgstr "Are you sure you want to delete view '{}'?" + +#: windows/main/database/routine.py:610 windows/main/database/view.py:282 msgid "Confirm Delete" msgstr "Confirm Delete" -#: windows/main/database/procedure.py:226 -msgid "Procedure deleted successfully" -msgstr "Procedure deleted successfully" +#: windows/main/database/routine.py:619 +#, fuzzy, python-brace-format +msgid "{} deleted successfully" +msgstr "View deleted successfully" -#: windows/main/database/procedure.py:232 -#, python-brace-format -msgid "Error deleting procedure: {}" -msgstr "Error deleting procedure: {}" +#: windows/main/database/routine.py:634 +#, fuzzy, python-brace-format +msgid "Error deleting routine: {}" +msgstr "Error deleting view: {}" #: windows/main/database/view.py:255 msgid "View created successfully" @@ -1349,21 +1448,21 @@ msgstr "View deleted successfully" msgid "Error deleting view: {}" 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 "{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 "{elapsed_s:.2f} s" -#: windows/main/query/controller.py:115 +#: windows/main/query/controller.py:117 msgid "none" msgstr "Engine" -#: windows/main/query/controller.py:121 +#: windows/main/query/controller.py:123 #, python-brace-format msgid "" "Query execution stopped after {elapsed}.\n" @@ -1378,14 +1477,19 @@ msgstr "" "Failed: {failed}.\n" "Last statement: #{last}." -#: windows/main/query/controller.py:134 +#: windows/main/query/controller.py:136 msgid "Query execution cancelled" msgstr "Query execution cancelled" -#: windows/main/query/controller.py:176 +#: windows/main/query/controller.py:178 msgid "No active database connection" 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" @@ -1433,147 +1537,3 @@ msgstr "Error:" msgid "Error saving records" msgstr "Error saving records" -#~ msgid "Created at:" -#~ msgstr "Created at:" - -#~ msgid "Last connection:" -#~ msgstr "Last connection:" - -#~ msgid "Successful connections:" -#~ msgstr "Successful connections:" - -#~ msgid "Unsuccessful connections:" -#~ msgstr "Unsuccessful connections:" - -#~ msgid "Session Manager" -#~ msgstr "Session Manager" - -#~ msgid "Session name" -#~ msgstr "Session name" - -#~ msgid "Connection type" -#~ msgstr "Connection type" - -#~ msgid "Open" -#~ msgstr "Open" - -#~ msgid "Open session manager" -#~ msgstr "Open session manager" - -#~ msgid "Foreign Key" -#~ msgstr "Foreign Key" - -#~ msgid "New Session" -#~ msgstr "New Session" - -#~ msgid "connection" -#~ msgstr "connection" - -#~ msgid "directory" -#~ msgstr "directory" - -#~ msgid "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" -#~ msgstr "Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total" - -#~ msgid "Next" -#~ msgstr "Next" - -#~ msgid "{} rows affected" -#~ msgstr "{} rows affected" - -#~ msgid "Query {}" -#~ msgstr "Query {}" - -#~ msgid "Query {} (Error)" -#~ msgstr "Query {} (Error)" - -#~ msgid "Query {} ({} rows × {} cols)" -#~ msgstr "Query {} ({} rows × {} cols)" - -#~ msgid "{} rows" -#~ msgstr "{} rows" - -#~ msgid "{:.1f} ms" -#~ msgstr "{:.1f} ms" - -#~ msgid "{} warnings" -#~ msgstr "{} warnings" - -#~ msgid " Average connection time (ms)" -#~ msgstr " Average connection time (ms)" - -#~ msgid " Most recent connection duration" -#~ msgstr " Most recent connection duration" - -#~ msgid "Edit Value" -#~ msgstr "Edit Value" - -#~ msgid "Query #2" -#~ msgstr "Query #2" - -#~ msgid "Column5" -#~ msgstr "Column5" - -#~ msgid "Import" -#~ msgstr "Import" - -#~ msgid "Read only" -#~ msgstr "Read only" - -#~ msgid "CASCADED" -#~ msgstr "CASCADED" - -#~ msgid "CHECK OPTION" -#~ msgstr "CHECK OPTION" - -#~ msgid "collapsible" -#~ msgstr "collapsible" - -#~ msgid "Column3" -#~ msgstr "Column3" - -#~ msgid "Column4" -#~ msgstr "Column4" - -#~ 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 "Usage" - -#~ msgid "%(total_rows)s" -#~ msgstr "%(total_rows)s" - -#~ msgid "rows total" -#~ msgstr "rows total" - -#~ msgid "Lines" -#~ msgstr "Lines" - -#~ msgid "Temporary" -#~ msgstr "Temporary" - -#~ msgid "Engine options" -#~ msgstr "Engine options" - -#~ msgid "RadioBtn" -#~ msgstr "RadioBtn" - -#~ msgid "Edit Column" -#~ msgstr "Edit Column" - -#~ msgid "Datatype" -#~ msgstr "Datatype" - -#~ msgid "Zero Fill" -#~ msgstr "Zero Fill" - -#~ msgid "Refrsh" -#~ msgstr "Refrsh" diff --git a/locale/es_ES/LC_MESSAGES/petersql.mo b/locale/es_ES/LC_MESSAGES/petersql.mo index 7a260644af32456b174a745d6ed8bc022a83d1e9..eb576fc7dbdc94fe563094f67976dac218ca2356 100644 GIT binary patch literal 18232 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\n" "Language: es_ES\n" +"Language-Team: es_ES \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.18.0\n" #: helpers/__init__.py:16 @@ -46,781 +46,888 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "Cliente OpenSSH no encontrado." -#: structures/engines/context.py:544 +#: structures/engines/context.py:567 msgid "This connection is read-only." msgstr "Esta conexión es de solo lectura." -#: structures/engines/mariadb/context.py:632 -#: structures/engines/mysql/context.py:643 +#: 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 "Table{table_index:03}" -#: structures/engines/mariadb/context.py:660 -#: structures/engines/mysql/context.py:671 +#: 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 "Column{column_index:03}" -#: structures/engines/mariadb/context.py:678 -#: structures/engines/mysql/context.py:689 +#: 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 "Index{index_number:03}" -#: structures/engines/mariadb/context.py:718 -#: structures/engines/mysql/context.py:727 +#: 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 "ForeignKey{foreign_key_number:03}" -#: structures/engines/mariadb/context.py:751 -#: structures/engines/mysql/context.py:758 +#: 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 "View{view_index:03}" -#: structures/engines/mariadb/context.py:804 +#: structures/engines/mariadb/context.py:830 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "Trigger{trigger_index:03}" #: windows/dialogs/connections/view.py:417 -#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1290 -#: windows/views.py:33 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1303 +#: windows/views.py:62 msgid "Connection" msgstr "Conexión" #: windows/components/dataview.py:115 windows/components/dataview.py:240 #: windows/components/dataview.py:253 windows/components/dataview.py:268 -#: windows/views.py:47 windows/views.py:97 windows/views.py:1024 -#: windows/views.py:1336 windows/views.py:1369 windows/views.py:1393 -#: windows/views.py:1417 windows/views.py:1441 windows/views.py:1465 -#: windows/views.py:1582 windows/views.py:1928 windows/views.py:2199 -#: windows/views.py:3293 windows/views.py:3515 +#: 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:438 +#: windows/views.py:77 windows/views.py:467 msgid "Last connection" msgstr "Última conexión" -#: windows/dialogs/connections/view.py:669 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:212 -#: windows/dialogs/connections/view.py:615 windows/views.py:65 +#: windows/dialogs/connections/view.py:615 windows/views.py:94 msgid "New connection" msgstr "Nueva conexión" -#: windows/views.py:71 +#: windows/views.py:100 msgid "Rename" msgstr "Renombrar" -#: windows/views.py:76 +#: windows/views.py:105 msgid "Clone connection" msgstr "Clonar conexión" -#: windows/views.py:81 windows/views.py:613 windows/views.py:1496 -#: windows/views.py:1864 windows/views.py:2154 windows/views.py:2378 -#: windows/views.py:3203 windows/views.py:3252 windows/views.py:3651 -#: windows/views.py:3683 +#: 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:1341 windows/views.py:1637 -#: windows/views.py:3115 windows/views.py:3570 +#: 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:1150 +#: windows/views.py:190 windows/views.py:1179 msgid "Password" msgstr "Contraseña" -#: windows/views.py:174 +#: windows/views.py:203 msgid "Connection timeout" msgstr "Conexión perdida" -#: 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:214 +#: windows/views.py:243 msgid "Use SSH tunnel" msgstr "Usar túnel SSH" -#: windows/views.py:225 +#: windows/views.py:254 msgid "Compressed client/server protocol" msgstr "Protocolo cliente/servidor comprimido" -#: windows/views.py:244 +#: windows/views.py:273 msgid "Filename" msgstr "Nombre de archivo" -#: windows/views.py:249 windows/views.py:368 windows/views.py:3809 +#: windows/views.py:278 windows/views.py:397 msgid "Select a file" msgstr "Seleccionar un archivo" -#: windows/views.py:249 windows/views.py:368 +#: windows/views.py:278 windows/views.py:397 msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:266 windows/views.py:1343 windows/views.py:1595 -#: windows/views.py:3113 windows/views.py:3528 +#: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 msgid "Comments" msgstr "Comentarios" -#: windows/main/controller.py:769 windows/views.py:280 windows/views.py:740 -#: windows/views.py:894 +#: windows/main/controller.py:772 windows/views.py:309 windows/views.py:769 +#: windows/views.py:923 msgid "Settings" msgstr "Configuraciones" -#: windows/views.py:288 +#: windows/views.py:317 msgid "SSH executable" msgstr "Ejecutable SSH" -#: windows/views.py:293 +#: windows/views.py:322 msgid "ssh" msgstr "ssh" -#: windows/views.py:301 +#: windows/views.py:330 msgid "SSH host + port" msgstr "Host SSH + puerto" -#: windows/views.py:313 +#: 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:322 +#: windows/views.py:351 msgid "SSH username" msgstr "Nombre de usuario SSH" -#: windows/views.py:335 +#: windows/views.py:364 msgid "SSH password" msgstr "Contraseña SSH" -#: windows/views.py:348 +#: windows/views.py:377 msgid "Local port" msgstr "Puerto local" -#: windows/views.py:354 +#: 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" +msgstr "si el valor se establece en 0, se utilizará el primer puerto disponible" -#: windows/views.py:363 +#: windows/views.py:392 msgid "Identity file" msgstr "Archivo de identidad" -#: windows/views.py:379 +#: windows/views.py:408 msgid "Remote host + port" msgstr "Host remoto + puerto" -#: windows/views.py:391 +#: 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:400 +#: windows/views.py:429 msgid "SSH extra args" msgstr "Argumentos extra SSH" -#: windows/views.py:415 +#: windows/views.py:444 msgid "SSH Tunnel" msgstr "Túnel SSH" -#: windows/views.py:421 windows/views.py:1339 windows/views.py:3117 +#: windows/views.py:450 windows/views.py:1368 msgid "Created at" msgstr "Creado en" -#: windows/views.py:455 +#: windows/views.py:484 msgid "Successful connections" msgstr "Conexiones exitosas" -#: windows/views.py:472 +#: windows/views.py:501 msgid "Last successful connection" msgstr "Conexiones exitosas" -#: windows/views.py:489 +#: windows/views.py:518 msgid "Unsuccessful connections" msgstr "Conexiones fallidas" -#: windows/views.py:506 +#: windows/views.py:535 msgid "Last failure reason" msgstr "Último motivo de fallo" -#: windows/views.py:523 +#: windows/views.py:552 msgid "Total connection attempts" msgstr "Última conexión" -#: windows/views.py:540 +#: windows/views.py:569 msgid "Average connection time (ms)" msgstr "Reconexión fallida:" -#: windows/views.py:557 +#: windows/views.py:586 msgid "Most recent connection duration" msgstr "Duración de la conexión más reciente" -#: windows/views.py:576 +#: windows/views.py:605 msgid "Statistics" msgstr "Estadísticas" -#: windows/views.py:594 windows/views.py:1821 +#: windows/views.py:623 windows/views.py:1850 msgid "Create" msgstr "Crear" -#: windows/views.py:598 +#: windows/views.py:627 msgid "Create connection" msgstr "Crear conexión" -#: windows/views.py:601 +#: windows/views.py:630 msgid "Create directory" msgstr "Nuevo directorio" -#: windows/views.py:630 windows/views.py:854 windows/views.py:1491 -#: windows/views.py:1867 windows/views.py:2159 windows/views.py:2383 -#: windows/views.py:2439 windows/views.py:3179 windows/views.py:3686 -#: windows/views.py:3823 +#: 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:323 windows/views.py:635 windows/views.py:2164 -#: windows/views.py:2388 windows/views.py:2603 windows/views.py:3691 -#: windows/views.py:3829 +#: 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:642 +#: windows/views.py:671 msgid "Test" msgstr "Probar" -#: windows/views.py:649 +#: windows/views.py:678 msgid "Connect" msgstr "Conectar" -#: windows/views.py:752 +#: windows/views.py:781 windows/views.py:2388 msgid "Language" msgstr "Idioma" -#: windows/views.py:757 +#: windows/views.py:786 msgid "English" msgstr "Inglés" -#: windows/views.py:757 +#: windows/views.py:786 msgid "Italian" msgstr "Italiano" -#: windows/views.py:757 +#: windows/views.py:786 msgid "French" msgstr "Francés" -#: windows/views.py:769 +#: windows/views.py:798 msgid "Locale" msgstr "Localización" -#: windows/views.py:790 +#: windows/views.py:819 msgid "Column content" msgstr "Nueva conexión" -#: windows/views.py:800 +#: windows/views.py:829 msgid "Syntax" msgstr "Sintaxis" -#: windows/views.py:857 +#: windows/views.py:886 msgid "Ok" msgstr "Ok" -#: windows/views.py:888 +#: windows/views.py:917 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:897 +#: windows/views.py:926 msgid "File" msgstr "Archivo" -#: windows/views.py:900 +#: windows/views.py:929 msgid "About" msgstr "Acerca de" -#: windows/views.py:903 +#: windows/views.py:932 msgid "Help" msgstr "Ayuda" -#: windows/views.py:908 +#: windows/views.py:937 msgid "Open connection manager" msgstr "Abrir administrador de conexiones" -#: windows/views.py:912 +#: windows/views.py:941 msgid "Disconnect from server" msgstr "Desconectar del servidor" -#: windows/views.py:914 +#: windows/views.py:943 msgid "tool" msgstr "herramienta" -#: windows/views.py:914 windows/views.py:2419 +#: windows/views.py:943 windows/views.py:2628 msgid "Refresh" msgstr "Actualizar" -#: windows/views.py:918 windows/views.py:920 windows/views.py:1845 -#: windows/views.py:2423 windows/views.py:2589 +#: 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:925 +#: windows/views.py:954 #, python-brace-format msgid "{mode}" msgstr "{mode}" -#: windows/views.py:962 windows/views.py:966 windows/views.py:2556 -#: windows/views.py:2561 +#: windows/views.py:991 windows/views.py:995 windows/views.py:2765 +#: windows/views.py:2770 msgid "MyMenuItem" msgstr "MiElementoMenu" -#: windows/views.py:969 windows/views.py:1895 windows/views.py:2568 -#: windows/views.py:3714 +#: windows/views.py:998 windows/views.py:1924 windows/views.py:2777 msgid "MyMenu" msgstr "MiMenu" -#: windows/views.py:984 windows/views.py:1519 windows/views.py:1526 -#: windows/views.py:1533 +#: windows/views.py:1013 windows/views.py:1548 windows/views.py:1555 +#: windows/views.py:1562 msgid "MyLabel" msgstr "MiEtiqueta" -#: windows/views.py:990 +#: windows/views.py:1019 msgid "Databases" msgstr "Bases de datos" -#: windows/views.py:991 windows/views.py:1338 windows/views.py:3118 +#: windows/views.py:1020 windows/views.py:1367 msgid "Size" msgstr "Tamaño" -#: windows/views.py:992 +#: windows/views.py:1021 msgid "Elements" msgstr "Elementos" -#: windows/views.py:993 +#: windows/views.py:1022 msgid "Modified at" msgstr "Modificado en" -#: windows/views.py:994 windows/views.py:1353 +#: windows/views.py:1023 windows/views.py:1382 msgid "Tables" msgstr "Tablas" -#: windows/views.py:1001 +#: 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:1047 -#: windows/views.py:1342 windows/views.py:3114 +#: windows/components/dataview.py:89 windows/views.py:1076 +#: windows/views.py:1371 msgid "Collation" msgstr "Intercalación" -#: windows/views.py:1076 +#: windows/views.py:1105 msgid "Encryption" msgstr "Cifrado" -#: windows/main/controller.py:1259 windows/main/controller.py:1281 -#: windows/main/controller.py:1285 windows/views.py:1088 +#: windows/main/controller.py:1272 windows/main/controller.py:1294 +#: windows/main/controller.py:1298 windows/views.py:1117 msgid "Read Only" msgstr "Solo lectura" -#: windows/views.py:1105 +#: windows/views.py:1134 msgid "Tablespace" msgstr "Tablespace" -#: windows/views.py:1126 +#: windows/views.py:1155 msgid "Connection limit" msgstr "Límite de conexión" -#: windows/views.py:1169 +#: windows/views.py:1198 msgid "Profile" msgstr "Perfil" -#: windows/views.py:1195 +#: windows/views.py:1224 msgid "Default tablespace" msgstr "Tablespace predeterminado" -#: windows/views.py:1216 +#: windows/views.py:1245 msgid "Temporary tablespace" msgstr "Tablespace temporal" -#: windows/views.py:1242 +#: windows/views.py:1271 msgid "Quota" msgstr "Quota" -#: windows/views.py:1261 +#: windows/views.py:1290 msgid "Unlimited quota" msgstr "Cuota ilimitada" -#: windows/views.py:1278 +#: windows/views.py:1307 msgid "Account status" msgstr "Estado de cuenta" -#: windows/views.py:1299 +#: windows/views.py:1328 msgid "Password expire" msgstr "Expiración de contraseña" -#: windows/views.py:1323 +#: windows/views.py:1352 msgid "Add new table" msgstr "Agregar nueva tabla" -#: windows/views.py:1325 +#: windows/views.py:1354 msgid "Clone table" msgstr "Clonar tabla" -#: windows/main/controller.py:1770 windows/views.py:1327 +#: windows/main/controller.py:1821 windows/views.py:1356 msgid "Delete table" msgstr "Eliminar tabla" -#: windows/views.py:1337 +#: windows/views.py:1366 windows/views.py:2457 msgid "Rows" msgstr "Filas" -#: windows/views.py:1340 windows/views.py:3116 +#: windows/views.py:1369 msgid "Updated at" msgstr "Actualizado en" -#: windows/views.py:1358 +#: windows/views.py:1387 msgid "Add new view" msgstr "Agregar nueva vista" -#: windows/views.py:1360 windows/views.py:1384 +#: windows/views.py:1389 msgid "Clone view" msgstr "Clonar" -#: windows/views.py:1362 +#: windows/views.py:1391 msgid "Delete view" msgstr "Eliminar" -#: windows/views.py:1370 windows/views.py:1394 windows/views.py:1418 -#: windows/views.py:1442 windows/views.py:1466 +#: 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:1377 +#: windows/views.py:1406 msgid "Views" msgstr "Vistas" -#: windows/views.py:1382 +#: windows/views.py:1411 msgid "Add new procedure" msgstr "Agregar nuevo procedimiento" -#: windows/views.py:1384 +#: windows/views.py:1413 msgid "Clone procedure" msgstr "Clonar procedimiento" -#: windows/views.py:1386 +#: windows/views.py:1415 msgid "Delete procedure" msgstr "Eliminar procedimiento" -#: windows/views.py:1401 +#: windows/views.py:1430 msgid "Procedures" msgstr "Procedimientos" -#: windows/views.py:1406 +#: windows/views.py:1435 msgid "Add new function" msgstr "Agregar nueva función" -#: windows/views.py:1408 +#: windows/views.py:1437 msgid "Clone function" msgstr "Clonar función" -#: windows/views.py:1410 +#: windows/views.py:1439 msgid "Delete function" msgstr "Eliminar función" -#: windows/views.py:1425 +#: windows/views.py:1454 msgid "Functions" msgstr "Funciones" -#: windows/views.py:1430 +#: windows/views.py:1459 msgid "Add new trigger" msgstr "Agregar nuevo disparador" -#: windows/views.py:1432 +#: windows/views.py:1461 msgid "Clone trigger" msgstr "Clonar disparador" -#: windows/views.py:1434 +#: windows/views.py:1463 msgid "Delete trigger" msgstr "Eliminar disparador" -#: windows/views.py:1449 +#: windows/views.py:1478 msgid "Triggers" msgstr "Disparadores" -#: windows/views.py:1454 +#: windows/views.py:1483 msgid "Add new event" msgstr "Agregar nuevo evento" -#: windows/views.py:1456 +#: windows/views.py:1485 msgid "Clone event" msgstr "Clonar" -#: windows/views.py:1458 +#: windows/views.py:1487 msgid "Delete event" msgstr "Eliminar evento" -#: windows/views.py:1473 +#: windows/views.py:1502 msgid "Events" msgstr "Eventos" -#: windows/views.py:1501 windows/views.py:1872 windows/views.py:2437 -#: windows/views.py:2521 windows/views.py:3186 +#: windows/views.py:1530 windows/views.py:1901 windows/views.py:2646 +#: windows/views.py:2730 msgid "Apply" msgstr "Aplicar" -#: windows/views.py:1513 windows/views.py:1692 windows/views.py:3603 +#: windows/views.py:1542 windows/views.py:1721 msgid "Options" msgstr "Opciones" -#: windows/views.py:1544 +#: windows/views.py:1573 msgid "Diagram" msgstr "Diagrama" -#: windows/views.py:1555 +#: windows/views.py:1584 msgid "Database" msgstr "Base de datos" -#: windows/views.py:1610 windows/views.py:3543 +#: windows/views.py:1639 msgid "Base" msgstr "Base" -#: windows/views.py:1624 windows/views.py:3557 +#: windows/views.py:1653 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1652 windows/views.py:3585 +#: windows/views.py:1681 msgid "Default Collation" msgstr "Intercalación predeterminada" -#: windows/views.py:1662 +#: windows/views.py:1691 msgid "Convert data" msgstr "Convertir datos" -#: windows/views.py:1670 +#: windows/views.py:1699 msgid "Row format" msgstr "Formato de fila" -#: windows/views.py:1703 windows/views.py:1731 windows/views.py:1759 -#: windows/views.py:1847 windows/views.py:2259 windows/views.py:2427 -#: windows/views.py:3313 windows/views.py:3330 windows/views.py:3354 -#: windows/views.py:3381 +#: 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:1705 windows/views.py:1733 windows/views.py:1761 -#: windows/views.py:2261 windows/views.py:2529 windows/views.py:3320 -#: windows/views.py:3337 windows/views.py:3361 windows/views.py:3388 +#: 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:1718 windows/views.py:3617 +#: windows/views.py:1747 msgid "Indexes" msgstr "Índices" -#: windows/views.py:1729 windows/views.py:1757 windows/views.py:2257 -#: windows/views.py:3198 windows/views.py:3240 windows/views.py:3308 -#: windows/views.py:3349 windows/views.py:3376 windows/views.py:3646 +#: windows/views.py:1758 windows/views.py:1786 windows/views.py:2324 msgid "Insert" msgstr "Insertar" -#: windows/views.py:1746 +#: windows/views.py:1775 msgid "Foreign Keys" msgstr "Claves foráneas" -#: windows/views.py:1774 +#: windows/views.py:1803 msgid "Checks" msgstr "Comprobaciones" -#: windows/views.py:1841 windows/views.py:3638 +#: windows/views.py:1870 msgid "Columns:" msgstr "Columnas:" -#: windows/views.py:1851 +#: windows/views.py:1880 msgid "Move Up" msgstr "Mover arriba\tCTRL+UP" -#: windows/views.py:1853 +#: windows/views.py:1882 msgid "Move Down" msgstr "Mover abajo\tCTRL+D" -#: windows/views.py:1885 windows/views.py:1892 windows/views.py:3704 -#: windows/views.py:3711 +#: windows/views.py:1914 windows/views.py:1921 msgid "Add Index" msgstr "Agregar índice" -#: windows/views.py:1889 windows/views.py:3708 +#: windows/views.py:1918 msgid "Add PrimaryKey" msgstr "Agregar clave primaria" -#: windows/views.py:1906 +#: windows/views.py:1935 msgid "Table" msgstr "Tabla" -#: windows/views.py:1948 windows/views.py:2219 +#: windows/views.py:1977 windows/views.py:2243 msgid "Schema" msgstr "Schema" -#: windows/views.py:1976 windows/views.py:2247 +#: windows/views.py:2005 windows/views.py:2314 msgid "General" msgstr "General" -#: windows/views.py:1981 +#: windows/views.py:2010 msgid "Algorithm" msgstr "Algoritmo" -#: windows/views.py:1983 +#: windows/views.py:2012 msgid "UNDEFINED" msgstr "Sin signo" -#: windows/views.py:1986 +#: windows/views.py:2015 msgid "MERGE" msgstr "MERGE" -#: windows/views.py:1989 +#: windows/views.py:2018 msgid "TEMPTABLE" msgstr "TEMPTABLE" -#: windows/views.py:1999 +#: windows/views.py:2028 msgid "View constraint" msgstr "Restricción de vista" -#: windows/views.py:2001 +#: windows/views.py:2030 msgid "None" msgstr "Ninguno" -#: windows/views.py:2004 +#: windows/views.py:2033 msgid "LOCAL" msgstr "LOCAL" -#: windows/views.py:2007 +#: windows/views.py:2036 msgid "CASCADE" msgstr "CASCADA" -#: windows/views.py:2010 +#: windows/views.py:2039 msgid "CHECK ONLY" msgstr "SOLO VERIFICAR" -#: windows/views.py:2013 +#: windows/views.py:2042 msgid "READ ONLY" msgstr "SOLO LECTURA" -#: windows/views.py:2026 +#: windows/views.py:2055 windows/views.py:2483 msgid "Behavior" msgstr "Comportamiento" -#: windows/views.py:2033 windows/views.py:2281 +#: windows/views.py:2062 windows/views.py:2490 msgid "Definer" msgstr "Definidor" -#: windows/views.py:2041 windows/views.py:2289 +#: windows/views.py:2070 windows/views.py:2498 msgid "*" msgstr "*" -#: windows/views.py:2053 windows/views.py:2301 +#: windows/views.py:2082 windows/views.py:2510 msgid "SQL security" msgstr "Seguridad SQL" -#: windows/views.py:2060 windows/views.py:2308 +#: windows/views.py:2089 windows/views.py:2517 msgid "DEFINER" msgstr "DEFINIDOR" -#: windows/views.py:2060 windows/views.py:2308 +#: windows/views.py:2089 windows/views.py:2517 msgid "INVOKER" msgstr "INVOCADOR" -#: windows/views.py:2074 +#: windows/views.py:2103 msgid "Force" msgstr "Forzar" -#: windows/views.py:2086 +#: windows/views.py:2115 msgid "Security barrier" msgstr "Barrera de seguridad" -#: windows/views.py:2099 windows/views.py:2323 +#: windows/views.py:2128 windows/views.py:2532 msgid "Security" msgstr "Seguridad" -#: windows/views.py:2176 +#: windows/views.py:2205 msgid "View" msgstr "Vistas" -#: windows/views.py:2274 +#: windows/views.py:2262 +#, fuzzy +msgid "Type" +msgstr "Tipo de datos" + +#: 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 +#, fuzzy +msgid "Return type" +msgstr "Tipo de datos" + +#: windows/views.py:2299 +#, fuzzy +msgid "Comment" +msgstr "Comentarios" + +#: windows/components/dataview.py:111 windows/views.py:2335 +msgid "#" +msgstr "#" + +#: windows/views.py:2337 +#, fuzzy +msgid "Datatype" +msgstr "Tipo de datos" + +#: windows/views.py:2338 +#, fuzzy +msgid "Context" +msgstr "Conectar" + +#: windows/views.py:2345 msgid "Parameters" msgstr "Parámetros" -#: windows/views.py:2400 -msgid "Procedure" -msgstr "Procedure" +#: windows/views.py:2354 +#, fuzzy +msgid "Data access" +msgstr "Bases de datos" + +#: windows/views.py:2361 +msgid "CONTAINS SQL" +msgstr "" + +#: windows/views.py:2361 +msgid "NO SQL" +msgstr "" -#: windows/views.py:2408 +#: windows/views.py:2361 +msgid "READS SQL DATA" +msgstr "" + +#: windows/views.py:2361 +#, fuzzy +msgid "MODIFIES SQL DATA" +msgstr "Modificado en" + +#: windows/views.py:2371 +#, fuzzy +msgid "Deterministic" +msgstr "Definición" + +#: windows/views.py:2395 +#, fuzzy +msgid "SQL" +msgstr "*.sql" + +#: windows/views.py:2395 +msgid "PLPGSQL" +msgstr "" + +#: windows/views.py:2405 +#, fuzzy +msgid "Volatility" +msgstr "Virtualidad" + +#: windows/views.py:2412 +msgid "VOLATILE" +msgstr "" + +#: windows/views.py:2412 +#, fuzzy +msgid "STABLE" +msgstr "Tabla" + +#: windows/views.py:2412 +#, fuzzy +msgid "IMMUTABLE" +msgstr "Tabla" + +#: windows/views.py:2422 +msgid "Parallel" +msgstr "" + +#: windows/views.py:2429 +#, fuzzy +msgid "UNSAFE" +msgstr "Guardar" + +#: windows/views.py:2429 +msgid "RESTRICTED" +msgstr "" + +#: windows/views.py:2429 +#, fuzzy +msgid "SAFE" +msgstr "Guardar" + +#: windows/views.py:2441 +#, fuzzy +msgid "Cost" +msgstr "Cerrar" + +#: windows/views.py:2609 +#, fuzzy +msgid "Routine" +msgstr "Tiempo de actividad" + +#: windows/views.py:2617 msgid "Trigger" msgstr "Disparadores" -#: windows/views.py:2425 +#: windows/views.py:2634 msgid "Duplicate" msgstr "Duplicar" -#: windows/views.py:2431 +#: windows/views.py:2640 msgid "Apply changes automatically" msgstr "Aplicar cambios automáticamente" -#: windows/views.py:2433 windows/views.py:2434 +#: windows/views.py:2642 windows/views.py:2643 msgid "" -"If enabled, table edits are applied immediately without pressing Apply or " -"Cancel" +"If enabled, table edits are applied immediately without pressing Apply or" +" Cancel" msgstr "" -"Si está habilitado, las ediciones de la tabla se aplican inmediatamente sin " -"presionar Aplicar o Cancelar" +"Si está habilitado, las ediciones de la tabla se aplican inmediatamente " +"sin presionar Aplicar o Cancelar" -#: windows/views.py:2447 +#: windows/views.py:2656 #, python-brace-format -msgid "" -"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" -"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -#: windows/views.py:2455 +#: windows/views.py:2664 msgid "First" msgstr "Primero" -#: windows/views.py:2473 +#: windows/views.py:2682 msgid "Last" msgstr "Último" -#: windows/views.py:2482 +#: windows/views.py:2691 msgid "Filters" msgstr "Filtros" -#: windows/views.py:2524 +#: windows/views.py:2733 msgid "" "Apply filters in data\n" "CTRL+ENTER" @@ -828,114 +935,62 @@ msgstr "" "Aplicar filtros en datos\n" "CTRL+ENTER" -#: windows/views.py:2525 +#: windows/views.py:2734 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2553 +#: windows/views.py:2762 msgid "Insert row" msgstr "Insertar fila" -#: windows/components/popup.py:31 windows/views.py:2565 +#: windows/components/popup.py:31 windows/views.py:2774 msgid "NULL" msgstr "NULL" -#: windows/views.py:2572 +#: windows/views.py:2781 msgid "Data" msgstr "Datos" -#: windows/main/controller.py:318 windows/views.py:2589 +#: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" msgstr "Consulta" -#: windows/views.py:2591 windows/views.py:3137 +#: windows/views.py:2800 msgid "Close" msgstr "Cerrar" -#: windows/main/controller.py:319 windows/views.py:2591 +#: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" msgstr "Consulta" -#: windows/views.py:2595 +#: windows/views.py:2804 msgid "Run" msgstr "Ejecutar" -#: windows/main/controller.py:320 windows/views.py:2595 +#: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" msgstr "Ejecutar" -#: windows/views.py:2597 +#: windows/views.py:2806 msgid "Run all" msgstr "Ejecutar todo" -#: windows/views.py:2597 +#: windows/views.py:2806 msgid "Execute all statements" msgstr "Ejecutar todas las declaraciones" -#: windows/main/controller.py:322 windows/views.py:2599 +#: windows/main/controller.py:323 windows/views.py:2808 msgid "Stop" msgstr "Detener" -#: windows/views.py:2664 +#: windows/views.py:2873 msgid "a page" msgstr "una página" -#: windows/views.py:2714 +#: windows/views.py:2923 msgid "Query" msgstr "Consulta" -#: windows/views.py:3091 -msgid "Character set" -msgstr "Conjunto de caracteres" - -#: windows/views.py:3121 windows/views.py:3140 -msgid "New" -msgstr "Nuevo" - -#: windows/views.py:3160 -msgid "Insert record" -msgstr "Insertar registro" - -#: windows/views.py:3165 -msgid "Duplicate record" -msgstr "Duplicar registro" - -#: windows/views.py:3172 -msgid "Delete record" -msgstr "Eliminar registro" - -#: windows/views.py:3210 windows/views.py:3658 -msgid "Up" -msgstr "Arriba" - -#: windows/views.py:3217 windows/views.py:3665 -msgid "Down" -msgstr "Abajo" - -#: windows/views.py:3232 -msgid "Table:" -msgstr "Tabla:" - -#: windows/views.py:3245 -msgid "Clone" -msgstr "Clonar" - -#: windows/views.py:3270 -msgid "MyButton" -msgstr "MiBotón" - -#: windows/views.py:3794 -msgid "Save Starments" -msgstr "Guardar declaraciones" - -#: windows/views.py:3802 -msgid "Location" -msgstr "Ubicación" - -#: windows/views.py:3809 -msgid "*.sql" -msgstr "*.sql" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 msgid "Allow NULL" @@ -968,10 +1023,6 @@ msgstr "Sin signo" msgid "Zerofill" msgstr "Relleno cero" -#: windows/components/dataview.py:111 -msgid "#" -msgstr "#" - #: windows/components/dataview.py:119 msgid "Data type" msgstr "Tipo de datos" @@ -1079,9 +1130,11 @@ msgstr "Cambios sin guardar" #: windows/dialogs/connections/view.py:773 msgid "" -"This connection cannot work without TLS. TLS has been enabled automatically." +"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." +"Esta conexión no puede funcionar sin TLS. TLS ha sido habilitado " +"automáticamente." #: windows/dialogs/connections/view.py:798 #, python-brace-format @@ -1111,114 +1164,114 @@ msgstr "Confirmar eliminar" msgid "Do you want to delete the directory '{directory_name}'?" msgstr "¿Desea eliminar el directorio '{directory_name}'?" -#: windows/main/controller.py:315 +#: windows/main/controller.py:316 #, python-brace-format msgid "{text} ({shortcut})" msgstr "{text} ({shortcut})" -#: windows/main/controller.py:321 +#: windows/main/controller.py:322 msgid "Execute all" msgstr "Ejecutar todo" -#: windows/main/controller.py:440 windows/main/controller.py:448 +#: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" msgstr "Consulta" -#: windows/main/controller.py:467 +#: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" msgstr "Query ({query_number})" -#: windows/main/controller.py:516 +#: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" msgstr "Tiene cambios sin guardar. ¿Guardar antes de cerrar?" -#: windows/main/controller.py:517 +#: windows/main/controller.py:519 msgid "Unsaved query" msgstr "Consulta sin guardar" -#: windows/main/controller.py:562 +#: windows/main/controller.py:564 msgid "Save query" msgstr "Guardar consulta" -#: windows/main/controller.py:565 +#: windows/main/controller.py:567 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" msgstr "Archivos SQL (*.sql)|*.sql|Todos los archivos (*.*)|*.*" -#: windows/main/controller.py:593 windows/main/controller.py:624 -#: windows/main/controller.py:651 windows/main/database/list.py:119 -#: windows/main/database/procedure.py:206 -#: windows/main/database/procedure.py:232 windows/main/database/view.py:268 -#: windows/main/database/view.py:297 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:631 +#: windows/main/controller.py:633 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "-- Consulta guardada en {file_path}" -#: windows/main/controller.py:657 +#: windows/main/controller.py:659 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "-- Consulta autoguardada en {file_path}" -#: windows/main/controller.py:722 +#: windows/main/controller.py:725 msgid "days" msgstr "días" -#: windows/main/controller.py:723 +#: windows/main/controller.py:726 msgid "hours" msgstr "horas" -#: windows/main/controller.py:724 +#: windows/main/controller.py:727 msgid "minutes" msgstr "minutos" -#: windows/main/controller.py:725 +#: windows/main/controller.py:728 msgid "seconds" msgstr "segundos" -#: windows/main/controller.py:733 +#: windows/main/controller.py:736 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizada: {used} ({percentage:.2%})" -#: windows/main/controller.py:769 +#: windows/main/controller.py:772 msgid "Settings saved successfully" msgstr "Configuraciones guardadas exitosamente" -#: windows/main/controller.py:993 +#: windows/main/controller.py:1006 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "~{estimated} (Cargando...)" -#: windows/main/controller.py:995 +#: windows/main/controller.py:1008 msgid "~ (Loading...)" msgstr "~ (Cargando...)" -#: windows/main/controller.py:1230 +#: windows/main/controller.py:1243 msgid "Write Mode (2:00)" msgstr "Modo escritura (2:00)" -#: windows/main/controller.py:1281 +#: windows/main/controller.py:1294 msgid "Write Mode" msgstr "Modo escritura" -#: windows/main/controller.py:1292 +#: windows/main/controller.py:1305 msgid "Version" msgstr "Versión" -#: windows/main/controller.py:1294 +#: windows/main/controller.py:1307 msgid "Uptime" msgstr "Tiempo de actividad" -#: windows/main/controller.py:1429 +#: windows/main/controller.py:1444 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "¿Desea descartar el cambio a {database_name}?" -#: windows/main/controller.py:1462 +#: windows/main/controller.py:1477 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1227,57 +1280,87 @@ 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" +"¿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:1467 windows/main/controller.py:1488 +#: windows/main/controller.py:1482 windows/main/controller.py:1503 msgid "Delete database" msgstr "Eliminar base de datos" -#: windows/main/controller.py:1473 +#: windows/main/controller.py:1488 msgid "Dump is not implemented yet. No action has been performed." msgstr "El volcado no está implementado aún. No se ha realizado ninguna acción." -#: windows/main/controller.py:1474 +#: windows/main/controller.py:1489 msgid "Dump not available" msgstr "Volcado no disponible" -#: windows/main/controller.py:1487 +#: windows/main/controller.py:1502 msgid "Database deletion is not supported by this engine." msgstr "La eliminación de bases de datos no es compatible con este motor." -#: windows/main/controller.py:1502 +#: windows/main/controller.py:1517 msgid "Database deleted successfully" msgstr "Base de datos eliminada exitosamente" -#: windows/main/controller.py:1503 windows/main/database/procedure.py:195 -#: windows/main/database/procedure.py:226 windows/main/database/view.py:256 +#: 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 "Éxito" -#: windows/main/controller.py:1741 +#: windows/main/controller.py:1792 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "¿Desea descartar el cambio a {table_name}?" -#: windows/main/controller.py:1767 +#: windows/main/controller.py:1818 #, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "¿Desea eliminar la tabla {table_name}?" -#: windows/main/controller.py:1789 +#: windows/main/controller.py:1840 #, python-brace-format msgid "{table_name} (COPY)" msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1948 +#: windows/main/controller.py:1999 msgid "Do you want delete the records?" msgstr "¿Quieres eliminar los registros?" +#: 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." @@ -1286,44 +1369,60 @@ msgstr "Se perdió la conexión a la base de datos." msgid "Do you want to reconnect?" msgstr "¿Quieres reconectar?" -#: windows/main/database/list.py:108 -msgid "Connection lost" -msgstr "Conexión perdida" - #: windows/main/database/list.py:118 msgid "Reconnection failed:" msgstr "Reconexión fallida:" -#: windows/main/database/procedure.py:194 +#: windows/main/database/routine.py:545 +#, fuzzy +msgid "Function created successfully" +msgstr "Vista creada exitosamente" + +#: windows/main/database/routine.py:547 +#, fuzzy +msgid "Function updated successfully" +msgstr "Vista actualizada exitosamente" + +#: windows/main/database/routine.py:551 msgid "Procedure created successfully" msgstr "Procedimiento creado exitosamente" -#: windows/main/database/procedure.py:194 +#: windows/main/database/routine.py:553 msgid "Procedure updated successfully" msgstr "Procedimiento actualizado exitosamente" -#: windows/main/database/procedure.py:206 -#, python-brace-format -msgid "Error saving procedure: {}" -msgstr "Error al guardar procedimiento: {}" +#: windows/main/database/routine.py:590 +#, fuzzy, python-brace-format +msgid "Error saving routine: {}" +msgstr "Error al guardar vista: {}" -#: windows/main/database/procedure.py:217 -#, python-brace-format -msgid "Are you sure you want to delete procedure '{}'?" -msgstr "¿Está seguro de que desea eliminar el procedimiento '{}'?" +#: windows/main/database/routine.py:604 +#, fuzzy +msgid "Function" +msgstr "Funciones" + +#: windows/main/database/routine.py:604 +msgid "Procedure" +msgstr "Procedure" -#: windows/main/database/procedure.py:218 windows/main/database/view.py:282 +#: windows/main/database/routine.py:607 +#, fuzzy, python-brace-format +msgid "Are you sure you want to delete {} '{}'?" +msgstr "¿Está seguro de que desea eliminar la vista '{}'?" + +#: windows/main/database/routine.py:610 windows/main/database/view.py:282 msgid "Confirm Delete" msgstr "Confirmar eliminar" -#: windows/main/database/procedure.py:226 -msgid "Procedure deleted successfully" -msgstr "Procedimiento eliminado exitosamente" +#: windows/main/database/routine.py:619 +#, fuzzy, python-brace-format +msgid "{} deleted successfully" +msgstr "Vista eliminada exitosamente" -#: windows/main/database/procedure.py:232 -#, python-brace-format -msgid "Error deleting procedure: {}" -msgstr "Error al eliminar procedimiento: {}" +#: windows/main/database/routine.py:634 +#, fuzzy, python-brace-format +msgid "Error deleting routine: {}" +msgstr "Error al eliminar vista: {}" #: windows/main/database/view.py:255 msgid "View created successfully" @@ -1352,21 +1451,21 @@ msgstr "Vista eliminada exitosamente" msgid "Error deleting view: {}" 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 "{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 "{elapsed_s:.2f} s" -#: windows/main/query/controller.py:115 +#: windows/main/query/controller.py:117 msgid "none" msgstr "ninguno" -#: windows/main/query/controller.py:121 +#: windows/main/query/controller.py:123 #, python-brace-format msgid "" "Query execution stopped after {elapsed}.\n" @@ -1381,14 +1480,19 @@ msgstr "" "Fallidas: {failed}.\n" "Última declaración: #{last}." -#: windows/main/query/controller.py:134 +#: windows/main/query/controller.py:136 msgid "Query execution cancelled" msgstr "Ejecución de consulta cancelada" -#: windows/main/query/controller.py:176 +#: windows/main/query/controller.py:178 msgid "No active database connection" msgstr "Sin conexión de base de datos activa" +#: windows/main/query/controller.py:227 +#, fuzzy +msgid "Database connection lost" +msgstr "Conexión perdida" + #: windows/main/query/history.py:55 msgid "(empty query)" msgstr "(consulta vacía)" @@ -1436,139 +1540,3 @@ msgstr "Error" msgid "Error saving records" msgstr "Error al guardar registros" -#~ msgid "Created at:" -#~ msgstr "Created at:" - -#~ msgid "Last connection:" -#~ msgstr "Last connection:" - -#~ msgid "Successful connections:" -#~ msgstr "Successful connections:" - -#~ msgid "Unsuccessful connections:" -#~ msgstr "Unsuccessful connections:" - -#~ msgid "Session Manager" -#~ msgstr "Session Manager" - -#~ msgid "Session name" -#~ msgstr "Session name" - -#~ msgid "Connection type" -#~ msgstr "Connection type" - -#~ msgid "Open" -#~ msgstr "Open" - -#~ msgid "Open session manager" -#~ msgstr "Open session manager" - -#~ msgid "Foreign Key" -#~ msgstr "Foreign Key" - -#~ 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 "{} rows affected" - -#~ msgid "Query {}" -#~ msgstr "Consulta" - -#~ msgid "Query {} (Error)" -#~ msgstr "Query {} (Error)" - -#~ msgid "Query {} ({} rows × {} cols)" -#~ msgstr "Query {} ({} rows × {} cols)" - -#~ msgid "{} rows" -#~ msgstr "Filas" - -#~ msgid "{:.1f} ms" -#~ msgstr "{:.1f} ms" - -#~ msgid "{} warnings" -#~ msgstr "{} warnings" - -#~ msgid "Edit Value" -#~ msgstr "Editar valor" - -#~ msgid "Query #2" -#~ msgstr "Consulta #2" - -#~ msgid "Column5" -#~ msgstr "Columna5" - -#~ msgid "Import" -#~ msgstr "Importar" - -#~ msgid "Read only" -#~ msgstr "Read only" - -#~ msgid "CASCADED" -#~ msgstr "CASCADED" - -#~ 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 "RadioBtn" - -#~ msgid "Edit Column" -#~ msgstr "Editar columna" - -#~ msgid "Datatype" -#~ msgstr "Tipo de datos" - -#~ msgid "Zero Fill" -#~ msgstr "Relleno cero" - -#~ msgid "Refrsh" -#~ msgstr "Actualizar" diff --git a/locale/fr_FR/LC_MESSAGES/petersql.mo b/locale/fr_FR/LC_MESSAGES/petersql.mo index b8b07a439fd8cc3b25cb88b88d4364b1f2b1c851..e2c096471cbb3b7eee45fff27072681a5a7e8b6c 100644 GIT binary patch literal 18773 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 literal 18905 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 ZBFR6zFUxo\n" "Language: fr_FR\n" +"Language-Team: fr_FR \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Generated-By: Babel 2.18.0\n" #: helpers/__init__.py:16 @@ -46,778 +46,886 @@ msgstr "To" msgid "OpenSSH client not found." msgstr "Client OpenSSH introuvable." -#: structures/engines/context.py:544 +#: structures/engines/context.py:567 msgid "This connection is read-only." msgstr "Cette connexion est en lecture seule." -#: structures/engines/mariadb/context.py:632 -#: structures/engines/mysql/context.py:643 +#: 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 "Table{table_index:03}" -#: structures/engines/mariadb/context.py:660 -#: structures/engines/mysql/context.py:671 +#: 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 "Column{column_index:03}" -#: structures/engines/mariadb/context.py:678 -#: structures/engines/mysql/context.py:689 +#: 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 "Index{index_number:03}" -#: structures/engines/mariadb/context.py:718 -#: structures/engines/mysql/context.py:727 +#: 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 "ForeignKey{foreign_key_number:03}" -#: structures/engines/mariadb/context.py:751 -#: structures/engines/mysql/context.py:758 +#: 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 "View{view_index:03}" -#: structures/engines/mariadb/context.py:804 +#: structures/engines/mariadb/context.py:830 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "Trigger{trigger_index:03}" #: windows/dialogs/connections/view.py:417 -#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1290 -#: windows/views.py:33 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1303 +#: windows/views.py:62 msgid "Connection" msgstr "Connexion" #: windows/components/dataview.py:115 windows/components/dataview.py:240 #: windows/components/dataview.py:253 windows/components/dataview.py:268 -#: windows/views.py:47 windows/views.py:97 windows/views.py:1024 -#: windows/views.py:1336 windows/views.py:1369 windows/views.py:1393 -#: windows/views.py:1417 windows/views.py:1441 windows/views.py:1465 -#: windows/views.py:1582 windows/views.py:1928 windows/views.py:2199 -#: windows/views.py:3293 windows/views.py:3515 +#: 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:438 +#: windows/views.py:77 windows/views.py:467 msgid "Last connection" msgstr "Dernière connexion" -#: windows/dialogs/connections/view.py:669 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:212 -#: windows/dialogs/connections/view.py:615 windows/views.py:65 +#: windows/dialogs/connections/view.py:615 windows/views.py:94 msgid "New connection" msgstr "Nouvelle connexion" -#: windows/views.py:71 +#: windows/views.py:100 msgid "Rename" msgstr "Renommer" -#: windows/views.py:76 +#: windows/views.py:105 msgid "Clone connection" msgstr "Cloner la connexion" -#: windows/views.py:81 windows/views.py:613 windows/views.py:1496 -#: windows/views.py:1864 windows/views.py:2154 windows/views.py:2378 -#: windows/views.py:3203 windows/views.py:3252 windows/views.py:3651 -#: windows/views.py:3683 +#: 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:1341 windows/views.py:1637 -#: windows/views.py:3115 windows/views.py:3570 +#: 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:1150 +#: windows/views.py:190 windows/views.py:1179 msgid "Password" msgstr "Mot de passe" -#: windows/views.py:174 +#: windows/views.py:203 msgid "Connection timeout" msgstr "Connexion perdue" -#: 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 "Marquer en lecture seule" -#: windows/views.py:214 +#: windows/views.py:243 msgid "Use SSH tunnel" msgstr "Utiliser un tunnel SSH" -#: windows/views.py:225 +#: windows/views.py:254 msgid "Compressed client/server protocol" msgstr "Protocole client/serveur compressé" -#: windows/views.py:244 +#: windows/views.py:273 msgid "Filename" msgstr "Nom de fichier" -#: windows/views.py:249 windows/views.py:368 windows/views.py:3809 +#: windows/views.py:278 windows/views.py:397 msgid "Select a file" msgstr "Sélectionner un fichier" -#: windows/views.py:249 windows/views.py:368 +#: windows/views.py:278 windows/views.py:397 msgid "*.*" msgstr "*. *" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:266 windows/views.py:1343 windows/views.py:1595 -#: windows/views.py:3113 windows/views.py:3528 +#: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 msgid "Comments" msgstr "Commentaires" -#: windows/main/controller.py:769 windows/views.py:280 windows/views.py:740 -#: windows/views.py:894 +#: 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:288 +#: windows/views.py:317 msgid "SSH executable" msgstr "Exécutable SSH" -#: windows/views.py:293 +#: windows/views.py:322 msgid "ssh" msgstr "ssh" -#: windows/views.py:301 +#: windows/views.py:330 msgid "SSH host + port" msgstr "Hôte SSH + port" -#: windows/views.py:313 +#: 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:322 +#: windows/views.py:351 msgid "SSH username" msgstr "Nom d'utilisateur SSH" -#: windows/views.py:335 +#: windows/views.py:364 msgid "SSH password" msgstr "Mot de passe SSH" -#: windows/views.py:348 +#: windows/views.py:377 msgid "Local port" msgstr "Port local" -#: windows/views.py:354 +#: 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:363 +#: windows/views.py:392 msgid "Identity file" msgstr "Identity file" -#: windows/views.py:379 +#: windows/views.py:408 msgid "Remote host + port" msgstr "Hôte distant + port" -#: windows/views.py:391 +#: 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:400 +#: windows/views.py:429 msgid "SSH extra args" msgstr "Arguments SSH supplémentaires" -#: windows/views.py:415 +#: windows/views.py:444 msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:421 windows/views.py:1339 windows/views.py:3117 +#: windows/views.py:450 windows/views.py:1368 msgid "Created at" msgstr "Créé le" -#: windows/views.py:455 +#: windows/views.py:484 msgid "Successful connections" msgstr "Connexions réussies" -#: windows/views.py:472 +#: windows/views.py:501 msgid "Last successful connection" msgstr "Connexions réussies" -#: windows/views.py:489 +#: windows/views.py:518 msgid "Unsuccessful connections" msgstr "Connexions échouées" -#: windows/views.py:506 +#: windows/views.py:535 msgid "Last failure reason" msgstr "Dernière raison de l'échec" -#: windows/views.py:523 +#: windows/views.py:552 msgid "Total connection attempts" msgstr "Dernière connexion" -#: windows/views.py:540 +#: windows/views.py:569 msgid "Average connection time (ms)" msgstr "Échec de la reconnexion :" -#: windows/views.py:557 +#: windows/views.py:586 msgid "Most recent connection duration" msgstr "Durée de la connexion la plus récente" -#: windows/views.py:576 +#: windows/views.py:605 msgid "Statistics" msgstr "Statistiques" -#: windows/views.py:594 windows/views.py:1821 +#: windows/views.py:623 windows/views.py:1850 msgid "Create" msgstr "Créer" -#: windows/views.py:598 +#: windows/views.py:627 msgid "Create connection" msgstr "Créer une connexion" -#: windows/views.py:601 +#: windows/views.py:630 msgid "Create directory" msgstr "Nouveau répertoire" -#: windows/views.py:630 windows/views.py:854 windows/views.py:1491 -#: windows/views.py:1867 windows/views.py:2159 windows/views.py:2383 -#: windows/views.py:2439 windows/views.py:3179 windows/views.py:3686 -#: windows/views.py:3823 +#: 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:323 windows/views.py:635 windows/views.py:2164 -#: windows/views.py:2388 windows/views.py:2603 windows/views.py:3691 -#: windows/views.py:3829 +#: 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:642 +#: windows/views.py:671 msgid "Test" msgstr "Tester" -#: windows/views.py:649 +#: windows/views.py:678 msgid "Connect" msgstr "Connecter" -#: windows/views.py:752 +#: windows/views.py:781 windows/views.py:2388 msgid "Language" msgstr "Langue" -#: windows/views.py:757 +#: windows/views.py:786 msgid "English" msgstr "Anglais" -#: windows/views.py:757 +#: windows/views.py:786 msgid "Italian" msgstr "Italien" -#: windows/views.py:757 +#: windows/views.py:786 msgid "French" msgstr "Français" -#: windows/views.py:769 +#: windows/views.py:798 msgid "Locale" msgstr "Localisation" -#: windows/views.py:790 +#: windows/views.py:819 msgid "Column content" msgstr "Contenu de colonne" -#: windows/views.py:800 +#: windows/views.py:829 msgid "Syntax" msgstr "Syntaxe" -#: windows/views.py:857 +#: windows/views.py:886 msgid "Ok" msgstr "Ok" -#: windows/views.py:888 +#: windows/views.py:917 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:897 +#: windows/views.py:926 msgid "File" msgstr "Fichier" -#: windows/views.py:900 +#: windows/views.py:929 msgid "About" msgstr "À propos" -#: windows/views.py:903 +#: windows/views.py:932 msgid "Help" msgstr "Aide" -#: windows/views.py:908 +#: windows/views.py:937 msgid "Open connection manager" msgstr "Ouvrir le gestionnaire de connexions" -#: windows/views.py:912 +#: windows/views.py:941 msgid "Disconnect from server" msgstr "Se déconnecter du serveur" -#: windows/views.py:914 +#: windows/views.py:943 msgid "tool" msgstr "outil" -#: windows/views.py:914 windows/views.py:2419 +#: windows/views.py:943 windows/views.py:2628 msgid "Refresh" msgstr "Actualiser" -#: windows/views.py:918 windows/views.py:920 windows/views.py:1845 -#: windows/views.py:2423 windows/views.py:2589 +#: 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:925 +#: windows/views.py:954 #, python-brace-format msgid "{mode}" msgstr "{mode}" -#: windows/views.py:962 windows/views.py:966 windows/views.py:2556 -#: windows/views.py:2561 +#: 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:969 windows/views.py:1895 windows/views.py:2568 -#: windows/views.py:3714 +#: windows/views.py:998 windows/views.py:1924 windows/views.py:2777 msgid "MyMenu" msgstr "MonMenu" -#: windows/views.py:984 windows/views.py:1519 windows/views.py:1526 -#: windows/views.py:1533 +#: windows/views.py:1013 windows/views.py:1548 windows/views.py:1555 +#: windows/views.py:1562 msgid "MyLabel" msgstr "MonÉtiquette" -#: windows/views.py:990 +#: windows/views.py:1019 msgid "Databases" msgstr "Bases de données" -#: windows/views.py:991 windows/views.py:1338 windows/views.py:3118 +#: windows/views.py:1020 windows/views.py:1367 msgid "Size" msgstr "Taille" -#: windows/views.py:992 +#: windows/views.py:1021 msgid "Elements" msgstr "Éléments" -#: windows/views.py:993 +#: windows/views.py:1022 msgid "Modified at" msgstr "Modifié le" -#: windows/views.py:994 windows/views.py:1353 +#: windows/views.py:1023 windows/views.py:1382 msgid "Tables" msgstr "Tables" -#: windows/views.py:1001 +#: 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:1047 -#: windows/views.py:1342 windows/views.py:3114 +#: windows/components/dataview.py:89 windows/views.py:1076 +#: windows/views.py:1371 msgid "Collation" msgstr "Classement" -#: windows/views.py:1076 +#: windows/views.py:1105 msgid "Encryption" msgstr "Chiffrement" -#: windows/main/controller.py:1259 windows/main/controller.py:1281 -#: windows/main/controller.py:1285 windows/views.py:1088 +#: windows/main/controller.py:1272 windows/main/controller.py:1294 +#: windows/main/controller.py:1298 windows/views.py:1117 msgid "Read Only" msgstr "Lecture seule" -#: windows/views.py:1105 +#: windows/views.py:1134 msgid "Tablespace" msgstr "Tablespace" -#: windows/views.py:1126 +#: windows/views.py:1155 msgid "Connection limit" msgstr "Limite de connexion" -#: windows/views.py:1169 +#: windows/views.py:1198 msgid "Profile" msgstr "Profil" -#: windows/views.py:1195 +#: windows/views.py:1224 msgid "Default tablespace" msgstr "Tablespace par défaut" -#: windows/views.py:1216 +#: windows/views.py:1245 msgid "Temporary tablespace" msgstr "Tablespace temporaire" -#: windows/views.py:1242 +#: windows/views.py:1271 msgid "Quota" msgstr "Quota" -#: windows/views.py:1261 +#: windows/views.py:1290 msgid "Unlimited quota" msgstr "Quota illimitée" -#: windows/views.py:1278 +#: windows/views.py:1307 msgid "Account status" msgstr "Statut du compte" -#: windows/views.py:1299 +#: windows/views.py:1328 msgid "Password expire" msgstr "Mot de passe" -#: windows/views.py:1323 +#: windows/views.py:1352 msgid "Add new table" msgstr "Ajouter une nouvelle table" -#: windows/views.py:1325 +#: windows/views.py:1354 msgid "Clone table" msgstr "Cloner la table" -#: windows/main/controller.py:1770 windows/views.py:1327 +#: windows/main/controller.py:1821 windows/views.py:1356 msgid "Delete table" msgstr "Supprimer la table" -#: windows/views.py:1337 +#: windows/views.py:1366 windows/views.py:2457 msgid "Rows" msgstr "Lignes" -#: windows/views.py:1340 windows/views.py:3116 +#: windows/views.py:1369 msgid "Updated at" msgstr "Mis à jour le" -#: windows/views.py:1358 +#: windows/views.py:1387 msgid "Add new view" msgstr "Ajouter une nouvelle vue" -#: windows/views.py:1360 windows/views.py:1384 +#: windows/views.py:1389 msgid "Clone view" msgstr "Cloner" -#: windows/views.py:1362 +#: windows/views.py:1391 msgid "Delete view" msgstr "Supprimer" -#: windows/views.py:1370 windows/views.py:1394 windows/views.py:1418 -#: windows/views.py:1442 windows/views.py:1466 +#: 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:1377 +#: windows/views.py:1406 msgid "Views" msgstr "Vues" -#: windows/views.py:1382 +#: windows/views.py:1411 msgid "Add new procedure" msgstr "Ajouter une nouvelle procédure" -#: windows/views.py:1384 +#: windows/views.py:1413 msgid "Clone procedure" msgstr "Cloner la procédure" -#: windows/views.py:1386 +#: windows/views.py:1415 msgid "Delete procedure" msgstr "Supprimer la procédure" -#: windows/views.py:1401 +#: windows/views.py:1430 msgid "Procedures" msgstr "Procédures" -#: windows/views.py:1406 +#: windows/views.py:1435 msgid "Add new function" msgstr "Ajouter une nouvelle fonction" -#: windows/views.py:1408 +#: windows/views.py:1437 msgid "Clone function" msgstr "Cloner la fonction" -#: windows/views.py:1410 +#: windows/views.py:1439 msgid "Delete function" msgstr "Supprimer la fonction" -#: windows/views.py:1425 +#: windows/views.py:1454 msgid "Functions" msgstr "Fonctions" -#: windows/views.py:1430 +#: windows/views.py:1459 msgid "Add new trigger" msgstr "Ajouter un nouveau déclencheur" -#: windows/views.py:1432 +#: windows/views.py:1461 msgid "Clone trigger" msgstr "Cloner le déclencheur" -#: windows/views.py:1434 +#: windows/views.py:1463 msgid "Delete trigger" msgstr "Supprimer le déclencheur" -#: windows/views.py:1449 +#: windows/views.py:1478 msgid "Triggers" msgstr "Déclencheurs" -#: windows/views.py:1454 +#: windows/views.py:1483 msgid "Add new event" msgstr "Ajouter un nouvel événement" -#: windows/views.py:1456 +#: windows/views.py:1485 msgid "Clone event" msgstr "Cloner" -#: windows/views.py:1458 +#: windows/views.py:1487 msgid "Delete event" msgstr "Supprimer l'événement" -#: windows/views.py:1473 +#: windows/views.py:1502 msgid "Events" msgstr "Événements" -#: windows/views.py:1501 windows/views.py:1872 windows/views.py:2437 -#: windows/views.py:2521 windows/views.py:3186 +#: windows/views.py:1530 windows/views.py:1901 windows/views.py:2646 +#: windows/views.py:2730 msgid "Apply" msgstr "Appliquer" -#: windows/views.py:1513 windows/views.py:1692 windows/views.py:3603 +#: windows/views.py:1542 windows/views.py:1721 msgid "Options" msgstr "Options" -#: windows/views.py:1544 +#: windows/views.py:1573 msgid "Diagram" msgstr "Diagramme" -#: windows/views.py:1555 +#: windows/views.py:1584 msgid "Database" msgstr "Base de données" -#: windows/views.py:1610 windows/views.py:3543 +#: windows/views.py:1639 msgid "Base" msgstr "Base" -#: windows/views.py:1624 windows/views.py:3557 +#: windows/views.py:1653 msgid "Auto Increment" msgstr "Auto incrément" -#: windows/views.py:1652 windows/views.py:3585 +#: windows/views.py:1681 msgid "Default Collation" msgstr "Classement par défaut" -#: windows/views.py:1662 +#: windows/views.py:1691 msgid "Convert data" msgstr "Convertir les données" -#: windows/views.py:1670 +#: windows/views.py:1699 msgid "Row format" msgstr "Format de ligne" -#: windows/views.py:1703 windows/views.py:1731 windows/views.py:1759 -#: windows/views.py:1847 windows/views.py:2259 windows/views.py:2427 -#: windows/views.py:3313 windows/views.py:3330 windows/views.py:3354 -#: windows/views.py:3381 +#: 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:1705 windows/views.py:1733 windows/views.py:1761 -#: windows/views.py:2261 windows/views.py:2529 windows/views.py:3320 -#: windows/views.py:3337 windows/views.py:3361 windows/views.py:3388 +#: 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:1718 windows/views.py:3617 +#: windows/views.py:1747 msgid "Indexes" msgstr "Index" -#: windows/views.py:1729 windows/views.py:1757 windows/views.py:2257 -#: windows/views.py:3198 windows/views.py:3240 windows/views.py:3308 -#: windows/views.py:3349 windows/views.py:3376 windows/views.py:3646 +#: windows/views.py:1758 windows/views.py:1786 windows/views.py:2324 msgid "Insert" msgstr "Insérer" -#: windows/views.py:1746 +#: windows/views.py:1775 msgid "Foreign Keys" msgstr "Clés étrangères" -#: windows/views.py:1774 +#: windows/views.py:1803 msgid "Checks" msgstr "Contrôles" -#: windows/views.py:1841 windows/views.py:3638 +#: windows/views.py:1870 msgid "Columns:" msgstr "Colonnes :" -#: windows/views.py:1851 +#: windows/views.py:1880 msgid "Move Up" msgstr "Déplacer vers le haut\tCTRL+UP" -#: windows/views.py:1853 +#: windows/views.py:1882 msgid "Move Down" msgstr "Déplacer vers le bas\tCTRL+D" -#: windows/views.py:1885 windows/views.py:1892 windows/views.py:3704 -#: windows/views.py:3711 +#: windows/views.py:1914 windows/views.py:1921 msgid "Add Index" msgstr "Ajouter un index" -#: windows/views.py:1889 windows/views.py:3708 +#: windows/views.py:1918 msgid "Add PrimaryKey" msgstr "Ajouter une clé primaire" -#: windows/views.py:1906 +#: windows/views.py:1935 msgid "Table" msgstr "Table" -#: windows/views.py:1948 windows/views.py:2219 +#: windows/views.py:1977 windows/views.py:2243 msgid "Schema" msgstr "Schema" -#: windows/views.py:1976 windows/views.py:2247 +#: windows/views.py:2005 windows/views.py:2314 msgid "General" msgstr "Général" -#: windows/views.py:1981 +#: windows/views.py:2010 msgid "Algorithm" msgstr "Algorithme" -#: windows/views.py:1983 +#: windows/views.py:2012 msgid "UNDEFINED" msgstr "Non signé" -#: windows/views.py:1986 +#: windows/views.py:2015 msgid "MERGE" msgstr "MERGE" -#: windows/views.py:1989 +#: windows/views.py:2018 msgid "TEMPTABLE" msgstr "TEMPTABLE" -#: windows/views.py:1999 +#: windows/views.py:2028 msgid "View constraint" msgstr "Contrainte de vue" -#: windows/views.py:2001 +#: windows/views.py:2030 msgid "None" msgstr "Aucun" -#: windows/views.py:2004 +#: windows/views.py:2033 msgid "LOCAL" msgstr "LOCAL" -#: windows/views.py:2007 +#: windows/views.py:2036 msgid "CASCADE" msgstr "CASCADE" -#: windows/views.py:2010 +#: windows/views.py:2039 msgid "CHECK ONLY" msgstr "VÉRIFIER SEULEMENT" -#: windows/views.py:2013 +#: windows/views.py:2042 msgid "READ ONLY" msgstr "LECTURE SEULE" -#: windows/views.py:2026 +#: windows/views.py:2055 windows/views.py:2483 msgid "Behavior" msgstr "Comportement" -#: windows/views.py:2033 windows/views.py:2281 +#: windows/views.py:2062 windows/views.py:2490 msgid "Definer" msgstr "Définisseur" -#: windows/views.py:2041 windows/views.py:2289 +#: windows/views.py:2070 windows/views.py:2498 msgid "*" msgstr "*" -#: windows/views.py:2053 windows/views.py:2301 +#: windows/views.py:2082 windows/views.py:2510 msgid "SQL security" msgstr "Sécurité SQL" -#: windows/views.py:2060 windows/views.py:2308 +#: windows/views.py:2089 windows/views.py:2517 msgid "DEFINER" msgstr "DÉFINISSEUR" -#: windows/views.py:2060 windows/views.py:2308 +#: windows/views.py:2089 windows/views.py:2517 msgid "INVOKER" msgstr "APPELANT" -#: windows/views.py:2074 +#: windows/views.py:2103 msgid "Force" msgstr "Forcer" -#: windows/views.py:2086 +#: windows/views.py:2115 msgid "Security barrier" msgstr "Barrière de sécurité" -#: windows/views.py:2099 windows/views.py:2323 +#: windows/views.py:2128 windows/views.py:2532 msgid "Security" msgstr "Sécurité" -#: windows/views.py:2176 +#: windows/views.py:2205 msgid "View" msgstr "Vues" -#: windows/views.py:2274 +#: windows/views.py:2262 +#, fuzzy +msgid "Type" +msgstr "Type de données" + +#: 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 +#, fuzzy +msgid "Return type" +msgstr "Type de données" + +#: windows/views.py:2299 +#, fuzzy +msgid "Comment" +msgstr "Commentaires" + +#: windows/components/dataview.py:111 windows/views.py:2335 +msgid "#" +msgstr "#" + +#: windows/views.py:2337 +#, fuzzy +msgid "Datatype" +msgstr "Type de données" + +#: windows/views.py:2338 +#, fuzzy +msgid "Context" +msgstr "Connecter" + +#: windows/views.py:2345 msgid "Parameters" msgstr "Paramètres" -#: windows/views.py:2400 -msgid "Procedure" -msgstr "Procedure" +#: windows/views.py:2354 +#, fuzzy +msgid "Data access" +msgstr "Bases de données" -#: windows/views.py:2408 +#: 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 +#, fuzzy +msgid "MODIFIES SQL DATA" +msgstr "Modifié le" + +#: windows/views.py:2371 +#, fuzzy +msgid "Deterministic" +msgstr "Définition" + +#: windows/views.py:2395 +#, fuzzy +msgid "SQL" +msgstr "*.sql" + +#: windows/views.py:2395 +msgid "PLPGSQL" +msgstr "" + +#: windows/views.py:2405 +#, fuzzy +msgid "Volatility" +msgstr "Virtualité" + +#: windows/views.py:2412 +msgid "VOLATILE" +msgstr "" + +#: windows/views.py:2412 +#, fuzzy +msgid "STABLE" +msgstr "Table" + +#: windows/views.py:2412 +#, fuzzy +msgid "IMMUTABLE" +msgstr "Table" + +#: windows/views.py:2422 +msgid "Parallel" +msgstr "" + +#: windows/views.py:2429 +#, fuzzy +msgid "UNSAFE" +msgstr "Enregistrer" + +#: windows/views.py:2429 +msgid "RESTRICTED" +msgstr "" + +#: windows/views.py:2429 +#, fuzzy +msgid "SAFE" +msgstr "Enregistrer" + +#: windows/views.py:2441 +#, fuzzy +msgid "Cost" +msgstr "Fermer" + +#: windows/views.py:2609 +#, fuzzy +msgid "Routine" +msgstr "Temps de fonctionnement" + +#: windows/views.py:2617 msgid "Trigger" msgstr "Déclencheurs" -#: windows/views.py:2425 +#: windows/views.py:2634 msgid "Duplicate" msgstr "Dupliquer" -#: windows/views.py:2431 +#: windows/views.py:2640 msgid "Apply changes automatically" msgstr "Appliquer les modifications automatiquement" -#: windows/views.py:2433 windows/views.py:2434 +#: windows/views.py:2642 windows/views.py:2643 msgid "" -"If enabled, table edits are applied immediately without pressing Apply or " -"Cancel" +"If enabled, table edits are applied immediately without pressing Apply or" +" Cancel" msgstr "" -"Si activé, les modifications de la table sont appliquées immédiatement sans " -"appuyer sur Appliquer ou Annuler" +"Si activé, les modifications de la table sont appliquées immédiatement " +"sans appuyer sur Appliquer ou Annuler" -#: windows/views.py:2447 +#: windows/views.py:2656 #, python-brace-format -msgid "" -"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" -"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -#: windows/views.py:2455 +#: windows/views.py:2664 msgid "First" msgstr "Premier" -#: windows/views.py:2473 +#: windows/views.py:2682 msgid "Last" msgstr "Dernier" -#: windows/views.py:2482 +#: windows/views.py:2691 msgid "Filters" msgstr "Filtres" -#: windows/views.py:2524 +#: windows/views.py:2733 msgid "" "Apply filters in data\n" "CTRL+ENTER" @@ -825,114 +933,62 @@ msgstr "" "Appliquer les filtres dans les données\n" "CTRL+ENTRÉE" -#: windows/views.py:2525 +#: windows/views.py:2734 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2553 +#: windows/views.py:2762 msgid "Insert row" msgstr "Insérer une ligne" -#: windows/components/popup.py:31 windows/views.py:2565 +#: windows/components/popup.py:31 windows/views.py:2774 msgid "NULL" msgstr "NULL" -#: windows/views.py:2572 +#: windows/views.py:2781 msgid "Data" msgstr "Données" -#: windows/main/controller.py:318 windows/views.py:2589 +#: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" msgstr "Requête" -#: windows/views.py:2591 windows/views.py:3137 +#: windows/views.py:2800 msgid "Close" msgstr "Fermer" -#: windows/main/controller.py:319 windows/views.py:2591 +#: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" msgstr "Requête" -#: windows/views.py:2595 +#: windows/views.py:2804 msgid "Run" msgstr "Exécuter" -#: windows/main/controller.py:320 windows/views.py:2595 +#: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" msgstr "Exécuter" -#: windows/views.py:2597 +#: windows/views.py:2806 msgid "Run all" msgstr "Tout exécuter" -#: windows/views.py:2597 +#: windows/views.py:2806 msgid "Execute all statements" msgstr "Exécuter toutes les instructions" -#: windows/main/controller.py:322 windows/views.py:2599 +#: windows/main/controller.py:323 windows/views.py:2808 msgid "Stop" msgstr "Arrêter" -#: windows/views.py:2664 +#: windows/views.py:2873 msgid "a page" msgstr "une page" -#: windows/views.py:2714 +#: windows/views.py:2923 msgid "Query" msgstr "Requête" -#: windows/views.py:3091 -msgid "Character set" -msgstr "Jeu de caractères" - -#: windows/views.py:3121 windows/views.py:3140 -msgid "New" -msgstr "Nouveau" - -#: windows/views.py:3160 -msgid "Insert record" -msgstr "Insérer un enregistrement" - -#: windows/views.py:3165 -msgid "Duplicate record" -msgstr "Dupliquer un enregistrement" - -#: windows/views.py:3172 -msgid "Delete record" -msgstr "Supprimer un enregistrement" - -#: windows/views.py:3210 windows/views.py:3658 -msgid "Up" -msgstr "Haut" - -#: windows/views.py:3217 windows/views.py:3665 -msgid "Down" -msgstr "Bas" - -#: windows/views.py:3232 -msgid "Table:" -msgstr "Table :" - -#: windows/views.py:3245 -msgid "Clone" -msgstr "Cloner" - -#: windows/views.py:3270 -msgid "MyButton" -msgstr "MonBouton" - -#: windows/views.py:3794 -msgid "Save Starments" -msgstr "Enregistrer les instructions" - -#: windows/views.py:3802 -msgid "Location" -msgstr "Emplacement" - -#: windows/views.py:3809 -msgid "*.sql" -msgstr "*.sql" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 msgid "Allow NULL" @@ -965,10 +1021,6 @@ msgstr "Non signé" msgid "Zerofill" msgstr "Remplissage zéro" -#: windows/components/dataview.py:111 -msgid "#" -msgstr "#" - #: windows/components/dataview.py:119 msgid "Data type" msgstr "Type de données" @@ -1068,7 +1120,9 @@ msgstr "Confirmer la sauvegarde" #: 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?" +msgstr "" +"Vous avez des modifications non enregistrées. Voulez-vous les enregistrer" +" avant de continuer?" #: windows/dialogs/connections/view.py:485 msgid "Unsaved changes" @@ -1076,9 +1130,11 @@ msgstr "Modifications non enregistrées" #: windows/dialogs/connections/view.py:773 msgid "" -"This connection cannot work without TLS. TLS has been enabled automatically." +"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." +"Cette connexion ne peut pas fonctionner sans TLS. TLS a été activé " +"automatiquement." #: windows/dialogs/connections/view.py:798 #, python-brace-format @@ -1108,114 +1164,114 @@ msgstr "Confirmer la suppression" msgid "Do you want to delete the directory '{directory_name}'?" msgstr "Voulez-vous supprimer le répertoire '{directory_name}'?" -#: windows/main/controller.py:315 +#: windows/main/controller.py:316 #, python-brace-format msgid "{text} ({shortcut})" msgstr "{text} ({shortcut})" -#: windows/main/controller.py:321 +#: windows/main/controller.py:322 msgid "Execute all" msgstr "Tout exécuter" -#: windows/main/controller.py:440 windows/main/controller.py:448 +#: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" msgstr "Requête" -#: windows/main/controller.py:467 +#: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" msgstr "Query ({query_number})" -#: windows/main/controller.py:516 +#: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" msgstr "Vous avez des modifications non enregistrées. Enregistrer avant de fermer?" -#: windows/main/controller.py:517 +#: windows/main/controller.py:519 msgid "Unsaved query" msgstr "Requête non enregistrée" -#: windows/main/controller.py:562 +#: windows/main/controller.py:564 msgid "Save query" msgstr "Enregistrer la requête" -#: windows/main/controller.py:565 +#: windows/main/controller.py:567 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" msgstr "Fichiers SQL (*.sql)|*.sql|Tous les fichiers (*.*)|*.*" -#: windows/main/controller.py:593 windows/main/controller.py:624 -#: windows/main/controller.py:651 windows/main/database/list.py:119 -#: windows/main/database/procedure.py:206 -#: windows/main/database/procedure.py:232 windows/main/database/view.py:268 -#: windows/main/database/view.py:297 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:631 +#: windows/main/controller.py:633 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "-- Requête enregistrée dans {file_path}" -#: windows/main/controller.py:657 +#: windows/main/controller.py:659 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "-- Requête auto-enregistrée dans {file_path}" -#: windows/main/controller.py:722 +#: windows/main/controller.py:725 msgid "days" msgstr "jours" -#: windows/main/controller.py:723 +#: windows/main/controller.py:726 msgid "hours" msgstr "heures" -#: windows/main/controller.py:724 +#: windows/main/controller.py:727 msgid "minutes" msgstr "minutes" -#: windows/main/controller.py:725 +#: windows/main/controller.py:728 msgid "seconds" msgstr "secondes" -#: windows/main/controller.py:733 +#: 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:769 +#: windows/main/controller.py:772 msgid "Settings saved successfully" msgstr "Paramètres enregistrés avec succès" -#: windows/main/controller.py:993 +#: windows/main/controller.py:1006 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "~{estimated} (Chargement...)" -#: windows/main/controller.py:995 +#: windows/main/controller.py:1008 msgid "~ (Loading...)" msgstr "~ (Chargement...)" -#: windows/main/controller.py:1230 +#: windows/main/controller.py:1243 msgid "Write Mode (2:00)" msgstr "Mode écriture (2:00)" -#: windows/main/controller.py:1281 +#: windows/main/controller.py:1294 msgid "Write Mode" msgstr "Mode écriture" -#: windows/main/controller.py:1292 +#: windows/main/controller.py:1305 msgid "Version" msgstr "Version" -#: windows/main/controller.py:1294 +#: windows/main/controller.py:1307 msgid "Uptime" msgstr "Temps de fonctionnement" -#: windows/main/controller.py:1429 +#: windows/main/controller.py:1444 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "Voulez-vous annuler le changement à {database_name}?" -#: windows/main/controller.py:1462 +#: windows/main/controller.py:1477 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1224,57 +1280,89 @@ 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" +"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:1467 windows/main/controller.py:1488 +#: windows/main/controller.py:1482 windows/main/controller.py:1503 msgid "Delete database" msgstr "Supprimer la base de données" -#: windows/main/controller.py:1473 +#: 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." +msgstr "" +"La sauvegarde n'est pas encore implémentée. Aucune action n'a été " +"effectuée." -#: windows/main/controller.py:1474 +#: windows/main/controller.py:1489 msgid "Dump not available" msgstr "Sauvegarde non disponible" -#: windows/main/controller.py:1487 +#: windows/main/controller.py:1502 msgid "Database deletion is not supported by this engine." msgstr "La suppression de base de données n'est pas supportée par ce moteur." -#: windows/main/controller.py:1502 +#: windows/main/controller.py:1517 msgid "Database deleted successfully" msgstr "Base de données supprimée avec succès" -#: windows/main/controller.py:1503 windows/main/database/procedure.py:195 -#: windows/main/database/procedure.py:226 windows/main/database/view.py:256 +#: 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 "Succès" -#: windows/main/controller.py:1741 +#: windows/main/controller.py:1792 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "Voulez-vous annuler le changement à {table_name}?" -#: windows/main/controller.py:1767 +#: windows/main/controller.py:1818 #, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "Voulez-vous supprimer la table {table_name}?" -#: windows/main/controller.py:1789 +#: windows/main/controller.py:1840 #, python-brace-format msgid "{table_name} (COPY)" msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1948 +#: windows/main/controller.py:1999 msgid "Do you want delete the records?" msgstr "Voulez-vous supprimer les enregistrements ?" +#: 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." @@ -1283,44 +1371,60 @@ msgstr "La connexion à la base de données a été perdue." msgid "Do you want to reconnect?" msgstr "Voulez-vous vous reconnecter ?" -#: windows/main/database/list.py:108 -msgid "Connection lost" -msgstr "Connexion perdue" - #: windows/main/database/list.py:118 msgid "Reconnection failed:" msgstr "Échec de la reconnexion :" -#: windows/main/database/procedure.py:194 +#: windows/main/database/routine.py:545 +#, fuzzy +msgid "Function created successfully" +msgstr "Vue créée avec succès" + +#: windows/main/database/routine.py:547 +#, fuzzy +msgid "Function updated successfully" +msgstr "Vue 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/procedure.py:194 +#: windows/main/database/routine.py:553 msgid "Procedure updated successfully" msgstr "Procédure mise à jour avec succès" -#: windows/main/database/procedure.py:206 -#, python-brace-format -msgid "Error saving procedure: {}" -msgstr "Erreur lors de l'enregistrement de la procédure: {}" +#: windows/main/database/routine.py:590 +#, fuzzy, python-brace-format +msgid "Error saving routine: {}" +msgstr "Erreur lors de l'enregistrement de la vue: {}" -#: windows/main/database/procedure.py:217 -#, python-brace-format -msgid "Are you sure you want to delete procedure '{}'?" -msgstr "Êtes-vous sûr de vouloir supprimer la procédure '{}'?" +#: windows/main/database/routine.py:604 +#, fuzzy +msgid "Function" +msgstr "Fonctions" + +#: windows/main/database/routine.py:604 +msgid "Procedure" +msgstr "Procedure" -#: windows/main/database/procedure.py:218 windows/main/database/view.py:282 +#: windows/main/database/routine.py:607 +#, fuzzy, python-brace-format +msgid "Are you sure you want to delete {} '{}'?" +msgstr "Êtes-vous sûr de vouloir supprimer la vue '{}'?" + +#: windows/main/database/routine.py:610 windows/main/database/view.py:282 msgid "Confirm Delete" msgstr "Confirmer la suppression" -#: windows/main/database/procedure.py:226 -msgid "Procedure deleted successfully" -msgstr "Procédure supprimée avec succès" +#: windows/main/database/routine.py:619 +#, fuzzy, python-brace-format +msgid "{} deleted successfully" +msgstr "Vue supprimée avec succès" -#: windows/main/database/procedure.py:232 -#, python-brace-format -msgid "Error deleting procedure: {}" -msgstr "Erreur lors de la suppression de la procédure: {}" +#: windows/main/database/routine.py:634 +#, fuzzy, python-brace-format +msgid "Error deleting routine: {}" +msgstr "Erreur lors de la suppression de la vue: {}" #: windows/main/database/view.py:255 msgid "View created successfully" @@ -1349,21 +1453,21 @@ msgstr "Vue supprimée avec succès" msgid "Error deleting view: {}" 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 "{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 "{elapsed_s:.2f} s" -#: windows/main/query/controller.py:115 +#: windows/main/query/controller.py:117 msgid "none" msgstr "aucun" -#: windows/main/query/controller.py:121 +#: windows/main/query/controller.py:123 #, python-brace-format msgid "" "Query execution stopped after {elapsed}.\n" @@ -1378,14 +1482,19 @@ msgstr "" "É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 "Exécution de la requête annulée" -#: windows/main/query/controller.py:176 +#: windows/main/query/controller.py:178 msgid "No active database connection" msgstr "Aucune connexion de base de données active" +#: windows/main/query/controller.py:227 +#, fuzzy +msgid "Database connection lost" +msgstr "Connexion perdue" + #: windows/main/query/history.py:55 msgid "(empty query)" msgstr "(requête vide)" @@ -1433,139 +1542,3 @@ msgstr "Erreur" msgid "Error saving records" msgstr "Erreur lors de l'enregistrement des enregistrements" -#~ msgid "Created at:" -#~ msgstr "Created at:" - -#~ msgid "Last connection:" -#~ msgstr "Last connection:" - -#~ msgid "Successful connections:" -#~ msgstr "Successful connections:" - -#~ msgid "Unsuccessful connections:" -#~ msgstr "Unsuccessful connections:" - -#~ msgid "Session Manager" -#~ msgstr "Session Manager" - -#~ msgid "Session name" -#~ msgstr "Session name" - -#~ msgid "Connection type" -#~ msgstr "Connection type" - -#~ msgid "Open" -#~ msgstr "Open" - -#~ msgid "Open session manager" -#~ msgstr "Open session manager" - -#~ msgid "Foreign Key" -#~ msgstr "Foreign Key" - -#~ 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 "{} rows affected" - -#~ msgid "Query {}" -#~ msgstr "Requête" - -#~ msgid "Query {} (Error)" -#~ msgstr "Query {} (Error)" - -#~ msgid "Query {} ({} rows × {} cols)" -#~ msgstr "Query {} ({} rows × {} cols)" - -#~ msgid "{} rows" -#~ msgstr "Lignes" - -#~ msgid "{:.1f} ms" -#~ msgstr "{:.1f} ms" - -#~ msgid "{} warnings" -#~ msgstr "{} warnings" - -#~ 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 "Read only" - -#~ msgid "CASCADED" -#~ msgstr "CASCADED" - -#~ 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 "RadioBtn" - -#~ msgid "Edit Column" -#~ msgstr "Modifier la colonne" - -#~ msgid "Datatype" -#~ msgstr "Type de données" - -#~ msgid "Zero Fill" -#~ msgstr "Remplissage zéro" - -#~ msgid "Refrsh" -#~ msgstr "Actualiser" diff --git a/locale/it_IT/LC_MESSAGES/petersql.mo b/locale/it_IT/LC_MESSAGES/petersql.mo index 5dc158827da054ff643d3d361704654b702e89b3..b6bd5f57af36f208d76eb796bec541f755902490 100644 GIT binary patch literal 18024 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 Lct4*RmOlM&=P6Cr literal 18518 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 XfOZe-KZbv7{{JxdPc!Oo-`xK{j;54J diff --git a/locale/it_IT/LC_MESSAGES/petersql.po b/locale/it_IT/LC_MESSAGES/petersql.po index c6f0fda..0134efd 100644 --- a/locale/it_IT/LC_MESSAGES/petersql.po +++ b/locale/it_IT/LC_MESSAGES/petersql.po @@ -6,15 +6,15 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-05-31 12:36+0200\n" +"POT-Creation-Date: 2026-06-06 10:58+0200\n" "PO-Revision-Date: 2026-03-10 18:21+0000\n" "Last-Translator: \n" -"Language-Team: it_IT \n" "Language: it_IT\n" +"Language-Team: it_IT \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.18.0\n" #: helpers/__init__.py:16 @@ -46,779 +46,886 @@ msgstr "TB" msgid "OpenSSH client not found." msgstr "Client OpenSSH non trovato." -#: structures/engines/context.py:544 +#: structures/engines/context.py:567 msgid "This connection is read-only." msgstr "Questa connessione è in sola lettura." -#: structures/engines/mariadb/context.py:632 -#: structures/engines/mysql/context.py:643 +#: 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 "Table{table_index:03}" -#: structures/engines/mariadb/context.py:660 -#: structures/engines/mysql/context.py:671 +#: 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 "Column{column_index:03}" -#: structures/engines/mariadb/context.py:678 -#: structures/engines/mysql/context.py:689 +#: 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 "Index{index_number:03}" -#: structures/engines/mariadb/context.py:718 -#: structures/engines/mysql/context.py:727 +#: 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 "ForeignKey{foreign_key_number:03}" -#: structures/engines/mariadb/context.py:751 -#: structures/engines/mysql/context.py:758 +#: 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 "View{view_index:03}" -#: structures/engines/mariadb/context.py:804 +#: structures/engines/mariadb/context.py:830 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "Trigger{trigger_index:03}" #: windows/dialogs/connections/view.py:417 -#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1290 -#: windows/views.py:33 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1303 +#: windows/views.py:62 msgid "Connection" msgstr "Connessione" #: windows/components/dataview.py:115 windows/components/dataview.py:240 #: windows/components/dataview.py:253 windows/components/dataview.py:268 -#: windows/views.py:47 windows/views.py:97 windows/views.py:1024 -#: windows/views.py:1336 windows/views.py:1369 windows/views.py:1393 -#: windows/views.py:1417 windows/views.py:1441 windows/views.py:1465 -#: windows/views.py:1582 windows/views.py:1928 windows/views.py:2199 -#: windows/views.py:3293 windows/views.py:3515 +#: 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:438 +#: windows/views.py:77 windows/views.py:467 msgid "Last connection" msgstr "Ultima connessione" -#: windows/dialogs/connections/view.py:669 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:212 -#: windows/dialogs/connections/view.py:615 windows/views.py:65 +#: 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 "Clona connessione" -#: windows/views.py:81 windows/views.py:613 windows/views.py:1496 -#: windows/views.py:1864 windows/views.py:2154 windows/views.py:2378 -#: windows/views.py:3203 windows/views.py:3252 windows/views.py:3651 -#: windows/views.py:3683 +#: 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:1341 windows/views.py:1637 -#: windows/views.py:3115 windows/views.py:3570 +#: 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:1150 +#: 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 "Usa TLS" -#: windows/views.py:203 +#: windows/views.py:232 msgid "Mark read only" msgstr "Segna come sola lettura" -#: windows/views.py:214 +#: windows/views.py:243 msgid "Use SSH tunnel" msgstr "Usa tunnel SSH" -#: windows/views.py:225 +#: windows/views.py:254 msgid "Compressed client/server protocol" msgstr "Protocollo client/server compresso" -#: windows/views.py:244 +#: windows/views.py:273 msgid "Filename" msgstr "Nome file" -#: windows/views.py:249 windows/views.py:368 windows/views.py:3809 +#: windows/views.py:278 windows/views.py:397 msgid "Select a file" msgstr "Seleziona un file" -#: windows/views.py:249 windows/views.py:368 +#: windows/views.py:278 windows/views.py:397 msgid "*.*" msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:266 windows/views.py:1343 windows/views.py:1595 -#: windows/views.py:3113 windows/views.py:3528 +#: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 msgid "Comments" msgstr "Commenti" -#: windows/main/controller.py:769 windows/views.py:280 windows/views.py:740 -#: windows/views.py:894 +#: windows/main/controller.py:772 windows/views.py:309 windows/views.py:769 +#: windows/views.py:923 msgid "Settings" msgstr "Impostazioni" -#: windows/views.py:288 +#: windows/views.py:317 msgid "SSH executable" msgstr "Eseguibile SSH" -#: windows/views.py:293 +#: windows/views.py:322 msgid "ssh" msgstr "ssh" -#: windows/views.py:301 +#: windows/views.py:330 msgid "SSH host + port" msgstr "Host SSH + porta" -#: windows/views.py:313 +#: 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:322 +#: windows/views.py:351 msgid "SSH username" msgstr "Nome utente SSH" -#: windows/views.py:335 +#: windows/views.py:364 msgid "SSH password" msgstr "Password SSH" -#: windows/views.py:348 +#: windows/views.py:377 msgid "Local port" msgstr "Porta locale" -#: windows/views.py:354 +#: 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" +msgstr "se il valore è impostato a 0, verrà utilizzata la prima porta disponibile" -#: windows/views.py:363 +#: windows/views.py:392 msgid "Identity file" msgstr "File identità" -#: windows/views.py:379 +#: windows/views.py:408 msgid "Remote host + port" msgstr "Host remoto + porta" -#: windows/views.py:391 +#: windows/views.py:420 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "Host/porta remoto è il target reale del DB (predefinito a Host/Porta DB)." -#: windows/views.py:400 +#: windows/views.py:429 msgid "SSH extra args" msgstr "Argomenti extra SSH" -#: windows/views.py:415 +#: windows/views.py:444 msgid "SSH Tunnel" msgstr "Tunnel SSH" -#: windows/views.py:421 windows/views.py:1339 windows/views.py:3117 +#: windows/views.py:450 windows/views.py:1368 msgid "Created at" msgstr "Creato il" -#: windows/views.py:455 +#: windows/views.py:484 msgid "Successful connections" msgstr "Connessioni riuscite" -#: windows/views.py:472 +#: windows/views.py:501 msgid "Last successful connection" msgstr "Ultima connessione riuscita" -#: windows/views.py:489 +#: windows/views.py:518 msgid "Unsuccessful connections" msgstr "Connessioni non riuscite" -#: windows/views.py:506 +#: windows/views.py:535 msgid "Last failure reason" msgstr "Ultimo motivo di fallimento" -#: windows/views.py:523 +#: windows/views.py:552 msgid "Total connection attempts" msgstr "Totale connessioni" -#: windows/views.py:540 +#: windows/views.py:569 msgid "Average connection time (ms)" msgstr "Media in ms del tempo di connessione" -#: windows/views.py:557 +#: windows/views.py:586 msgid "Most recent connection duration" msgstr "Durata connessione più recente" -#: windows/views.py:576 +#: windows/views.py:605 msgid "Statistics" msgstr "Statistiche" -#: windows/views.py:594 windows/views.py:1821 +#: windows/views.py:623 windows/views.py:1850 msgid "Create" msgstr "Crea" -#: windows/views.py:598 +#: windows/views.py:627 msgid "Create connection" msgstr "Crea connessione" -#: windows/views.py:601 +#: windows/views.py:630 msgid "Create directory" msgstr "Crea directory" -#: windows/views.py:630 windows/views.py:854 windows/views.py:1491 -#: windows/views.py:1867 windows/views.py:2159 windows/views.py:2383 -#: windows/views.py:2439 windows/views.py:3179 windows/views.py:3686 -#: windows/views.py:3823 +#: 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:323 windows/views.py:635 windows/views.py:2164 -#: windows/views.py:2388 windows/views.py:2603 windows/views.py:3691 -#: windows/views.py:3829 +#: 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:642 +#: windows/views.py:671 msgid "Test" msgstr "Testa" -#: windows/views.py:649 +#: windows/views.py:678 msgid "Connect" msgstr "Connetti" -#: windows/views.py:752 +#: windows/views.py:781 windows/views.py:2388 msgid "Language" msgstr "Lingua" -#: windows/views.py:757 +#: windows/views.py:786 msgid "English" msgstr "Inglese" -#: windows/views.py:757 +#: windows/views.py:786 msgid "Italian" msgstr "Italiano" -#: windows/views.py:757 +#: windows/views.py:786 msgid "French" msgstr "Francese" -#: windows/views.py:769 +#: windows/views.py:798 msgid "Locale" msgstr "Localizzazione" -#: windows/views.py:790 +#: windows/views.py:819 msgid "Column content" msgstr "Contenuto colonna" -#: windows/views.py:800 +#: windows/views.py:829 msgid "Syntax" msgstr "Sintassi" -#: windows/views.py:857 +#: windows/views.py:886 msgid "Ok" msgstr "Ok" -#: windows/views.py:888 +#: windows/views.py:917 msgid "PeterSQL" msgstr "PeterSQL" -#: windows/views.py:897 +#: windows/views.py:926 msgid "File" msgstr "File" -#: windows/views.py:900 +#: windows/views.py:929 msgid "About" msgstr "Informazioni" -#: windows/views.py:903 +#: windows/views.py:932 msgid "Help" msgstr "Aiuto" -#: windows/views.py:908 +#: windows/views.py:937 msgid "Open connection manager" msgstr "Apri gestore connessioni" -#: windows/views.py:912 +#: windows/views.py:941 msgid "Disconnect from server" msgstr "Disconnetti dal server" -#: windows/views.py:914 +#: windows/views.py:943 msgid "tool" msgstr "strumento" -#: windows/views.py:914 windows/views.py:2419 +#: windows/views.py:943 windows/views.py:2628 msgid "Refresh" msgstr "Aggiorna" -#: windows/views.py:918 windows/views.py:920 windows/views.py:1845 -#: windows/views.py:2423 windows/views.py:2589 +#: 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:925 +#: windows/views.py:954 #, python-brace-format msgid "{mode}" msgstr "{mode}" -#: windows/views.py:962 windows/views.py:966 windows/views.py:2556 -#: windows/views.py:2561 +#: windows/views.py:991 windows/views.py:995 windows/views.py:2765 +#: windows/views.py:2770 msgid "MyMenuItem" msgstr "IlMioElementoMenu" -#: windows/views.py:969 windows/views.py:1895 windows/views.py:2568 -#: windows/views.py:3714 +#: windows/views.py:998 windows/views.py:1924 windows/views.py:2777 msgid "MyMenu" msgstr "IlMioMenu" -#: windows/views.py:984 windows/views.py:1519 windows/views.py:1526 -#: windows/views.py:1533 +#: windows/views.py:1013 windows/views.py:1548 windows/views.py:1555 +#: windows/views.py:1562 msgid "MyLabel" msgstr "LaMiaEtichetta" -#: windows/views.py:990 +#: windows/views.py:1019 msgid "Databases" msgstr "Database" -#: windows/views.py:991 windows/views.py:1338 windows/views.py:3118 +#: windows/views.py:1020 windows/views.py:1367 msgid "Size" msgstr "Dimensione" -#: windows/views.py:992 +#: windows/views.py:1021 msgid "Elements" msgstr "Elementi" -#: windows/views.py:993 +#: windows/views.py:1022 msgid "Modified at" msgstr "Modificato il" -#: windows/views.py:994 windows/views.py:1353 +#: windows/views.py:1023 windows/views.py:1382 msgid "Tables" msgstr "Tabelle" -#: windows/views.py:1001 +#: 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:1047 -#: windows/views.py:1342 windows/views.py:3114 +#: windows/components/dataview.py:89 windows/views.py:1076 +#: windows/views.py:1371 msgid "Collation" msgstr "Ordinamento" -#: windows/views.py:1076 +#: windows/views.py:1105 msgid "Encryption" msgstr "Crittografia" -#: windows/main/controller.py:1259 windows/main/controller.py:1281 -#: windows/main/controller.py:1285 windows/views.py:1088 +#: windows/main/controller.py:1272 windows/main/controller.py:1294 +#: windows/main/controller.py:1298 windows/views.py:1117 msgid "Read Only" msgstr "Sola lettura" -#: windows/views.py:1105 +#: windows/views.py:1134 msgid "Tablespace" msgstr "Tablespace" -#: windows/views.py:1126 +#: windows/views.py:1155 msgid "Connection limit" msgstr "Limite connessioni" -#: windows/views.py:1169 +#: windows/views.py:1198 msgid "Profile" msgstr "Profilo" -#: windows/views.py:1195 +#: windows/views.py:1224 msgid "Default tablespace" msgstr "Tablespace predefinito" -#: windows/views.py:1216 +#: windows/views.py:1245 msgid "Temporary tablespace" msgstr "Tablespace temporaneo" -#: windows/views.py:1242 +#: windows/views.py:1271 msgid "Quota" msgstr "Quota" -#: windows/views.py:1261 +#: windows/views.py:1290 msgid "Unlimited quota" msgstr "Quota illimitata" -#: windows/views.py:1278 +#: windows/views.py:1307 msgid "Account status" msgstr "Stato account" -#: windows/views.py:1299 +#: windows/views.py:1328 msgid "Password expire" msgstr "Scadenza password" -#: windows/views.py:1323 +#: windows/views.py:1352 msgid "Add new table" msgstr "Aggiungi nuova tabella" -#: windows/views.py:1325 +#: windows/views.py:1354 msgid "Clone table" msgstr "Clona tabella" -#: windows/main/controller.py:1770 windows/views.py:1327 +#: windows/main/controller.py:1821 windows/views.py:1356 msgid "Delete table" msgstr "Elimina tabella" -#: windows/views.py:1337 +#: windows/views.py:1366 windows/views.py:2457 msgid "Rows" msgstr "Righe" -#: windows/views.py:1340 windows/views.py:3116 +#: windows/views.py:1369 msgid "Updated at" msgstr "Aggiornato il" -#: windows/views.py:1358 +#: windows/views.py:1387 msgid "Add new view" msgstr "Aggiungi nuova vista" -#: windows/views.py:1360 windows/views.py:1384 +#: windows/views.py:1389 msgid "Clone view" msgstr "Clona" -#: windows/views.py:1362 +#: windows/views.py:1391 msgid "Delete view" msgstr "Elimina" -#: windows/views.py:1370 windows/views.py:1394 windows/views.py:1418 -#: windows/views.py:1442 windows/views.py:1466 +#: 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:1377 +#: windows/views.py:1406 msgid "Views" msgstr "Viste" -#: windows/views.py:1382 +#: windows/views.py:1411 msgid "Add new procedure" msgstr "Aggiungi nuova procedura" -#: windows/views.py:1384 +#: windows/views.py:1413 msgid "Clone procedure" msgstr "Clona procedura" -#: windows/views.py:1386 +#: windows/views.py:1415 msgid "Delete procedure" msgstr "Elimina procedura" -#: windows/views.py:1401 +#: windows/views.py:1430 msgid "Procedures" msgstr "Procedure" -#: windows/views.py:1406 +#: windows/views.py:1435 msgid "Add new function" msgstr "Aggiungi nuova funzione" -#: windows/views.py:1408 +#: windows/views.py:1437 msgid "Clone function" msgstr "Clona funzione" -#: windows/views.py:1410 +#: windows/views.py:1439 msgid "Delete function" msgstr "Elimina funzione" -#: windows/views.py:1425 +#: windows/views.py:1454 msgid "Functions" msgstr "Funzioni" -#: windows/views.py:1430 +#: windows/views.py:1459 msgid "Add new trigger" msgstr "Aggiungi nuovo trigger" -#: windows/views.py:1432 +#: windows/views.py:1461 msgid "Clone trigger" msgstr "Clona trigger" -#: windows/views.py:1434 +#: windows/views.py:1463 msgid "Delete trigger" msgstr "Elimina trigger" -#: windows/views.py:1449 +#: windows/views.py:1478 msgid "Triggers" msgstr "Trigger" -#: windows/views.py:1454 +#: windows/views.py:1483 msgid "Add new event" msgstr "Aggiungi nuovo evento" -#: windows/views.py:1456 +#: windows/views.py:1485 msgid "Clone event" msgstr "Clona" -#: windows/views.py:1458 +#: windows/views.py:1487 msgid "Delete event" msgstr "Elimina evento" -#: windows/views.py:1473 +#: windows/views.py:1502 msgid "Events" msgstr "Eventi" -#: windows/views.py:1501 windows/views.py:1872 windows/views.py:2437 -#: windows/views.py:2521 windows/views.py:3186 +#: windows/views.py:1530 windows/views.py:1901 windows/views.py:2646 +#: windows/views.py:2730 msgid "Apply" msgstr "Applica" -#: windows/views.py:1513 windows/views.py:1692 windows/views.py:3603 +#: windows/views.py:1542 windows/views.py:1721 msgid "Options" msgstr "Opzioni" -#: windows/views.py:1544 +#: windows/views.py:1573 msgid "Diagram" msgstr "Diagramma" -#: windows/views.py:1555 +#: windows/views.py:1584 msgid "Database" msgstr "Database" -#: windows/views.py:1610 windows/views.py:3543 +#: windows/views.py:1639 msgid "Base" msgstr "Base" -#: windows/views.py:1624 windows/views.py:3557 +#: windows/views.py:1653 msgid "Auto Increment" msgstr "Auto incremento" -#: windows/views.py:1652 windows/views.py:3585 +#: windows/views.py:1681 msgid "Default Collation" msgstr "Ordinamento predefinito" -#: windows/views.py:1662 +#: windows/views.py:1691 msgid "Convert data" msgstr "Converti dati" -#: windows/views.py:1670 +#: windows/views.py:1699 msgid "Row format" msgstr "Formato riga" -#: windows/views.py:1703 windows/views.py:1731 windows/views.py:1759 -#: windows/views.py:1847 windows/views.py:2259 windows/views.py:2427 -#: windows/views.py:3313 windows/views.py:3330 windows/views.py:3354 -#: windows/views.py:3381 +#: 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:1705 windows/views.py:1733 windows/views.py:1761 -#: windows/views.py:2261 windows/views.py:2529 windows/views.py:3320 -#: windows/views.py:3337 windows/views.py:3361 windows/views.py:3388 +#: 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:1718 windows/views.py:3617 +#: windows/views.py:1747 msgid "Indexes" msgstr "Indici" -#: windows/views.py:1729 windows/views.py:1757 windows/views.py:2257 -#: windows/views.py:3198 windows/views.py:3240 windows/views.py:3308 -#: windows/views.py:3349 windows/views.py:3376 windows/views.py:3646 +#: windows/views.py:1758 windows/views.py:1786 windows/views.py:2324 msgid "Insert" msgstr "Inserisci" -#: windows/views.py:1746 +#: windows/views.py:1775 msgid "Foreign Keys" msgstr "Chiavi esterne" -#: windows/views.py:1774 +#: windows/views.py:1803 msgid "Checks" msgstr "Vincoli" -#: windows/views.py:1841 windows/views.py:3638 +#: windows/views.py:1870 msgid "Columns:" msgstr "Colonne:" -#: windows/views.py:1851 +#: windows/views.py:1880 msgid "Move Up" msgstr "Sposta su\tCTRL+UP" -#: windows/views.py:1853 +#: windows/views.py:1882 msgid "Move Down" msgstr "Sposta giù\tCTRL+D" -#: windows/views.py:1885 windows/views.py:1892 windows/views.py:3704 -#: windows/views.py:3711 +#: windows/views.py:1914 windows/views.py:1921 msgid "Add Index" msgstr "Aggiungi indice" -#: windows/views.py:1889 windows/views.py:3708 +#: windows/views.py:1918 msgid "Add PrimaryKey" msgstr "Aggiungi chiave primaria" -#: windows/views.py:1906 +#: windows/views.py:1935 msgid "Table" msgstr "Tabella" -#: windows/views.py:1948 windows/views.py:2219 +#: windows/views.py:1977 windows/views.py:2243 msgid "Schema" msgstr "Schema" -#: windows/views.py:1976 windows/views.py:2247 +#: windows/views.py:2005 windows/views.py:2314 msgid "General" msgstr "Generale" -#: windows/views.py:1981 +#: windows/views.py:2010 msgid "Algorithm" msgstr "Algoritmo" -#: windows/views.py:1983 +#: windows/views.py:2012 msgid "UNDEFINED" msgstr "Senza segno" -#: windows/views.py:1986 +#: windows/views.py:2015 msgid "MERGE" msgstr "MERGE" -#: windows/views.py:1989 +#: windows/views.py:2018 msgid "TEMPTABLE" msgstr "TEMPTABLE" -#: windows/views.py:1999 +#: windows/views.py:2028 msgid "View constraint" msgstr "Vincolo vista" -#: windows/views.py:2001 +#: windows/views.py:2030 msgid "None" msgstr "Nessuno" -#: windows/views.py:2004 +#: windows/views.py:2033 msgid "LOCAL" msgstr "LOCALE" -#: windows/views.py:2007 +#: windows/views.py:2036 msgid "CASCADE" msgstr "A CASCATA" -#: windows/views.py:2010 +#: windows/views.py:2039 msgid "CHECK ONLY" msgstr "SOLO VERIFICA" -#: windows/views.py:2013 +#: windows/views.py:2042 msgid "READ ONLY" msgstr "SOLA LETTURA" -#: windows/views.py:2026 +#: windows/views.py:2055 windows/views.py:2483 msgid "Behavior" msgstr "Comportamento" -#: windows/views.py:2033 windows/views.py:2281 +#: windows/views.py:2062 windows/views.py:2490 msgid "Definer" msgstr "Definitore" -#: windows/views.py:2041 windows/views.py:2289 +#: windows/views.py:2070 windows/views.py:2498 msgid "*" msgstr "*" -#: windows/views.py:2053 windows/views.py:2301 +#: windows/views.py:2082 windows/views.py:2510 msgid "SQL security" msgstr "Sicurezza SQL" -#: windows/views.py:2060 windows/views.py:2308 +#: windows/views.py:2089 windows/views.py:2517 msgid "DEFINER" msgstr "DEFINITORE" -#: windows/views.py:2060 windows/views.py:2308 +#: windows/views.py:2089 windows/views.py:2517 msgid "INVOKER" msgstr "INVOCATORE" -#: windows/views.py:2074 +#: windows/views.py:2103 msgid "Force" msgstr "Forza" -#: windows/views.py:2086 +#: windows/views.py:2115 msgid "Security barrier" msgstr "Barriera di sicurezza" -#: windows/views.py:2099 windows/views.py:2323 +#: windows/views.py:2128 windows/views.py:2532 msgid "Security" msgstr "Sicurezza" -#: windows/views.py:2176 +#: windows/views.py:2205 msgid "View" msgstr "Viste" -#: windows/views.py:2274 +#: windows/views.py:2262 +#, fuzzy +msgid "Type" +msgstr "Tipo di dati" + +#: 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 +#, fuzzy +msgid "Return type" +msgstr "Tipo di dati" + +#: windows/views.py:2299 +#, fuzzy +msgid "Comment" +msgstr "Commenti" + +#: windows/components/dataview.py:111 windows/views.py:2335 +msgid "#" +msgstr "#" + +#: windows/views.py:2337 +#, fuzzy +msgid "Datatype" +msgstr "Tipo di dati" + +#: windows/views.py:2338 +#, fuzzy +msgid "Context" +msgstr "Connetti" + +#: windows/views.py:2345 msgid "Parameters" msgstr "Parametri" -#: windows/views.py:2400 -msgid "Procedure" -msgstr "Procedure" +#: windows/views.py:2354 +#, fuzzy +msgid "Data access" +msgstr "Database" + +#: 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 +#, fuzzy +msgid "MODIFIES SQL DATA" +msgstr "Modificato il" + +#: windows/views.py:2371 +#, fuzzy +msgid "Deterministic" +msgstr "Definizione" + +#: windows/views.py:2395 +#, fuzzy +msgid "SQL" +msgstr "*.sql" + +#: windows/views.py:2395 +msgid "PLPGSQL" +msgstr "" + +#: windows/views.py:2405 +#, fuzzy +msgid "Volatility" +msgstr "Virtualità" + +#: windows/views.py:2412 +msgid "VOLATILE" +msgstr "" + +#: windows/views.py:2412 +#, fuzzy +msgid "STABLE" +msgstr "Tabella" + +#: windows/views.py:2412 +#, fuzzy +msgid "IMMUTABLE" +msgstr "Tabella" + +#: windows/views.py:2422 +msgid "Parallel" +msgstr "" + +#: windows/views.py:2429 +#, fuzzy +msgid "UNSAFE" +msgstr "Salva" -#: windows/views.py:2408 +#: windows/views.py:2429 +msgid "RESTRICTED" +msgstr "" + +#: windows/views.py:2429 +#, fuzzy +msgid "SAFE" +msgstr "Salva" + +#: windows/views.py:2441 +#, fuzzy +msgid "Cost" +msgstr "Chiudi" + +#: windows/views.py:2609 +#, fuzzy +msgid "Routine" +msgstr "Tempo di attività" + +#: windows/views.py:2617 msgid "Trigger" msgstr "Trigger" -#: windows/views.py:2425 +#: windows/views.py:2634 msgid "Duplicate" msgstr "Duplica" -#: windows/views.py:2431 +#: windows/views.py:2640 msgid "Apply changes automatically" msgstr "Applica modifiche automaticamente" -#: windows/views.py:2433 windows/views.py:2434 +#: windows/views.py:2642 windows/views.py:2643 msgid "" -"If enabled, table edits are applied immediately without pressing Apply or " -"Cancel" +"If enabled, table edits are applied immediately without pressing Apply or" +" Cancel" msgstr "" "Se abilitato, le modifiche alla tabella vengono applicate immediatamente " "senza premere Applica o Annulla" -#: windows/views.py:2447 +#: windows/views.py:2656 #, python-brace-format -msgid "" -"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "" -"{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -#: windows/views.py:2455 +#: windows/views.py:2664 msgid "First" msgstr "Primo" -#: windows/views.py:2473 +#: windows/views.py:2682 msgid "Last" msgstr "Ultimo" -#: windows/views.py:2482 +#: windows/views.py:2691 msgid "Filters" msgstr "Filtri" -#: windows/views.py:2524 +#: windows/views.py:2733 msgid "" "Apply filters in data\n" "CTRL+ENTER" @@ -826,114 +933,62 @@ msgstr "" "Applica filtri ai dati\n" "CTRL+ENTER" -#: windows/views.py:2525 +#: windows/views.py:2734 msgid "CTRL+ENTER" msgstr "CTRL+ENTER" -#: windows/views.py:2553 +#: windows/views.py:2762 msgid "Insert row" msgstr "Inserisci riga" -#: windows/components/popup.py:31 windows/views.py:2565 +#: windows/components/popup.py:31 windows/views.py:2774 msgid "NULL" msgstr "NULL" -#: windows/views.py:2572 +#: windows/views.py:2781 msgid "Data" msgstr "Dati" -#: windows/main/controller.py:318 windows/views.py:2589 +#: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" msgstr "Query" -#: windows/views.py:2591 windows/views.py:3137 +#: windows/views.py:2800 msgid "Close" msgstr "Chiudi" -#: windows/main/controller.py:319 windows/views.py:2591 +#: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" msgstr "Query" -#: windows/views.py:2595 +#: windows/views.py:2804 msgid "Run" msgstr "Esegui" -#: windows/main/controller.py:320 windows/views.py:2595 +#: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" msgstr "Esegui" -#: windows/views.py:2597 +#: windows/views.py:2806 msgid "Run all" msgstr "Esegui tutto" -#: windows/views.py:2597 +#: windows/views.py:2806 msgid "Execute all statements" msgstr "Esegui tutte le istruzioni" -#: windows/main/controller.py:322 windows/views.py:2599 +#: windows/main/controller.py:323 windows/views.py:2808 msgid "Stop" msgstr "Ferma" -#: windows/views.py:2664 +#: windows/views.py:2873 msgid "a page" msgstr "una pagina" -#: windows/views.py:2714 +#: windows/views.py:2923 msgid "Query" msgstr "Query" -#: windows/views.py:3091 -msgid "Character set" -msgstr "Set di caratteri" - -#: windows/views.py:3121 windows/views.py:3140 -msgid "New" -msgstr "Nuovo" - -#: windows/views.py:3160 -msgid "Insert record" -msgstr "Inserisci record" - -#: windows/views.py:3165 -msgid "Duplicate record" -msgstr "Duplica record" - -#: windows/views.py:3172 -msgid "Delete record" -msgstr "Elimina record" - -#: windows/views.py:3210 windows/views.py:3658 -msgid "Up" -msgstr "Su" - -#: windows/views.py:3217 windows/views.py:3665 -msgid "Down" -msgstr "Giù" - -#: windows/views.py:3232 -msgid "Table:" -msgstr "Tabella:" - -#: windows/views.py:3245 -msgid "Clone" -msgstr "Clona" - -#: windows/views.py:3270 -msgid "MyButton" -msgstr "IlMioPulsante" - -#: windows/views.py:3794 -msgid "Save Starments" -msgstr "Salva istruzioni" - -#: windows/views.py:3802 -msgid "Location" -msgstr "Posizione" - -#: windows/views.py:3809 -msgid "*.sql" -msgstr "*.sql" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 msgid "Allow NULL" @@ -966,10 +1021,6 @@ msgstr "Senza segno" msgid "Zerofill" msgstr "Riempimento zero" -#: windows/components/dataview.py:111 -msgid "#" -msgstr "#" - #: windows/components/dataview.py:119 msgid "Data type" msgstr "Tipo di dati" @@ -1077,9 +1128,11 @@ msgstr "Modifiche non salvate" #: windows/dialogs/connections/view.py:773 msgid "" -"This connection cannot work without TLS. TLS has been enabled automatically." +"This connection cannot work without TLS. TLS has been enabled " +"automatically." msgstr "" -"Questa connessione non può funzionare senza TLS. TLS è stato abilitato automaticamente." +"Questa connessione non può funzionare senza TLS. TLS è stato abilitato " +"automaticamente." #: windows/dialogs/connections/view.py:798 #, python-brace-format @@ -1109,114 +1162,114 @@ msgstr "Conferma eliminazione" msgid "Do you want to delete the directory '{directory_name}'?" msgstr "Vuoi eliminare la directory '{directory_name}'?" -#: windows/main/controller.py:315 +#: windows/main/controller.py:316 #, python-brace-format msgid "{text} ({shortcut})" msgstr "{text} ({shortcut})" -#: windows/main/controller.py:321 +#: windows/main/controller.py:322 msgid "Execute all" msgstr "Esegui tutto" -#: windows/main/controller.py:440 windows/main/controller.py:448 +#: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" msgstr "Query" -#: windows/main/controller.py:467 +#: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" msgstr "Query ({query_number})" -#: windows/main/controller.py:516 +#: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" msgstr "Hai modifiche non salvate. Salvare prima di chiudere?" -#: windows/main/controller.py:517 +#: windows/main/controller.py:519 msgid "Unsaved query" msgstr "Query non salvata" -#: windows/main/controller.py:562 +#: windows/main/controller.py:564 msgid "Save query" msgstr "Salva query" -#: windows/main/controller.py:565 +#: windows/main/controller.py:567 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" msgstr "File SQL (*.sql)|*.sql|Tutti i file (*.*)|*.*" -#: windows/main/controller.py:593 windows/main/controller.py:624 -#: windows/main/controller.py:651 windows/main/database/list.py:119 -#: windows/main/database/procedure.py:206 -#: windows/main/database/procedure.py:232 windows/main/database/view.py:268 -#: windows/main/database/view.py:297 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:631 +#: windows/main/controller.py:633 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "-- Query salvata in {file_path}" -#: windows/main/controller.py:657 +#: windows/main/controller.py:659 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "-- Query salvata automaticamente in {file_path}" -#: windows/main/controller.py:722 +#: windows/main/controller.py:725 msgid "days" msgstr "giorni" -#: windows/main/controller.py:723 +#: windows/main/controller.py:726 msgid "hours" msgstr "ore" -#: windows/main/controller.py:724 +#: windows/main/controller.py:727 msgid "minutes" msgstr "minuti" -#: windows/main/controller.py:725 +#: windows/main/controller.py:728 msgid "seconds" msgstr "secondi" -#: windows/main/controller.py:733 +#: windows/main/controller.py:736 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "Memoria utilizzata: {used} ({percentage:.2%})" -#: windows/main/controller.py:769 +#: windows/main/controller.py:772 msgid "Settings saved successfully" msgstr "Impostazioni salvate con successo" -#: windows/main/controller.py:993 +#: windows/main/controller.py:1006 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "~{estimated} (Caricamento...)" -#: windows/main/controller.py:995 +#: windows/main/controller.py:1008 msgid "~ (Loading...)" msgstr "~ (Caricamento...)" -#: windows/main/controller.py:1230 +#: windows/main/controller.py:1243 msgid "Write Mode (2:00)" msgstr "Modalità scrittura (2:00)" -#: windows/main/controller.py:1281 +#: windows/main/controller.py:1294 msgid "Write Mode" msgstr "Modalità scrittura" -#: windows/main/controller.py:1292 +#: windows/main/controller.py:1305 msgid "Version" msgstr "Versione" -#: windows/main/controller.py:1294 +#: windows/main/controller.py:1307 msgid "Uptime" msgstr "Tempo di attività" -#: windows/main/controller.py:1429 +#: windows/main/controller.py:1444 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "Vuoi annullare le modifiche a {database_name}?" -#: windows/main/controller.py:1462 +#: windows/main/controller.py:1477 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1231,51 +1284,80 @@ msgstr "" "- Sì: apri flusso dump (prossimamente, nessuna eliminazione).\n" "- No: elimina il database ora." -#: windows/main/controller.py:1467 windows/main/controller.py:1488 +#: windows/main/controller.py:1482 windows/main/controller.py:1503 msgid "Delete database" msgstr "Elimina database" -#: windows/main/controller.py:1473 +#: windows/main/controller.py:1488 msgid "Dump is not implemented yet. No action has been performed." msgstr "Il dump non è ancora implementato. Nessuna azione è stata eseguita." -#: windows/main/controller.py:1474 +#: windows/main/controller.py:1489 msgid "Dump not available" msgstr "Dump non disponibile" -#: windows/main/controller.py:1487 +#: windows/main/controller.py:1502 msgid "Database deletion is not supported by this engine." msgstr "L'eliminazione del database non è supportata da questo motore." -#: windows/main/controller.py:1502 +#: windows/main/controller.py:1517 msgid "Database deleted successfully" msgstr "Database eliminato con successo" -#: windows/main/controller.py:1503 windows/main/database/procedure.py:195 -#: windows/main/database/procedure.py:226 windows/main/database/view.py:256 +#: 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 "Successo" -#: windows/main/controller.py:1741 +#: windows/main/controller.py:1792 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "Vuoi annullare le modifiche a {table_name}?" -#: windows/main/controller.py:1767 +#: windows/main/controller.py:1818 #, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "Vuoi eliminare la tabella {table_name}?" -#: windows/main/controller.py:1789 +#: windows/main/controller.py:1840 #, python-brace-format msgid "{table_name} (COPY)" msgstr "{table_name} (COPY)" -#: windows/main/controller.py:1948 +#: windows/main/controller.py:1999 msgid "Do you want delete the records?" msgstr "Vuoi eliminare i record?" +#: 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." @@ -1284,44 +1366,60 @@ msgstr "La connessione al database è stata persa." msgid "Do you want to reconnect?" msgstr "Vuoi riconnetterti?" -#: windows/main/database/list.py:108 -msgid "Connection lost" -msgstr "Connessione persa" - #: windows/main/database/list.py:118 msgid "Reconnection failed:" msgstr "Riconnessione fallita:" -#: windows/main/database/procedure.py:194 +#: windows/main/database/routine.py:545 +#, fuzzy +msgid "Function created successfully" +msgstr "Vista creata con successo" + +#: windows/main/database/routine.py:547 +#, fuzzy +msgid "Function updated successfully" +msgstr "Vista aggiornata con successo" + +#: windows/main/database/routine.py:551 msgid "Procedure created successfully" msgstr "Procedura creata con successo" -#: windows/main/database/procedure.py:194 +#: windows/main/database/routine.py:553 msgid "Procedure updated successfully" msgstr "Procedura aggiornata con successo" -#: windows/main/database/procedure.py:206 -#, python-brace-format -msgid "Error saving procedure: {}" -msgstr "Errore nel salvataggio della procedura: {}" +#: windows/main/database/routine.py:590 +#, fuzzy, python-brace-format +msgid "Error saving routine: {}" +msgstr "Errore nel salvataggio della vista: {}" -#: windows/main/database/procedure.py:217 -#, python-brace-format -msgid "Are you sure you want to delete procedure '{}'?" -msgstr "Sei sicuro di voler eliminare la procedura '{}'?" +#: windows/main/database/routine.py:604 +#, fuzzy +msgid "Function" +msgstr "Funzioni" + +#: windows/main/database/routine.py:604 +msgid "Procedure" +msgstr "Procedure" -#: windows/main/database/procedure.py:218 windows/main/database/view.py:282 +#: windows/main/database/routine.py:607 +#, fuzzy, python-brace-format +msgid "Are you sure you want to delete {} '{}'?" +msgstr "Sei sicuro di voler eliminare la vista '{}'?" + +#: windows/main/database/routine.py:610 windows/main/database/view.py:282 msgid "Confirm Delete" msgstr "Conferma eliminazione" -#: windows/main/database/procedure.py:226 -msgid "Procedure deleted successfully" -msgstr "Procedura eliminata con successo" +#: windows/main/database/routine.py:619 +#, fuzzy, python-brace-format +msgid "{} deleted successfully" +msgstr "Vista eliminata con successo" -#: windows/main/database/procedure.py:232 -#, python-brace-format -msgid "Error deleting procedure: {}" -msgstr "Errore nell'eliminazione della procedura: {}" +#: windows/main/database/routine.py:634 +#, fuzzy, python-brace-format +msgid "Error deleting routine: {}" +msgstr "Errore nell'eliminazione della vista: {}" #: windows/main/database/view.py:255 msgid "View created successfully" @@ -1350,21 +1448,21 @@ msgstr "Vista eliminata con successo" msgid "Error deleting view: {}" 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 "{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 "{elapsed_s:.2f} s" -#: windows/main/query/controller.py:115 +#: windows/main/query/controller.py:117 msgid "none" msgstr "nessuno" -#: windows/main/query/controller.py:121 +#: windows/main/query/controller.py:123 #, python-brace-format msgid "" "Query execution stopped after {elapsed}.\n" @@ -1379,14 +1477,19 @@ msgstr "" "Fallite: {failed}.\n" "Ultima istruzione: #{last}." -#: windows/main/query/controller.py:134 +#: windows/main/query/controller.py:136 msgid "Query execution cancelled" msgstr "Esecuzione query annullata" -#: windows/main/query/controller.py:176 +#: windows/main/query/controller.py:178 msgid "No active database connection" 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)" @@ -1434,133 +1537,3 @@ msgstr "Errore" msgid "Error saving records" msgstr "Errore nel salvataggio dei record" -#~ msgid "Created at:" -#~ msgstr "Created at:" - -#~ msgid "Last connection:" -#~ msgstr "Last connection:" - -#~ msgid "Successful connections:" -#~ msgstr "Successful connections:" - -#~ msgid "Unsuccessful connections:" -#~ msgstr "Unsuccessful connections:" - -#~ msgid "Session name" -#~ msgstr "Nome sessione" - -#~ msgid "Open" -#~ msgstr "Open" - -#~ msgid "Open session manager" -#~ msgstr "Open session manager" - -#~ msgid "Foreign Key" -#~ msgstr "Foreign Key" - -#~ 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 "{} rows affected" - -#~ msgid "Query {}" -#~ msgstr "Query" - -#~ msgid "Query {} (Error)" -#~ msgstr "Query {} (Error)" - -#~ msgid "Query {} ({} rows × {} cols)" -#~ msgstr "Query {} ({} rows × {} cols)" - -#~ msgid "{} rows" -#~ msgstr "Righe" - -#~ msgid "{:.1f} ms" -#~ msgstr "{:.1f} ms" - -#~ msgid "{} warnings" -#~ msgstr "{} warnings" - -#~ msgid "Edit Value" -#~ msgstr "Modifica valore" - -#~ msgid "Query #2" -#~ msgstr "Query #2" - -#~ msgid "Column5" -#~ msgstr "Colonna5" - -#~ msgid "Import" -#~ msgstr "Importa" - -#~ msgid "Read only" -#~ msgstr "Read only" - -#~ msgid "CASCADED" -#~ msgstr "CASCADED" - -#~ 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 "RadioBtn" - -#~ msgid "Edit Column" -#~ msgstr "Modifica colonna" - -#~ msgid "Datatype" -#~ msgstr "Tipo di dati" - -#~ msgid "Zero Fill" -#~ msgstr "Riempimento zero" - -#~ msgid "Refrsh" -#~ msgstr "Aggiorna" diff --git a/locale/petersql.pot b/locale/petersql.pot index 0042d82..16a6e5b 100644 --- a/locale/petersql.pot +++ b/locale/petersql.pot @@ -27,887 +27,927 @@ msgstr "" msgid "OpenSSH client not found." msgstr "" -#: structures/engines/context.py:544 +#: structures/engines/context.py:567 msgid "This connection is read-only." msgstr "" -#: structures/engines/mariadb/context.py:632 -#: structures/engines/mysql/context.py:643 +#: 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:660 -#: structures/engines/mysql/context.py:671 +#: 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:678 -#: structures/engines/mysql/context.py:689 +#: 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:718 -#: structures/engines/mysql/context.py:727 +#: 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:751 -#: structures/engines/mysql/context.py:758 +#: 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:804 +#: structures/engines/mariadb/context.py:830 #, python-brace-format msgid "Trigger{trigger_index:03}" msgstr "" #: windows/dialogs/connections/view.py:417 -#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1290 -#: windows/views.py:33 +#: windows/dialogs/connections/view.py:775 windows/main/controller.py:1303 +#: windows/views.py:62 msgid "Connection" msgstr "" #: windows/components/dataview.py:115 windows/components/dataview.py:240 #: windows/components/dataview.py:253 windows/components/dataview.py:268 -#: windows/views.py:47 windows/views.py:97 windows/views.py:1024 -#: windows/views.py:1336 windows/views.py:1369 windows/views.py:1393 -#: windows/views.py:1417 windows/views.py:1441 windows/views.py:1465 -#: windows/views.py:1582 windows/views.py:1928 windows/views.py:2199 -#: windows/views.py:3293 windows/views.py:3515 +#: 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:438 +#: windows/views.py:77 windows/views.py:467 msgid "Last connection" msgstr "" -#: windows/dialogs/connections/view.py:669 windows/views.py:61 +#: windows/dialogs/connections/view.py:669 windows/views.py:90 msgid "New directory" msgstr "" #: windows/dialogs/connections/model.py:212 -#: windows/dialogs/connections/view.py:615 windows/views.py:65 +#: 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:613 windows/views.py:1496 -#: windows/views.py:1864 windows/views.py:2154 windows/views.py:2378 -#: windows/views.py:3203 windows/views.py:3252 windows/views.py:3651 -#: windows/views.py:3683 +#: 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:1341 windows/views.py:1637 -#: windows/views.py:3115 windows/views.py:3570 +#: 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:1150 +#: 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:214 +#: windows/views.py:243 msgid "Use SSH tunnel" msgstr "" -#: windows/views.py:225 +#: windows/views.py:254 msgid "Compressed client/server protocol" msgstr "" -#: windows/views.py:244 +#: windows/views.py:273 msgid "Filename" msgstr "" -#: windows/views.py:249 windows/views.py:368 windows/views.py:3809 +#: windows/views.py:278 windows/views.py:397 msgid "Select a file" msgstr "" -#: windows/views.py:249 windows/views.py:368 +#: windows/views.py:278 windows/views.py:397 msgid "*.*" msgstr "" #: windows/components/dataview.py:70 windows/components/dataview.py:92 -#: windows/views.py:266 windows/views.py:1343 windows/views.py:1595 -#: windows/views.py:3113 windows/views.py:3528 +#: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 msgid "Comments" msgstr "" -#: windows/main/controller.py:769 windows/views.py:280 windows/views.py:740 -#: windows/views.py:894 +#: windows/main/controller.py:772 windows/views.py:309 windows/views.py:769 +#: windows/views.py:923 msgid "Settings" msgstr "" -#: windows/views.py:288 +#: windows/views.py:317 msgid "SSH executable" msgstr "" -#: windows/views.py:293 +#: windows/views.py:322 msgid "ssh" msgstr "" -#: windows/views.py:301 +#: windows/views.py:330 msgid "SSH host + port" msgstr "" -#: windows/views.py:313 +#: windows/views.py:342 msgid "SSH host + port (the SSH server that forwards traffic to the DB)" msgstr "" -#: windows/views.py:322 +#: windows/views.py:351 msgid "SSH username" msgstr "" -#: windows/views.py:335 +#: windows/views.py:364 msgid "SSH password" msgstr "" -#: windows/views.py:348 +#: windows/views.py:377 msgid "Local port" msgstr "" -#: windows/views.py:354 +#: windows/views.py:383 msgid "if the value is set to 0, the first available port will be used" msgstr "" -#: windows/views.py:363 +#: windows/views.py:392 msgid "Identity file" msgstr "" -#: windows/views.py:379 +#: windows/views.py:408 msgid "Remote host + port" msgstr "" -#: windows/views.py:391 +#: windows/views.py:420 msgid "Remote host/port is the real DB target (defaults to DB Host/Port)." msgstr "" -#: windows/views.py:400 +#: windows/views.py:429 msgid "SSH extra args" msgstr "" -#: windows/views.py:415 +#: windows/views.py:444 msgid "SSH Tunnel" msgstr "" -#: windows/views.py:421 windows/views.py:1339 windows/views.py:3117 +#: windows/views.py:450 windows/views.py:1368 msgid "Created at" msgstr "" -#: windows/views.py:455 +#: windows/views.py:484 msgid "Successful connections" msgstr "" -#: windows/views.py:472 +#: windows/views.py:501 msgid "Last successful connection" msgstr "" -#: windows/views.py:489 +#: windows/views.py:518 msgid "Unsuccessful connections" msgstr "" -#: windows/views.py:506 +#: windows/views.py:535 msgid "Last failure reason" msgstr "" -#: windows/views.py:523 +#: windows/views.py:552 msgid "Total connection attempts" msgstr "" -#: windows/views.py:540 +#: windows/views.py:569 msgid "Average connection time (ms)" msgstr "" -#: windows/views.py:557 +#: windows/views.py:586 msgid "Most recent connection duration" msgstr "" -#: windows/views.py:576 +#: windows/views.py:605 msgid "Statistics" msgstr "" -#: windows/views.py:594 windows/views.py:1821 +#: windows/views.py:623 windows/views.py:1850 msgid "Create" msgstr "" -#: windows/views.py:598 +#: windows/views.py:627 msgid "Create connection" msgstr "" -#: windows/views.py:601 +#: windows/views.py:630 msgid "Create directory" msgstr "" -#: windows/views.py:630 windows/views.py:854 windows/views.py:1491 -#: windows/views.py:1867 windows/views.py:2159 windows/views.py:2383 -#: windows/views.py:2439 windows/views.py:3179 windows/views.py:3686 -#: windows/views.py:3823 +#: 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:323 windows/views.py:635 windows/views.py:2164 -#: windows/views.py:2388 windows/views.py:2603 windows/views.py:3691 -#: windows/views.py:3829 +#: 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:642 +#: windows/views.py:671 msgid "Test" msgstr "" -#: windows/views.py:649 +#: windows/views.py:678 msgid "Connect" msgstr "" -#: windows/views.py:752 +#: windows/views.py:781 windows/views.py:2388 msgid "Language" msgstr "" -#: windows/views.py:757 +#: windows/views.py:786 msgid "English" msgstr "" -#: windows/views.py:757 +#: windows/views.py:786 msgid "Italian" msgstr "" -#: windows/views.py:757 +#: windows/views.py:786 msgid "French" msgstr "" -#: windows/views.py:769 +#: windows/views.py:798 msgid "Locale" msgstr "" -#: windows/views.py:790 +#: windows/views.py:819 msgid "Column content" msgstr "" -#: windows/views.py:800 +#: windows/views.py:829 msgid "Syntax" msgstr "" -#: windows/views.py:857 +#: windows/views.py:886 msgid "Ok" msgstr "" -#: windows/views.py:888 +#: windows/views.py:917 msgid "PeterSQL" msgstr "" -#: windows/views.py:897 +#: windows/views.py:926 msgid "File" msgstr "" -#: windows/views.py:900 +#: windows/views.py:929 msgid "About" msgstr "" -#: windows/views.py:903 +#: windows/views.py:932 msgid "Help" msgstr "" -#: windows/views.py:908 +#: windows/views.py:937 msgid "Open connection manager" msgstr "" -#: windows/views.py:912 +#: windows/views.py:941 msgid "Disconnect from server" msgstr "" -#: windows/views.py:914 +#: windows/views.py:943 msgid "tool" msgstr "" -#: windows/views.py:914 windows/views.py:2419 +#: windows/views.py:943 windows/views.py:2628 msgid "Refresh" msgstr "" -#: windows/views.py:918 windows/views.py:920 windows/views.py:1845 -#: windows/views.py:2423 windows/views.py:2589 +#: 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:925 +#: windows/views.py:954 #, python-brace-format msgid "{mode}" msgstr "" -#: windows/views.py:962 windows/views.py:966 windows/views.py:2556 -#: windows/views.py:2561 +#: windows/views.py:991 windows/views.py:995 windows/views.py:2765 +#: windows/views.py:2770 msgid "MyMenuItem" msgstr "" -#: windows/views.py:969 windows/views.py:1895 windows/views.py:2568 -#: windows/views.py:3714 +#: windows/views.py:998 windows/views.py:1924 windows/views.py:2777 msgid "MyMenu" msgstr "" -#: windows/views.py:984 windows/views.py:1519 windows/views.py:1526 -#: windows/views.py:1533 +#: windows/views.py:1013 windows/views.py:1548 windows/views.py:1555 +#: windows/views.py:1562 msgid "MyLabel" msgstr "" -#: windows/views.py:990 +#: windows/views.py:1019 msgid "Databases" msgstr "" -#: windows/views.py:991 windows/views.py:1338 windows/views.py:3118 +#: windows/views.py:1020 windows/views.py:1367 msgid "Size" msgstr "" -#: windows/views.py:992 +#: windows/views.py:1021 msgid "Elements" msgstr "" -#: windows/views.py:993 +#: windows/views.py:1022 msgid "Modified at" msgstr "" -#: windows/views.py:994 windows/views.py:1353 +#: windows/views.py:1023 windows/views.py:1382 msgid "Tables" msgstr "" -#: windows/views.py:1001 +#: 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:1047 -#: windows/views.py:1342 windows/views.py:3114 +#: windows/components/dataview.py:89 windows/views.py:1076 +#: windows/views.py:1371 msgid "Collation" msgstr "" -#: windows/views.py:1076 +#: windows/views.py:1105 msgid "Encryption" msgstr "" -#: windows/main/controller.py:1259 windows/main/controller.py:1281 -#: windows/main/controller.py:1285 windows/views.py:1088 +#: 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:1105 +#: windows/views.py:1134 msgid "Tablespace" msgstr "" -#: windows/views.py:1126 +#: windows/views.py:1155 msgid "Connection limit" msgstr "" -#: windows/views.py:1169 +#: windows/views.py:1198 msgid "Profile" msgstr "" -#: windows/views.py:1195 +#: windows/views.py:1224 msgid "Default tablespace" msgstr "" -#: windows/views.py:1216 +#: windows/views.py:1245 msgid "Temporary tablespace" msgstr "" -#: windows/views.py:1242 +#: windows/views.py:1271 msgid "Quota" msgstr "" -#: windows/views.py:1261 +#: windows/views.py:1290 msgid "Unlimited quota" msgstr "" -#: windows/views.py:1278 +#: windows/views.py:1307 msgid "Account status" msgstr "" -#: windows/views.py:1299 +#: windows/views.py:1328 msgid "Password expire" msgstr "" -#: windows/views.py:1323 +#: windows/views.py:1352 msgid "Add new table" msgstr "" -#: windows/views.py:1325 +#: windows/views.py:1354 msgid "Clone table" msgstr "" -#: windows/main/controller.py:1770 windows/views.py:1327 +#: windows/main/controller.py:1821 windows/views.py:1356 msgid "Delete table" msgstr "" -#: windows/views.py:1337 +#: windows/views.py:1366 windows/views.py:2457 msgid "Rows" msgstr "" -#: windows/views.py:1340 windows/views.py:3116 +#: windows/views.py:1369 msgid "Updated at" msgstr "" -#: windows/views.py:1358 +#: windows/views.py:1387 msgid "Add new view" msgstr "" -#: windows/views.py:1360 windows/views.py:1384 +#: windows/views.py:1389 msgid "Clone view" msgstr "" -#: windows/views.py:1362 +#: windows/views.py:1391 msgid "Delete view" msgstr "" -#: windows/views.py:1370 windows/views.py:1394 windows/views.py:1418 -#: windows/views.py:1442 windows/views.py:1466 +#: 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:1377 +#: windows/views.py:1406 msgid "Views" msgstr "" -#: windows/views.py:1382 +#: windows/views.py:1411 msgid "Add new procedure" msgstr "" -#: windows/views.py:1384 +#: windows/views.py:1413 msgid "Clone procedure" msgstr "" -#: windows/views.py:1386 +#: windows/views.py:1415 msgid "Delete procedure" msgstr "" -#: windows/views.py:1401 +#: windows/views.py:1430 msgid "Procedures" msgstr "" -#: windows/views.py:1406 +#: windows/views.py:1435 msgid "Add new function" msgstr "" -#: windows/views.py:1408 +#: windows/views.py:1437 msgid "Clone function" msgstr "" -#: windows/views.py:1410 +#: windows/views.py:1439 msgid "Delete function" msgstr "" -#: windows/views.py:1425 +#: windows/views.py:1454 msgid "Functions" msgstr "" -#: windows/views.py:1430 +#: windows/views.py:1459 msgid "Add new trigger" msgstr "" -#: windows/views.py:1432 +#: windows/views.py:1461 msgid "Clone trigger" msgstr "" -#: windows/views.py:1434 +#: windows/views.py:1463 msgid "Delete trigger" msgstr "" -#: windows/views.py:1449 +#: windows/views.py:1478 msgid "Triggers" msgstr "" -#: windows/views.py:1454 +#: windows/views.py:1483 msgid "Add new event" msgstr "" -#: windows/views.py:1456 +#: windows/views.py:1485 msgid "Clone event" msgstr "" -#: windows/views.py:1458 +#: windows/views.py:1487 msgid "Delete event" msgstr "" -#: windows/views.py:1473 +#: windows/views.py:1502 msgid "Events" msgstr "" -#: windows/views.py:1501 windows/views.py:1872 windows/views.py:2437 -#: windows/views.py:2521 windows/views.py:3186 +#: windows/views.py:1530 windows/views.py:1901 windows/views.py:2646 +#: windows/views.py:2730 msgid "Apply" msgstr "" -#: windows/views.py:1513 windows/views.py:1692 windows/views.py:3603 +#: windows/views.py:1542 windows/views.py:1721 msgid "Options" msgstr "" -#: windows/views.py:1544 +#: windows/views.py:1573 msgid "Diagram" msgstr "" -#: windows/views.py:1555 +#: windows/views.py:1584 msgid "Database" msgstr "" -#: windows/views.py:1610 windows/views.py:3543 +#: windows/views.py:1639 msgid "Base" msgstr "" -#: windows/views.py:1624 windows/views.py:3557 +#: windows/views.py:1653 msgid "Auto Increment" msgstr "" -#: windows/views.py:1652 windows/views.py:3585 +#: windows/views.py:1681 msgid "Default Collation" msgstr "" -#: windows/views.py:1662 +#: windows/views.py:1691 msgid "Convert data" msgstr "" -#: windows/views.py:1670 +#: windows/views.py:1699 msgid "Row format" msgstr "" -#: windows/views.py:1703 windows/views.py:1731 windows/views.py:1759 -#: windows/views.py:1847 windows/views.py:2259 windows/views.py:2427 -#: windows/views.py:3313 windows/views.py:3330 windows/views.py:3354 -#: windows/views.py:3381 +#: 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:1705 windows/views.py:1733 windows/views.py:1761 -#: windows/views.py:2261 windows/views.py:2529 windows/views.py:3320 -#: windows/views.py:3337 windows/views.py:3361 windows/views.py:3388 +#: 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:1718 windows/views.py:3617 +#: windows/views.py:1747 msgid "Indexes" msgstr "" -#: windows/views.py:1729 windows/views.py:1757 windows/views.py:2257 -#: windows/views.py:3198 windows/views.py:3240 windows/views.py:3308 -#: windows/views.py:3349 windows/views.py:3376 windows/views.py:3646 +#: windows/views.py:1758 windows/views.py:1786 windows/views.py:2324 msgid "Insert" msgstr "" -#: windows/views.py:1746 +#: windows/views.py:1775 msgid "Foreign Keys" msgstr "" -#: windows/views.py:1774 +#: windows/views.py:1803 msgid "Checks" msgstr "" -#: windows/views.py:1841 windows/views.py:3638 +#: windows/views.py:1870 msgid "Columns:" msgstr "" -#: windows/views.py:1851 +#: windows/views.py:1880 msgid "Move Up" msgstr "" -#: windows/views.py:1853 +#: windows/views.py:1882 msgid "Move Down" msgstr "" -#: windows/views.py:1885 windows/views.py:1892 windows/views.py:3704 -#: windows/views.py:3711 +#: windows/views.py:1914 windows/views.py:1921 msgid "Add Index" msgstr "" -#: windows/views.py:1889 windows/views.py:3708 +#: windows/views.py:1918 msgid "Add PrimaryKey" msgstr "" -#: windows/views.py:1906 +#: windows/views.py:1935 msgid "Table" msgstr "" -#: windows/views.py:1948 windows/views.py:2219 +#: windows/views.py:1977 windows/views.py:2243 msgid "Schema" msgstr "" -#: windows/views.py:1976 windows/views.py:2247 +#: windows/views.py:2005 windows/views.py:2314 msgid "General" msgstr "" -#: windows/views.py:1981 +#: windows/views.py:2010 msgid "Algorithm" msgstr "" -#: windows/views.py:1983 +#: windows/views.py:2012 msgid "UNDEFINED" msgstr "" -#: windows/views.py:1986 +#: windows/views.py:2015 msgid "MERGE" msgstr "" -#: windows/views.py:1989 +#: windows/views.py:2018 msgid "TEMPTABLE" msgstr "" -#: windows/views.py:1999 +#: windows/views.py:2028 msgid "View constraint" msgstr "" -#: windows/views.py:2001 +#: windows/views.py:2030 msgid "None" msgstr "" -#: windows/views.py:2004 +#: windows/views.py:2033 msgid "LOCAL" msgstr "" -#: windows/views.py:2007 +#: windows/views.py:2036 msgid "CASCADE" msgstr "" -#: windows/views.py:2010 +#: windows/views.py:2039 msgid "CHECK ONLY" msgstr "" -#: windows/views.py:2013 +#: windows/views.py:2042 msgid "READ ONLY" msgstr "" -#: windows/views.py:2026 +#: windows/views.py:2055 windows/views.py:2483 msgid "Behavior" msgstr "" -#: windows/views.py:2033 windows/views.py:2281 +#: windows/views.py:2062 windows/views.py:2490 msgid "Definer" msgstr "" -#: windows/views.py:2041 windows/views.py:2289 +#: windows/views.py:2070 windows/views.py:2498 msgid "*" msgstr "" -#: windows/views.py:2053 windows/views.py:2301 +#: windows/views.py:2082 windows/views.py:2510 msgid "SQL security" msgstr "" -#: windows/views.py:2060 windows/views.py:2308 +#: windows/views.py:2089 windows/views.py:2517 msgid "DEFINER" msgstr "" -#: windows/views.py:2060 windows/views.py:2308 +#: windows/views.py:2089 windows/views.py:2517 msgid "INVOKER" msgstr "" -#: windows/views.py:2074 +#: windows/views.py:2103 msgid "Force" msgstr "" -#: windows/views.py:2086 +#: windows/views.py:2115 msgid "Security barrier" msgstr "" -#: windows/views.py:2099 windows/views.py:2323 +#: windows/views.py:2128 windows/views.py:2532 msgid "Security" msgstr "" -#: windows/views.py:2176 +#: windows/views.py:2205 msgid "View" msgstr "" -#: windows/views.py:2274 +#: 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:2400 -msgid "Procedure" +#: 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:2429 +msgid "RESTRICTED" +msgstr "" + +#: windows/views.py:2429 +msgid "SAFE" +msgstr "" + +#: windows/views.py:2441 +msgid "Cost" +msgstr "" + +#: windows/views.py:2609 +msgid "Routine" msgstr "" -#: windows/views.py:2408 +#: windows/views.py:2617 msgid "Trigger" msgstr "" -#: windows/views.py:2425 +#: windows/views.py:2634 msgid "Duplicate" msgstr "" -#: windows/views.py:2431 +#: windows/views.py:2640 msgid "Apply changes automatically" msgstr "" -#: windows/views.py:2433 windows/views.py:2434 +#: 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:2447 +#: windows/views.py:2656 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" msgstr "" -#: windows/views.py:2455 +#: windows/views.py:2664 msgid "First" msgstr "" -#: windows/views.py:2473 +#: windows/views.py:2682 msgid "Last" msgstr "" -#: windows/views.py:2482 +#: windows/views.py:2691 msgid "Filters" msgstr "" -#: windows/views.py:2524 +#: windows/views.py:2733 msgid "" "Apply filters in data\n" "CTRL+ENTER" msgstr "" -#: windows/views.py:2525 +#: windows/views.py:2734 msgid "CTRL+ENTER" msgstr "" -#: windows/views.py:2553 +#: windows/views.py:2762 msgid "Insert row" msgstr "" -#: windows/components/popup.py:31 windows/views.py:2565 +#: windows/components/popup.py:31 windows/views.py:2774 msgid "NULL" msgstr "" -#: windows/views.py:2572 +#: windows/views.py:2781 msgid "Data" msgstr "" -#: windows/main/controller.py:318 windows/views.py:2589 +#: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" msgstr "" -#: windows/views.py:2591 windows/views.py:3137 +#: windows/views.py:2800 msgid "Close" msgstr "" -#: windows/main/controller.py:319 windows/views.py:2591 +#: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" msgstr "" -#: windows/views.py:2595 +#: windows/views.py:2804 msgid "Run" msgstr "" -#: windows/main/controller.py:320 windows/views.py:2595 +#: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" msgstr "" -#: windows/views.py:2597 +#: windows/views.py:2806 msgid "Run all" msgstr "" -#: windows/views.py:2597 +#: windows/views.py:2806 msgid "Execute all statements" msgstr "" -#: windows/main/controller.py:322 windows/views.py:2599 +#: windows/main/controller.py:323 windows/views.py:2808 msgid "Stop" msgstr "" -#: windows/views.py:2664 +#: windows/views.py:2873 msgid "a page" msgstr "" -#: windows/views.py:2714 +#: windows/views.py:2923 msgid "Query" msgstr "" -#: windows/views.py:3091 -msgid "Character set" -msgstr "" - -#: windows/views.py:3121 windows/views.py:3140 -msgid "New" -msgstr "" - -#: windows/views.py:3160 -msgid "Insert record" -msgstr "" - -#: windows/views.py:3165 -msgid "Duplicate record" -msgstr "" - -#: windows/views.py:3172 -msgid "Delete record" -msgstr "" - -#: windows/views.py:3210 windows/views.py:3658 -msgid "Up" -msgstr "" - -#: windows/views.py:3217 windows/views.py:3665 -msgid "Down" -msgstr "" - -#: windows/views.py:3232 -msgid "Table:" -msgstr "" - -#: windows/views.py:3245 -msgid "Clone" -msgstr "" - -#: windows/views.py:3270 -msgid "MyButton" -msgstr "" - -#: windows/views.py:3794 -msgid "Save Starments" -msgstr "" - -#: windows/views.py:3802 -msgid "Location" -msgstr "" - -#: windows/views.py:3809 -msgid "*.sql" -msgstr "" - #: windows/components/dataview.py:25 windows/components/dataview.py:52 #: windows/components/dataview.py:75 msgid "Allow NULL" @@ -940,10 +980,6 @@ msgstr "" msgid "Zerofill" msgstr "" -#: windows/components/dataview.py:111 -msgid "#" -msgstr "" - #: windows/components/dataview.py:119 msgid "Data type" msgstr "" @@ -1081,114 +1117,114 @@ msgstr "" msgid "Do you want to delete the directory '{directory_name}'?" msgstr "" -#: windows/main/controller.py:315 +#: windows/main/controller.py:316 #, python-brace-format msgid "{text} ({shortcut})" msgstr "" -#: windows/main/controller.py:321 +#: windows/main/controller.py:322 msgid "Execute all" msgstr "" -#: windows/main/controller.py:440 windows/main/controller.py:448 +#: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" msgstr "" -#: windows/main/controller.py:467 +#: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" msgstr "" -#: windows/main/controller.py:516 +#: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" msgstr "" -#: windows/main/controller.py:517 +#: windows/main/controller.py:519 msgid "Unsaved query" msgstr "" -#: windows/main/controller.py:562 +#: windows/main/controller.py:564 msgid "Save query" msgstr "" -#: windows/main/controller.py:565 +#: windows/main/controller.py:567 msgid "SQL files (*.sql)|*.sql|All files (*.*)|*.*" msgstr "" -#: windows/main/controller.py:593 windows/main/controller.py:624 -#: windows/main/controller.py:651 windows/main/database/list.py:119 -#: windows/main/database/procedure.py:206 -#: windows/main/database/procedure.py:232 windows/main/database/view.py:268 -#: windows/main/database/view.py:297 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:631 +#: windows/main/controller.py:633 #, python-brace-format msgid "-- Saved query to {file_path}" msgstr "" -#: windows/main/controller.py:657 +#: windows/main/controller.py:659 #, python-brace-format msgid "-- Autosaved query to {file_path}" msgstr "" -#: windows/main/controller.py:722 +#: windows/main/controller.py:725 msgid "days" msgstr "" -#: windows/main/controller.py:723 +#: windows/main/controller.py:726 msgid "hours" msgstr "" -#: windows/main/controller.py:724 +#: windows/main/controller.py:727 msgid "minutes" msgstr "" -#: windows/main/controller.py:725 +#: windows/main/controller.py:728 msgid "seconds" msgstr "" -#: windows/main/controller.py:733 +#: windows/main/controller.py:736 #, python-brace-format msgid "Memory used: {used} ({percentage:.2%})" msgstr "" -#: windows/main/controller.py:769 +#: windows/main/controller.py:772 msgid "Settings saved successfully" msgstr "" -#: windows/main/controller.py:993 +#: windows/main/controller.py:1006 #, python-brace-format msgid "~{estimated} (Loading...)" msgstr "" -#: windows/main/controller.py:995 +#: windows/main/controller.py:1008 msgid "~ (Loading...)" msgstr "" -#: windows/main/controller.py:1230 +#: windows/main/controller.py:1243 msgid "Write Mode (2:00)" msgstr "" -#: windows/main/controller.py:1281 +#: windows/main/controller.py:1294 msgid "Write Mode" msgstr "" -#: windows/main/controller.py:1292 +#: windows/main/controller.py:1305 msgid "Version" msgstr "" -#: windows/main/controller.py:1294 +#: windows/main/controller.py:1307 msgid "Uptime" msgstr "" -#: windows/main/controller.py:1429 +#: windows/main/controller.py:1444 #, python-brace-format msgid "Do you want discard the change to {database_name}?" msgstr "" -#: windows/main/controller.py:1462 +#: windows/main/controller.py:1477 #, python-brace-format msgid "" "Do you want to create a dump before dropping database '{database_name}'?\n" @@ -1198,51 +1234,76 @@ msgid "" "- No: drop the database now." msgstr "" -#: windows/main/controller.py:1467 windows/main/controller.py:1488 +#: windows/main/controller.py:1482 windows/main/controller.py:1503 msgid "Delete database" msgstr "" -#: windows/main/controller.py:1473 +#: windows/main/controller.py:1488 msgid "Dump is not implemented yet. No action has been performed." msgstr "" -#: windows/main/controller.py:1474 +#: windows/main/controller.py:1489 msgid "Dump not available" msgstr "" -#: windows/main/controller.py:1487 +#: windows/main/controller.py:1502 msgid "Database deletion is not supported by this engine." msgstr "" -#: windows/main/controller.py:1502 +#: windows/main/controller.py:1517 msgid "Database deleted successfully" msgstr "" -#: windows/main/controller.py:1503 windows/main/database/procedure.py:195 -#: windows/main/database/procedure.py:226 windows/main/database/view.py:256 +#: 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:1741 +#: windows/main/controller.py:1792 #, python-brace-format msgid "Do you want discard the change to {table_name}?" msgstr "" -#: windows/main/controller.py:1767 +#: windows/main/controller.py:1818 #, python-brace-format msgid "Do you want delete the table {table_name}?" msgstr "" -#: windows/main/controller.py:1789 +#: windows/main/controller.py:1840 #, python-brace-format msgid "{table_name} (COPY)" msgstr "" -#: windows/main/controller.py:1948 +#: windows/main/controller.py:1999 msgid "Do you want delete the records?" msgstr "" +#: 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 "" @@ -1251,43 +1312,56 @@ msgstr "" msgid "Do you want to reconnect?" msgstr "" -#: windows/main/database/list.py:108 -msgid "Connection lost" -msgstr "" - #: windows/main/database/list.py:118 msgid "Reconnection failed:" msgstr "" -#: windows/main/database/procedure.py:194 +#: 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/procedure.py:194 +#: windows/main/database/routine.py:553 msgid "Procedure updated successfully" msgstr "" -#: windows/main/database/procedure.py:206 +#: windows/main/database/routine.py:590 #, python-brace-format -msgid "Error saving procedure: {}" +msgid "Error saving routine: {}" +msgstr "" + +#: windows/main/database/routine.py:604 +msgid "Function" msgstr "" -#: windows/main/database/procedure.py:217 +#: 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 procedure '{}'?" +msgid "Are you sure you want to delete {} '{}'?" msgstr "" -#: windows/main/database/procedure.py:218 windows/main/database/view.py:282 +#: windows/main/database/routine.py:610 windows/main/database/view.py:282 msgid "Confirm Delete" msgstr "" -#: windows/main/database/procedure.py:226 -msgid "Procedure deleted successfully" +#: windows/main/database/routine.py:619 +#, python-brace-format +msgid "{} deleted successfully" msgstr "" -#: windows/main/database/procedure.py:232 +#: windows/main/database/routine.py:634 #, python-brace-format -msgid "Error deleting procedure: {}" +msgid "Error deleting routine: {}" msgstr "" #: windows/main/database/view.py:255 @@ -1317,21 +1391,21 @@ msgstr "" 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" @@ -1341,14 +1415,18 @@ 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 "" diff --git a/structures/engines/context.py b/structures/engines/context.py index 46092d1..53797d9 100755 --- a/structures/engines/context.py +++ b/structures/engines/context.py @@ -46,6 +46,18 @@ class ConnectionLostError(Exception): 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.""" @@ -588,18 +600,33 @@ def _handle_connection_lost(self, error_message: str) -> None: @staticmethod def _is_connection_lost(exc: Exception) -> bool: - """Return True when the exception indicates a lost DB connection.""" + """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): - return True + 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 - if psycopg2 and isinstance(exc, psycopg2.OperationalError): - return True + # 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): diff --git a/tests/engines/test_connection_lost.py b/tests/engines/test_connection_lost.py index 0f1ea73..4b3387d 100644 --- a/tests/engines/test_connection_lost.py +++ b/tests/engines/test_connection_lost.py @@ -1,5 +1,6 @@ import sqlite3 -from unittest.mock import Mock +import threading +from unittest.mock import Mock, patch import psycopg2 import pymysql @@ -12,17 +13,36 @@ 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), ], ) @@ -30,6 +50,32 @@ 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( @@ -69,6 +115,29 @@ def test_execute_reraises_non_connection_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( @@ -123,3 +192,59 @@ def test_global_connection_lost_handler_is_not_invoked_for_non_connection_error( 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/windows/main/controller.py b/windows/main/controller.py index 2720373..6770336 100755 --- a/windows/main/controller.py +++ b/windows/main/controller.py @@ -2101,8 +2101,8 @@ def _on_global_connection_lost(self, session: Session, error: str) -> None: choice = wx.MessageDialog( None, - message=_("Database scollegato, vuoi ricollegarti?"), - caption=_("Connessione persa"), + message=_("Database connection lost. Do you want to reconnect?"), + caption=_("Connection lost"), style=wx.YES_NO | wx.ICON_QUESTION, ).ShowModal() @@ -2110,15 +2110,15 @@ def _on_global_connection_lost(self, session: Session, error: str) -> None: try: session.connect() wx.MessageBox( - _("Connessione ripristinata con successo."), - _("Connessione ripristinata"), + _("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( - _("Impossibile ricollegarsi: {error}").format(error=str(ex)), - _("Riconnessione fallita"), + _("Could not reconnect: {error}").format(error=str(ex)), + _("Reconnection failed"), wx.OK | wx.ICON_ERROR, ) self._remove_session_from_explorer(session) From 104229fa81ddb8e03a3b3b709dcd6c45563404ce Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 6 Jun 2026 11:13:23 +0200 Subject: [PATCH 84/93] add concurrency guard to QueryExecutor: reject re-entrant execution - executor.py: execute_statements() returns early with a warning if _current_thread is already alive, preventing double-execution on rapid repeated Run actions. - controller.py: QueryEditorController.run_statements() checks executor.is_running() and returns early before resetting UI state, so the progress bar and cancel button are not disturbed by a no-op call. --- windows/main/query/controller.py | 3 +++ windows/main/query/executor.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/windows/main/query/controller.py b/windows/main/query/controller.py index 68e6650..e145b15 100644 --- a/windows/main/query/controller.py +++ b/windows/main/query/controller.py @@ -202,6 +202,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) diff --git a/windows/main/query/executor.py b/windows/main/query/executor.py index 8ef84dc..2ba989d 100644 --- a/windows/main/query/executor.py +++ b/windows/main/query/executor.py @@ -60,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__() From bc5660136ddeded04a460f60c23695fafee002ad Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 6 Jun 2026 11:13:47 +0200 Subject: [PATCH 85/93] fix SQLiteIndex create/drop no-op semantics; add alter(); clean up SQLTable API structures/engines/database.py: - Remove misplaced abstract create/drop/alter stubs from SQLTable (they belong on SQLIndex); the concrete methods were already on SQLIndex. structures/engines/sqlite/database.py: - SQLiteIndex.create(): return True (success) when raw_create() returns an empty string (implicit PK or auto-generated UNIQUE) instead of False. - SQLiteIndex.drop(): return True for PRIMARY or sqlite_autoindex_ UNIQUE indexes instead of False; dropping a non-existent or implicit index is a successful no-op, not an error. - Add SQLiteIndex.alter(original_index): drops the original then creates self; replaces the old modify() implementation. - Keep modify() as a backward-compatibility shim delegating to alter(). --- structures/engines/database.py | 28 +----------------------- structures/engines/sqlite/database.py | 31 ++++++++++++++++++++------- 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/structures/engines/database.py b/structures/engines/database.py index f4c1656..13a2756 100755 --- a/structures/engines/database.py +++ b/structures/engines/database.py @@ -248,33 +248,6 @@ def is_new(self): def generate_uuid(length: int = 8) -> str: return str(uuid.uuid4())[::-1][:length] - # Abstract API that concrete engine index classes must implement. - @abc.abstractmethod - def create(self) -> bool: - """Create the index in the underlying database. - - Concrete engine classes must execute the appropriate SQL statement and - return ``True`` on success. - """ - raise NotImplementedError - - @abc.abstractmethod - def drop(self) -> bool: - """Drop the index from the underlying database. - - Concrete engine classes must execute the appropriate DROP statement and - return ``True`` on success. - """ - raise NotImplementedError - - @abc.abstractmethod - def alter(self, original_index: Self) -> bool: - """Alter the index to match ``original_index``. - - Implementations should generate the necessary ALTER statements. - """ - raise NotImplementedError - @abc.abstractmethod def raw_create(self) -> str: """Return the raw SQL string that would create the index. @@ -598,6 +571,7 @@ def copy(self): cls = self.__class__ 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. diff --git a/structures/engines/sqlite/database.py b/structures/engines/sqlite/database.py index f627e3f..8aa1d93 100644 --- a/structures/engines/sqlite/database.py +++ b/structures/engines/sqlite/database.py @@ -350,28 +350,43 @@ 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 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) From 5463f6b334dbc75e8511285eb473dbcc6551e6d6 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Sat, 6 Jun 2026 11:16:47 +0200 Subject: [PATCH 86/93] add splash screen shown during main-window startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PeterSQL.fbp: new SplashScreen frame (640×480, wxSTAY_ON_TOP|wxFRAME_NO_TASKBAR) with a large logo bitmap and a horizontal gauge, designed in wxFormBuilder. - windows/views.py: generated SplashScreen wx.Frame view class. - windows/splash.py: SplashController wraps the generated view; Show() centres the splash, start_close(on_done) runs a brief animated gauge fill on a timer then hides the splash and calls the on_done callback (e.g. main_frame.Show). - main.py: open_main_frame() creates and shows the SplashController before constructing the heavy MainFrameController; the main frame is revealed via the on_done callback; error path destroys the splash if present. - screenshot/: refresh connection-dialog and main-window screenshots. --- PeterSQL.fbp | 160 ++++++++++++++++++ main.py | 13 +- screenshot/connection_dialog_configured.png | Bin 46629 -> 2201 bytes screenshot/connection_dialog_ssh_tunnel.png | Bin 42019 -> 42037 bytes screenshot/mysql_main_window_add_database.png | Bin 107011 -> 94852 bytes settings.yml | 8 +- windows/splash.py | 29 ++++ windows/views.py | 30 ++++ 8 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 windows/splash.py diff --git a/PeterSQL.fbp b/PeterSQL.fbp index 847cf95..d5b2430 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 diff --git a/main.py b/main.py index a0385e8..d0cce66 100755 --- a/main.py +++ b/main.py @@ -51,7 +51,7 @@ def OnInit(self) -> bool: 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]) @@ -110,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]) @@ -123,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() diff --git a/screenshot/connection_dialog_configured.png b/screenshot/connection_dialog_configured.png index 280d291bc94a1cbd9db604c59503ab858ea2c3a9..4e3c196057622f22cdc060dda5e7972260017a2f 100644 GIT binary patch literal 2201 zcmeAS@N?(olHy`uVBq!ia0y~yV3A;8VA{;V1QfCE(9{G{%*9TgAsieWw;%dHU|`?~ z^mK6yshIQjpdlkr^3Z~>{<$3S3xLc~Fd71*Aut*OqaiRF0;3@?8Un*_2KpH$<{an^LB{Ts5Q(+jK literal 46629 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%0AF6k{yGE0jY0gavl~No4Jr|m>07V3B zTB51R5u7aQr>lDQhw*c7@VqVB0*{#vhsRX=vioWUnBOasvLA3ip2~?G@P7GM?Wiw7eQO3>Y(qObo{jC@{7$-SyzSTVo=tVhQE!AR z>jKhi(VI2-?l$9Xq!qP~i%CHH!s@EGkemLxxL-@WDkee89 zVL!adBcVM%+rQXITowC>xs5F-um&K2#87}7p*nkw5Dk})` zc>~eh`g4?i`b+L^Vp-W6Hd~svGhU05M9`pYU#WnX!(rLZb@MN)7-c|~NlC6JdzqLr zTLTU=q~CADw*PL`OENXQv6yUTJ>>2uD#hfj|1lZmQD{}|c@(A|${nw5fF_D6!X94$ zV}cFaUJx&63z;y#WvJ-8H!Ia2D6;(0C1u@kxwTqY&3SraGKtxBBRIjay;`{S(sX3D zJDTREp(fl`{y{cpc3hXDF1cEsm~nX5{e;PxrAYIe_C@m9hUQ-TT`wxseG<~iCWf4d zPEkq3NR>I;ZaAkzZP+O+*Qx8q}TAzvvwSHEr8iV&rTgW`QROa|v7E6lNtsM4X% z8yiCpRFq{7(&f-AO}0t9D!E$Ox6W60U(X$WCbmZgT0TA-B!tR&h&pH3om5U8@G4!C zFwJyAftxR(n^#9`Nj~0Hi>(|oB6pF~l6Bmgk8gj?UN6AHE1Y-k%_*bpF#PY}k0-Rz zLR5P7=1^8Y;f2lJy61*6Nm&(0 zUkd_4gSWW#RTUN1hb_#-&pMb$G%4%dIcI*cwNc80(8dw|NiF!=l#e))pEhQk>O)X? zdeZ$qlo^JqdT95Yv!>PZrS1WFs^j&~s>stm>x)upvDnb(gEO}$SLY|Nt|O~M2Fx$? zM_aTSC84-pux+(x;~|pcQqb`JC*-YiuXC)CS3j*Bk9_mEGXZX?OE=7%nT)nm4H<^S zDY0WVj7I&H{<&ncWu9nM-71-SBADcDdsvj}w(QQo~JtBF~n z%p>lWusbv8g%(tcBhd$uR!={;^K%BbV@Le7FwdU--W6bA^C;ZN=sWuU*c>RgU_uwS zD6;Qob!BC8&m2di3daW48n2likuF%INuy*x_k#P)y*z#SX(DOlxmk-I8Bm&`9l7w zi*I^XmQkz^MeYoo6`lrsQ(>RxkQWN~lyI5i&fFmC0d=zhsK4>Q%IZD4){Am`hx-3W br#t|C3Ir_Y0s(gqA0NuX?(B2qwYa|lv=Hu4 literal 42019 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 ie}%L5zqCDA(h0qATVEQYkNooVh@qY-G#VPzsR% diff --git a/screenshot/mysql_main_window_add_database.png b/screenshot/mysql_main_window_add_database.png index b038c696d969a2e79386cd350ddd84c16b23383d..423689963e29fd20af2eb9d37b0a076ab26f566f 100644 GIT binary patch literal 94852 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 literal 107011 zcmb@u2UL^Ywl>NqpNd#OP*AE!lO{@)u0g?2l@dBAz4u;KKtu$TDukla2>~T^NKmQ} zIs^z1dJnz#+=YAZv(Fv-jQ^g0jQcu<32$BNU31O(%x69;;m_0*FHJhCY3) zK}L4go{Wr~n&J%DBW+Mk0w3gV5U3Uf1;y}$`ZyWcEi&liM_S$~O9U_PYudv^Mh?TC!3>ArEcEoaWFe~$i?kKp*nU7RVPfB)GvP8~z>k7H~c z|2XND{L|Zi_K<~reEg3uSJ?jboNodPWGn1?bP{wD!yGf8Gc#4N9l!fr9T$HIU~NS6 z{g(7%GN-5G8Zt8r+S-;ER#a{f&>X zN4tYQFi+1)*BQzoMrvwj;k$to;p&P5ep4*xtEA`!4GW%qg=BDHysq-u)u{bQXiQ=nIjP> zLD0_Wv3W8gpy?R}<+O{96*`t7U;W_*jWO4YdiT!R<8<5J*rUXordEW%JyDW{Os|)XxRUe#}W;(TqFMks(XxN7NO=<&WBqS>J zOKo8Y?P7fXKrUZP41bFlG{&Mb)AF`Ce5RK>TAx1w)(*Frn+>cLEc^iPZ-2s^7>9UH z6zUZsZ0{c2px_K2UtUiA{P_keYx+uxYhO)gr_7h&AbPlctT|k@7;iq@ja6(uT3bAl zHKFVMbX}k(B;;>;diqqgx#?*RtgEM|r>m>0ii(P^?q?2Y^osKzqovWBma>I&tk%A$ z3|!8yH7@G5zQQVNSnenTjZsf~zz+qjv=@V$lp9k8a@CQ}xOKR=x(-!t)~c5{(d#zN zEOUkhpf9Zys_9i$>Xi%m*KF-R_{fhf=C7`*vMtp|;0_cwY*@KXOpNfc7n%h*jY>WX zTpuAv!Vst@aT{~iSQ@u%NLT&S(w=pX-umc};Fzs{sRNP` zQ(}?7RGq(h5wSa$P9p4JE#Wv`sK&S9nVGyz4mOrGm5`d(HZ{Df)w4AEsC_Cq`!sQ< zKxe(>l=}ix-^}XCFO3EEn`BfTCUV`08@2>~q*NaXBPu^K;|f6^77W-edl<#Rtx_8; z^b>FL#vlB$qTZN!0NhNwr6xpM7F+BummntwZk2()hB7@ zU{LFuCo|Zp>sZ}Z2h?$zUPkv(>a*{<<=jVxl^YFQ9mtiLbMFR;j0D_ zbjzI37!J?~y}&N)O-f4p!{-Z?SJ~ft@x$8_Qc{+PL`3f~TK^Hk^I&CB-^wepTb{I) z{|H+opqAM4c8jqqH}F{DrRsaHt)O`E=9ebVoTrD&l6b9z?9ybXQBwOc z<6W=SDqe@fD#Ihq(hZ*aEY>&VXh-|~8r`@5^vSo|9)DNbcuNq6v@*u@6dQWja+mh& zv8&KwBN0#}gZ6;6s2D<<26D8C+d959-2F3caoBinNvozFhtxdmny8QG)46ixO8^7PaJEv+%od^sNIC<;`|YOPw5|%*DO0L1&gF9t z%=J2cD@id|ubMBViAy2(tef5LGNht3*QK}Cqv|>11kwXc5iVG}?36uwHlZF-1{2bM zZ!*P}OI5;04=x^d*}L2L!g$_22ARkX+0nNJTku-ax}u88hht|f0~wBX69UEtMc-5U0Qs8(}w3Thqa28U2vJ)5vc)HkW!FjD+`J1ArD zAMs!M#gnJErO6sSHji?rr2n*m1;V+5mihD5yW~IN*MGshkpB&iI|YrL%zw<9%=_+zlDb@fB!DoI!XwjnlceAyq!!HX_OAZ!jt$ z=QW!VxO|y+Sr3*3^7#cn^ALZTAy$YRWS?b53=HX=hKDFrc6r``W)CJ;pPs2Eyc zO2z`=jsFhSfGVE$X8gx&7avQx!x+YUp*RFia^l@Cl<^aD+F!t&Jtuu@xXg*P9G$vP z8FVm{NIKcpx$xu<;-=yWi=lu}=p~RwN|h%go}E6Pj72Ak>uT%4L3U;)i&WsGZ1X<` zjYT606Z&g-c$h#q9t@}b`t>>*y&>mwpPhZG>r;lfjL)5OX zBI&dD@_~n-dZ!w*lp)%8dphrTN=nLmHZ}+S(vsS9lMOBJdY?a+kdUybK{NIh(OL|PYK*WX`W zD>)f1{%N^URLA4*_=5Iiv2NSapyeR}tm~*z499WJ@<2gB0k@>rVlOI3A33@{yt;$7 z>P#=vMm2T)3N|z{3OcbcsB4F#{dVRFC`A?fr54ScILWYRCYh@)#Xh~+$A?>2E?gKb z3uxYEyx-{i`6pp`{cu&RrN79l2!r;`%F0@n^Uhtvo{WF3&&lX0vF@)?FG^DgW?MaC zzUy(?xM&eYtJ_aw>_u)Qi`mrJ?WXh4ke=pK4AI3fu_qPze5IeJ|wi^PF5<7uX75M9@x24{KH?0GM;< z7Lq=Hc0XuK`bqiM?sD$y+fUUybFVSXE-dhb%;VAUjf0pscm0qsMxR*~>}-2IgpZP~ z)!UDIR7pu$c~0XzXyr4?2g#40X%)ei>p~;WQ?XvSaHYebkY;i+OZkF0ZEtU{6QPPT zocweGm!@mWquYzbrK++kBiYp)4Q_g+di$?Zy0jeCV3H|7!g52?;O8 zVx?nXV7itI%PT)QuzOy;Jdjh4mD@i%V>kM3hirry+23ONW10^DS~V86*0~`7?+b3Y zZV#1KR8+Kcv)c~j<1?RUfENW#2&G<>-o@GCNY$TNE0qcMWeuDhRes(fZ{OaG1Ossk z>#DD>ub|LWYBykNIwddvCM7j>X?yI@x&H_4RR~4A(#*;VEK5VTHciGhL`R^|TcJia z+bP1^;Do)hMkpyuIAf7PiBvnudl(W`qD2vw2Wq!s;bne#puN z`n*85qd1++Gw# z=tN5g%>Mi;ugW0GJ72uJw)PBxyCCjVRb9=;>9W4w8m73V5v{I53FgA3OKnn9KY>3O z9+tXw>z0tvhOdgfZX-!mRRe+h>m)F?IV#3Nf>YF^^CVbvG+p)}o?fcN!D#OxYEGap z7e}j5>nRsK`_-(}My&zSCT6fnZ8ssx3H1${U5ZF7+FI!r5P2|fIf-N~ca(CU9&GN- z@$r#8v#ED#q>|Sp!f%D-YF76*Dksv=#>Ry)d;5C-E@lFT{q)&0ecuBN`T2-GcxTR>mCIY>S8t1b$*n_ZM@llHeX#1=i+!GMZtBX)GM zdM0B^U%$2nUn)DP65tmpfVDGNqW0?=BU<@xP*yE@badeIweUe}A7RAliHX3OMcm$= zH@DTC;;x1`yltj6p3mCp;lrPe7wI>+u7*&5Wn;T8e{$F6Sy5qa?bx>&lL3U$@G#tu zaHImWtABsswticN7o}Wb__SSMxJ)rsN^UjdCU}KP&|z46v$~I0%fOflgVe^O#*4uC zn+XJtt9`Z}!~ICi*ilCD!tyd9PyUenJQcHqZyOj%n<<6i&6?C-S0fok`xe~8qL|v- z+T2AMfFCuUnZO_+Hj~rSebrW-^@>qUGJ&_bxtp!;Y2_K_y*xf>g^*iW&9M-wmgStC zHxtrBsIQ+nqc38ozh^O4{KO3z+gnJe7J%95S~pgCET%-g*YK2ipIF5KE#~Lu-~cmd z-d&QLn_Djd_<-2hSTj@8y1F_JC~r*|MyA^7#D=(XeBRfCcsm5dhLVHf%gyX+Gs!BV zbqHBfTh|xL>&6VCvUk}YQLN9Up?kAcY@d~PFe7-F^#u|-F}g1ZEp1^lx)c&_In%eCJGA`X7s4wW%kDw%}xt9ZvrnGe7J3+TeLzh zkw=QPDAqTxd{eI3AWCQ@*Qj4Tx;h$;a%I|+#B%#0oNSWZSJ&x@9O&A_3Ut#PZ0CP|HeVz3%{3@#TLsm z5Q-VKwnDXKeGl6KUf-jK_q!yd%I!yMEH-DoMu^0zUX)+?F!o~A63*76w<$7N+~J!& zi;U+;pxn+K<3%N!M+h8<+S9YMt;P1G`oLSOa>c?B@fH$eQYKD0&k+b%p8x)$Hd}~l z%xN>LKMV}VHdW3W!it91CQ^JNX4{iCCk|NygiMhADmRYv;Rw4&xVS5|hwF2NsZ#zM z6G4s*4gQtxp^c`2FZX&wMUwVg{Jp%&)veSLEqq4lBNBs9o?Co^lx+R>dyd0VV~Us~ z(-*Huuj>`%K5w^_lyl(_n-7Dek|c^-__Hhw_~PAUoqrvzM-_ypZkOu_8}JV}HAstZ z<@wKmNC={gPN5M(Tc+mYb4s}$0fV3b&-w8IlN-6{Ld4}yN_RfE-j@r^1(o2(AD@v=UMC?`~uQI1W7$E81i|2Ww_Ps zmYbU@JZMzRXDL_V^5O-ygY$dowcako9cHQr;U5#sIz)-F!u-7P6uSe**DV&0@yPZJ z>@*@F=@Vy&vzuGH&Wf!FC0ntO@Wq>xCebAU!~Lm4<=V1RQc~g$eQUMVJG{^kx!tAh zovni1r&(z5CY((^Jtljv7prg?_=kv`Vz zR!bBQ9B?HGGeJfjw?cmZZrj?MyBfnAZ`=7gc4rC&S0J zeC>8iH$w=das11h;#RXY#gKaR})sYo-z zy>PRi38fliPEWtxG1YR?k-bn~<5V=eG<{8%?bhVhO$)JKC-RELC=`f+%vXK%GrB|m zK1&$o7!#sxjxJF{GK?AyMYXdOqkGH|vI_?mF)+Z!tGJ;-T6*;HijLI`zO6bniRyLqmqF8>1Cw@#Dq zL9(;~YB|08iGp6!do5C8KcGGP7wP#zVM{xYa~##(5B3L``7ryQpl5%boOE<_ghQe6 z5I$Z}_0(|QRic5kRr|{pi3Fxt)w#?AqOQ_M*|~Zvf69D(rF^w0Q5^VA*`H&AckaCy znJ(An#Wl4XB#Syu&)Qyt%zNDB;mK37fX2``a>4SiafRGj{FIMd+;K2imhyhUDS%Yi zwgn&;P1SrE1UJR&r~UoJ0SFiihBq#v+SI_7{5RUPj(U0!T#Y8Eu8Y#MDH#u7okK@>L)@Nd1Qp-SO0vsmgR z=08wu6Kgl%RA5+LXqR(EGPUpvSN6z=msOiBp+>LMQ5`^})ghD|$DL;sU%k4wSNgX% zIk5tCcFxa2bLTgeF~-{m%AMs-n-8r%O`Gex@%Nm>=EWPTuF|J~;IXxI(9mth&83>! zpQBj*p6wU-=O$;8`5YnVzv_?$K{8AX2Ogp|be}j0uBMkeh$pEk?He)K5%BS?g|dX|)M{s!wrQRW z|madupoO$_xnBEwq&hNj-M+_s!3WmLfd0GZ?+OX_z<;E|)p@y{nSd#KYyodV?36q2_K#(wXxX{9( zfE?*$uCSqj!@}WBZIBO@mseJuu=-^cRpmqTUx^ACfzD%?B)r~E*%L6&Edmg*D^koJ zWj?V@jqYE-B)G!~*aOlk)ad!u=0$M<8EobH$9J+GHTp2opT_Q{wuX?XV_ZnL`C8z9|`PNQ6nEL&&i?`k=kd0MTDZ$EtaF#l&uXottGW9o$h zAAIW`{G7I(l6|EX&Pfh3(hM#P?uP^X*YV*szZaTvpY<^Q_yH6D(brX{4JwVHncN^) zjJ)K6Y~@W+-Q$qHTT#fg`z)0YB)h()c1D$+$6E`%F&sB9okQS~Q&RXta;e7$vNZi> zZvI^%6*fmWKD@Q%d)yPc$&{-r0-|18RFtd-*J9S(TYA4a@Cffd0$VsW6D*74_Frp) z8|_lwFANRc0qo3&WyQ2xVq073AQe>%KFcOHqGlaAcnJx16JOcyZ-SuBfGBts*1;S@ z;T!zp^y=@u)*>q_j#soA*zLP(BsiH*nFJPN|HOY3+x}gz`ya@d|18L@ti4wx%Q7aF z9>iRk=-wfGOo_R2N|LTITzi$4$9R1|<;bhfsSlS&G zY5wxA*^ArcTR7!r$fQ3o`FxW2-`wl}j>`Jal4N&+J9wk_#o)_f>4Er4I~@q*TUfPU zFW2SsyTqxLSy-l;3RmxZxKpVe!Pc21yDI<+xq#&Rz;Wi)?T;J_$4MwbV@yxAVtLp2 z7kGGi;7HYS*C#Vp_4L`91wHR!{3e}k-{OS~6I#dvfOKs7zPo01oxlD7+~bw$L*OQ^ z+T+V)7Dx7dSlcPqb~TlT%4}r=TdS2ZCzd^fpQ3g%UQP+=P z=ctV)muRkmVjdo?Y{eGBj~yfJZ5Gv%KE=c^jIWC4nxxaIGV&cqdk3hDt*EC($@)$2 zT|h@*oT9n?cIUH~l>R5=-b5pLA2ji&+3WLsXPo2krj>#0*`AS$`>l2Z1us$fLc{88 z`@rh;!!=v&grNID8++XV;-O5p8Q2<9sK$x_SX66IMqm!W2gAOvF6yLuwftynOEXwD zY5!4mzkpm?`3url_8l}){CKbino%?xB>Baw(*iM|kSdiE*Lh@)X*4}{pWRZIJ-Q_( z8@MlSx4L=y*2q}AfPs(awtWDo1P-^uxawp)r}z|~lxN>hK^0W&rpOi&R(&Wee@@`Y z7XSSbIe{=DEh8gpyS*H(_PbrwVYF&F%PQTm_gfP6ZH_zlCM+l4s>0!-j=5U%U1i#c z+4c}>nKXaX;niBj6iHg+toPKnPb?usI#s^E_b#$o)*P0hP`%_5AW8z-F(QeAjA>@k z@)`sbQD^x&+gfQLH;hPXceN-H)??aP%>v(Z{-p)b_>8xQ(Ft9fR0gX+^~AB$~JLrc{KV#HcczKi=SlK?XjW6rvPbd!OWR?25F2v%qmtrY+(tC8tbL6fiojrwdUZYqnYM3^aE1FSea)HetaKH3M)alfJ_{*CM z%&d9*c6dg!no6=HQa#%7y8IhjS|^9m1Az5?7AkX=J?WewJE+0h3j};P! z@`BZJXd&KyKB>~zeU23hqSbGrBRY)78lMcHh_rkdJw&g>`LnPGELSU+%jLj>wY@q1 zXZWxd$nr-T0X*pY|A>Qt*5SI$C@uP>C@icRtyXJHC^D|S|FX3j%r)ia9gyB?wH~^! zdal$p-jB`s;JY8{i8Pp9-dV^w$8hPOXrMqDs)_)U%%$zOWhCQumhfLM_SVqliAZDN z_0-)^>TTNr{|S7~MW<7DJI<0)*Mz z{!-LC3IC73&x^&_4 z_F9j%2q*JtVN~ffIzLrLyKHN~EUSWE{5IP9b2hRhZrUJRyxwkLz;?fv1)K@V==Sza zbn)1I_x^#UyGL!rK7}->izp%CF5+k8AuYGSIkyBs^?>ccmZf2vQ3)q_yIC+8EE^uC z^N06;6&6*UqvHu{G7{l9J(ggOT4y?C_D9q*x(DsOHv1g@Gl2hR`1&tJ4H#MPkw_+S zjB%{x)tuX#bb_Opym?>D!#D zhd7Hs>Xp-(qE|IZ)G_h>k!kj*MH7Kz4LWjxDy~~kF`XfM)$<ok zUz#MeEfd?4sTFJC)0mGl^4oD7s#@-4Pa8vfjRng*xOQi(VXw?#7_;8Q=r!WeT$b*? z(emX{@CbpxDDLRLy@)$|kq(f*fH!zq|8{12nu3k)uHpQ`LK27vPPjbHoY&;%DcK5* z(L>cG!`&}Rw4{0f%`Lpn7umLNRr9*z8rom%CepE@@3H* zmo77kjnw#{(EdA1`0IQycMlJT{sO#U?NQ9`JRQI8J?HUM$5Jz3a=-kfDs%eK$mg+@}`+&Q$|YSU6-J2$p`0|-?fwTCO!bxW8Eox#zX2$5{?Y`dv87n-fGJ=a`G^-B=jgOPdJ-${83e-_7G6y|b%D;a7N|p4GbRPei zCgZoWLa26|Q=2LDoA3GxY5@ji_Rklao0?WeD%~31U4^_mIX;A|WY>pLu}XWd0f`mp ztJdO&o5Gfj@2_8^zboxL{;H^G6<~1Y=$tRIIclkrcKrq5Fw+C)7e8P9MSlLu2qvJK zdXnHZmI@vRZX3-c(NpcU$|B>tH29-_bQs+1V4LW5aPw6i9~~@rN~f-~E%jU( zk>KL8+?;Jc+*x*WbzK}NN=LWU_-ulLu;*f5UOous5$!DXU**HVo}E5Tb-de6ueep| zwj^Qh)lof^<=J!RSU|H;ow5eS=HbUr0fc2COAscb7K2X{iNcOsxm2<#`^#@5<+RK~=+3vm0wR`zEsa6Mxk0(eUpA-^lvj$1 z)B!!LmM+15Dx=aXwOnpy?XV`~XHkDMefoPr*s9-QWU#;{mPW?(TCgLbs?@q0LUG@= zFIUWA$evJz8#~=O(|K4V&oK77`Pg!OCyS_Yvyz9x`mWEwn2I?pwhDgY}A7;&-OM~}$cv_m96>T<-Hao{ybTKzh8vq!2 zFg12TAEM506wQ5gnQB_CC1HyBWMl%i=DT}y)O8>rQdW!s+ut*&9YvQNR|Wi{Hz~90 zm2|39${Jd4X3e>+4g_ISOZ%G;ie9-`1oq&7mo=qC4MO21E^eVYw`5m!#0Hxn;4oCOfJa$@86HXvIxGxtS2Bg1PVMGc3a=$$Nl8g5scH`* z$*2by@HGgNjIRjTp%~2qHYFcWnF0=L83(9><#cd2cv4cHKP*oRcrsvmga3n(jt zNfS@D@%DMyaq61o6;d5Y&@}h@kHha z$(Loran>N{)Th<1tNcwyqP=!wuQfvOsM2nCq1P%dhP^qenVZ9ZyRq)2`ejE#zQuFa z2|604g5HJ6$*H5`qqy|JiU)9(fLlAH!P{krb^BrSqN!0PKsoT zr4GZM-QS*EW)K1LV%_gA9(j819N(C!oJ)g?+Vun19cOXWDtxBOeL={&>&tz!rp&CY zfWzhTQd=Aiq@uDCy*C*V%caVznXLkL^%t42UcLI#gkox9;wsn}8HuZ_L5FwFZrg}3 zGy`{Q)58HIVW9uaQl2#+Cc(v>3U!J`fms*_Y-0xVYyLfyf%e3*mYO)9(?N@&Ms7t84dX;Xo2JF)=~xNBV8FaEDR@ zcecB;6F8UZ1ttUCjT;p&e!Zy^rU2#4fg%$*o1RQywW9WeYZ=k9t%sg(A%kY1jEsOG zhlWb+KtwZmdh9>UqNw(^NricfgeQ>)>} zfvQ2gp)?t#`_j*qwW00JgvY1K9iPL|VdXulqaUtdx8G4DLMz{&6O1hH(RN5Vu`f?k zS9kE+lZf|{2Ow?&5cPQ#|Gtr?NB)}#GpKQTPmi2} z@!rhM%_UTOojQp0fE~O%xF+i#*QlIIl&?w^OuGqV^zIC^z|{BxDX@fp)XBOs;bWIpBF zSZ>1*qO(Ks#V3Dxbtn8egT;8%*uozqXIUdkrbee~8$kNny7u6I(}Mjn(&*K1=a&QC zQIf5FU!%Hozp85NLqw(TLds(n(4+D7?0)MH3gwh+U%e6^ooev2(!&zV!v>5_)K8PY z-w`vHzz5(A{#Tm%|5uk!FZ-Xw`9m=v(Q&T*UCfVl^|f_1^|yyvW^>?`ByW>ZCF>(W z3-=Rty>_=W^0av4^E27ckVy}L$oepk6YJVmie1PnKjIAufvxvj?+defFAWy-m-NLk z>InK|`knt<`!tiOP&^8y0hv%qoB=pHU#o}@8-q8cnH`CVqWDRkqfuE^sf~PoxU>*k z2xL+g{z{U!$bM!-{&}6@>py15AqB7qek>>R#aA@9v9OkWz?$>^&E3=-18ii3{ZEz$ zr0NNyFKz{sJxs7b|N0!8s@L-xjE@gsd0uHK7vr^fAJP2viW-uofwz}7x=!X?blL|S zE~q&*8HI%q1R4fN&&&UnRn~zJbBD0Ea+tJ2WqMjL;cNa zms`}flmlA3vWL`*0mA&Ceogf`3c-;eOSs=Q~=>sz@9m5b9exkS|1zQ;i z_VnrPcHISfiD~G!R3K^i`B!LkPnK2DgnSr_l#A)^Xn;cS%f0VkJ1;T%%Bi=Q<&1sI z3A@D9Rpg~^;nueou8n}*+~0@@P7A!tB?hDT;n3`~(~6XozB;~I8@NpwufIG!ZT{nP zD0Pharn49JciVTb2VcC3guCaiFiBp!c1>go?X|NsT)R@O+QP%*pkMY``elmRTtQ#S z=XPPalj&X3iH*pO^rfEt#<{n4A`FMRV~&IK%SPD&aM>KmQRm2UH*9x~*Pok8NvR z>?(7>*jW1hvOQVkVC*&3e9(s0G$uIg!?|Nk zasVXm}L@xq9?}_`Mjt`>pn-t_zRL^ z!VIWC9_{rCw(zt7tEdmVVnZmd1Y=Hm?GwZfW*rq3Ag-kQRV+8Nu!)S~)*p*Nf}I_F zKnf7w-v0)Q^+<;8A%q<)kg(iR7&{hdHJ2WAy(g#L(rz%HmL6P7QugMr}HrzzP#MR{H&|Io1e_M~T(g zU#J%U`L0h6!(Lm9{q3U@Wn+P?Lf${s;h2kSyNtr??ZVwwI{u~JGck>d>M2K_^ANMc4wDjiiX!CC1lz-g2g98>Ik|%*{bpOUOeCbFYGsy#Ndj_$B1>6( zutL4^m(z%%0{yBI&9E!0f%m4mzOt8-6!s<*_kBFt_UMh7<6}~i%kObYZ13wRV#F+K zjbo-bbn88Pbf7@8p~Pj)07syGu%Skasr`Z1e3?Mz9^f@3gEx?+Cw)Q*4MTSb1(moCTga~`Io;OgdX9Zx9+5Bs zqbUbzN?Bk*kWTc9gF*RseWX$tQon?v2&SEo`TF<}=&k4X-7G5^j7AKPhf8dIc8+{3 z`1Rf;i{{tfo798jXmo!G@)2?P^bS9l24}GDhpiz|IYHE6Bv)ZOe_h;ZNWRMK2+%RT z3F%zwHwLmTgrPCUKI{9aIYGnfKqER=N5_?_Rk47rUxz^M^^wzNaQyZ2?rwP5QQ!BM z0i3S@l(!$qj&HXl4&)5%$~nq*(kh ze6YR5xlP0o){eQLF@1|?a)k1<@|gki$Gfq$O%(4)lPgm3xx?`xh&ju!(sjpZ)OZvm zqfextja=4Ch=+UWC4}xB_QOuB+(2NHJKCFygVG{&5WYJ{p`CPrj?Ufaos9^kVIhke zPPi(?R~pD^O`qp-YHA4`0w?>pea$%npIM-o%VOMaE<|`2iMXb}uF{7mos<%XwfUHQ z?*7Eti32q*xMzOmy*5kD`h)F4qgux?QJ@mc=+4#ZXl>2EL1XGfoYGXVvH0;BEsX}6 zD|TdS{C26nz>X@4XSY+vuk-Pf=7t923U@toN5)u!9YHmfy+uMT$5m^uhw!uke-_HsZz>-kyhVrbgZ99scMwK^78ToqnM_Mn%QQDKnPOSBWa@;88k)mGWdBx zCohD$Sb%i1;#mC5ovpi7g0r1L9$TLy67NQ)OFyA-S!j(1@;N~{xRbL>f<@sCvY)|! zz`^!1fw=i3;&6AtA*jsM(sFhy+OpKHSD)f41CTFIx*j#OjDNXhvARyn4N#8ZZ;_CH zuC(exvV_-tR}Rue(bU;_Y#}`u%~A>#Z!l8^2sfhlTjwm#CXl0E4-S?~C*_ScLSvGCo+0yo z_y^Lk1)n=9_1j&WGhvdow+6v`a+0>+{}2h(8SDE%%zRoRUi(s+F3?3Zs)NYyvPze@ z^irYZQVL5v+&pB0P9UbHF@SnJc6;jlodZe~3mnMS#@Y=ObtXxT$tD0Y_LqFvU0}@^ zLy6!=PJ)8|*G8N!6LH5A&FQ>hp$Bp6_!6H8p)8Y$sVTR;`A>WUg^8Qn?ZXZA?MQ7b z9cyPQSZLdM?(DhIcM*D3{uTtuQgS}6J3aPC7wH8B3@WRPh_J*WW1|vty;@l`SW(d$ zFL2oY-eO1c#}Aj-(Lk$311GUulijG)8<*b#gmL7*u91o=guEI{J3g#~F3XY_=(~c7}&X8XfJwsY#bhlbGu5r1Rg$#V|}v zm?@$QYf>Lk+y|fnEIc@W+3x^dp5q==Y&Uw3^5)syhS${*i6%^+QeuRTGopb8uak-u zERDOmF?JcCOlL01#ZVnZH3sn}?C|F56$`zDnT1shYKs_;)j_IN#V^e);4!MiL=$i0 z)RbP`w+`dAVFqKtGa;BliuO5gnmM@mxYA z^Fv~ef6k>)e(aQDbRXjuYv&<9w^X&+p(ju+9T2GC@efS=9V^1%!#&p#G>)VEb3`ed zx(c941;XH(8A6WNqk=l&*_CXd>58&|T0hlZ`8BD9f@?ri*3pJky2i4`0 zU7AhWmD$rgL~qau8iD>YF);~k2JgbfZOl<}>aV5bN5c8BzNx(czg%E*bt_)kV+FW> zH~RmE@c5pOBb6pAW%X3fi|PaKKo<+mz$wL;i_P#>8*ZH|RWNI9|74+8ByMS$iHA!_ z5lht6T8D4i!y@qJ8kno1HwgT6v5oeyVlCfbJ5WU|R?cyIo+nx{wY?49Xcel+_?zH~UD{$xq&m z)ESqvDaWA0F5AK8hC$UG3u#L`)B`JXBG}*h`S=0}TVVYz{2bMilbk0CZ!QLJaM)<# z3Hj}p8C6aL(Ji1;PD>QkJqhnW)qW;G>h4;{4TFrG%=;b*;xUxx4)|e)SBh7Fz~N525>7hgO-peK9M1muCHD4d@_$-adzc5S z0%abs`pi7<-#?u20#ICk*d;Egt?0ff86?J@Au>BN13YWv&qWsZDrPM$7z_zYlGJ3J zg5rG2PR;@gcK{lY#-yEjrGZevZ?u%ha2Rt^Nbhh!l`!#I*@pWuSNcX2 zSJLT!^NSGw-|W>|&=SGQ@Y=xW&vjJw&%5a0o=4YyMBLGRwj`Qh{edAeyd|=7VP^Jz zi|UH%Dw=!L`kR8PNE1VKIQrbxi*yBBTxfoKAL9>r>QBajZ1N1HseR?LdI!$DR;mjcpKnl}_=g_Uvc zRhbpJI_@9z&Fn1iJNV#}Vw>-Bre0W}9{o^CPNqvU2iVGxTlZht3H547x-VLu)XKAk z=$kS2SNd7%ySm{nlD+WxbGyRFWX_Q=1LM`8Fq+aPn8B25zhaud?QR-cTT?RB%dl+z zkPB%x6`zu)0#XaebhS>0&&Jmf-!tYm>dwYd}uP$>NSb1{sWNu<5aH{_zS-BW^ zg~=Z>g3WZpt|SybS&mbh(TtX}T+kdr72cvP=oKUC!~#H{H!!Vsi;V&-4B zJ`+((MR_Ssqv->K&wz~Es1K>$Yt(zW2}w`p=+G8UHvX1TG_bCiHOP0iaxUY1(-me> z%kCIf6+MLvmtVUPO)8)JtbYDn@ai00KUC{goE6md9t$RYc(>v;yM(upL(YSBfMwt6 zwtiJo$sM%0b(-)UJ(#KBZ&Z5MGH^3Ele%W1UAqdr@e;HS7us2wXY z-`?JdPLed61)S7$A01g2hrNRXIXO9yux62O)IEt%j84BHbI-Fs<cfCEXl4hRP4R}hZglkI zYb9y$WZ3C!Xs8g?o~7DDvM$N1kIA~2KzAP4{~q5)z(&0~mv-e&IAqx#DUps4Mf7*| zx8~(ZhQ-dm*1pOE)^R`Jkm{+Pa)JP>xbIdcHW5IxC}z?8Z{JMk3XL=XyV~)UU6CuQ zR@L_qC%)9nf18aGM>0Yc3gO#gH7A zK6h`P9KYcSe3@NF_KFMq8cDCP5aD!1q>2A=yH^CA+-6@amM%ctyX#AHny)YEb?_g6 zx;vpLj6%u!uBHM*0WoE}k0iqvaJ0NDr;BzxcI%WznNOV2(HU5^JKQKj=2kebo2DD1 z^RsrtKY)c&b1^ZQ<=ZT4?dh+`zFeR5JV&N#x!I3|mph8017`EMt<=)F&@6I}OTQ=T zL#c(c36-X}eEG2Gt@(arL4o~xkHuzMpzYRgpia253>E_h2LuGDuqKJTtl0uT20_Il zVtrf$SGdH;6*BnUCN?KbaV+-^Pr1(HhCZIyP-?WH5v$+cSDjJs%}+tH^B`pbn%yi7 zT+MpvkneC$x{Eu_n7YIUrjf@WY$7BF4)$_ zmI!)hXGaX}>E^-5$43h>tZ}cbsj1OW3{5{K!07*B@2#VveBXXi6a@qXR8S;DK#&gU zRwSgm8B)5W8&p74q@<*~yJ28JTDo&!=p4F+X5aYz{ob?Ad(Pf#pS9k7);ecx{=gY| z=AP%dd)4N(zQ4rwe zeYf7$)&{KXA*%qLRyad*&j zC$7wiA5vHx{~iYRGfvrMH%<-F$8V?)0tg1@b z>Evtf5~w2`fwse6%4^nc#K^?3#w$xpOE;W&T!l?&u8I_kg|+;1|3D`Vy#M|^s4qdt z%EQXW=Fvm4_onVTI`F!znNuL5rlu@KKfgVu;N9I@)%Q9?)RDJX>iaC%EI#&Wq+@2j z1Trco&Ign;_cvadwhp9%h}Q`KgzWtCs*^~6AnNAf@l4QZ4TK_X>S0@()>aU^y0EZI zO#DL4Bb?Cl+aFivH#W}ZE>YeWr%xi>&ikeAkYD@5C2Tbjn-Gn z=bGAoRoPyJps8Fou?)^KJ!`ncl+}s>c zQOsGsjnK!(M|wzZ_5uuMDm!|;xeRcPfrmXh(M*UNXH$kxsX z*S>_KSzi$m+nzP;@%V^%QZnKbuyOIo1#1vr&|l{}U#@=5%bMkLUS`doJ)QIG7rvOg z5k8wshm$e-nP%@R_KtmyV|{11U4C#HFVxvJTs6H+_Brjn=$oHc5IiecU&rkiJj#$r zcR!eMm64Y22!7^$4MQ=V8CvvK(nWRki(Kv2waJVX%!UbH`?9dGl%it_pM;*zOzR?T z+5qbAa;$DAGb_{k@-+Le7Q&{NmieCFpdkA-=xa!>r#sr?N3I60Zq;A&=)c{wSDPK{ zDU3BT#Z=(;F006(D{yo@xTd!3?(RzvG+mt|CK;=p53bR3r0^N(??<(*CUIr{AQ3S< z?ui}FMhsp=-TFATn8hnOt;nVD8tnrz_lQ)olJKwTq_0@F6eMMj>0(v6Z2DNmit`yl z*yqz$T&blfmi_!fX3fb7wY@!9S0|2Q610Kl{s%mSKoaJBe}4NT$cD%n)Zq!=YHFX# zs^wOqBODtWt3-HvycIg_xh9HJd-_efwx<)Lc0l*JvuVr4CJ$3;n`0q~{ z!GHe+TPf8*US}--4W#`ybMAxTqDNwA+5-4HRnbK^IEjxd(GjtTuL|}8=zqrw3wVC> z-)Lz6^)K4Gh5(x2k-$aApa#OH*wn*Kb3&l_Dtj!n$NgjvZbvF$JMtC7L~DA(nG{58 zDwllqrzR$VtiKL_>4ExQW7eJ+Ds(VC_lX@;$d~Do7k78t!8~8E=y}+fnT^vIv?OS` zo}Qm_I&zI!PgEEVN81Tsro@iJm-3FYGpw?AdGtKy+Y6QQM-dsMhJJX|Ja!zUc`LMp zBo8mq<&^x^Dr0=^A3NV3l zbq2#1yc(DWxkc?f=h2)|%h@WMSLJ1yS;1udyjLgRT3b0N8jiFT4Sx4qjoD3A39+)y zj%RNrM!bx@%_xz+r>*Yu=40~C2>j*o)*}la&LfY@G!qfq+1k2#T>;)KUs5|e74p0p z#Qud$G02BH%oaSYRL{+{VRmMp&4G6W^SPh(gs1OSk-))D<=Cq#9WttP*+dT&j!KH9 z_doION5mvuH|hyNHQ-oC5?T&MON$vb#{xgU+xgtXz`}y;9Yk+6#zaQrZ9XBNVtL!z}FVM0PqmdaCos-W1moRaP9| zM$>){B*DPpAx2A3aYJl`>(T5rYN@UmmZ6-BaND6)+3w;(&InD<`YuM*I&LG%ygQoP zL`InA1C!N@wClF7Cd-Ar4l39F{Lz^8xn3(>R33}p@zvGJJ)i3xNBAy|R{Z#JS0l7pqH43C-=ISx=UZmt;_f7!ewTzetNzas%ZpiDJg7R%*@n`i~>9wSoRl7 z^f0~Id`TXhsv;7|Qd)aX9*S>F%VsS^6IGjlU=d_FQXU%k8YRe95hE<^?yF>&h>xODjB-y#qz z%c~Ru_LdWKZ;q<|Pm6G4;~%O@FJsy@q$z7zfE;qGe`&g&+j4Jn8r3?+`Ee?xt6UL)C$9!6>t9(>+WMqt$M)54~&NoR<)CRr(uLYzd0EOR=uRh#Fs9}YNNAZ>T zRwNgP5-c2DU%=bjq3$FmR#xzruZ&g^m!<6J{sf@<8y64z5Oq*kr8yJj3kB$8`;LcH zp2xg#VFQ^eFJVd)zMChzl%>98GHcWH(+C9Ob0Ns$X1qV`&j-&X*9gJ?M$YT?ZnJXC zX5!?xP&vlY4r8!^srs=(zB+2_krp!}FWc|g{&6^bxW_y?D#~VZ@4N+nnkU6BQgx~p z=gH78$Fl0aotmY)P7r|+s^PK@wZ#MTK4@fhS|_u;qJC>^Y;<-+Bs$G)y3}q1e;AUC zy)%$n6yogQ1OVVl^if%wq2FQ+UzMjlE;}84PlB|p(BAse3xAftp)=+486CY(zdkZL z$xyia0z<{onc3_S)c3gHp0Ok!?}Q{OEEZ0H+wdv?+zdl)L1>4GV9v`J*vWa8pXjn)%PevXcN|^Xr@zm$mDq0z1~^tdNr@jw=f}~Gv~)ULLU^N zxJ|>ui>KKXsN?;}vU(w*ys9Dq7|&90#>0o#IM+294l>o_#2z9Hei`T2;<%_ZEw%T&cHST5Okd@9XDbj}~yHFu;g~-3EXPWhDRV1Bbbq zZ@$h{*0QVHAUU+uUg1VVGgrFNJRTrcrXYk$1IOfJ!ywG_ST!$_6}H!>KPj=O&l-?g zBr5EMtjr6=05nxYLEnreQ9HxuiqnISvLCJ%YKa(bu1#`m*DEof*8NGE*|O#e47r%! zSp^hDq{AZ6A{whgFQ*;7>vJFb?a_MU4uv1&e7abrfEXkpUqfYhNKPMzc>=myQW8i} zqmimOF*OCPgECmjs#5y1VH?{}6u!*w8{{O(Bpta{mxU~olIk^xpR4p0xUG;n+dM5N zhpSSAYnIfJG#QNGP)6o8a;pwoacKR7a{r3co}(RsS($+}rqDT_(-xV}-q5mHvPVxO zO(Vp)wzldP+DCD=m$yG=xh{;XCrsxT!%n8b83z%S+;L?7N|JEQdk^o8L#;~ft>N~% zjoS^v!ra{4lYD%7NzaD#O=$mhMleDr*o^-HXS=rQP6>~xq&bPzoruB^PMZ72H@>8A^7oK zt6%$Gqlt(pxT3VFK?$=jg+7R6xSbWyFORI3H@&$9wt(r_w)Z)!ZvFeLw-;XCApQl; z;4AMJ^Q}_DRyvimtgQMTHf+QhTF7lCB|LY zW-3QDk3%iw<;zF?Bk?Ate!00O$KRB5$sUrHul{ZlI>&dpw=*#YuS?y^%i93r6^?^u zZcziuvWE|jEC4p2+Zr*JtJd#%S#icikFP()g*;pbzyr+)W)q$06I? zWP!*xcyP`3_L?kA3?}#MD}qP`?yiiWx9yc{&@-R821-Apisj{N_sgwMmuDdq0%aK$ zPUA_JfVHD{vSQ*Ex}ML$*3!txJ!6S+=*L0{h{kdz&Y;FMl)X$6e;ug{KpSHhP>Y-I zGh;?TJuEmk_R=L;bjwr=tM2Wh1mZpzEskv+BzaY6M+2_5i&Foc=&O_FYaybCWU3m0 z6PBFRfp~f~?{m}B{gR9Ole{s1stgreCpb1(t?q)uA5et@0)aY>_T^QgI#y9VPHs4x z3bDa?d3_;7fK=ybV((EWy~VR<+f{0&MC?ErRs7z5)sH~A=&RD|!paub%(_5|7Z<(mFpseODaVfv0`iJ(Z9!E$~tyQki6=C@;@e zT*B{aWeNig?eQC6y$<7G?@;q2d1Pj=nLM zkrGOdY`LhRmkKpo$h)&9rY5kn1xJqTnfWz?#`mY+3lhi_@KLpnrZU&q@X1No-mI^0 z-ASOCo|_idV7X4r`A+O;f*f+B)X&e)SlUwix)w|OrX zI_tW#{%BIg!)kA=lilh}RX_l~q?Bu@296tt-V z>2_u~viDW_8+GGJ;cr0-VM%sW7RW7Tp)_S^0eyk@TJEr!Y)r6sS)UoN$H4GjdY?6Z;i9We85WXLA!8_@j!<1mWg!h_daSxl9cPDF7NT~AH^@fkljE=u3I84c|@ z&YOR~sOCT`gvgtEkn>UY>PCsn~f8~oh#I)PLu>d#s9Tb(xh=x}35tfb3aEOtpM@N{&ncf)4UlE)r zoym5t1g@#jyNQl#zHvvyZk{|^_a%&8MdEr?b`{CM|Jd4i*1BNk_aL+4CjXxJ)BpT1vN-8aojY(39jkcs?&um2;pX8yC`McSP)by8#t3$`@5D^#M6*HaY zu*wNTP`5%v1@fV{`c{*syi85pUiaThO~<}3-mxG0V(D5bi-~;DyR0r7-t4E}2H_j| zY6J+n(i9osZk{_+b>|o(XYC&I%b+6q=)TN&TGJ0vIzrJ*LWWF5dO~$+@m%%Qya$O> zIgUY$q>zFp1GoKHvly0Zi^H4;cCfRd9t&4cVd}O`9r}cbh{podu||Xhi6uv=`CQZ# zD(J|aA7evmsTRLeacHTfI&V&Q$``Wy5Xg~Juu%NO5XCSs>$_Jy>m_oXe=W$Sgk#|j|%=HG_=A;V5f)Wr(A5R%BER4+plJIP)_irqs!o2JIEv zkITCDhq}tHgFK|L_9UhcOfmz`WgOB3aP=YqE;M5{>o9?(QL|9J4$!-uU=Nyhe5n8ybYO6kwY_mCpRhL&4Q;t;@7@ZaetZ8GOf9s~XUVVNquFC#4Uo6`dHC zs`fuvq^;lGte=028Q+!s%GgxeGLFD#xXOkCcCPLON}`@V)$iB;F!v{Xe}8{_1}eO_ zE8^$(IU@jQ7d>BOBC~)`d38A{kLls``LL`P*RpFAWDLQ?I7EzRF4xy;sfFuTA zv??)?C~<%v!a$Xwtu%7K#Vp7(U`j`)6Y5~Ib#@;xnsJB|_!9u)@q;7}Dw@J*#@%3v z#RZdYjbcc0d_3vgj_=M)`IlC_$TAZC52MlzxgD9Wm}j~qEVi~R>b?3>hMyiqse2^u z_2<9Ejp>JD-i>LyR*Lf)EHxgNC^lKC5V+ataJxo@I^(hnjIm11UcAQTzxO0FNB)tnfMeyy*+06C>x3qWkE zCHl4I#4S;8mM713w7Kze{{Y0uJSD}fPfSdTlY~Gx##lilA}Zpt4_VxIy`aL#m~W|WvC1#;Kg&mOL8qvLqw*$ury4iHT zqqBPylP-bhyBDS5x{WaIvKjcT#(26y{)Ez>MW}bGyX9A+l9rx!Y_v^JS8)$B_vO8G zwk3UM^F@7+<>sRT@l0js^`TQ;<)4S@rszoR0`0)+FE`R~bOC5fhvg{9s3si30@XAe z5FTH3RykXZhynHM(}L7o*P>5Qe|nl{dmgLItk}TM#Zjf@!&sCU_#_n;@Z($++b5!t zkvC8jdqiUr8NXW?K^o-UZZ3zJvWiNt*(fI{-Z6ZOfkT-kUwau-X%*Vq<_uEHAnm=b zz^He<6LfhF-0?fTSnmYAJwVDU-J@{~phVCE@lin`{BBDhKsmOD-__ha^etBM+N!PJ?WseWxA5uX4*2upU_rNp-7 zIlEaA)1skiGXn!^X=vSLr3><36@w!3c6GMoU$-{(3Go=IS!x(4}>N z5|o6zlb1R|9l(Aj=mMMptXuzyHELt>?RUaooq3(z-Q9;%qobAe1-e-Fr_v7Y?x(~3 zmv96^uZw}6{vIAKp=j&j&UJTO<${v^+AgR876Q~~snotdCa4NiGO^F8&A(AYzB%0 zn=zfLSXPw~>6KXczVF-I+7g{V9TQz-dCADcvzfYhWe+E0m1Qr#UsH-I11bB5ql)9K zr%xzxG1Ao;87dfzx9Wa28K~YfB>aNZRd}Nmo*z+ufzh=SsLn16j34zI9gtaCrO;cq znd0(?weD=G=v<}@yD@e@!s$L>yvtQQPBFLxg!rs&J4 zmO`PMsnG^#snj43=xsA=Gq;7)6yvRa`G;7cMPQp2g6!mT+0&eMV@jLN8{ zv8m@py+5B5Hs`2fMJcB$GpI^uiqb9l&Qbux&qgJZ_2tXIKnKLtT>4VvM(7J%Y%GAa z=m??U$s`l-srNP)SbyaY{j-x6?*t}P-Nsv5MI|6TUMXKA_nO-^MF}fikpVJ|UB)_z z;ji1T-JrAYC$G=v^se9nBTm1-^4~&|N)-Qf(KBIhmqfS^+d8U&vgKG0k-@PB1Y}Fj z6H#KD$hyM`v2MUZ!j&3lh1e9lhm zeTBHPda)>5e_vfj7ivz5Fle8NlFrnjfU!NR*8#%?)$4(w7xsJmM0N_rztMa_bjIN* z4y-8PANIO9jZuq{1@))#cW3VhlRJU@`3;0)$RD0z_L_GB4lp%g5fRAwdD|>?IT@Su zi%avWDlqTT3Z9(Nq_{%2$?|)^N{CXbHcL-_w8F@ePXZNj4Vce74vQ>Q1S;@*|MZ;M zw@F7y+XFiy<8B`JabIV(@A2|@6(A)0FwoTEdr{_cX?5*A=yke+S(Nv8MJz=c_m_e) zV_2rXRE;O{LKp1U0buyuz7=dV>Nna`K~Uvl3@)eAL0wWR0^<_U`V>XYQKm zWA&)Lj?7Gx8@t$S1tQ}MJtoB9(0bJi^9_lJSR9}Ai3bFITgyj>i=iP;-M6~if8V+U z^;s59hn1M@he07z70$?yK9#fGAI3*=!4@!^z5WewZ$cieyW{$2{n`57myH3lfq~Az zH$d-iI2k|OcQozicW1Eg^TVHnPs%~Js%laF;X`JC_o15>{#`xyjt3OXML*ww6vt_&*t!zoAL=j>YD2CkRhBBe$tqf_Sfq&T zV*B@aEzzW3Gp3tBC6PpUf~(2FAo-X?T}PlG5h-`Zo6OW@%}Nnh*~lRE?7utahlC*; zcA^rmRTs~8fAU2C%n*?^q-$6OJCs~0l1RY)B$tdVfFMfzu0}Zv=?J&&gUd~CKa)t{ z@Ee3A*kN)ts(hfi{qf5>5yA56!x~15j;diDhwrW%xE_*~2a($xfv#lpNCp(ior>4e zhKlqfB#bS|986cSMT(4sa`0mRKEi~ zNkHonV4eWfb!*y@3)vv(+Vv?p%xk9X=RDoes@+Ex>QD`ysHM8A5P%@$Ub z<{qnGP1&7>-2NDU6Xd!dk?_-jg+3R!&6$itWq^SsH!`m|tr-xX7s6eMgWCsOwwNHf z?E?Ff)8RF|l>Q(ts#mNaC!-+KPt0MN(U_d<=l2#kIsu}b{f+y;72&cPhxB$w7;%dK z{dvL)0qob@Hw=logP_!EXlNL-t<~QZ*Q(iwxrPb{x-2j}Gb5LIh7_U8lpd{dh;z5I ztT2V&&(GtpaZ)`nzc2X`P3|*TbPRa)_3Mv~joO7(PU6~O^KZX^{siC-L5~IfdZ+b? zy+s~7f+hntG9edPEOs{2*V!TD0R*1MTLSj`su!hq0B|lqbWos7qg>7O`V+dw!T#pf zmLx9u#@3day87{X5hSMgh288P9N>533rSw94PE(ME^{6rM$8}2-Usy=|0IrxH8f&? z|L2W~hxd%LL|ffnsuNVg6}`w_JMsWY7vP3h;@sng4CVoj)=;ui&wfO6S{gvTgA5D8 z7Z5cC#SmhPqii97l#$lkJ2)s*#PJwd;jt0v#;fx4iOnrU*Z~1kQwG=pRJ?S%piv8$ z#_@*W4hjLsV{0jkTv#Hcqof2_!t_yH5FTs7Mm((xq4qe&!o#-wqQ2$?ILKK2OG>Mvl>w%9oA6nY zGDAKj+1kd0W~N_x6lf!0bntX4s;jCd$?&sAf#wvTT})*qIq{=%i|)sUgfy9%44KM# zJpe-vdU5Qm&+~aKVR7UaSv`WBEh+>A08DCPoh!w+Ab{0`#)@`zb-V6N#wYwVva%u_ z6(}2v>(*B?mdx$^h2eBjQ+kd^)&r1!A6zL0iUE~o;A#6%teVseFein4HYdP9tIMl( zwY7NPzwLc+1kiH{Qsm72 zt*y-s{;^S=nw}=u;<1$4j$e+I^NM6Djca62`O;CcY_F42FF6Cic2ccAM`NaV#CY5M z7g)-BlmP7-9~Z611xrhF7~L3M{n_W~;RYJ^tS+tC&NK+|(X>^3RL{S(S!MC`^aP|& z3ro49R^uk`O+jHZfDV)My3tW{s9^2E`w}IuzCMyLHh2xLpMkmRPY=4uRFv&0D=P;O z;2qpVD{KR=!ncWuNp5sUR`nDVn3{hM4z2`7w&>Tb#>)o#gJ=M! zj*f|`tAzrpF%2E%RK=4D8^+7pOZU>E^mJpD`^%&C`Cv!ns-B&n#y$j-Myrs(>pmpr zDIlP-06I`{Sq3{T+C%cW%fEfObA6OtdPv?bd&y&qaF|~Zr}R(vYuSdKe-Dz$Q>%N! zXvtT#bJ9E6IoR1rPtD;tQ%nk2u6sD6`wG-dOqQ$21x73ZA_NH}O|MQBVDqPJT`AOw z=u74y36>8D@kH&g(lJ?^-+L^`-8Fe1$v+l5zShLm2!)>QO4MD2{B(@A{uyCW2vIHU zXJlYta>lzouy22(h4=P%v9NLLolf_`9cRfm%tRKcGJI)m2Xt)137^gS6YGH6do-In z@rm6AUhz#N`or8n>i4d$b(I}OhA5F@Jy7x)5fudp2l{CeHRuSdgGuw%^8p&xNBh)K z*UJl>-NFLxvnEUMM2TeNIM_JSGTr_JkUA&eIxFz|7VZ3c$S>0`WW!DdIQZBk6D88w zRmv`!m;t1IV8im*kb_JxJ|n#+nFez8V~P9hy=9(kFH7FoDqn5wV?oSY=JVQT*UM(f^z`)&B9B0?3V_%jtZ_OG^(~YyW@TV-aklvk54FqrdGli>lGc8>Lot>L z_`6sA2>?WVD5?TX5U82Bd-pDwCi1CjyTz@<=%l3Z*r=p%KzP&K(k!3ApE+vD@#<9o z!KYidY>SEj5rPINTrRV&t&5M3$u=`L7XaJ*jA4iqkdO^Dm7<@aWTd1(>?OeokWu11 zQDs;To0!m!nXS8iEg|s*e4=s9oKj5XekSLRgt&MBpmOkCu3DG*da5>F_iaq)&*+60 zrGG?vdUkdoL38);aOYrYNlwm2$MPl)vmy{Ls_r8C$*sd?AW#j@8Fl7T_aU|G>^lUa z;>_`txa)YM+^_NWx?qCl&dyE%#Ysy8JL8o|<7?AK@mXz#hhO$cJXo-SQ z*xk_JqwO!?`JKs;pfy8p)x+s)hixlDvpe$LOcpw}=WJ}hK>^dya`%14 zlYSieOp0o`jOz&Pnj0I(!9n|YQ9-<|Z3??^(u8Q}Nma$+VNXF1XywDIpa3$fkFw<< zP_N9Cl7YUyv2H-1X@Bm&2ko zzx=3lA+4L{8!?!10+W~DE7+-{Pa>% zQsQlu$*iD(VNp&FfNb(Rob3Mj`c>uXylYxN&xSNA1Jh&A6OGOs0()}JaKNG0ZsmAjGy79WnuW@I}26~r=Q9$x+ zvSbA`fa9w>-h3d68%(}AH}{-|dsGpiVc%811zkFL1Oz1G8f;g$XTVh$=}oCsfv{Mu z%2PR2k8GZ`-X~O$UL8%<7c-;@QHhX4lzty_=1E>rjyvG@zDIdX&Pz9A6%iimzR)TK zn#pk^)Sd{qvPi|69Y(MEiI>j$OUGtGk>B}u^kGuTVe*H zzSbS74%4$UHYWEKq`Y$b>}2)^*v9debmyq8_^8B))kNz z7@l&X*jceeP(U_3v>5UXG#w-1=dCIdtrrx6!Ck!_l zQ{PFZD1!%h7%Y3wj(`OOP#%?~Rb%R^Sd_ZDx=@#GU1z}S8miv_IDPn%&Z!@~(XN7ire`D1MzFVdkbJa)%kB_ErLv!z z+CzJT>`uz^O{zb5X?=dMQs&7p{5UlgM&&W3sq->PxKNeB(`5(o^;O{gr_=dn3IJDp z)qFQme!N@~^`PYow?&~HQ-J7$rw=Iod42ZZOIfUgBEFkXPfWbX&H%r~z*@1zC(qSxSVOKJaE-nq;3wHpOOXSE1h)BtY$opa5$3_k`xRbo{X#EOmzcPJo z?qm2>$Bct9XP_>*RhkxBTJ}=kIJ-S8E#nSf&NR-bzRgw7f2oi_FqnSRnsLy4#GI2{ z^zDuqtg!IkSb%!t@3A^fNNsR-Dm29W0Yvrt_b!mK@_->^FC~MCLCG5vh^jM@06Y8e z-Pb&liZqEsskiPP9@E11qJZ&3qOfRWos68k&Ux#VMYCIc<7^+B^&bLygOPT)=kzc3 zAz8bBmh1%&%2J(ZMWaxWP9l`#DQNkrq!bR6=45^hp_KJNn|8Di_gEe2m~(*&9U<^i zK)le*#B5a$roCm8j`{bPk<2K`71@)@jtaYirY>`QSg&!i>6qz@&V<=1S*ZBi9gc1( zX4jzCwbY5L%1Oz7%|EL$we5|x6mXmVG;@?SZ6r@2GF57&0MhE;W;TPNlgQbJoKXDC z)pi`p?L=?aO09?5g|xHJL00r&e;-scX3#(M0oB8ROWek|{p9S7b6&gXQC+I66XD1B zn1t&KtIKvJ<^Yg-1W98;zoy;c{`~LXzk7Lm0r$SF%<+3r5NPK)Rb^v*gLD`m28!7AVo%7&-Eg5;$c_Jx4b9(wav_``qX)D zliy8=q6Xju{-lrh!8p@KAZ%j$)1fBue|c=@|Ap|;{};dFZ^+Bs{{M(xRfvIYav>+? z^y8oUn3RD3DU1+sL<`CLjc)eF^G9#)q^K=w;oiJiTKum@82yC~{ocpkFEY){jwF0S zit8aWzTB?NpmvNGqG#is2$^~Bs=vI}z3i15>|Vcvbr177vy%kp zYOp+BS*<$4!Cs%sDucH5(|RNpJr`kJ-oLvl$}{`X;07F)G74?G-a6-!7g)4Jwn;YAP>)7CYXiDUt^b@~^lntqPU7~Au#=L)YgU)OGSa7I%FdG5J z3oT!_vjcmF`GzTHg2?k5ti#E1M-J9$Wn?=g z2k0$$s))J1l`^IoJP}B73JWtnkOJ~!`Evk8RzKK{M*=9l!Y{2l+W=ob&Aq&+-r^?X2zJ*L~a zq8gusPgy^i;}<)bpo0?#Ar@Do42mMZ=ioe%>F)YEd&EbEgA){fbv33Qtr|^U6|9^V zuk+x=!_In|ZXInMR!NCN-*YXf2OtjZj$zizh?w~MjbXRR#9m`ryk&?=ssljtRK#q5 zf6quoWhU3w)#XrG84b>plF7}~5Zkpu3r^}!2JIEOXak7By_A$WS=Et9sR+<=&8hNx z_u%-N)7c??gtMAq^7@+n7ted&oG@CZy7pJL1PnQ{Q03epSdq=K>wN$3w4mR?1amU= zuK!O?Q6U8C79RfkwJfl+h3)yN=xMm#Z~g*bsmaF0#l^uzW4IN`%G&-i*a4wsMO-fTF;P&w0-&eScG%WZ;QSo)tcl(JbPC5;0~HW z`6NiO+u7PuURhh%z+%EQ)%C_^n9HilNJLzIl$NsF7yEo{)CY!=h1OFQ1y<0>?{~j~ zx)f+`Y^v@eDH&KAKA7`69K={!4?!0GM^Ax#>OkuR9jKTdMEDHoOzTr$UVt#|0S@+s zxA!-CLek8#iB#hbfS9=ToK?hkdy@@Qpss^cM$~Y;5YDbsU2pY!8Me25%_Zn%Ze%Un zqYX8^j3esicvWsY-O#(ZztWd^0Y00H zi~Zgq3O}Ab(awBgW+pK%N!WSl#Y;&^YU=dYzP^B>_WWtA--K?WVYyafb5T;evN@p? zyEmSWesX^Qvcu8QQ>U}A_fptHSJ%3=F5U|#1dnC#&Dm_aB4q~0msgr|Qhv7ut&~s7 zQm}^l+WOE?ZRkpx1-5I6S)ZM4(nrujpdP_FTSKGyE2gGzs3ng@R&Mn&+=z<-w!lyh z@Q^>;oQ<+eA~JA;B^c?L@`?&AY%Pm?Cjf0uDHgC-FfatQ*uA|&a&&-6E+!plarWAb0_)dfjYg{x|4gkoby%B4=w zZP-w3G98`&yg1US23@#kuXl~9RC86a(E!!mY=sZAQc7K&OjR_C;S+XVaaqb(njq$Z z+1cry&5a|Ss|Li~0P$I`tEUKwzcO}2f%of2f*}wwlJqkA855JeIrAMjL_9mKyaN2U zUz@0zSn0sKOk|D@57AO;GQ^AnBqSs}JUpV*Mn5+)dMPSCC&2n~=eCd#Y-~*3k%Pm? zL)?UDcEI2{DbI5dL1}yVEk6hcP0^ar9cXEZ(j~O@>)^|n^y2gLv#V9ub%a`l_v&`{ zphhyJ@yp49k3_UJv&B;sA6W>jkeNN;ee`B3dakOV-S%d@>$ae^H7+)pg`pvB_*^2^ zU%`2~e*)=!i*+Bf0fBgIYMR}jBquM&OwZWe(|1qAFJT^8sIJk|5 zhTYzkOa0ILSC`&i3ky3!badwC=I`Vb8a*y0bUXzjIuB7tF!yu$E8tb(Q<$2XNJ459 zm9)f}+DuC~NJ*yPaLk4_@3DaaMGbv@(0E5z8T24MYRy<0*jubUI}-}d9H84b67;_O z^5silaPZ8`%o0stHuUUlq~h?%N<&LWYvSUv+@b|KRQA1KlPpcvRu2J6K(rJ!osQ0e zA)Y8^NZGGblm>H7yotHN?%bO<-rnd+*4qWK7V9Wh{a`&#UzU*Pe--W#1>zf6JB&p6 zWC1f55Y?2o)swM$75Y~_Vq?EZI^|(!kL9zm+=QL@XS~O8EVnn7==K%x<2RPTs}omts)D=SAocEl4ueAPS?toefN_qB_xwTxr;MXe)7 z!=U#;N$H;0i3Qr+22w{;lPQ6xf2vEFJH{ye-S@jE=c6N|NmwF6Ab|2<+x7(wCieOx zIub~5Lu&v09ZU@AR-;KVR}C{y9XOB+^@0*;zuTRy4k(ryFZ##b1rbASLHg4bN=zHH z(s}WfhFfuV4d<+zV{?l`T^*z?*G|HGFn;{iYyc#V7-P70Ro7M?s2NBN z!lcYlSDt2Oy?%a5uVo>%@5|7ymXucF_p)tfABTj4Z4b|NT?+LF-}M*S&)ZV=)w)9U z=jGV6LA^$$aP4@aEJyb*MoQI+6SYPySzL@71-fTHo<-0{NsG_SKztj!Bi=_pi|8~( z2GK0^9djFXHO^iSAo`AvH1r2Y$2o-mbhhG4|2_BNs!8^Avi~x5(v!`WAdu!Fmel>j zs;bAQBA}oyrTneQeFigUAcS2}(}jw<%5fed)bupVz2jE_`n@2ShDDk2=;m;WzGL*; zb^54!sr}CMa?4NQ3O4m3f+1|5 z$y+z04h#nhkKFpZ+5YO)y^pR`&kt48If#u)e>EB4IzRW5V|r|1%|#n{C$Wh@=7rOl z3eQ94aKYgCJ2#&X!v*6VsxI!~!nNa0Bxv7~|1q$#vRK*2?d&#ic=w?eBhjW%7yW)) zl3=2Z&6%!WYRK)IFEall==>y{Ky;s}z=pBA8;Ea3%vU*LR7xi9u>rx%?(DL7_f&B5 zznt-Z6MLm;e;?=Jw~W+=zL6oz9P~FgvqAC-OviwVoPo8GwPjq4bK|<9pC;3@jSm)J2-DcTv~!~M>T%}wyFrwXlfmgaNt8@O{EB8cJQN+4|cqTk@ZA?$ft9KJ>;b z&Rm}-xa&utoS&H+p59NoLHEzSh$?Wky}9$qF=+oMRrotjvAyU1ss!J}mTAOkA3uIe z4l3_%?q)4F>qPzzdIL~LjEu~keS-mZ?|Tf`zRucyU&)`pS zPi~_N>m7)#?R}YT`uuzXUH#99|M?3q zNO%QWoZS9cH5T1|2JH*M%T;|P_UYe?EoxpexQ^@XUG%Ls;Ff-hpoTs0$TqC<<4H-G z3kr_hF`Ujm)T(`AWqYS0K)MTKk z4d)seqm$Jj6IWp@;QOVcxrC{EVqz5 zu@h7??ZgD9Wf}JEPO0Y4ut-4$y0H3RiZ7-gG%ohq+i5@5lymyUYd_mMbvzZE%~P}w zG3lsA`VwnZY9Uq-m#pY$?nmxa-~P0yw#7R7y!ENBfp_a-`eAjv3gN>ho5gl#{KNk) za8-%#h)=Z{>C1o-{0y?w!6zdB;aAFkzE9!zepoLA>NY>Jsh0k;A-{uSKe<;G8ryF?mw5m@39#$gb((BxFWyh5^~L!IO%TQo)y9_UUsz@7yxF zYE!C{?6mDxBG$rF8o#(2)82Df~8UNTG1i!00i$KCr4 zx=u0rSdFwH6NgVQkw-sB3*QoMlNMZK?UotLzN9&7CctDycba*dXK1qFv zR=Sf_$JfH!2uwcZ4IN2B%LIrw$~ZW^rZaNPc98l!+^6sxsb*}3j*H-A^M73Qqd6`T zS-rDUKG+l{>f^LJB)FM)9nl>FytLBEo`PaUujW&pGR}b`7sNoPr=FiMuhK?5m1;; zFrCo@;4@7#feBeU%%BU)t*W#?Q&!uv$*#`&Ko=aABnVnby^4;y~pp4YZ*JU>)d zHbK>%t_&C{p=-DoIdfBMDiDPlrJnEYV@^TvtK+EvK{w}b!e@t9%ZEB`uyy3BG`%=q z0fZ$^EVuUSQL$9H6_y%@_6I$zOwEkP>7}HQGs`2Zu;Ga1Hapwi(vaN#+&7W8;9^M~ zy=ykCk5F+ZC$;bX%Y~R_)A;mlqe@E3*3{J2vUA$n+EUZF6l_Xbd!Ev6sDP@Sr&U3V zxE=O|$D_`##~hKX#qw?=2@FaZg+!VjAzW$`r6V3!-D$lKL&y7iS-aep`_CCjpO6w5 z5*WI%;r!o;Ma@i3ZIam8vDqu(_&KEbraujQ%)$~FdkDQ}Yh!#GL$F0(xTrg-uoWKW+XpDD?L_Jf)DMwU>1rN=qqj13xf$Z+0yMT3aY z6e;tYLd2Ch#6CHPs;KwS!;&qUg3_o@CZwHA`P{MNnm{>{Izr}1e$ML~j4K2J zkw?b(kyr1oVDr=S#yP@*#`iOtHZ|@w@6S>@En6lWK!e3&llOt0UMXAsTZ_|+=iMIX z9UN1fEXD-O2q&S(O=-}aXhmZn{Xy3Eprs7>k>5#BAPMF&FSUxVEvC=5+eKgHw2m6s zCpG(4$RGp|9?^^r?2g`ZdlP%eojfYFt@WDqqx8$@^5_HkUYV7GjrZA2?y)^n?@*y# zine2B3(Z3OZ~qrLmODunCkmn5l8B585pX}4ZP;e4oM2V?sL44 zg*~FcI-EPCxG5V2N%X5DV%tNi6jgN+Pde_(Tg!c;-kN~LXV!ZjZ#OS;CC~Ar%hHWR zS}t-zfFVI6xB*OjISab;n1Y0fVS<_vN$WY^$epngs|9O_CzSYO?pw)ZvE2WOT#&+D z$?Ac*EAw;vd%%E)`-80Ds7zjOSP&VLc35b8nLOla@qFQ-Do$sNY9OBtD!ZX5iSG;+=v{=%WRMc)NSnY2NWg5$)wudhLlIN5N;M8Zpnvm{pO zSn(LH0r$7tSAnhDSN;M(nImF~EhAZ-Ln&2BJvEGqim!kW@_6Mq^ej$vk>PCIa*+I0 zc>#x8;^%j7?tddDq=ApN>WE}^EvpZ8;31#FK-u?x(KSZU(9b*Ja}5ihqjzL z^32e95&s=1 z_2(FHa}YhDD(Q|K;T&>SJ|QK z^fIs{fDS$c$uB9=V1Af`airf~oPDRnw5K;(Jam6}CWk|;9p9;^LzPD*>~iW2TmVnU z8eL2kg(`(E-EWT5J98)e47bmGc~k*HV7KIw*PuRt<_zJgw zBDG#LF$*{)L)K&w++J_GwJjTnjs$fH$-=^ad|vSzaJA3@UCiErA3(_|skj$J?xCBN zfjV&+R3p0Ii_wyXBD66yX4NrkId=-`n5M`Z~2gBY-0z!UrC2(BQ}B z&ysuc+CGekyOk@osAzpXxl!QA3xR`!E{8THPS?4p7Sv&8itspm$lllEy6QgA6FZk% ze_QF^2wB*qY1VVpb7>tktRAo8*_fPS24Zro0kwOL$2Ffbj;TsO_0^2joTDPI)OmBU zfd8k^lW&2N#(ot8R6eep_F&fJ zS`EOf-X6WLP%4$Lc;-R7B7poPBdvK*kSp;T-HC~27fWgFMK3cuu_A18U2Ro5<{jg; z<#P=m*#Y>PeAAW7h#D_PkKg`ZCvJ9XT>Nxmh(3$2(654 zuXzIRNIer(PEnFL+*+MSXN!>m`9F!F!>7}z-{jY`RPFj#H%I-mz|S%LJC-B=1jPggj{S1d z5xdl@`kct!50`-jPWQGwnloGf@hV511ADUXJN`6Rh0oT{{r0{*9b6r}J#zsY?SsYm zpT_S2TB?iZY653c+w0De!BLI(*{%G*WDfGagddIQf8}7i?CYTy6e$1TVE<0-L%1UX z;Cr_E!v2%!qvF5*UqOHzpmbtxFD8K|UO=IQ>hUWiHH}*`tTpZN>C;?mDI6)gcE?st zaq~|9DGx*mutQEx)oP3_w!|l^A5v1Xi{SFUcgm4tD{}iJA@-@4?Dmf&CJIz8k6u@$esGN=lNf7{iCs zux`V@H9(}$Mj4(}%R>}D(Fq#w)KT^=Br)=tn9n^m{yJXRuq>V+?Iw?g`|3W7)6sFf zQ)XQ}@zu+XDxk~5#IXZg!Q2WVp}KGDY-dO3>k_q==5VvNquj)B5&BGTJ00gJ zi`pypF8<e5k3-wSfku^4z7PDjp_F#%Nyi(fJZkIW}O_I}mRs?8I)xkd5g!?5GK zFMF=8BB^x*JQ`Np1wRmX7w>|yW%z`TqBew~-|5|6`Klg6zmwce!3=;Ri`k<0_P)6E zcU||#qVu309{IiH#C>MOU0msDDmn!gt*l2UAGdzKEwgCr9=0qVQC)Q{XpS9ny zOAg*=%MC@2&sa9Z9s#%$AMl|9WdE^rIv5n&`vC;SR?Sc5Q9c%wd$3j(3FQw&IR~6h&?wcz$jH3Jnw&CrPpoJW*a%HbRJZADfcmYAGcud9)rI zn>Lb|L4};BSi%K4>0e%c67f$=gv`dbi~dwgWaJs!O^pi{`qnapUq?Y)*B@PMH8dRL zC`S{2eqbgpyZ*_?qHo!GiZJqnyX?t{@cG(PM@fk~i5Lj zYP)2|=i@oPZ?Jshq`lX}5EeK>Z=|3+<#L9k%vn~}E_p!~2d{*G@SbunK@T;-MR-C< z><#hMyakKI4wjs=4#OGX<=tyEE8GptMwyiX6A^oi6rs~EGP}ET9Wo}SzpT$TmpHY-4zWu7rbSZv3}Lg z6g4}bR;&Ddc*H$0Y=OxVk4^mcl0TGHLLfxKiP-kX?c{xAgN&u@UQe6ruK}6a)u6?V zzk{9nhYOqRu&hj7>vMnDU?$AHD~qBSb)MlODmfkZqPXcx9N*PgJh0L5@%3p~aqzl< zK76QlpcJR5w!r$F1y)h*?h3GbWET#s}kl*h| zfbU?t@yNw~jbVAbDMTa`Jt1)87n}p6hm-5`ruzE&hSIluk!x!gb7~+^n`@60lfUxE zs&=69tY<+_fHlN!XOO&!05wlzCmsjNuW2Lb7h-i?fYAU6#rx8-zYqJQv>)dye_lJO zcM{E4!GXlZ{igDuKwDeNYU74@$te%0oik=m?=llk3;@@Ie4$fn{3pPECb!7zdZd@G^88!Di!8eN6{e`7@`6QR0r# zJT7_ygT|8G8O<(4RW;9Ng&;b~b$-H@++CKQL5x)cc6Ya?w?$7!CsBTJ=d2_9H+}zz zCCp`~zGz>&J?S~Lpynf#@0(2R@1^CR>D(>lwXEwy=B@n~;*kGcPIGH5+?Bag^Km{G zwr0T*>zKqE?&3!o?U}CoR;>+8*_Xz|jEoTwFRlbb+!8^PFV#6LmyDp~iBz$8wanZa z^i$Tt|0$wb8B($7Xhc#(yj(cmHyd81uRS1aLe+Z$_m6bKDda(Fp$C3>!f+DJKC{f3W_)aWfP!@(K zsi{Q=^=H6UCySBiknmq336TwKY;?&S7DL!^a=J!@Lrr=#XTOI?iOU58jYhW;4@L1H)81V9%AAXH{ z2O=j=%2uf2_=0V&kT#BF-3c}&QWZx|8_VL|MS$86(t*Mr(H~uGBIf1VSSs0#3!F`p zW-;B3hRwwY?jfR$8rs3fYhmY1FdO}lQ3*C;3u-o#)~4fNxvX|g*48DGk|rI81)9yE zK^cV$QwmH?*z3b8`z60fuIsesriQS^JNV325ikeAnc8nCp#gSW_}NG$RgcS4AKL|4 z=HAsO&<3-^uT<3j`ICf)=;GruA=8dz-y}r4%iUHhEj=eQr5=inM=(h-a&YU|`DPmPCMJLXu50=Fx6{h?GffMyQhho< zAK2T-Vz3~4mnUW_Fep$|X)NWqv^!FD{={H+#~1~;o4dnJCTv$xy_dT)9rD|Ags_M; zs+bTE3E%0TDo?Lo!<_QPl#nktOjfEmJH)=fHtJTcdP9L7DeWkA_4V;t;T{GPHdTCW z?TmUpI-jj_i4vf>KVdV$f)GoFF^-P-1o~5B z1OnycSg2hOoRc1!!x?uL6`Xdu8C(IY6rb)QBqi2`r3LQ6z>w2pDE$T5g#h26;n?g| zB@s%%T4W*XwKxV#>FB_+P*S>2^dVH8Ep2;0Fa7}hU48w_KlcpW-AIxiPN)aaf^ed= z`#b_CO%ycTSe(~!?x9-axe#JBu!n`I9j~`Dn&RB%8nM`Ei|M`__>U=GUSD5bb+}#q z86Xx3>@m-2ZPjc+%-XoB^mjhi?}ENRq%JOdr>AGT)g1R+tu zf$ytprj`J!5{lQ%m3bb#01> zb|SBk)O zL!&=);0q%*0s%fGK5l!Hd1=A+odgt5+a-EvaSp#$+l``?LzbZ`yg3QCJ=aN2RBU5o z;_-2s;!tS2ckzjI6<{FyO@C#a-p|hu7Z#F%ia$qvVXyzf>HzDPovr5JNKPhOH9Yi{ zQXn;3!M9cAla96jPB1*AT`(bY)Kc5_A3bp3!jDEOk*hyC5cBXiEQK?qQ$aRIZPZFQ zaN?sY%Whpp+?v>%6#=2XzkWq1<`p%f)HJs8i(3_L%6bZ@0!(4#$d@$shtAN@mF=yL z4;j}qQQ(5`Xe=ZH9C+v-pzOT-;xhm4;P?op3s0L3zeh6hDCjCY{9|*QAQq#|NmAS| z9}^h@W|h@j&>3~#gUl>`;IVB^QzrA`0ba12UcFWMl4!?dgxtz%U};Ha@mf2#;`BYJ znc%K7zxUI2e|wu(-x6Sp8Fe11?Z*q{70J*tzScEXG+3Oiwo4`boH_K^pXik4W0R2J zK|-`jWDvmMKAai-)x+Y-Pi-TM77lb5Cxxw@?Jk;)%@vdG;`3dZI>}e}ro8QXaAJ_>lYDEac{A_0zPZ6V z<%xqsHMkaFg0>{(R!yYwNj)3}Z4dm7;M}~o5BU{1DDL*+3_Oh66Zf4j?{#(CJLR&F z)hf+&+aP2&Hz3gW?_nsn2ipG%JD@MDy@nYaSIosRU z*Im))RF@QA45xqf#FQ0%#82oi5bvg{e6ZvCj(=Q~`TlfE4GjOJW8^T4rC%2T%2q1g z&4AyLp%P=Eh{!0us)hrjBpjYtNy{!Tt*xDHai>Yjo}5yxcic!QDY0bP>M7W}E??Us zJ-B#v2y1Bk65LeQC9(u>INmJwIgbMeSVH4ztGz*s?u?Goo0?kM!WrY>Q=eDHARinI z|1gT7G>9zo!cT$Rn^<1?wRJ6eR>ACKi&D(mS{U#o!~gOmHUH&FxUC{%PH5w*fms=g z*zMHO!%FO`xuX3}$vZ+%tJHc-UE-Vh&Oe7Ams;=z__ei7^;+-cKz8;YluAobg1|V{ z(qra`d?o6$DoUabS`oRGE{pAmgaHPt9uYSOhh15|--LzFN8VoV^Y+IqfBu}DeedCM zv0|%l0L9b+|BdvX_mLeo*r_gMmq)&}ZAD4(Hc0ky(Yu!n#Qf#P%A8S3{C=p9 zQaXnMM6czOD7!$uUV_OC`57`(#=}D}xIsy8WPrzPdZN`!3z$!vwP&-FX4ge4e?Tc@ z$7wj59zfA)7(lYVydsx7br2>*^@h*k31@$vQ**hOdNg}|y>a@93QEw?F|_UJn~{+k zNWK0(Br|jQZq#}awA#4=*Wkm)%XB9<_6u)w3)76gQie~S_)(tP~}>UJ*aGcv&Qv-#-cen*eWImagK!8xT2 zNBEVs=kcn4O{`9+Ji}cC|2Xy%S=-1Se$4|M3bU#5`7RFvTOBYf$<){+I1)wSv#BX8 z97^}?soeu>IuBehO3zf@AHyzU-|NH=ACJ7Vin53c8t9k|4WWCR^VAU$zrQUPX38%r zD>_*BZ&Y#E6W4scy8heW|80r#6I?J22GuJZsCR38bTkjGNR2`JI;o<Z?xbo zv-+rsqt(?J*X<4$`%Fp!9iwQ_9{?rck{uuBJv!l~W8(ZVhRp^1I^?qhv#=~$U{8}^ zfRSZTyCT-ss(Jf*#FX{~q5Dd^Q6V1tzG7R=-0Vq*^y@4F{Lcu`9|Oy)H>iY)5Rt5J zWQQUd!F*hv#occz26e7Lz=eAEId;FdcQ#<;3lmcm*2gqJ6hi-;oKPnw_1)i>R~9#O zx-KuMZE2abX7_4th5do;d3*|m6g~SahaaK0|NBILmzUhFKQ!Bxxo7=*x0Yx z-@Uxf&oPKUsW37HLm-(Ot|xvG1VXR8QWPG>B7V61%n1Z5EUZ7OnM1;U954KRbO!lh z(Pf7~u2^7U=W91^T1?m0Xmty+Yl`lJT*BsF+m)CPq(Pf@9=c=21Nz~8I=$!Kr%Dk9 z^5)^chC7sWf7V?|FP;D3k4!;M5_<0rw4bl8mP;On29?u?gh3-9ELKA$G6nCBkkTWn zR;fOFM0LI@KG~o+$Kzbyz;F-%FpfjPT}4DfLbV;KcmXQWx+ABo0B}I);cZO1Za3(s zdcDUbAQH~I`V;3b^Z?u~_na@0#le}7U-VLWi7DEqh6_DeDEo3W_*bLJ#+%~JKZiy9 zN7KS^IL;d>CddBXcQ-f2f+@^kU8h$=(b#G{#KxK$Eq4t>mg-?+h-yVE2m}LzgO2V) z00KbxsYrezG-EArc-YMVoY&Gqe5rx}go!pAulXJP zTA!^{JN!oVs|hm-zjDjG9Bott;#tNBsF4B4x5n#s`qs254Ajw$^bdPDv#t zIrcYr{kOMb2?gzuyIRwOi=-U3JiAq!;OZ1Ih&?f=D@50~9U7+QV*GSnj6nQ&XgsLK zD5n7nn=udu;mgscrRhUW$gET8i@Vxupfr?%$YJ#MpVIkL1Tl=RE=KC+dEkn2aYH)5A zu^rds(uX9bp7gJL7+RW}dorK{cd70Th9AzyB(xuGHiFt*fhepm{4ZPvf38S-QSnT! zoHIetaL3FkZBQT2KTSbEbAXh|wle4j35hpztER2w!@4_MrZ3UiDwJUvS$}){$0I+% zNk^s`x~gm56eTq{z{h_0K!Sc~3e>oPIJ><3dxfsI`&=nPnW~O@`(td})vr~;0=0(c zh#nR)VwWRoa`Jr^qjh@v-_omLGesmB>EPO>ALZqFDk`A>CI-hS9%kU_UNJa$fBd%x zuD4B2Mw(ArT1;8FUrPh-U9>k__3*Nj<>1rWil_7Uem%hS3>G>$H>@_l`WPAIJVIC8 zo%tMZv1+yPU_C7xTd|3VUp9XbS3>ng%5-?{T(y%(Q* zm5D#m)&`=_Hl2U_eEO@Ta~vfAgR556vCD55lQ+W-pcCZZ>BAyl?>65CxUzLfY%DRF z8Wh|)io|+-NnYWfj|tTyv(@E{DcCuCTAfjekcqwDJGI2+cYj-K!DwyW97Oy4`QX9F z|7t&BVR=Q`$cU4V?`1e_-SRpi;nLJp*LMSXQmZ!i*~U_8H@7bs2v=PlQW??0KYzKf zRZ$soC=;KYnW46U2Erer%#n{o>A-BJ-xa4^KIfJJo@&;{C%Huf=H@hPJ1b}c3=Agd zq3Rq#eY=|X57vlj?T+1nC;(n|L&J=Rjllnm3ffGw_2@^t_fl`i2kBq!WnPu1;;T0y z;}dj^BkzrQ8efWr|CvO6eMG<~Fqk4^W}amH*uZ`h&msP#eAAKL!#sPF0ilIJ#^)71 zkHcyW?5UwMWcM%K>|M`8fBXO%q#UoV$}BCw6X`kfR;yge{GTdye}-ZJU%tGOlLH0X ztY{*lX|zx|S4)5vbmJPd*^0};LInxD7Q3KXTHgPK12W?iD_lN&H(oPaxDrf6B+qrK zWnXJ+6)Y@Q@Whgd;Sp&XF5&Ys0Q{AeDTP!a0?|S!j(=Dy4;}6K?rug3`(bBocWuji z0#dNg z-IZ@+^}+B^+{AkTk2gJAn^ufTtxfuF^ghZb;`1hPbxAWjXExO$(_ys zX3wUbG|-;fR}u0)A$o_#b!&T3>Erc{j98RAIipKj(;cnW6&KvJ^gT@p`GgLu+1$DlZ(?GE=}KuR|5W3Vp8T-I2O+BAC!<^9fUIzp8l4_ zt=WPDPf=3%idLxa$GWtT@y+CXVFA1_XChuO-8f&m`a9(L_aIQMO5oGAJRp$AaxbN6 zx$_m-rJ?x8NmRtGPLSu@2;9MCVgG|;CiCbyIXB6qUemDfIZYP@qsT1W%lB;0!N|~j z0A+&*_qnf6UX7oONn9@f!v#2u$gn$OQ@_?j4bLV)1_?&NAPdnR$J8{!f6nT0K$jvo8$9v%C$l&qJ)GGKmft( ze8hn=Q9!0J0Kigft2nUlc^4?HhQ`9PA%zsq95#X0D1y}&v=?$QCL~^ zugx|ca`N8gD{fwHZVKtw3B9hfQK($%|_5H>U5+0H~6yBxLZ~pMYIvYb( z-Mzi-^*g3kCygyvx6o#eWjEEhA|SsH+x=7p`F+%s&d$!jZspN*j=ojtzJ!Pn`1>O| zE+4{=2>wHX_S^bbri&K`r=%rOkw+hYmV$=_{PP^pFff4YX|xX59>~dmFD+wmYjwYc zkg*mNo#gpFYRK6somOB9C{qY-CZ}P@dYvW4uac~%4CymJcm-c>Z`@s;u6Y_ee&Fx0 zERudubZ-^hY_@qJ-$<7EA-j;q@shSZfGv-r2ux>Ck%SP*G0iQk&WftsE_56@qOI#a zXG46ygjp-VrzO|g{nZN~-?PJoDvvKIu4ukLt z>EC=yGhPCL@))Ek zTQ1}Fi$E7C+xFyIv#mP)JA$;DnZmb;$;m_tTl}jYV@Re3zR%s+8vw810qR1|0tYrOQ^1Q^^ zKAIwcnJs^4kE^_?)pV+C-HA`=%P)Fk@qP8-Xxs0yLLM+1p0(c3)wOd@1A~k1Vcnb? zkgga>I^w6SAc)yF+JlrUaudE#y<_lbZ-*(}O@X$h%JbxPrbs(`-LEgzV3g66cUq0t z+5&JOSJ#m60N*zPK>2ELdHDc%$AjoRiIerB;OHnG0Fr^jY6u=++y1t0QiEt@)M;f~ z#=_x}_QkR-{FpF-vd8p1q~+?N-E%`r{XRI*D#n{fc(Qn`$;M}K8X!Q%EL$db<>^&b z%JSQLk%pUWZr7Ot*nn}}y_cud$e*wrF>|9vCA%AO>0=Ubb;%Tkc+F-@XtxzpUcCk$ zhf4e3+FpP9*2DX1uyP)N;wj0~!))8bp(bo&#&axjlw{Dnq?Gh{)E~sdxw`Q(X>;m$ zREU^LZC>Uk&4;H$k|1sfmP1ulgFWLhWw$_NQ3!ugHsOncsd&1S?#1u;6N5|iO)k@a zrHf~o*qw;>A5zR)j4cZ>qbpgqf1}5Z$xoo3S4m7eE$Jx{qrX!;a7p~7n>osN@%Cx6 zCv$q7d32?EY%ZE$dE6uR(FE7#cDCht`xLmC;ixy0CQ}Ui3KjR^Z1>p4uz}gQDVQ71 zs*h!K5d$*u0#^%$0xB94Mnd5}UiZwMxm&vK?%D?%6D`@)6R2mKTp<_r(Z1^T_o!bu zLsQeFsmdFHt6Zh`kn-mhm#(2{K#o9fa34Xnl_3J zF}%u6r+HvpXLJ5EVRfXU2kuM@|vse1=9vkU+TGk}aSz|heF zv;zJ|cx@dh5MX2P)|J+xEf@5Mrc<4(@G$cgs{68?8T*}>*CBBgM(nH zuqq}UkKyUkJ8Qg!s?G)ZEa5;g5)=nWJd>CA!jRgEe3s+=g*0ry&G4QVLoT#m{0`sU zmap4c3k4rvha-<35S=f(?Iwa=|*I7 zY-%kh|LefMo_N8OvXw2sIbfn`0R~pEdwXL@jl}p1rCD2fbmE2Nn-A9k7(0W8gLKXl zyNiE0^Fw2PwS+iDZhX96U0oz6XVTGeT5EGq1knq3un`$Tnq+6$iVQ9yYA7c7@Hij} z<@XhGx%w23LL){(yk8BAbQbb?CyK6a*cabhJgc?kw)iaK)lL);vn`RxcVMwg)soTz zf^sc|R$?D(eJ%bMgDp5JD)M66hxfZ{VDVN(Azd302gq4a5HFR|#yY=~_8>O@hHhg& zz|#SUM4Fnz?z~b!jk~L>>hL&?c3+Q**Xw7@AM>O7s*)2-&3&1rXu{9piehf(e2a}p zyu2hlZQ$XfIlCQrE*@bap^11Tg3Sp5gCk`wSJ^&la`A7_-D<~#j!K0xLoKCz2e%7xeosHJ-qyiKY|j- z55t>n*yB^{%h79dbI_$-QobDzR(SqQu@P?0&de5&P2zU?C7GuQoI5`N=l(IFdTH}7 zlx;f<0Vg!*`4Y}*72b(EKi@A)Pc9|P=f{E`95P&OUDuQXlm zZf%RQmsYYh+H}y9uJsJq|Jzl z{mpzxIQqRr-V`8LG=y?>|4pG8_U_G1PG$-V&(&Gm8>RvvS`N;nNZcCn zmfypVr~il0Lc+h5kF{$Ll2;|#?mK_LWMGPFXdtL^0FrStr=XY`OZ7#U63pclOUMAG2FtmI)>N4W^L%8+PX-+wp-u4A8%h+uJafTq!gzk zm8i2c+V^PF#{QcYL@~d(tQZs9+LXVuS8GzVWChjtDB%+Jx+Al+Nel=!b(zbf6Cl|R z4%r<<(lSsWn>A>lU>gW=sBVA(H9Hm+Q8-g%*&N*pMIa!*p_EXYY86E0Oc>iG1B}AM z6uGO-r)3N6e1=Yr^Rn55?ZZ3s%)n~;pv0n1|o&T;Q^w= z)r7DzX1NZ+;bFB8U%4I%WFh?j&Y2O$0!|!@gY9nVsmxc@Jv}cjh^a5(f91@etC@w) zWd0A%OxQqOeFM_a2oJfhO>`E{&_2mi^wt@+p3A=b}KQIz9IM@O{)Bf|9#W*4Y2 z>e7Ww!~zi#5PDID_O6aTou_&Oa3TP;ul~ODZ}rGEESk$bDm(UjaOdt9lD4|~1-QTWc`pSo73{IiRru%K` z7gq-1=mpk+4c52^&)b9G{y*p=x*5`bHy9CusgdMH&-j--Xm%)Pl(oP z|4q7qG`f%iHlVEh7l46e0z`*aZVjIZn=^#SPZ@F17gvEFh~>+(j7I>1bUf0l{|^f1 z+K<$-@Bcx7Lqh#w5Ip*k;hSu_N>$`l<{YKIWx=9}^RLaG1{`6>bB+q+XC_-GaB~Ms z>>ogZB4i!CJz*0>1LuGSm^wqujG=6KuWWgg`%sEwE*MOKk3)BcTe6ff@VBP{V`+c4 zqYU<>;Xf#7YZLWt2mJ1Qg_e^;4F^)AW(OE#;-J28|L+0BzJbxGKzTD67gvDD<-~7T zvbt8$Uha(fe~6?j8L&vGrLA@N)ydvPv&AX&86KMDLV^%ARO*Et-|K-gzo@5Dr!}gm z1rPZXG-$D8Wwx(y1_X85sl^+{T$y|y-Ts4yOL1NCDXAtBWrTQyM52B-%vt_{P5 zI|3n!?#>FIxOFD_O%d%CVww}p47Uu!K|3j%+;u54Qm@&9CBw=hCJ{RF}0WKSrXR0fJ1Xa+SMr3Gh zp3|Lmupb!&1VbRdN%_*_xmHrZLH{@L2Gv(_6;g7(tqE5m3VXob9vSJ;Qh2Fa$t{-- zOIH;$wkMYf`RR8)Su{8|<-QL4ASe{ejfV`^9|Z>r358m+hVP{m+j8CFc+cMnUYNxC z=`Bwk^elWNv8+mCXy?IXHW2ip?84P?cPD&_XPfo=tLaKc$wt!adR<@t*jnb}Fe#`R z?OJqo^%MkB{Jew%V3_*^U3&dYD@vX;QmXD&i*kLQHq-)l12-{QO{QvSN|L@&BlmYh zY1!rTC)=J=IG{%_IH#83$VrvGpws1KZ?VcI@`V=oyu#JS{r~_0s<$*Ta*H*FROiy% z>~`9`_jLy!-BH3BdLENue|UP>OSTTRyqnuN;)^<$qs8Qw4SEzK_4i_$p9x6P{eeUv zNc)q|bI@_VDRq|xck>tlq;EXSW^$qqAb%kJV^uTT^OpR(C9uB!WD{=Cvm0yrKT*IX zD?Kb)uc5t^C7qhyo2>LdD}{j>!Wrp_j4|SvVV4hl=l?*5*z|rt9MQ^saRcM(x)$v8 z#_!|;9S&0mzLTx5@xONy7Y7j#WK^4pS3iM~Bp@xBYKAI{VOq z0C#L_0!TFz_gL@H!k?7?O<4Jd(bvECw#0MZTzK~K))^~^-VIMKy%a(+82gtfBy66$ zROK&sS;gGC)@gIb?fSBwr)N#Yy2a_Fg(ZWm^6H$$8iEi^0z<8Wd<-dh?B&yTz62LA zA@qZp|G7K=J-OH3)77ijde+s~tSda0vvybkAFrm4j`xBpyOfs4x{ZzeqZm65W7^t} z0?k?0@wnHx<*Mvx=VaQN(JcOR|Z{;w}M zIG86JolK+xzE!sbsU3uTd}7xKDI?kr_}n_S6U}*(Mcn8jWmHrMKHNN{TF+CkFq=D_d>Om(uMHRkW!|br~Be6EPNiBoK z1g*u|U&(8-qRDkZ_cPh=VGcb9Ah>=wma>^43scP~ZYxcFV|cxFTAILl*&rZjxH1`N z{*(5$sNi`S$xR-ol2f&nEbws-x8Bgu%;SbOqPv(S*+B!(W~1N#ow#g}cwk@n7hMU7 z>e}wZ86dn~?Tosx`z>OB?GN<|dCjLo@z79j#nCb33Ai4@0O8D3-n+R+;U>=A|bma(hIl+%40uIG$aqkz7Qs$$F`T3P3F?lO!dQ1<1%at|t6uk_e z?~(HVHq1lg=eaFEjBAJg}>+lvQ`_R}_l*vAw)DiqNd9}}9QpMCzb6H#}Nr2Tf zyxhOm-(Q!zm8*WDqv1ZZ))Qg^{~kK0LRoVn-;AP)hUw&F&Jxv$SsmuPkXJVLM{9{Q zXnzJjRr3t3iEf|3%4A{!W1%7XbtoAR@D|lYzdj zQN)Lb!4Uw4!N&>0rH40RV4jUlou6+#Ug)s3w7k7h8lz{mdbr*bo=_^41a!o)H z&^d0W%#4`Ssv6!E1_)<&5K2TVOX}0z&f^HKeEx5ZQ2cIhe}6!3Y%EBnB!l;h0M6Fd z7@Sv$oWv;P$`o)L7lNKzL{Nal8fq5Z`Aj-zv|ReLsEI56H>5WgXYPp*ju1JxmWwg2 zC%qr$on01|Qp@pCUcI4xb6ZPB$hkg@A5+cNMKdI^F^s|d^?ZOwU;)Lipi{|GMQ7;I zF;)SpRD|kht8B!itSalO9^qf8KQFc{3;S>Ax7N9ThBP;g8Ss{hSlT{6UAT_m%STFR@z%zXc z_vE6D6^}LWtRQaNx4gJwsiRC(#(*aJtb|KRWpK>(HOB9+ce zAYCNgQR}!*j&x7AAE3v<6WW1*(bb|iQJ~F(&wuTk_ZfA=5K%Dd1wfKyLJq9+6+M^R zTn_J7nt&}L7Y~cIq8LG3SV4l`Vf1{h3L(g?9YhXa-Q=tTasUxkb+Nck{?gm|>P@wO zvZAY|R{qL-cKFp{aMBkldPK|7!R2ftiP;>YI@~00End{3uGz|rM#aTdldZ3}ybGWl z{T6Kmn6ZnACx>6iIKf3x6N}wHUZDLKXDmg0blc+o%EXn*{Xr1()mmNFq{dvvUgMWO z5#=nMy+sCEQ;bY@DNE{KhvEhTZ&&$(R8_SRk!8zETUiZz!O|2{-+_6I%t`(4V5d}Nc3nc-jQI|6F9O>u=D8vtHhr|lZGY763@>&lrr|qImlAV*EU0O z*}ig_Xy3I^pPyHIsp?9c==Cyl zlT?w|+`;5FJ<)7=mJoe-I9^ydUt_!JJA|{gTBUKM5K^S^i=*Bs_#jw&C%dr1cJ}$R z3b?4Dm`Kox-O6fm(oZTWIxFgrAQz9(y`9z4k9u}v6Cqc0QZ98zMomXH2S=eFdxzi6 z1Ek5OXGDn<`SL0}#1B!=g2tb);OJGVG4YIrXsP2t+qEhUT$*a$7xls-FHX zRsQ@{8yRH41Q|uxSW3MYA}q<~PmQ5tR^p8*gSO|o^PMnnR#?(jed6lG_uC5n^<6o@ zkeMbNze=+N%1?^MsK=(CMSCm^cJ)-#)VZto1ITQ2a~F!Y z>K2Fl_t(l@{}xiz8MtM8o@oIy0HI!8l~)S*Vp(m4S6f%()82n~+H|=c^_d3fvo_;NaktLJ_^}?OEGCVGBxo=7#SD7{~HLZaf&B4+Bo;f|8*_E+S zZs7VbBudPX>;aZ*OBpwBGGe+a>qxyEgPQ%S*7e+Qn&NYUxXfvKU#@5q%E+$uKxD@) z&%X242_rxUd71^cUpyop`5#{VeXGjpIN6KPL%c02%-rh^jnHW^dRlbc|9@zE>!`ZA zCGB_P4#71LT!Xtqa0za~-QC@S1q<#F+}+*XNpN>}w{Tb9KBxP<=j$G~zdOcVf5OmZ+F?KBht5`8@!pqH z`wXI}zMvlgQAsGLXM3)zVINZ`*P63Ml4NNj_uFWMRcQvPn2bSJ_vSEgMv>Pe&jAt6 zUp{nBl|Ufsa$6-TGO}cMWl6I;L24{B5(v@XyfZT&(DX+zyk6_8cjq+UjeOB7BKP!s zV=@G|D1?21^78TfD?CZ5kd9pzRw8fS{1U4ysP$3L_$1epxt)cVwp9q2cJUNLjm4DU zC&gPDd+mf+RR=t)$Bfd(&b-_y6>U%52ps=Fm}+O*c&S;PwiwcCV}!diQE7zq43*ZX z*^@O{`~F;ZRyhtSd?U1&O#0jJM`zacC;LQaZ}#y!2fX^pVZ2Msq=Uuj3dyU@7qEvNz7Q z1ow`GMvD%V1U%+`U;9?i<$~B(=Fd)XkFpf;zEp~g84Uu)U|XzAtC$N{=mS94OG#0A zP*OM*|IV{S{*wvd5D-QqAj3#~TWgR`59``>0A|x$oLOQytEkP}HEYq1y2w2}c>ujk zEPCaSNGdccSz;fvdg#fj)t#fyw)#unTcQwdB&aD{D)CshnhSONU?&ar^=-q!iKC-M zQBeFoJ7sD4AfTe_mOa%_9=Yg$OQCOhjE;V7Vt;v=no4IFbaRJG`YzPb(RHoWm5tHP z!!)dKJLUN{Q(nuf?O}q6qY;OeRw*ji?jGlh_xV%1=RB~z6=|)&aB!?L46W~PyenGU zn)qI-Hb1;;-Ts=zA%qqvuj&s62}N#VYFqB9oaf?xwR4$Jr_#}}?WFeRB30nkanw@G z(5W1$U}g0^85Sg+SQJrI0A2g%P(3?KX3W;U8IIGpjx5(yPzxE3i{vjI22$GTSm1~j zL8FU-jEvN!jFV(x?jR-?UcZX0xBvN-RTZMV+*!cuJS(g6(JDuN$hZb;3z?>b0Dm(5 zcU48jHS+Os@>(WuQFB_0(%ixin}7Tcs3@nzrU2yIDc|^7gPC_lC`o^wZf&iQ`H7@L)hz$BeLa2@v!}= zDzcVu577>1v-0~en~#U$sR&cNgQ&ME)cSOZnt}2~iMh`kfl_9=Sc>TQ^vPiJy;&@p zJC7ZdNx90cFo)(=WqpYbHOrRr2W}O4{a?9LKQKp)7V6+Sf5RigRJQeKS>!&$Y%ReC zfit)4QVd)ICn;IW#*d_+eCh6OdrJ!oWI`@QB}d))M(HvDkH?xhU$_+bD(v4_C~mFo z{lz2L*H(Z(tMa|chG1VmKol}MQLU^_uS6_;dDr;MBJTUtS;5LL<%LBBx1b>XalCc~ zE$z$Oc?mgj9m1SkeeKAdu%V?drDrd;f3MN`78@^{Nco-PVjVGb-mes3L4C%d)WrGToc zBy+Mt>iX8_?@E;G)&}%eh5uv$%!^fg(`u^h?CfCMoR8;!{16G`c7I?z?+&!eS6sY# z&=K1Fy&M#>C02Wm1kC+-RC`i3o(K+zu2)NG;YH=|%3P|-6v4EFSC*r^i8Q|ht`9)oCiPdTi5malE zLKk2U#Uv$-g@ruLWUJK?kAV^Yqy1x0!7d9&)&yE?HuP8F<+XOobiQi^C8hdsM-e4Q zS=GIQhK2&aE_0Bu@IH1@`V??yNrfTIOpI1X)AtsBUdDXOBN=nCtW$0!)Qq~Iv0Y#;8M8j zw~pVWMyE2V8fKZH=S;48-G$=u)(+CXgHOkpLA23OFP;ITc~eoP!bD?4L9D7PjX)Bn zpk3bb;?@t3h&w}{GyEE8LxO} zV~bo`baymEa^x)~H8m{2^~a&-R_yGVfiS)}D{|KX`FG^7XB?J0LvKed;r%+b`*p3; zsnGQER>~_$)U6cE*ow09^1^$!NdDf%rbf4tD%H1zN2Jez{BK{@$pX?e>*URz4-{f_ z&*9?14t~PuS+7dd-IY`_%=(l&+~dRofiSESytLGl!7~6s5Hr-h*!+f|jqRtH%C<{f*Dw-WgFD?R?y=h*) zT&LBq-qw5NjuqB^H7fF7P|#g4a@#Yqm2Q0@&$8X!W7kt&QJb4<55=JP`~ui2&cg^k ze#~fZ-_FTNf)8@JF}LXLqmr3RyKiQl`k~ZZv{WwM%XNTzVfH+d^ELsJ@EV$Zr9>vHgIKTsnjU?*{hMndj)`gj zPz2fdI=?KXXMrss-QD`S(s6}{M2mLBZZH&bA+lVQ>^*ywH7{&Ua1?$ypF_~@^XA7H z^n&)+!F*yoQYf1I2DZ`+5eE4wSnp8r3Jn_l31%dGAXKDDtEpN4%E)WYy;es%Pk{LH z&WaBi;PxZm8yqwLl9EYh<5`R*sueUc$5yMats&S>ND(7nm?Lxi_Y&p@XH z0g{@jtj$<6+s=)OW3~UMhAbLwRRKD>_x{!kbgNJef932j&Ug|i)X*Q`#QmNY`i}$zwet-Q6;J3wMIi9rac>bq&Vd{w@MwA>f4O* zJw+5&n?pg67T3dPmv;5HE~Nt*DC&|oU(i8$^wd-x{Pmrg>?dSBZX%Tx@ z7{3AvGIITS`uL>YeyC3&Tiel_`!FC$$%hSyqndQ?pcvBSW&&>3&bl44%e2Wx7yB>* z<{MJaXa29@!o;IvFH8&^`PtbkJw4s?4E)?4De`8OQKKA<1o&^|E&HQrmv;mau4~vGx8}aF&vNgwdx^ZT~*M~7? zM9PKqDbf0-d(7nf-IK(<=~h`mjUeClGjc#6nBWINSpe)&KC^&`jvk!Or4dg-+}i`$ z9isL0vhjgJfQ9|~Yy&ykb^fg3hVIgLprW{3Cq_shfIoR5@&asM7UGj$TpAk&KksWL zZ(@J$NE>1!!j#@bi%R7(KKhp;QKR~Uz`)#&KaQorfHWKQ)_CojhsH!CLTSd9>eU+% zg^91OHg;#}>j_PgExKEVJe5qRRVYx?(+RS|iAsN~QCL22-)LA`S}Imy(q0Txf;JA} ziZKd5Z;s&-a0NWL$tG7Z!qx{vf@GOJL@=h{)N~buUh?^6*GE#FBHJmK= z$H2fy*;pibKV?CClN^}?D^+vavR?{ZCu-;YnL87iQ)cIz2q>PEC1270YKMo#gmCwA zj9N(vK~m2|Y_=FD?t|ael_O|=sbD^XXAm5G>9&CYnS;R~!XykFyT6L>Npf|FxKFkzX9g_iJC<8bEwJ%hwVfBS!k~IembrDq{Z;hqB-iax0jF-_TeYw{~8DZ z0!&_@{{EsekxCtq-DUs{nWNd0qqmqfV~&k`t6{$UOmA5*9FW^(P7#2TZ1*^6x(aj| z0u@qi))}7Qvi_!$mX zIn$bq=Zy9Y!pzPB(|rb`0LlI0+*b+aSn?I*T>x0q-94f>J@3|m*G!D4L~qOEdS?hU zqIr2kj;9B1I>(;^aTIWPMDjv@flfjm9^0AVyW+&hcheZO)80n?UZq1Dy<_KZp)_F` z@`c^E>rW$_UHSHqYT%u0%1bI2V(&Jbgp97Fy}~d-`NApnZp+&*o+6;b)!?g-NtO&4 zqBhz;*u9*$Ahi$@BnSxAc23{D>?%_cdp|VonSI`q)&zfB7fBk{9~&)_b4c~NU4-iG zrg=4-{-Kck(luM)>#MH*qYRP5M(?**Xn!&@l;hq;>-qP@U=)GA?m*DgOh6tP2v{Xo zh~PqU%rbj${M+{h$TPr7c<}1T?Gt*nb+I9g9z?h}FMnT9jzp^l-t}~=UI!~ho=D%{ z;{31`?h*&f<5H(-BPbI+or@3EwQNhugMW6mP>&D@4NwdS*jpF+KvI^*!DNcOH|Bpg zUpty~SObIxS*+exW4QEAL8N|A5X>jxJ)kUlTx&07F(GKEzb5s1zDcR!G2VJ~q?M>O zi@$hOJu>1Vs7S~Uf6~=XPw%m?sHxD=#zwy`5F$>|V-x~GJwCU!iKSkb19Cx3ME59d zL*7pG(h~UXPL3iq!;swV4S|A=&je&K)v>~|baq!b0|^^uscLbHKu%(NTa_nAPL8~G zq?%55cVIFfej}Ui#D{l)y=7ca>8H#)F9f;~&C&OLO3p_msfqzhQ%TtR>RQoIC*%B8&>2@0NCw_xue%aQe z(6wZv`BM*2YKwdbE;j}^Se~L*TWA=%iX#l<{sb{G*!@{?A{lT)kl@d(V-^c<)Wgx` z(?2JK37gCY{CKl43y6Sz@ceXICP$(aAJjA+x|4d%o$M*DM{gG`qHQ0amj@CQ26s8> zn#jP2g9WX5p>GfNu4QsV86?EX1ZVm5#FDF5gBuR$O?~+Gov)aPG5N4+^UZefj;Yb+ zHy+*{nqOHNaJ9!hh1{Z8N=&KcuRs4!P%uF4md5y3u#c4msru9R*#hhAFSnYG&Ps29W9q8w(^&0qWGzY=uWwZx1CSL`fTT`a?_Qofxn>WRYEX6<`Nj7 z8|2HqRU7p-ue}lErZK>k3S#$+!rS32Y3`iLUu3PHKtIB#4-s+RX8$+wZE~y%2`x-N z*ksRX4-PcaM=&nS-&b?R9=95lh9Ag8$Z$^Wogr{+hz)A^Y0c%V(>*4Ev% zlJXRm?Mg4Ljgw|XSxmdT`>AO}X3t9tZrq$V1Q_Qlx|1J+6`;zi49yO@$`+il6?*o6-yt6hW&&*a7k(rSaf zWro~BfV)gn57slxt*M^~F#1ISIJJO4vtoPNfMX*%qwlXQU&yR@Z2!pje9C8P7)#&x zyYdBlmi29Sf6n;h8uW(D+a!YkAfU%nJs)o6+pnjGJ=s2m%!8L zh)7#%;+=~0XzyM@YQ#;w!t`ia*$NdzT8aknTXxoX0VGK?Jdm6j{gsPZIWYctvPWxY zcS`XRwrjP0ezicngc3GX8-m}L$o@^lM$;R?8Dy#7)tOmq*T^EIH<<)&4`sqO=pYjc z_(te)622s(sKOl(g%99CUB@qDehu+?1$E7-?lS9vOeUfIc1NdI%AROm7EP;anA2^e znZ|7H*F@yh(nYA~Xe=z_prCGuo0k`gCh&mG4V}{9^b4<>pU>;Y`T8UCN=wk3>ct}t z$LH;Xz2F-sBcra!mQLfStIKUE1O%97E#TgYg;8(D;pJ8DDq-SM@8m+tmarkC@=w8D zKr>b@(@PDwH6{>JTIw?uCNo2&y6Zi$SI6vWPvBAqd{CKOSNuyGAAFEST})cz=(4BN zpzH7q@RC7pZi3pQ;9vt5E;C&NVz+y7G1>NH9T|Xh*7qPr*3;#`(|4<%x+iNvSW+PR z(ktJ4Gy9AB|J3?DWB#G_XFq8f^UCSku9m?}sbQ1BvJn59)~^`}X#EPVem1Jq@i#c7 z!*4geO8RSgoy(=abMa@44a^?oeu+}8+D+KYY;^U$U3j<;omNK8o2r6BS6ciQs(By! zY5>(Aw4&i!;`9@0J-=FE8Jk;xDE9SC=d|(oYKJvx{}WJKVm?a1Cbd4-u9)^CKAk%I zr*S$eIDAqSefxO*{Kow@onWwat4Xb9M88B+ZVs!#Q|o&rO~7(i&Y=`qW}Vx10{wu< zbz5)>vW=~~F_2FbC2CxgQm7lwaiNP?Y50+UvU#dPDWL%b%?1XM)A8wQiT1i6kWC}^ zzL5HQ4_ki`nJz(hR#Rb_?qAQOe}2q)^EAwL={Ui`cDp9xv0zwJFg2>7IiQVF4+i?A zBqSS$VGZmjvs}FUS#OYj<7xIs7?Bjp?;x5>)sR9?a9=@uF$$FkU$=lDSF0#%sBu6- z@f8ISXT|9S^=_lErqs{XTKU3@UsU9*B&RMv9j|&gdd3LTSkkoEVt;BNZN;(8mzh>p z&t>p?eb=c2S1@Me=%$s%ct7|!OVKx*n9h%p|0lX=@WlIbQeFZEh4jyYk|<1LXAf&4pVSs7eu!nuuZ&EaFE^V?EpRQzYr$*jHgqMI8SwEWy&c zIMZM8QqB4K*Biv6a#R-Heb~n4gA`yTD{3RaS_|JP&V;f#ak7k)UDXU8dJJ?@Iq;RoPa8~_mPvB;arG+Jp zXA2Mqr3i{QO`Kjgz)Zgnam59Q1CfD^95uCo93h?bqcusW^*sM;fj}Xv1qG_}VN`;! zWOOwif}E{Ar~41v>2Qmc5wMWnY`Un?9mYcYZV(XT%4aQ4RORXfXErvJTRiGu8`5Q* zycKQvd{teE05 zBoVj}ph>L(StP_mTL!6h8CJ{Lrr4i-(*Z{inf?YI6%;u6nup(Nlh^Uu|cXcjTWG zhF>L8M&&|;)kB*!?2NCO7$A`3b%4k$qDqt1==2yFfzhy?mT+eN-%#?~cIC745pI9Sns@^VFUJ3UZ-rAWYz zQJH4Q_MrU^Y=iCKM8?K-wQ)EFqJ18W{}Zgdhg0smITc-$W`eef1vbf&=?DAu<)x>Q zUBz3=WOhaK_n_>qF270EfIxHv_`7`V-#zoDbi?6J7$N!OxBN`Oa8miV#>RzW(Hfs# zj@L~cCOS@?o%;bE?287cr$^+__~PLx%FiK@BxL7^slVSF=?gTd^TGmAYjguz>A+!p}Z0VC8cZ?rQF5sJdijC z1p#9vOiaEX$|bld&G-~p{v>?{^YHQ-;N}WSP19^S_L}}k$Lscdhq`4wxYRP=ba^~G z-^v)%Z8TcWmFoR=Phfx>FeSTketc;0leFCyrZSb{%HhM^=+Mzny*x)pY|KOFe3^V)>TXwC@am3+nMyHao{pvPg4f-8IHT69Z4TcQsaA`r=|1s`7mdD5CAg7d`5>m;<;X^?v&0)0(b}r@Irge9WMMm z#h8i-82`DnGK+ZX!j0?~D> zp&~#Puj@N5LS-HClQ(WJyVBhJQ>A|JY#vImFy1eaHI^5{xq{AGwPu{*^|cEM!408t z6wt}TJCdHYtaVnAV#WEbW_KWKuZHu5kMm{Ey>c_9j9qJFit!c5zZ*N6A*G(4zokpbE_amH$xDJ!|?MesV6CTobG1Lm6pav7QfC<4nw5Stl;b+-B_OlBVeHgTOUqM z<{_=RaYU3#1xMb$OBzhGndd}2m6cJ}c;e!3Q52?&Q@B2mf*XvmJUq{15IhaaNxTw2 zdXK~kLmMkUf{B!ZxhAdbN_-*Wd z5T!znh8yM&QEgAETJbAA;={WSbcp2S7}1_+d`pb2BY#D%DG5eWJUa&m42(jhU3ujo z&$-^F@T{;#zT)lf;uZA?Bx<-y(hqu_vL&0F4KN*d(R#)Cuj7r#$o^=ORU&At>^>PX z6yrk=!FDL{QezWqU+#+}-d_QbmfDQ!<5LuA$0aw9xU!12jm6AKkH^iy-RpB>x;9DC zmHyZAaygl?wEq4xlt`yA>mZ9{GAXA0er1(OU%2%kjs^_Ta>vrJVT9-Hy=s>aUC%vGJmY=R44ivrvG#8DPE*J%kXvE%tX~!eNu* z5DS@Nv&cQ@0CE-AM!f%lY5<1ARQ&$Ztl}4b#}iTgD?dr&-=+hUOz$y9Lxd}?c#CqL(BfOjF#mk*e#1_z5v}I zm4b>Nc&MBWb4%8hcRubcDGwZ`NQ~fkuTo0qg@Wo|Lz3x4$ z&KCMfKc4={0*I)L%A)%Q$k4j@>ZI~@${Ec zG+GIJ%o@i_S>nxtrJnPejWyFE&z`W%P&QZNu&J{>s2dqz#N#}7kyk{@Qyr+nO;6P# z)oQkJ;O1GDi!`NV{9K+?GZJbKGXFbVQN~?$U*5O`B`sm*I&N-3b8x>E4)Qt!gsKlSR+eB^63o%|IV zszx3q6p56OqJh8b0Q91=-fad3pgTG=09YSe&Kp{FV%lz+{vQpc5)vSehidFC}~ z)dDaA_Jr@z34y+?oV|pJcpeFpEn#e|ygI;ms@(sq(21l~`<@YbU=}I@?1+kR($Zv< zpFWtmauo$Z70CAPje`(ZWMxtE_ipv8+|52&e6euFLt?2c`2UPRbi2%=w(pj^oo_ZP zAvM;3|Gwp-UbmTtn3{)zGeGlKH{>HO{3vy|JF!+X}7Loo%LR*7ePF{@$Mbs zu*BOQVB6MxujUCyP)?*xZ_{xtxM9$XZjS78B*L`w(V7Fx))5c;gr8RIPW_AHPNE7C zr301^fK;4gc)yAdEv2wtZ}Ot98=iFq$VF2v?s!hB;!Ui~yR%wD!c5>oj(yYU$G|cOEkM6cvTZJNBf0n0 zt@l~*R=w$Efl3A4AC3CTZeKL%gtNL@4&!Ntg{#3fd9lP)9>LBQ^rQ~D-^WYk*R@pd zHJ+sAF8XK5DiWD(-5K-&9h%DV)stqus-RYF$Kl?Y$o=x?X08DkA(+^mn@7bSuPUi) zbyhPB3IRK3baONLQ#k?-WVDu8+=4{wHDq);u6w?Cyw?12k#Ff=yc?HM@H zcSwt0mO+ZX@jUJbx5PY*nj`ljM$mcd zMzdjSt~~B31Kp79@TN(a?c~W1Q>6iDK&l+4)Y^Kyy$xVz1r#SsvuAm(0f2U|2~4`9 zR%~^IL4ylM?4HTrsjPoGzx=Tr?%%un)%&fnCW~QhIZ4Ig zzO1=i0&RKFbJ(t<-NKt0P`h3?ZH)zr!C?u?-v{5{c{nHE-K8%fh#v7sfz;g=ka(|9 zwp^8_UvCZt0w6lt^JR-X`zEjZUfCExyr`XTmDk~&T-q(0UYgeE>(e8~21WN>BI&Avn*@~}+r|bcEV5h&(iNjw$80XwL{JKw53BFqnmH)x_V}y9y<+_PsqT-!Rk2n0j7i73c^sI32V+m9i)Ee zV({);{$S4?1Rb6B_cxy4#d;gle`7KZYpUX4>Hhvi=ux7o|NX(~{$BweBG@e%o1g86 z$4^cM24xcyb2d7>pKtDR5$}G=J^c3E+36ZXjEPb2Ah5RwO)UWn{oBR=710s!7tz7? zmKgtH|1c%p(hbP>1)rBa52jvbV&lFdWZ9l>p>Y`FLq0!k)RxdD8mbzX2_Mehv#LRo&bW z@i`B?@$peSx~WD-+h$R}<9!mh2ve5>t>nj|mQ)qgt3k0KjXUmiQG22BDwhL+}5<6ik zzrW<^yW}aF-z8WT6AD?@M|UBR#lvr&w zPTRkNISja*-TC*-Zk12Bjg8Ih>}*nHURqkpqnl?;K)JfV96uOHPEI19(;41$0vPz# zH)z@OHAtxFmI{@zi7ka$O8b2rlrvs2>Uug zR#r>oX8>wv(qr3q-tMV`2$!*cE`aK{wD#ZP6SsO9K=8chWqjW3Z3D8%_hkM+w`sn@ zM^n}1DX6&BotHljw1kUl<|-p&G$mH740L@aB2vWNc^$p=M@A>Xv@FwfT+av)V3SKu z`#0^b9b99$lUCX==o_^I^H7rsy)|ePt^#59c-_9E6IKJ3FpqyD8K5ntcrrf0?5}qE zhD#JyS2NQ(8cJS%!LjQg@*RSvf1^VgbVliZu3GP(NClmgY|#H?|}FWDwfoC6-X_(RNvePy-U z5*r#x*qIHBzHhTPnGJ!%)?k7F7~^z${xOL`iE?6JQqN1Z>4$W`vH@fR7 zK~~J=_;+F=+%oD@C&15AlJPlQQ!KX^!@ZE_eVg08 zO-rLILAzedE?WtJp8hkisH~O#m3s(={krSnu77VMA_{OCm2ClmFif88HkKVZLLa)u z(}sqHhGsAicHPh%RoBGxU=&hpS_ zOo=@N6;tW7tPowB4liqj<7D6JTEP64BP1%gDg&EcA@{)VC4OOw2W$o=uU&-o%QE8v zBdeqn_4?r>a07(`hovTu(7`x5AjtZ96fD&B~}QkaiRP7ZYE!{u6`H%01Ej8|8pki=zXZ%D*^KFXWf;y zFe0A=FLyk=Drxg1h0Un(c^vi?R5JQM$u8mIxNfeE?53f1@VFGO`1nex{{&w%Xz?es8p zeR-yKea(@b;_#3B?yKNk(fqQ~Qr5o=N9r_f0M+z?1$DWmDd75f{i1vvHX$^crl-DOPzw+`C<1WxxCK6~g zDYseU7W2AxQqR~p9g(a>6#G~IpELC66q{R@)s5o1%#xWgzkK zXEh*dO%1n1%Cy5_P^K!0US4^0Gc_B5e2|}Ld%&Dvb>(|zR0LvEv$Ic>Z;sbXlVW>w zj39jyusgOUJ?kk>6&1~kM?a=Y`bCWj3R76VXWk(tlykeOS zUq7r z51U$1RH=;WQB*ubkxC?z{%*mCjV1I@NfR8j9Q<12`t=|F{!z^T^!L9~{^jo%`gean zGqV-%skv!1qGcgz`PO3N#YBxPUZ(%r&Chxd=ipaF{SSRc-HWxLDn0)_j!k|8NcT8} z!D;sW!cX%h$Wk5=`8&Q(o5hT_GwY`+Z_`MJ%uK8=lt(1-Hk|vM6m}b%zHaTjJ9lb> z$1JoU5gY9(03ZcHXQ^W~JA?W09Dh!*Y1wFHWw&@1-rxUJv%XsT>Y|;QzIWv%A#+qp zLIk(z;Od+BT$U`<*-meVZb~m@GgNZWY3%c+yD!k9DAVRcyZdcSR5PCqiP+NgPR=LH zaG?Fx1U{zTIzDL?lto;)R`-5GHa1ZQair#8drp)38@tA@sGyi-th#uhK|J zg!HXIoA&nBJ7LsKm4?UIUdFc}U0sJXEQPuwkyVvhO@qtE!cm`&gSWMXq6`L}0jRZ? z!6?9$@(VYm4Bi_sE(?_n{Mba#y)0ET#}JF`jvpPdduB%_5OiP`A)(VLyOfd&NWlD@ zHDFn}`P=(z$Ts=6T$G4>d?z7ouk^_o(F2-gT#x2v=(|e+_b0l`%Fe9HB+8)v!n}=0 z4!}h6DFoP86F>Hjr7TosuQEaQ6@P@Z2N3hZn7?3PLpgkgYrd*eGC7G<<+j^c*%;fUGVEFaN!B za?}!ITI1o7Pfh+i9$vwrM}j;gq)fZlVff5U96!Hd{>)SC7QnUHeuy+nfh`N~D8scF z{_r$|3Tp*cC#AA(4n!rkZs#S#=k=_F=F73eU1km1YL&5Wp*I?xt{)}0Lb=O-ZIknZ zE6wl1lso^j<3ADon;k!q7Ax-K{9kr_+4%p%j!!Bnpro^~SUUe9I8r>a;va`2_fON6 z9@*n=*uTX0a=-;#ljP013GcW7Wu09n+AZg#ixssOH+kLWJaOE%=c;^hf?2Iy_rcTZt z2*2lbgNn&F@{?r||G0X{qwsfK?LO>cgMg{cD^cE_r}WzL*ncua)IS+wq~;=2V>oob z$nx^?Jg^0xdKCCgMxPJ~!BfP1;KDtg+kCJG;O*1pYDdZkFRR5IxK9Kz!aw*G69`wS zL~Q><@h)szjxrLy-0o66Q3wRvGuK7Bsg@(KhCWl@vZPx|AqPx6o5 z*ysxg@HS8Cytu_JJ*7{WOG?PV<1<0Jtn|$q=6Y_JpTz{EbF_>P^F0A-Rr2eG z-Z-)8`1sH`6kKW{OFkD%*w9ycoxf_>zsuqjTmSE}cq2ncE!$-vr=pNIE2)}Zfg6b$ z)RrB<6aKWzt!Xa1v-e>G^`qlP7D4yxDPpndga>1m^IvW#cK~}V8G9G^LIN{_WZ7PX zH-g+ampiz)C5bo5NTlxxW3A$QUMf$z`Wr_tj}pD#e+px3KuHR@Tc40UQ>@ddg|@LN zD(+5Y0~`X2i|U;^+s}8)50~Qc@p?04eFI0bUo+1ZpirH;(i(%2 z@g&mosO+n-;eveAxt1Gkbz_3|R%ZWOEjl8SlcfJ2TD0mBphc7I{-+kr<5->F-u{+c zZ3#9Jbe%Ofx98~|Nd~oX3F+d}PD@LVrbz-gQvmNXc$4G4nb-gaZzfk469(vJKM5yZ z2qk@CA%UkMn$Pvj(0Y`i#akVq)m?7t*SRXu#oNHCs20SzGsuh1#|#I5(H?@@Evr%V2acsa=@jf?g+ad-DY8`-Gw z$z?vfDNl;{Bz}azmeYtOru-8!iTRq?HZSk#LKG?K%Y&E^CNfB2;xC%^FPsC>YFzB> zC3AX~=j*C8K%s$5H1K#WAss03m0Ibde!3L=HZev~01-c0$TP3z zu>-R87o5dz)m5Q(^Pe9*oS%Gx2YW+NIo;lAHVPLjpOO=Bp#dh3bDg@nu0@UdAocOH zRSY#=i%-{11+1nNs3}t)O9ErhY^aR|-t69@bIHW_;gnX;_gSbrP^oyh_&~tc#)8HU z=pO{zfds+Nj&3VuAOpxlBnx&^rPSjOMVq$PO-{Er@>`ILf!-9kL^vEj;PiHd~ zKD!8M+X3gS^8uPE$q*)dkg&cSRDdryzCF{}q0UF>|1Jg^qR3}WKyR_i()ltUX0nZqV4LIiZ*K#GL zc22u6lj~Jy5mv3jI_F9}^2b8Aza1zG1u38y1O_w2>Hnm7d7|YcXuE=UERH-=47DW1 zfFC+YOiLENKMIg=s{fA-rcC7B#pBsoJAGlkG-+mNxiua>ex>m$U<9tKZkR)6xBIZQ zbumbU!;NuvW@aO1{V|kQvtAO=W38?JH+pQkH=epWr^sNN{iC9yynKNA%2{1z)kTuD zl-$|swXoX*iFlz8P-eO}x1O!FKlS{=2nGX5A8CRsCOraHNVx3O*Nsj`EEn1-S}3-kqZPp`y z$(fx_JDys_2Oj+C>33mMGvN7XiHX3JE5WI=9yJ66nY7^XG%&q6x}V03B@tq5gx+@& zN<8n)F$>@psXw+US=Ex3Fa7wtWj(!FLc2=MbZzn-KrzD`ZLy=<+Pb-C*I^9koJj~? zJ5fxrnR=sYYe!DBTdIaseE$=Q5HK&tj=3lOMgZvu=$gO`m<^3&%{!N}*SC`lS1ski zyRON*5pZb@5Ajr1C`f9ooCVq+W-tYm|6~CMGINHd4Pte5WOQP&W!F5#lyt7HWauVw zatFX^Nc5tSgubtp&Xk5?P$Ovi_-fVAX`ZcVhvuZ5hoEG&J5G+Jr||5rG&`E^6{xcH znYer9-d7o2#_nFXNc-!l(TfFCsp}AM-s; zJ|sXXodwGo?3ud`T7fJ0roow7SGd-BziOMgUfG2UQCM|2cMLxIX0O9dwa1y}wD(Kx zq-E#o(5qHd=ZF-w9Nzg|C+ap%M;bmSr=axcRAEx1ZOFz3Dyp*!^+=eLGg5h1Dho_R zWZ187^t|Zw5-F7Hlj6MS5#d6RpkypwHipg*92}iMmC~=LzO4T2QLO%eLxj@mB4kCU ztO6DZ?eBl_bX;Y#!+X;f!_fOxc(!mS$-FqCXTNu~6&PRLHy~{N5eK(BovX@lkf_Cy z)y>_Sj)priz0JEj;M3Kn5wr7jj;>?PeSBiZ#Lz?3=lR+?#E5U7;}`S9z^n!gtxsha zvpYIYQ|n3jpCc**xrdC7#x5!i46zJCFn9NhSR2YuL-P&6{i8cNtlc%zg;9D}y^U=q zeAGB*d*hBUD&B7OMjzeo->*|6l9-L9$`sIRxsg$N%1N4A-LHBei|d(UpY3-B4HLL8 zPeX=@XjEO;eZJG7dyf@vYbrqdVME(lbCpIphTCN9B{s=iNy~|j@06B1!&0Bg*yLf} z!z1x%r@q7jDwGv^wIT`}aEUq~1k!BP2nWfGWq3=URT=T}Ob{K_Bg{B)De2VBP&=I7`-AkS#ZpJgVp7121F ztvGDDq!?Pt)WB(#BDu+Y=Iv?yqdd*p_N3Y}Wf&n>9Ri}up8fX0$N~F7_yGu&C zTj@@z-}c^jUw!xe@r~gSIE0+z+H0@1=3H|wp49jTE_7qXZkp%ACykumS=;T*Y1hm> zp+Z8{Ni!P8w0;0=wm!IeKzk%wl1JEGpp_@MV7&Qx?ch-{^9|P{l7G6C4=hv%({#>E zv`*#*Oy}dhi7jeZSCvK7<_maIgUDg~QqzdIA@DK*&D|I=kLiOn2MJ{1J3rCQw1DZQ z3!SG%8nLAzyKi21h49M8Ed#>6yLYAyo9=~2N0z__Z=JE2>&@(b-%4Ld7hjFlpWMgk zZgtaTEf#1Fy03UK;+Rmn$k31q?g&{;}OYExzV^I%uG=V3gkDJ=a-j0 z!t{&tI;AfT_5w47DCyw$_W>d*DQU9A>)-+l**!tPv(fq72aSQ@@$4HOqvfat$Y(mb zXpb*n!ed}SAn-7H`hf#5KsXlX>6&jw(6|FVFz`!4-)sJc`ub}+2qaF2rCoe|OK8s( zB?{t6Sghf5O278>G^Usi!oW@lYNd3jj8oIt(k$B!Sqy}cnQ z_)LX3BqTof*LFOvm4U6T!2%23ufDC;9jFnYKAzV!mX)ml-jtV@R(k?(OH1WXCY~V0 z&B2Yhii$;CW;Oz1Z-(qXd)8F(5692)3T7?M`#LPt2m^@z&kbpBMBTJ3oV$?*CT3G& zege}Dbhcg(89w??Tc~HO&Go4f7yhJQZgGFw44F%Q z8C_k4nwW2(@6ENby!a|AMsMjOqck_)4F>vM@5F?h`BtL+KGD%D^F5cZUdB%91|KH+ z8Xy$U(Kts9%=DxSgBLZ4NQu5#)9QC7KR3{jyZ;8J$4f%CV8mDX7Qme~!KW3Djt)&x zaW27L83U!dPK!i#cQ+solYt2uh$82dAeWR-yhK-11BN#`KbD9IQ2!d$Z~?uMxYs5- z!&sJSKQ^R7mw$Vhn}zxzhB#cal<6ZtOstXA3n09Hsyc`esW6CCE-Fz%A4NPPd}s_7 zR!hFx?vY)2*&AWv2D5j!+7%V?oN3L?Zy_~g2;kC_V#-s63hMzz3LP14k_IoUo)08(I4FUu*fQqcnYb&eW zbo=%uvp!_Vx>Mb)%EgX?eca%*ZtW8cvtk}9;Ycp-eA_!@MHS7>!sjgM zcHe5X1aE&%deoo!Q)VfgW>Sv9CCSsyRkV?}ZBm6us>i(U?JK@Siix=ZFYQY6mGx!h z#am8X;1Sxjt-nM<`dfGDV{rt*)#m;hH?NZC3il$4-=0fw_cztlo?#3B6T3XX>{)&dMqc=qB}LgnTzY0 znB)QJ%mtCxkgIk`jP$gk==JHPK`c3zF-LU4$Em8btFHMr?;CylwmRQ?w?l{iPlP!+ zISuw}MpY(I5CQ^%+gk?F{ZkKNU5XIN>hIq*k|!evV$&g==9@%^8#&);X8YfL@$YPb zP+;DQdZ_5AVRO}wSC~};2R>u)%PA?|rN~1?m(z-!{QmWe#WN5ICpjb1&_l?xrCAqV zdZH^OroWbv9poWrq;{^}rmqDBtL5dMy1FeE4FaCwjr^{nl-!3GI0KfrcqC`%tB4R! zPXsToFZgT%1!~OTO2|g{&`!+0xLgcarGL*R+S+KfzW^W?tt z#5To^ObzbA%{y}gkGm(o92Ev`KCXn2)O4gs1%>+%$%18+$BLH#BZrublOcjC029&V z*-xjHAWzN87WOIpCP-TbnVEpeTCwo7qX@>d2eA!p7mfysoca=)YyejFqHQm!rWB8K%KbdGqc1N+eP4 zrxnS90(KC?55?%HC|;hc?q*}6!g8`F+QAVUa|MMDibrTtFarY4H(CH%8xfgUTKcul zZ`uFJP=l$K;#oy+QJbwTLoo-nF3$Ok4WtPy=WB56Xx0|BjI*I3*XXr9-J%1lq;fRh zzJF&^P%uM4@HR9^CL(V6{p$%My}uI6)f)PBZS4&nF4@tMugReM^~KUANJ}VeEW$t6 z=EnG(u5Q-MY=qRc1j*MF6{Fp4-d9Xnk$~qb+MXWjv{$06@LW=HJ-cpF8S+ZpVQ{Tl z-~!yOPB$La?ZM|NGLDkg*gMuVKa|#P*CMG7w?W1z1gfnUwreT!Vw4TazgdL6= zI$joD)ZtpULs#jUtixM%;2Rq?}*7|5m!;JtfY{T4FsrfE77&O$L9W{FBpQ*qy<7C z3D*4V6RZjjb z!QNhI!{p3UJ6ma4!^0;fC)jm!GO~KVpOoUg?yg)EwbdP)zuq$KDF9vMR^MD6%+BjJ z#aA^lws^G7ZLAArClb-B%+6kQbG4F@^;CE9@s(NxKi8JG*mFY@(3HR`)=2*Ql(b15 zMT0S?rQEn7!`K!J%A;5^ zC~;%s>{cC9%F7#=Omyd8s0eyG;xh0cH-(gVUnVg_Ao4gw8o`QEh)7Vz##PmUT@w7c z_kScL)O?AHa0L&pE`FlGXu|{`Bt$0=#|$WK2RG;N!Gu5MK8cFO+(wR zq3x}Ztf%$MpOoZxxvsO#`8J=r+S9^vVu;96wopU!Z^w&PPlROI{`jG36nx&1Z`(Uh z++7xX!93455hne58baK}*Ek5aQPwVT-3@?7w+34r1 zMLaNO3J)P>voD;m!mEpMZQTPK>Cv4p_qVMjN@&FzKUWNueHPFqyt^1Pp)Hm;CbDVki2Zq^Pf0t!yiO_zgG`iEFH=AuR!I%36#v?pI@?gw{zO)B_s0? zT2S!w+zApVFSGmrKuQQZJEVlJ+}Hz(gyict=r8F|kX6E_tpPFz?Hn}Ymf(Vrs6M?@Sh`Uj{~#p*++2Cb~>WL~|J3^6Darg0(W zO)x{Pu1mN6aEt&kG9um@lO8p82=Bpi;D`3U*9CVIq|;(F z))X=^qj_-HIyfAAI8Ss2mL8}*%1S z_Z|niJa~)bAsiN$x}tVCw7e|#lf_6fpoIF~A2n2VJb4)#lh-2!6&BLFU2hL1&?{#p zBKd<)owRbn-bTg%?rKPUn0a2nafi40vm`FuI|PWhVH2U zjO*jPKd3oT;OJ*U+q{ zp@ZboVQC&=e}8GQJf zy>_*|bQzzHW5$dkC?vrG9~oKh6KZW05E`1ZCiaAQAX{b-Dwh!2QF!W&K`t*JdTrBt zEpOaPaD5zdFb9mM-zXyCN?6noVO_hk>-s=2k%eq*Dm&N@#B>@jKKAul#=5&`5E9In zMwc>1Mvq>u$92F2=qwu>_;5ZQ4!?@>ultTlg*`VtZ8Lj#(zhA-r+G<0;C?K_2#+u+ zK;#wkp;q5UDq}IPUSmh09OV=wV2EUKBvRfqybosHp05EnB7(RI-3qKGXg4rmN1#Id z{Pfk--N6nU=>NVqWLv!7hl7xk-t=usa9wPye$JoK*bGK2neuT_rrEEpN~OZ7ZODd$ zVCbPpe{0X{jdp1!!xLqn82v4TkrHF`@_y8e7z6#Y%dkLre|&FFdG_5ZQb+BrW2|^Dp~dZ`g&k`<9oDY4%SJ%XoR+QsB%UwaeI| zf+eY;0&M2$hy849{gB(9!C?o<{FEA7<5T07WLe2M?i~_!-o-zpZfDGvJeI~v8Y;(&;1N8f&-^FNeQN=+3v2hc-zlGXjGU# zVkC2x7DvC=rJo&45`wu3bg<}z<2^_5rpv&jxdmzW1|ky^_K(p=a#5CazGn#D@zF(O z@YXV@NutrLEo*K(?pv{6Tv9>`0y2mei}E-X8Q1eMNi%!j5)Gpdy+EsbgR3rKXjCqFu}sIQ;mk)bIfKnGKtOwsp0O@CyI`LDKmR#a4kVUVv@ zn};<=Q}|2N$gRVvda6#G!K8ZrfZXn(PVac;Y{AvyT!1q?*)c%`x1 z+sTeJMXTE7!bwm-kitzuj_ci_prenQ(>d<~CX9sgw@Lb{`F@vtduEe#My2d0xVYCY zf+;;5>Yi!SIr(ix44Ke%Vkf?*ORqx2yB=t@O2dyhB%tSxP>x@&X;)uh_h`g=Nc^1> zo|SREyas-~DB2ZT8mqZed-I(Vo;xBoHq0S=4uAe&lA=LQ7j5FwkRTJY@RjYzX=xPL zj30c&3wsJdtKoB%6zK?S@irJS3LoRqbWl;8%)6$>EvheAX$%FF;=-#xY|l$HwY4f* z8=ZFgG)rGDEp$+H$fj^E%9wJZy=7~2+`PVVKU~=|(Bf^S{3uO@7xCx&pLN3M{=GfY z#kWdKNmg}^FC<>m)I`~AKYHT>BCd%w;o#Q7V86w@t-A8~73h6n5&S=AyJJF7(MT9y z==u7Vun5*GP+?6MSno>b*L)%3dixm{7YT#x1tEc;O7V~5V=s{`-0Qg&L$>(vGiPht z*Z#{Z-#vynz_oF7*iCDOg|(9CYW02?M3?#;$BYif=xC2O6fcj3S~1JcO~dhVX=J3| zk%x;i9w+;R??t~XDQRy#JMDx^^e2PpXpv9RE0(f{co83KES7GuWGhnzr}wCH+uAbw z%t%GNH1MdfBfPx57pP?=UE$}xP9Mp#Q^bdF4}7rV<_&OJcc&M}Oj7(-TU(z`TUd3v zn?+7~{jlqBd#V8mkJkO$H8Dxgd8le23Ad*|MF;ngtl$-bD>LHa&l&D8>3pNeS~gvuwBrcC@GjsX>Ln+F;0dcIS?s z6;IGtBfh%pZ#6FGmosy2xZ6w*SA4uhFSL+GlI?H2be68pE4pRuMuPq}B$g>>oH$+6 zy)@K9VbXMGmmWNIuC7;TD8HFr3cv@Pvr0ybp#3p0;C| zDymQ|Zh_0fW++7kCH5M-#n_8*bYp6ILLhR>l6SA^?_4i+bUim!Q;B7a!xRm-{zPBFIyu-4k zQvKLn0lo=>iFVD$yHZ}nkB@haLFJg4@oTO5PEJ`lDy_4juz`o|rm(Av41aDXHIGFX z6*V~R1jxzcji`aAA>uCgk$cGP;0t;ept~2HeA=S7YNWW(V1=!B7 z{jA034UMp3?RLa-omWErGdrERXi$HqJ_Fj7-JK-P2G>X$Ve`iixdU(CoQ;CcUvP{;ze2pdKU zp;*Hp?l~&+v1}}+Br(w+vlz;g^wTZQ{{H7E75n(S4AZ=*@jX|EASLnaB z&E3(~_Mo(nk3(OfN+x`Mxu~M)dv&>I(cYdOt+?FMLVV$b5%>Cq*fXoN%WO$qt&YqS z)JfjT!rpQFXL*ZFS(1D+X`=d8(LYZ5!l!o+gM)*7s~)-4)sDl7jEahiYrZ!tTLZCk zX4zqIySuxCqoYYfTm-0mcXk@wf#xbCF<&c=uXAOS&TB^jjY+8GG@VZr8(9bqjZ{de zWa?)-m?qEhml{JQDF%)3!~$$@$i;oexb=4NQ(8+|xg_p4n^p#+RbnuKKtpli+5Sa} z85IH=ogr-1M(#IsUg?kXj` zj9fh(vs1)+eXI(2TQ~Np(G=`fHuh`5%-XA_>>hqk_9MKpB>Eo0Hi4ZD)n^sCTNAQq z!04P-L8&0`;o`D5A7WyMLxf#}CIm*^(emai&38lVCgZRv7NVl*AT{MMPlJL%CLQBx_WGs`-%bV=eRzge*J!!F>Yu&1SNL+?Fl1e^ijM1_y*$PVfW)h!{E4Ozr@n)@lIWJ z(XNiuxf}CHF-XzH1o5G)mE%EhVcR_zB>Vu3j(NPl@joA;_ww=rV{-%yvhQ`Ie}aDH zE2O+MBzk%unwx8GVL@xD?eGHvp@7IqOK4z~vpb*;54fu#&rIR8b2`-rL~Ru03- zIgkCk`5x1=vheUm=DQrn5*bU3Rkps?$f+>6*ji$u;w8k|<$W7aUv8D!dPNz}fPzOh zHnbXrRHfr1bgv5qbu#S#EV=R1lf zg#qS#85wxDN?zqf4p#8d=rIbVI{ZfHYe-xC`Xq>Y+-Ywz37<4=e@Nb8mJ3G+W}nK6 z_Zn@hzFe6vm8NX*Yp1Z;QT8{$`-{>L*vEnYsS)y&ZA+mhMIWy}MInXCyBv@+)U%!s z#37j8_X{=UXoyZPpI&9PvtCQ*;CgzB;ckB>6pcB*UV<)7mu8gN$U@3@dqT`{O_6k z(POk^kCVB>{rxRyXx8A^$KoXQw$edUQsUD6k~P*HXd>k<-RCNACX zf7dG>gYvgq3qQjEn5!_)Ha_b?=iNmmI;x9u_EX5PMTPRKe8;{E4Lo2Q#=0-unb=tl z4u@g;!#jI><}mZ)owmvG*ogv4W%%vJf1s%Z&tG>?|GqAN3-5ot2^e$=Jv;y304&M> z4*<5eR^Hf9(b$gXb-uDb*54ocpz9%MY>62~>(0eB!hEtYuQQH_)&;GvPyY7TulfeD z&8h{m;}19)B%1vq9+=mMftzMu)qo;Au#`u{K==nXs}SO2NJ>DAk;%jer9HQy&aYZl z27!2anSf*z5!fsU{F&o0fuk-7DHO!*_+!VTqU&CTaoFBe7{;q_?KlI}1_mj2_Z`8; zthhU8pod&sVz%N=u5-0AnFtfks;mEu9l-!lSKYnh<70S0kg(yK8n;J@viO*(_-0wW z(D9w@14Xu;fx-8{_LWa*#%9nwGjrSHUsAnfWa)>L6w}+yH+y-`p#j(!L!SLAXTsRK zO%2#xL}EN`FRiULSvzw?_jj>*m~DnGejy%7$;L`WJDGRf>R($VPiEEklQI7Si`&Dv zNG@x%stk7*He04h<_q|6|AFh8vRrzF5;_OV|68y*EkGtWPRzoTu+haD)z4Y8zU#KO zw9(+=6#uDQ?Q8R&hCFFp@(nJ&n*=^g&(lW;U0nKo$Is966%~SbBZFNW-D}Ul?HRBb zmH0!Q;?H=rUn}OyOSgWk=^kv@-XZ-lw91TqRbiOYZL({VK8bTGe6DMiPU;x;mCOGl z4`ZsZ67!7J6GlSi?~bPUgUw@d=d7<%33Iaf*)$)Tq~D+xLf1KcAYy}68jxB!3J5=Y zxBOV&c-#92ZLcN{3@_I@+)Y=SH9+Z#_c_Izf_h7gt>be_;^j{(FJ0KSjQ)4Zt!6y( zn=P`3e9`^9xVIleKX7q6C1h&4<52Fhp5eB*pFH%TRI(ZpnVS+a2x4Q%?}Z`tM=`J#^tY3_E6$a#PnpVC7s=jEJWi|Klg$cmXV-6J&UEkU|)}# zXdCM+BD~-=e9{=X>u#2^7P%o;N#*XLKp1d;$H|KJ;&b1GGvAHSU8K)oPde_MeU^%r z^4gpiK!f@3HV-nM`;m~-kI!Z}L*6=Se6Eb!(EC{+77&OkYrzt-S^D-eGaC*Cy;#AZ zU!6dD_von2Yb#lUikEkD1X@V2<)v}U@%HYzn6=tcPnbIw=g`~F3BkdV%STHawc`-5 z&nnLF@nCM_mm-k7prQ)3Xl)A|A`yMd&Rv?vDKCrg@uRpZ76g)xrU~47;T#>~K0_NB z{RehDq4H1cxN|G|b%vxVKfg30tmv&^Vsfuh$5Yqajs{7|1~kMLFq{Rte+wn}{^sl+ zy|h3Vyt}E+&aMx@B%li*cjx={i?pb`4VTa}*<)?8|DE2%G=@c(#{Q`|N`&-F)(F*I z<&?z!&qI-D#T73J1Ih+!Tm7FuM@4-*P?cdy0^5y#myuB~kQFmgmHrv`>f7h}e6Qc` zm+3xxE=CFye`=D5AAA8LH4q-YqQ(d?CoL_ytZ01>t_NoAxz}l^N+YWLv=`lrUm)ms zc{P|O# z`VzGK5S6vK{r;$aP&ps#=qUo{W5tucq4=*UQ}c%r+GR3wBsUDq;Z+@6Lf$7$w=*`g zs|Azug812uX8!{{PUQn2LxR1XvrC4h9Zo@6dN?+9MywjGm)rnEa;%jv*ApY$_>i zSecsNlxRbcyC&-Cb5S`-%88B`gv6vhkb@EJj*j5}v=*yFtNr{*+cDnG%)rF?>jKPj zAf3JQMaB$2hL}|dqzOnq8n!?nzy?||9r)<7>YLnrxIG`i3Pz+?`jq-1V3fa8Rsb2& z3XCam1Cwxjd&N~S0UvvWO$z9D8Uf+6DmPXDTth$;|MUcbSX-Ci(Ek%j)?vMa3u|rV zg@78m`*gc#8)!l1OP4=)a~xl!1UyJl7M#JTxo3d$Ai7YDLs*ErwO}9 ze?J+M^1>o;S9vOLYs(nV4hVUjzjS;>Pl@Co|KYVR|g;~XE3hEwodskq<2RR zo?bC1a^nTsSsD8;csK}M|8WN&Dh4QM;o;F6KKPm0kq&R5M)z)gCvh$O&I3iV>r%mj zf*tW=Cv}6*Jlc=q-=oVaFF662RwUi4z;Ho-!x~G@2Nf{T%cB?FDlCQRf+%?LwhXB- zX_I(zJ@-1M%mu=pCSr_NG?{~zqSTg6bL@Poh}r>iFcwh@-##kgv)k864>)LYqRgcU z2LxhiD;s7kWTDJVeBSB8=B8KXnOC4H36#)x#EwUxmklyfGOwjKswSd!T+a28_@pR| z{xDuzO*uOIfo@%LLjS!6%m0OIb5j2&uHExaAi8{jj?4P(uhWYcnz2K!JUxB~Oj3Vb0Qp-gFDB#xEkOrGeWI;9^pQmVDEhC{m7QN;3_kff%{n# zDQP4QDQ=LaSzKAd$go}7MJqFxDCq?gtkhE0C(c2K?+)w727iQoFVxSogGug15z_Pqal8{#JUg-Q9cc^0co{oR>PrkA{;PLb`J}Kg&(Gw};g* za1&Yf4WlrTdpOWUMa6o1lqIj2RrB)M{XzH5icZG;N<=o^KR8&PMeDl7iUx5q03l z#Dr8=_Y`t+QBpyB2%B(rjbm(XYDlNNmM$(9LVwzBv9BD7J|K_mgQ-zpF}%{QXpPPU z^#dOB-XwJnm>ca_IEo$C7nir?D4|NRkTqT}8Q&abo?djxxgQ@UzLw@^+~VeiSi4~F^?R6$DFjy?p2t!I$gs|(xuqNA3YQ(oXAN;dP#-S-p~T?ZYK52&Qx;~laqrM0g=Z8 z5D;x5FYD=)+uS#cEbYy)--C|Prvc(c8hQ5md%3m(^JUx9L7LtJIw*%!j^s~Kr=*osW2?BJVIuyAa8znXrz+*TiwuClL-qX)2bTef~lq?TqGiOFbNF~ z7RymdovW||l&N9pTWf5MH`IoPAfN|gy0(Wf36VTKtIXgGnqeUm3rK9OkQBC_@77gb zJc)^5U5}|C`!6FzjRc4HC_7&Dj3qg~ESo7l92jn`Tr_&Fw!(~1Zvnz|2ueB>1t-&L zH|NAKlgE~VE-;JU?`5y4y>V#515Z%0N0jZQU8Qd))$H2#19PyJ_r zGqa&UF5Bq02G{w&Pu|KA@(;&qHhg?7$R8^F$4pN@Y&aB^XkVR`1-gu-P}FaEdO;Tr z5{>45dC7u?dg%IE7ivW>sQG??pwMB$UV}(1=!V?l3d*9Z0d|E z56Q_5Z|c==OCMxjt$bm{b|iXa)f_H{nUbvYG7BF1mtTpiNRMD zhG3XrXBr(;5x#3&IFz$Vka1C3d$^XZYgp3=dY_=%JZUAywVmbk!hPZ*%`!hhx7 z<$o2o2z0LbREZIhL{WExo zyHoows3lpPL$JG4jDv`@^6Ti4^I9Jn8$ZRu+P%H3xapm}m_EZ{Sw*v;`>|h0i_<@d za%nZLHak0x^KZFG^3*uOs8fj^&Om1V^_v`wSmh)jV*e}}g5^V~WxuwOehZrw=-%j& zO8=mtDPc*kQhYk4Z1Nr%fE1FH9-lIFxY#uwLkrwejG*l|4&-rR|?>;wiL0~TTdY`b!9^~@{P zg?V@cZ)fKxe2(9;ha#VHHV(>JJ>lRw29vQ$dS`f$^^bRU+N!HgU;Fw-rro{GkOWc$ zD{q{g#UQ8Wiv;d;@1KICNMZtdu;!P>-`kB&>Egi2*sCy^vZHMeiabQjYqDC_CL z@0mcGhscP1RSM{0hXnZL#BgEBsJ{(jvcAM-@;vrMi6P}k^WhzUh0NRH*e~GHK1ek@m33D$Kh?3~%Stk+WT^vveJ5q_F@gZOm`+?qnc0spi zOP7zgU>x;XCf2fVYXO|%{evYQ?&azlvaC?{5~E}#R*4l~hs*Vrf#G2q+c+f_GLu@M z?|zt%?U5s&*Nr6kpNKqjTfuyPSyMF)PO(@yIyIxn5h55bvTSj=j5}@M?OM@-v zcDe3!Hfoa(p&-PZismPKj-lZS#kG-R;zoAvzh-1l+nN2WVN&O1wWgkcZ$MA(-E{1r zw;(pA@CV_pD<S^DY0al?{LH z;RB|r0V>sm6*q+7MZ`8tpBYDV1wUc`JV6Xu$V$t`FfM2!W5~l2=mV9)dcwji=oE>b zegzfMV@e1hXJgExP*oE!B&sGVA_3MoC@e}!Ds*(<7njC4d9FTZg1^4-+JsO{STm-T!}_8I?|ejs6M!$mJ3KT z$H%J}qBS+?977<-$G}tz4$j0JW_5M_1fx_dFl#k@_doIdmKcEVm*rwWXt4K;EYuVf zlq@Y>_4Sc}c$DOhd?ywc4l0J`m-PE6>Br2>ng0Ic{clCKwxqGg5z@YL5yX3!lt&2g z&38U%7oLp<&I|TbqW-_d! zgX#{tC`+i$;92Mp2j{4O?(ST*VpvNyLAyH5F+)mxSHCx;%JW}btd?MoFrWh7S%tF| ztWRtcMrThKwe};^g!j8}6LBhW@DQ;iPzmm{+AuZimq8Z2Ul(SaZQ8G`9MZUv;Lk}Gc zB}r+}CfI6Yu8zZ^2dl=|+ZR~3Z_oC=f9W?a!a&o^j5aax&#|{BXV;#z3|1dU!GK%8 zC9U3#%TGiwx9n2GWCCITvs&`@MJDmSI@q4|fFbr;S-v}dL2Y+@|b=veqp^BIN4}VXb zfC{v_8b!c1FyR7Pzr}$w?X!bYp8=bqnOf6FTB6)_?t0J((>XV;$rbEwA|v7H0mCEA-V>@mtQM z${mH*pC)zN^fG6P@)i}WHk+y75vuhK;q)SCb%6mo~o0;OIPW- zL^ovxq3Bu>)mz9ZW-6Zb`y75w#2jbxlW2<4YLF>P&Y^{qxpcjt0M*n#EFyybS{DI) zxYQu#eOsl$aQHbpg8h$Cay_fWmahsQ_bgN9SbQ{JNQm$1jIKUF6Z~Rt%{Qr|ac}YmH&sd%cK?VOCN%P|`3?m0=3)?jf9}G{C)LL@)Wg8m zE2rwWDh8KxQ+@i?l@NKTuL8y5^=VY_X-XW(&b-L3O2)V57fn844VKsoI#~aV&K@g` z{|HW^u_8gSTj3B9EDf^{9{}r_TBy}ny~nf~^-|U+wxkwk)t}tRk5EkwaV^^y?tHvcH+sku_a9NwFvO=>%B;g{^USF4btdfD(xZ#Ba0jY zxZ6x9fGXSN$IY83cKkuFbvw1;X2i{H1H1a6HGM8-^Pn1^L%;9F^u5Z;bzQk>e^ zzB$;ljvo^9(zsif$m#r{-Ef#Vxr^}MCWn`^t z9X4n%sdhZ3h7y*9hAL?=l@@bI;i;*{pRP;9J9((9hgMe;k8#5WiQko$y`0TU z8nRicCxmu}VAhLtf`($^^f3>;NW-tJnf7*Xo#0#tcWPKLFJGXrNnmN%%ZeTyP+Z$Y zFos9QhzOf%bl&sTPor)Attv1PqWL6H?MQ_ZtiL+CjVF8@;rUJ6bb5!=Xn;bRA#Utz zr!7&@ZR~FzT)#JgI={J^liA)BTLU z)8GwK$@gJ9K2vzIxH0q&Y3GiO;|)3_5-R>i$adtwfr1Frdu4PuFp6-Ebd4RGdV8Cy zItFBK%J+4OBVvTD`2j$sswQaF$jTl8a@+AtOj++l%f6GG43eYmx9kT-d;8mQ|7jTx zzOes;7V#lm+V_7`3Cf4fIngryFQNiOrTH;9Y-p|p`NH6-9mW_8bfq+}W~K^L?*4q> z<#jwc{_tZ&M4U?X;h_OjTaDrHy$!L^mUIp~Dq7|?+~C`isDtywrDse;A6&Z8>r3vx zC0$xeiv2bf%H_q96O)^&g2Kx#WzYND4<$vft?Whzw5D;Cab!7yUUBguK*D3gZq~xQ zZ$j%udN3o_b7M_RAQeV%=H{SAf|$&fVeL)r8o1x!tE;=Sr-g=w#(;)4mP}KP!KEyh zEh!S^+#j5*nFZCP&*vDrEa8pfIVKKRfiqY|ANe)+9loP z)G&7St6$~(zu}E+Ya7_u3UG4sYP-j30y+Ni45MFQA<;K$G&jauS62*hX94ZJWF%&2 zdqlZsHe5z*9TfHjzZR>+wWU6+LRIS!5M8IqkS6v3O5<*MR=E5*g9lQOa7uefPF~Ps&r&zWGVp9a(PADS`#lw0NWO6|2UYRVGTBka+Ayg4(q?TGQ75hu=Z5^ zH&bHhRDys9fuQubPO*RNZ?K38+4NJjbh87P4-)w}9K_Vd-BPQ3po3aKsesdTNij#4 z?P*#QfMEeE<@%hK5$v)rgjS8?rz*w-9oF=QAC{ z|1U0peSG$>lK92N^!ay-1OVQ!nWbYMY;39gF}iTFUvMi^@$G>mslUE1laibP^LF=O zDLJJN%Qgk9yMu+qY7P|F*Xo&Ouu<2WQ7T60V$Z1eH$BTuryF6XdxcVk8JU!nxkb0cG<}1L4A3EFrL0sGakJWJ z*dRHDl)lvT`w>g&UIun>)QF;MZ9NUV{>iPyMqK0B_LLtc z`QeEPzSlLJbR{vLuoDQ2J!2U+f8oBn+Sbno-S101pEq5)#buG5U&~#ZS2Kl-FRz8y zFEXj9{Hp3Q1vp#(i&~OY8~3a@VOcU{$bnDd&^o$X9uYpoy#2b>j#xQcvl`eF|NPb^ zd&J$7volIW`URClteALsI99F!RxEQl=xo9$k-DQRLd61j7Gj9x)|qXA(&M1{<|z|P5Fb{?{T}%Md5M(waNBjF zH$=QZHB~D#=J_V*)ie#ns!c`u-Q*`p8R6SAVOE{lP}Yb$VP%xLalF}w6SuA6!~5qp zr5$zvMm74i$64o8#<9SUEV7?Iq)T5;rvBrY)rWrp3J?#IMgKTwAHL`AJ*-~{YXi~=rK*|C=cXMr6|+-`sF8Fce;mDVsia6 zwBC_ly*$o^m0h3ScoT>5t=cFzEnwc4&O{MQ+>e(~Vr~Wg9?GV`RQ^@I=e+;meOQfn-1SobHy#LPxFb3snf!|u@|PVVf%t#3 zKsI&rD=P{r>xuGy7M3Ia{W(u8!aLEpeP%-|KYpAKd-L+na!E>QgfakkAb_s)^@-;L zdlR4vfk}oAnOMVMNseRzEBW&tyBcyxjm_xU-4SLMaFB^97$GpvN1dJtOe6gG5m>0M z13NpQvGH(ZUxMQLDEKB@)Q91}FT_7A7BR$*4=v2C8cF0oeylmfCkRbPaiCmAKIF2!V5-o32h?WO>b^~0|ybXoUn00NJAD8Tw*mJxBkWTj;)2Vc=gXE4sQhlW5R`ZiHu zMcWgM8mbWEkgT|o@6_><0~J#G*ZH(9h(C$7M2_%a_;0K+!IhNyE66Vwl{GarWw!d@))ThcO$@fH>RGYMkxH9duY$ql_ z{3E`|{iyi`yp-|Mr>ubA@#B(9hv7o7nRnD>a$Vbwdc`sq{;579&!f=>`0mfC7qFyX zT1A;2X&gsr&CK`#2JSrNLFph!w*LV^`V-&QaRV3|%-X+B2{CA_x_fFvIhf7gKP#(> z12{Y(;j`ymQfppbipuZ@7$)4R8~^+Z$>QqLx164IH6VeleeBLY<7?F(nK7&S zYdl%nJ7mHNTDw0m1cMG5{b~&D@-l08x>f6O&Ak@FqY@th;#kV}L5|kUoVvXk6RzGq zY+MuuQdJF=g9%7V>3kaxp5fsH$!FK_bk4VNT@z&y!wjUPnAp&-UID%bFPH;s;`a7Q z$Rm4r2IkYgNrRpjmjc9MR8nY2EFYEQzn1UU4^;H9y&wM|Bs{JnBdzdK9FC1W0YNQ` zQlp3Hu63-GMEO4O%GKB5f&!S6*A4K)Q`6nSdY;zXZQV^@S%7d$sEZ|%sulOZu+m^R zeFxKdF+(*X1v+Nly#Jvk5LqD$6fT^O7ViP`O^t!um6IQ!IH-kI%JCy9s}%9YweKE( zTQi^A0v!Aml{#qVsHxl(^1;Tho^F~4?}t7hXJcSUZNbUUD!Y;R1I5M$nqNofjc)Zh z^!5FZz1PZ>S7T!+I~Qu=x#V&3eGy0p`>WPVN%I?-Ov6lfgm?|mb>w5$e)8?3TTjL)-TNis<5#Eqjx$I$;_+s;uXPIdb z_W2p(d*}D7|F5>U0E(+?`UeLH7Bon32r#%MxVsIm!6CujU4vVY;O_438Z1C?cY*|W z_q}<)_sRF{R&CY4YHO(}>Q>#Edyn+#?sNLr{VULeVQLfmjDo}~)sAL}F}0j^q?!E5 zw~)S#i@(y*Bj>1;RF355Tc?{jVwn+L7wYvx3l+Ws&l+6A1bw85Ov_Cmka*1l#L_>O zKFaPi!F;i76Z}sAJ(T<3@pJRKKgY(u%uvYy&x2t1qCgH&5^#DX^Jzu4@c#b;Z4msQ zfHp)V4$J;s6ChNa=shlSMs;=Yka9x8ym{=x-aD zSXmWL?Jl3!)QT?V7ZxRT;>2r#S@X`UOBd9N-)ek2qJv~73u&-}V zd1|J*RBTxsVg*AvLJZwbh=?bQ8EWE6%2T&C14>yg48XTK`}d@sXTq4E+_6-zYDh#dov(TXELgqIPmR zB9M&nSR$;sqiIjR@(ESw1ED8oPo-eFc0m7e1h+_kCy^Bq`msg8o(l_iEefOKcGc==mwZkFfSM8mXfvgsfbPJmkIdTNGK1F zy=NMMuo7);9MFQgtG+0jMhR|T$q4dqe;8=XbwCC4g7~-R=H9 zCD$Ghd5-_IH8o zTJ~7&L=Ch`&*lOyUUPh0?+;;f3+vZ`=|F1qRv(aP{P3S+v8u*Q07|yJv{dlH$iRRD z$b7A>b#81-EXC@Q2SiDgb zISq7Ydfps40i{Pos$B)0OIk~x0VqkeGN7RKDfryMbwH@Gjs7@zg&jb@Plx`=X`J(Z zIOu4{^ycT>8%phxBmF>f!}ksejj!M=gki{tpok^hsV}GfG_>{weIDiQ=#pq1&%>EG%F0HKl{K+ zZRF|2p-laCXh@XoJMBdGtA6wdZ$XVOayxJ`ZYQhlNKa2hEk^e*M=LJs%5x8((?qb~ zJo%4=h;@A?SRugzEulV*@|oG$l5)+?kEOG-i$+>EWwY(rH(d<0HNk#aIX}d|@v#8a z~!pGeH|Wbr#kw{93P!`jiK*_>=mBpT<5cvRclfJaFjF5 zKHQ*?*;GBUi)oAE6FkH!fO68~UjYsU!^5?^&C=q*?nL%jrrz7d4yW*=Z#~M@J$ z`dAM?8@i==Uqb}Ma~w2dJ)HvzCod&WaYie z&Mqg2h#=)B#=z)O_aqS;%nz^SN-Owbw?jrYrt)!XHT)@g{&?fu&Dq)Uau48PPs1WS z5``?r_L$K#jeYn)L|xaJoUGURth^ni4oVz8HsaWa%pyK#RwipmFwh4f=FnQl!_1uyq7Xr5WAqbMP zfoGe)&W%7{hNN(rUsY&B2?zjusz6DkL-h}dnaj=XWEW?OdYzeMWVxy;>W>#HjEP>q zf95TwY@D8^8<5LQn0^h%F@X^jHLR-+O)M_Hxx@>lq+E@0CI?ekDXMWqhVZidxEUCT zLVXh-yu}bhtEIi1ISA46^86?o$zx-LZU4OdRn4t&P@3|?Xa9Jo+pWPy9i7gwh^OKv z>Ti$Zpy+s~0%c)R@qTed3qW-VwUTz`HYyMrmR8P*~14l z63p$=(S)*Dxspfl;G<9G+|k2EiJgNnCrw9#%zghO*QZ`c5T3A*uc*k6n4khL44gN3 zva6r+G4{*AM{jS|qw;!3balhY$#ShMgLFDBVeP{CeLS|BYTFOR^_tJ)yB_^;u^wa~ z5;z`M)laygau%}ZGghD?DcM&b1_`woFG=pY>&sYAaJS2}8DhMnB)Xe1TNY8>|V_cD_iyu^(y+KE};d8@%`72ekjrXm_&%Ks5A~1Z}CYQ6nhJOc9sItr zbcV4zKnka@PYm4R3uWRj$*sm+SLw zC+Xq!{{A7H<`17gF9nKkA5s;LjKJL9lpC33k}HFPb0&*`}kb+$JEvFMMkm& zIKPvR)&B(6Ab*crWkV8|*wd8oK=}D&H9?HS-XXgN0~dL|9_jK5KzL_WzqxN{uL3HM zQnFl}u6J7i(+>>_H`3Eg5l=H5H~kv&qm~ulzv*Cb!p!(`uP7+Uo>Ey&wmuj-qMKw1 zd;|x&hl?c6SVO1bU{hUdZvQ@I@hvB(B(E`~+;U{ZiDr;TSeOHIt!H=FQ&BS}CF7ao zgfX>+?OD6u1Zl;`2?CE zaD5{Bf&Yn?vy;?j~?7?6sp2YMq)EUS94C=Z?DpXSlHkw0(ugCKM zDehM0W%w2*1t{`l-c%(5CeS78RkMeQs?oNz>_|pnpdFO2kGW}zxp~U=&f3&u4K;O> z&0_5pbAWTY(V!|OIKux(VwI2I%RM70Z__qKMfD26#xqlUW=rLF(dfTV@`o#Lv-PI@ zW;b!wj1Uo_ig@;7%PBj{E>b*@rEi?Qy+zqJA-e3o^ExXltIOx3^Y&T!fd;6{YJ<9Z z`o1DOi>49oqdmX>wuN+=@;G^s%ix{k6ola*;~H20#Qk(i&4Ywj!^?MRyzIcrs{Nh9 z$j40A`pZf3?#@T6x9~S<8;Eh={w$IhN#tTHUxfga;IuN7!gMMDl{S-D1b279E3#Y$ zvgE%9A_3eAgj~>4QdKTC2mFkT<(VG%)KEO%gVE@R@Hj|Wu3<6DM@WbBS++Rn~ z(tGai%(*|v^NiJO6(NE*KY2(psIt^{uksH=ed#fCeY}+f>^`|T@w?Z-ui~K#M<Hj=x7lS?YfYq@?|jB%5l)Zk0ety63L7ME{!X_#HJj z0F&I~FFcB^Um1b=qBEo5ePec_C;r9*Kj2E5>znpVjdovKzMJ_a+y2FYsWs-#k^hdH z$1#=c%BH(*P)UBqE^W{d-1kz^*l2g{n501mS{KQjs$bDmbb38zV}nsDCsSZyW6CrE zwEO$_PPC%p!iHcV=S2l8&B7yh%@;=hBLqx!pWyu@sDX!1KuTHQG}E6$hWi0PNdr7` z?qM%SVta(?#+n+z?XGW>)lb(AC*MjGCd0thnXx#w#59Y2X%2kxeR`I!iS$6Zl~Yp6 z@z~#Ae7m)V*)qk%bn;PkTl$&8Cx^}fK_`*UX)lt$eL%0{5!eZ+Iy={;KdHk8ve(j& zWn}EwrZDNHIEZ~_v_r%L8Jc7kG`vmm1Nbn5(O}n_YlW-a6l7v~-_vRg4?)Yz7Zv-y zA^&u1`<^XucQ+w1E^fUWX1koh0>9LVUQ|bphhO&Ndq38h;3ENr6&0z(4cUO=4<|{BnEb(E?MAb( zFi;nT4!>rd2xg*k@&kmnTbe;I3?eNXtS|C5RW@umJ~_o52fL4hUFC9jBJ)-QURn91 z)kUq{Ep}#Jox+tu_+hNZupCioK`5L$dgu4Phz&J;*2r| z#}^=`jzav11UTU|#ihk~uSeCCLP>3IW@{`G1K9DAiHz_mw&6|p%!hh2pFp2lS?l;; zte&%7H_*-}>7trP(j<`Nd39wTU%x{Gg^d+n;3$K~<96xwv}WHeHT_BrAIsAiLCUJB zB10@5pa@MC^$^AgL<#2Aa3hdwmGl1Lw4H)bQ&JWn7QQ|uPs;u_BsT5V6$Gk}4^#%?~MM*jRZL1<`5 zGdWozEFAObh&)htWHgeU)iENhh5PgOd_+t}F)^U#4Pnfa=(6zV4O0s(cUSr!BkfY! zXN<}zTfT~R$Al`X0QTB;AFE6qiq-e}TPl)PgP<}dfUq0O>tDFg%(fI5ljYRWpS_R~TV1M&chxS6n_>p_$0o@@mrEIjruDm)DEYp{Xs5X9!2 z%0it}87{K1lT$}f0nJ%u3wP%5_76qvtqG}_XURW)gp|zkk6)}y^Qf^jJf2%KGhJQH zRi{E$R)|2IPMgexcq3d~?qL^BMWqKQJDzFYuMbm0v??7)QA-Q^Mko+VCeoSU#YP1i z2{6h>MaA-lia6Jwn|2gT0+idYRf&iQ;6(C^-e+`DQ0-W1ERateiHV)LySGCaMY85i zVn}rM74yt}EMkfWhcz4HQNDt*&YbWD`D3_Q&%KpU_5DwZrZCR`_|OtYQ+0*%5IcK2 zyBM>vgm_LFS@_wR1S0{i5&ebIB1!FeCH`p(zdXC>fN7!VcNPPqhUU`kErl$_r38C5 zhmKAjz{T(c;`GjrYJMTYlLa+>G#inam8zW1Fn1FL4s#u$ET7znv%rVpr(XJ~*xD<& z8(OYRD9}j!RpYWZ4Oil*4|!D2HY@8Cw|<~-%%)Vt-IE`Rgo+0shwbueWUg*PpWbP= z!twJ1s9}HKS)ocbOr3A!->sDbl{^z=&4-Z8-E`P=PVa`M{;8vyxGMsMo^ z&&F0%3PX&>%BtyIFT`)@{ItNLY-uUM$vtXnUN$-!N_h<6H|hb0bhV?ttf8TIysNpP zf|bok&B37@P<@~uns+OyJRBv=N=FHmq-(XEJygw1`f_Qwn}@twl@giz``@SI(qF%J ze)`3&ko>6bh!<890xKUx8k=fa|IB)v{gREwZ7S1?Y zn!UUp=R;y*u)+kkkG6Q}fQd9S!?%;*yJCev=O(d{lY_qOnyS|!Wbj#NTz~B{?wQOV zpMLe>LslU?4kHNk3br1irwD16&kVgeF8|EzgO^<(mwg{yKW1gGllSSpxlGn5ID3A2ZO$lx2%>lc` z7riQtNSkGLfmW@mN7MI;I8KuM+}{Z05*0OW0Ugi);JE z0bt3Z#H?NaL#?7l(-=4ledcw=e(xvZrd` z*{TcRB`W^os$;df09Jrt4rgvI@q#H!LGg37`ZXTj!~JDw3=I=jlw|dv-6o0zl9m23?Em2)=mwuqn3ibAX^GAIqs*+@xuFz~DH zS}>HLftiz$zV4dS8*}#i$Mg{s6sfePQrdQf+D^NutknepJxluXS;zNgw54wK0+Gsd zhMdbn)EdyTq1op75;V1wk1aIVu;*GZ!3`_<{rlOE00lX%uI^`TW?GIqlNqbH;QGlxBx)kL zEW}sw=VEVYbUI3_OV9NY`dc{Zgf29Jx;8@#BsAi#?}6x;FodY{OFfTw@2yf!Nj>4p zKT($~1B#-#xgl`{SFN!!L~L4v{vmpNbahhTXxG;3K^C9)7H2=7ww^>mAq$P5 zgM-VAuG&8=tRm1TI(PuOn`sK%_pU`pYq;jAJVQ!S*bVEDqA2la@El->Ccr4O7F`=H z_cf(zJ9KmwEzOO0`E)pQOvlU5*)6m-y+8bg)8prJPVuYMA&|}IC%t|1VMiw`VPW>< zRQS3T-I81bOv1K_A#RyVbiXK?>tu{bM$dmOCr8nJCoLOWh0#hniyC6KP6oAUv*vaU z^Wc7oN%~9A22yz{3O61%5y~;jzGEGc9#QGxwwm+&bv?=gzIJ70=hv&lg0Ow$ zN@c))APb!1pG8f(LM}k=*pgFFu+Zfz{cvs3jugJ}PMA5PgXe@b=zQK}yLt@tr9jdo zWnbNgrI&LO79_>QpHoa-X+C^<(QekkIu@#Kl>WQ&PS0{tE_GM45NmFE18F#nN^< z51+1<8Hw4zDnjnHV3pq|PByXJCYo-C3`>0iNpd;&si+7}93)CUehP(dg(!s`6$#y+ z2nC!{T+L0amzT0fe95=1|053r_1?k z7z4J)@A&hWeJVW&N|`ztqS{q$55Mg7?!Eg1-k2uxlzc)IPA{+^K$RoQ8YOo&# z8H4YwC^1x1t?`7RK73F-;3v_T`)U*mB<9Q-{2KCYv;>)%&l zlp;~S(M?RWurNctybSR|AJ{$%iCEy0w<;^YpI?Ic;*Hi06m?HC8MDKH`lloN{4Jj_ zy64Boe@?5IZi3{|3S#Vc;Qjx$C9LJsXE8XDpFaa56B5E+$GU1OYo+Qbm>L?+?`8k< z5Mjw2>ze~)Le!n#N3DG7^cG0r6_K!i@0PdBfuW-YTAipGs;ejF2DF5PvP*k}{%N>E z=`maA>H25Sg;Wk9sOLv5g1>K{mUYa91Z>q^0-yv`yu;Gs;)$8+zauTFj`PUvuP$HX zFgeMRMs)Asrf_|a0vb1a-O|#cLxl4`^8vg@(=ibeP$v-;8vZ6`YBH38miDKefw+s4 z_CHNPH=l@)Meybas5rmnN;rQ8|Gg1YLQoEf!+#%@eT@8nIPHIA=%4pX+5Yc%3&*X4 zq5ruNK-~>_^^e>jDEh@B{C#iEtN-^G1?^IbiRD*>h=$#_ELNcYoy+?oM7>bK+$m0N zYaH0WQ<%-zpEU&~2+;_YD@1As^M+=9S1B#6*zf&cft%Jjb!~&n$QT-ulJoLlQJoIZ zRsD}T&{bB|P>QqpY{105_P0OTXz9@Dat02ZW0}0{@7Y!7#w{;W#=+2_IBcAcZ44am znN_Ez^QLUK#s2Bqu0i)N`-75Fsc_Z*;Aj^eEl&CJU4I%-+r(=Z$TlT zqow7h`;^Sa$|@?C(st0mwhf(QAoVVERxiE^Pdd8g^HPgdP z>nfLc*+*jz4(*T7p8mr51%gTlmh2uU-6D$00%u6CiSW2SuLMQJ$H)5x>o%BQwIaz! zLN7G$biR4B3EN1o(eZtlDXXX{sdIaLVpFI`e0pY3qaKxn=gkMZnzEC9d-y0q6l9{s z`FZL(ub%-BpNh5d=gT!G<#sGgOg5($8-?AbLFKVD9ww)kYQkpVEet$4UQ`=Yy_AdI}*y?P(rQAL(GtW#w=*xe)1BkVs{ zq08m={kEy;c0q5Xw4!2fd3}&xtKNNkpRLz`f`UTNCbOgcXsyq8RHx-~lgR=eh4A)< zr1PojvUReiMnlg_M|sLfjMT;0jCFA#JTZ_xqt!K8u29;G2|{Yyylm6?u;wcwAuQrI z_guFhJBUfr%a_LQF`9aQK8gs8<(aJ$0|VVK6E!&F+_h~hP6Z?>DfjCnHZEpk|L~__ zM^DKN3M@R_EC+l4Y82*jQgXbix(XHs?mRGzX3Fh1?~VsD=iuOwSI@`vdcF(KJ>SjE zSa3*QcRFu=6#D#GB0p$cqGAfpNT``f&%52Wy1e|V-^u4I0Si#Jc`mcvbx(&xg>pqe z7{dMj9JLtD54#K|J|TYO@p1U_@{+&Z!^L$2f%Gy1BSUDM1zkt|8|i%m`GCB!q4DuT z^-PGMgLJ{!nCgWguZH%N?sz8h|l4kb)z^ucKEPaudpEB z1C8)BGE!3R_$TV=`PX(Aftx-gX(S|lq{QKNPqzNyDRO?&Z;$QZK-;a`K|lw?T1eZD zN6VXx-C6V$WPYSS2{_%Nj_&YsFD)(t=jSD*;KFoG?TnRLmq z?)#a1QfKZY;_gnbMpalkR|$)Jy>0Wg*?X#YzDaM5|49BNJz?igI+1r}#l)LA;x;66 ziEZ z-g}$&WjtF%LV}RjK5j^wm)K@4yC-)*{t}Id+hJ7ghPW58W7@uKy-MEL%llwd2NaUT z$@Tj_Li)o8U@4#x^JBGODRi7ZFIL%&w!s1{21Ts9j`vIFvszp#s>u(|R#e95z5U`D zL@}VSuyC~sT}*EKpX<@5Zs8rnUT1zU2`dlRW66zOJnur9j#KoF191uP7S*-V+sE%^ zzxy`@&#l&!I(Z4bh@4f-(q|F4Y z&$<1`sEB$G11-kREH4z0%!%zJiw>b|6cm)+sUkBLGAdb8Vh(opd)PqdLAAB+Zjs}a zsM7Mv9h+wF183V$U#7ztRHVbhrQiB=^1h=7KAylbe>_N9)lr63P*76Z5SW;pd^Jeb z3Xe)MF)@L+A)sy#`4NpTOoEv6RH5D8&%{aZIXyqJ|;`1@iJ#%^V2aR_VmV^?7 z3#0rKxL&Lv&Aq9)Li72LpO!}i&Zj&GEnOBNh8)FTv4|m%7Wa<6Q_xhet}dpz*_P&@ zqRKwXAU-ybhyVk!Q}40XEs{C*c)4b!1t_P4m!_08G3c&iuZJfuuPBmv9;?aTtJKB< zBi@+%$w;4@LjhOUXEg_Bo3)LD&Gu8DwShh=1R{&M1X(Emrw+Y4gXNxE9WC}|POg^L zrZfSk<(UNyYL&b1rA0;bOg_UF4@=mf_Y~&e<1OYoG*6H6OVtA32B8xWxH*GiyMEbr zJUc7y9n`FO>$V$Qz`^-f8)_W1y9Gz3BcPyiSD7RxcdhNKFw=$f{Cb6k*yg(Z+HO>0 zId7e>OIa))!i$1}Ke@c@$#G45c6oKcfjBtW)6*mQT1d&t&cV*Oc0oRq4~HPs^Vc_= zZ|`F8f9(9)Pl=mkHi}M4JKP#y8(Ao>D?3#6CIx{h3-R%BFIm~LGj0KcI;PuJXS>=4 zbZgJlf`oI~9q!#8%lY;BmMtfQrWka3Z6&r^ssVDJ z@2Z8ld6);T&)xaVn_s{5FVq2srw;$%&gTt>n9Av*LO_`t?Hu!=K0Sz$@LgV%U?)~+ zQ>3upWg&xhm~@B*oPQ3I!$%-r?RdAo-#ptqBA7dwujA+EN`8T7fuNB$%m54lA5~P6 zQd2J)laf;=_Z*+g%{^o8O&~q52TdjOO=8J~L7cW*yMj!WM5(h;gzSzzzsm=U&3c{p zbKB6l?hWtA3eSJN&SsWD`bP$1WJsb5|0{Zf3xP6Re?(8x7+n2;ZhlM$xES+%t2%i6JYWgD@nFSF7L4CT9 zX;T4ybGlodsYXxLvCxoYnvfjt^f+xM@4}I7u%ND`k&;S)hi7JHwzt2}Z0eX$VJ#8Z zXnuF2f59{FGG0-hib}%wywuW(kDt=UyWE+^H@rBcq+|(RAVvHlS9r485GYrOxw#PV zquK$7zO1ZldUBF%m07hU7<=~Of_9B8Jg)Aqdh-lT zB;b?9-AHS$2kj*ouX?ZvQl~#_Mk~OQS28eftI$>#587?eu0=~IdGPQSB4kGq-XZ(x zqQS7I$`(%CV%~p`MZizN2Pvr1oQ-uRx>W*&lG6x@?zGYf3FX8yx9yu*@limRS66pT zd%L@=(;TOnVP2t2ha7vn9tzMLMZO_+|1pLjox3j#FXGjTzq02 z90B(&gk-5om_TH_8ppnpl6`Yol!${yYq77yVykk9!u5{#(=-H))~%QxV+(_3(KlaK zot=dx_D&00Jl39fYj-*xj62$OD$@$b04J&*)tJxEYF*CD~DvnX+6yMrsNnRYmV$c9{sV z+tJZ{8d(6pv63rV?RbV`ER5 zj7=C+1G?eC)ol{*@-93<5(N_ZGZFz!tHIn!R(1rigx~%A14f0Hb#yM(%Sz{LxVbMq zG9ES>GXs4Nq>IoylGv218kSoe8VeenO-&_*m6-jztEwCJyH2<0$S|xKN6pOeRA*+K z+uh%+-L^h@wXgkdIaeG)O*u_4I;!26WHD!Q5FWTFrCP z^Zx9e2h2A5&1e-DVK$3>STX6PucE4AaVh3=NpnHPsCM9>&yaB=Bmcl>ms4R!W|VYu zb8}5iO(wk#M`y=(4Q)eApK2F+!KfSAE)`LNY3ilSKZ6Z9>v z7!@tUp<)$q@u1@d?M^gR^R=_9+#6vMhGG^36`YTRA1rtGSax(f-22u5BCm&omQKCx z9hfhH$#}7YGP3eJohd09))oc~PDw>w_Z`XB}On3LpAutL=KKQiR7p$7UGK?q-E{2;o)H>y@=|Dn(2v&f{eK2 z=H{FneSIYiA8CCh=Cd!4EX?f8HFZ^-9?!zc4z=y+>Fw!6!NYtG&wlyMT#C0k!itI? zx_nO4=RF#ad%3wTImYLxYzlKY3FYo{CFm2Zdw0JHRBC^?2_K6h6d;d;mwnfcB<9JBCAfa7KpM z(gBX-y-CbxvBUp*Cm35#d9RhXM^Y2e!iTrtzQvZ7D!d?1M1w@WqQiw)eIqXf1CH=O zXsPEPNBDnMx~2-SwP7ry26zOQ=OM2dy03g!k`iH93Sn8sqiwn-1H2{ zecfyEz(dFu3G%GnYQMErX-Mn|Kx`N3QmC7)uEB&k+iE$v9%yrc*FuqJ=d57Pra7grI%|0 zu}r>3tDVjgTG~;$$l(Nfc2?GRp@7bc0bJ?lw)0>Vt;VC}Cq0{1UA@tadc)@1Ok#90 z3FBZi6am+*-Kuf)>Pmb9ysIOF%$StplLP5|bW_&FmNoAMpIZb)tq>#B5gA#>I<3yZ zC=XnDdRZAwUTbzEH4P2oDqBf;J}jvH=qMyL7*?4Q0;sI`5?Y6W-zZDYPpuwnzKz(} zfHHKo9sg>kjD?MT_aGCRa{Aa8j+Z&6sH{rF=XraW%y8sqs8A4+7yNB{AMndw?yZjc z%a_K|I_E{A>eoxT$b&+Yp%bPBV$tYwa!Da$4Q2<|Ndz47L|oK*01DuKkcmH4;AqVH{#WV?Zl)J{afi{yIQ37&Tqr}n}amM2oQ=TWWNj{r}(-+?1=&T z8zWu7I4ha&`?)!6%ZbXWUTyt?A;SL?J(+ehBtp^qy^Vr|>Eorxaq}$BSQoYuTryC#>*N)7(Rby|XCa!G@GXlQ7h9KPN0H!=SEr;-2j z(<9`6f*s?*GD1SWJ=?1t-iWU2yBpcVley0-5{)BcX`BBWEV=Xrb}~h!rLdrl8tCMV h1_=DWBP4w 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/views.py b/windows/views.py index 2e26a7a..baf2be3 100755 --- a/windows/views.py +++ b/windows/views.py @@ -22,6 +22,36 @@ 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 ########################################################################### From 090b7369a4d1c44a43ac9c0869b73dee5a6a97cd Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 8 Jun 2026 10:23:24 +0200 Subject: [PATCH 87/93] Fix translations for all locales --- locale/de_DE/LC_MESSAGES/petersql.po | 141 +++++++++++---------------- locale/en_US/LC_MESSAGES/petersql.po | 104 ++++++++------------ locale/es_ES/LC_MESSAGES/petersql.po | 119 ++++++++++------------ locale/fr_FR/LC_MESSAGES/petersql.po | 119 ++++++++++------------ locale/it_IT/LC_MESSAGES/petersql.po | 102 ++++++++----------- 5 files changed, 244 insertions(+), 341 deletions(-) diff --git a/locale/de_DE/LC_MESSAGES/petersql.po b/locale/de_DE/LC_MESSAGES/petersql.po index e48d29a..d32b06a 100644 --- a/locale/de_DE/LC_MESSAGES/petersql.po +++ b/locale/de_DE/LC_MESSAGES/petersql.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-06-06 10:58+0200\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" @@ -162,7 +162,7 @@ msgstr "Passwort" #: windows/views.py:203 msgid "Connection timeout" -msgstr "Verbindung verloren" +msgstr "Verbindungs-Timeout" #: windows/views.py:221 msgid "Use TLS" @@ -190,7 +190,7 @@ msgstr "Datei auswählen" #: windows/views.py:278 windows/views.py:397 msgid "*.*" -msgstr "*. *" +msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 #: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 @@ -264,7 +264,7 @@ msgstr "Erfolgreiche Verbindungen" #: windows/views.py:501 msgid "Last successful connection" -msgstr "Erfolgreiche Verbindungen" +msgstr "Letzte erfolgreiche Verbindung" #: windows/views.py:518 msgid "Unsuccessful connections" @@ -276,11 +276,11 @@ msgstr "Letzter Fehlergrund" #: windows/views.py:552 msgid "Total connection attempts" -msgstr "Letzte Verbindung" +msgstr "Gesamte Verbindungsversuche" #: windows/views.py:569 msgid "Average connection time (ms)" -msgstr "Wiederverbindung fehlgeschlagen:" +msgstr "Durchschnittliche Verbindungszeit (ms)" #: windows/views.py:586 msgid "Most recent connection duration" @@ -771,122 +771,107 @@ msgstr "Datentyp" #: windows/views.py:2269 msgid "Procedure (doesn't return a result)" -msgstr "" +msgstr "Prozedur (gibt kein Ergebnis zurück)" #: windows/views.py:2269 msgid "Function (return a result)" -msgstr "" +msgstr "Funktion (gibt ein Ergebnis zurück)" #: windows/views.py:2279 -#, fuzzy msgid "Return type" -msgstr "Datentyp" +msgstr "Rückgabetyp" #: windows/views.py:2299 -#, fuzzy msgid "Comment" -msgstr "Kommentare" +msgstr "Kommentar" #: windows/components/dataview.py:111 windows/views.py:2335 msgid "#" msgstr "#" #: windows/views.py:2337 -#, fuzzy msgid "Datatype" msgstr "Datentyp" #: windows/views.py:2338 -#, fuzzy msgid "Context" -msgstr "Verbinden" +msgstr "Kontext" #: windows/views.py:2345 msgid "Parameters" msgstr "Parameter" #: windows/views.py:2354 -#, fuzzy msgid "Data access" -msgstr "Datenbanken" +msgstr "Datenzugriff" #: windows/views.py:2361 msgid "CONTAINS SQL" -msgstr "" +msgstr "CONTAINS SQL" #: windows/views.py:2361 msgid "NO SQL" -msgstr "" +msgstr "NO SQL" #: windows/views.py:2361 msgid "READS SQL DATA" -msgstr "" +msgstr "READS SQL DATA" #: windows/views.py:2361 -#, fuzzy msgid "MODIFIES SQL DATA" -msgstr "Geändert am" +msgstr "MODIFIES SQL DATA" #: windows/views.py:2371 -#, fuzzy msgid "Deterministic" -msgstr "Definition" +msgstr "Deterministisch" #: windows/views.py:2395 -#, fuzzy msgid "SQL" -msgstr "*.sql" +msgstr "SQL" #: windows/views.py:2395 msgid "PLPGSQL" -msgstr "" +msgstr "PLPGSQL" #: windows/views.py:2405 -#, fuzzy msgid "Volatility" -msgstr "Virtualität" +msgstr "Volatilität" #: windows/views.py:2412 msgid "VOLATILE" -msgstr "" +msgstr "VOLATIL" #: windows/views.py:2412 -#, fuzzy msgid "STABLE" -msgstr "Tabelle" +msgstr "STABIL" #: windows/views.py:2412 -#, fuzzy msgid "IMMUTABLE" -msgstr "Tabelle" +msgstr "UNVERÄNDERLICH" #: windows/views.py:2422 msgid "Parallel" -msgstr "" +msgstr "Parallel" #: windows/views.py:2429 -#, fuzzy msgid "UNSAFE" -msgstr "Speichern" +msgstr "UNSICHER" #: windows/views.py:2429 msgid "RESTRICTED" -msgstr "" +msgstr "EINGESCHRÄNKT" #: windows/views.py:2429 -#, fuzzy msgid "SAFE" -msgstr "Speichern" +msgstr "SICHER" #: windows/views.py:2441 -#, fuzzy msgid "Cost" -msgstr "Schließen" +msgstr "Kosten" #: windows/views.py:2609 -#, fuzzy msgid "Routine" -msgstr "Betriebszeit" +msgstr "Routine" #: windows/views.py:2617 msgid "Trigger" @@ -911,7 +896,7 @@ msgstr "" #: windows/views.py:2656 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "{database_name}.{table_name} - Zeilen {from_row} - {to_row} von {total_rows}" #: windows/views.py:2664 msgid "First" @@ -935,7 +920,7 @@ msgstr "" #: windows/views.py:2734 msgid "CTRL+ENTER" -msgstr "CTRL+ENTER" +msgstr "STRG+EINGABE" #: windows/views.py:2762 msgid "Insert row" @@ -951,7 +936,7 @@ msgstr "Daten" #: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" -msgstr "Abfrage" +msgstr "Neue Abfrage" #: windows/views.py:2800 msgid "Close" @@ -959,7 +944,7 @@ msgstr "Schließen" #: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" -msgstr "Abfrage" +msgstr "Abfrage schließen" #: windows/views.py:2804 msgid "Run" @@ -1031,19 +1016,19 @@ msgstr "Länge/Menge" #: 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:175 msgid "Remove column\tCTRL+DEL" -msgstr "Spalte entfernen\tCTRL+DEL" +msgstr "Spalte entfernen\tSTRG+ENTF" #: 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:190 msgid "Move down\tCTRL+D" -msgstr "Nach unten bewegen\tCTRL+D" +msgstr "Nach unten bewegen\tSTRG+PFEIL UNTEN" #: windows/components/dataview.py:214 msgid "Create new index" @@ -1095,7 +1080,7 @@ msgstr "Kein Standardwert" #: windows/components/popup.py:35 msgid "AUTO INCREMENT" -msgstr "AUTO INCREMENT" +msgstr "AUTO INKREMENT" #: windows/components/popup.py:39 msgid "Text/Expression" @@ -1175,12 +1160,12 @@ msgstr "Alle ausführen" #: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" -msgstr "Abfrage" +msgstr "Abfrage (1)" #: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" -msgstr "Query ({query_number})" +msgstr "Abfrage ({query_number})" #: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" @@ -1333,33 +1318,29 @@ msgid "Do you want delete the records?" msgstr "Möchten Sie die Datensätze löschen?" #: windows/main/controller.py:2104 -#, fuzzy msgid "Database connection lost. Do you want to reconnect?" -msgstr "Möchten Sie erneut verbinden?" +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 -#, fuzzy msgid "Connection restored successfully." -msgstr "Verbindung erfolgreich hergestellt" +msgstr "Verbindung erfolgreich wiederhergestellt." #: windows/main/controller.py:2114 -#, fuzzy msgid "Connection restored" -msgstr "Verbindungsfehler" +msgstr "Verbindung wiederhergestellt" #: windows/main/controller.py:2120 #, python-brace-format msgid "Could not reconnect: {error}" -msgstr "" +msgstr "Wiederverbindung fehlgeschlagen: {error}" #: windows/main/controller.py:2121 -#, fuzzy msgid "Reconnection failed" -msgstr "Wiederverbindung fehlgeschlagen:" +msgstr "Wiederverbindung fehlgeschlagen" #: windows/main/database/list.py:104 msgid "The connection to the database was lost." @@ -1374,14 +1355,12 @@ msgid "Reconnection failed:" msgstr "Wiederverbindung fehlgeschlagen:" #: windows/main/database/routine.py:545 -#, fuzzy msgid "Function created successfully" -msgstr "Ansicht erfolgreich erstellt" +msgstr "Funktion erfolgreich erstellt" #: windows/main/database/routine.py:547 -#, fuzzy msgid "Function updated successfully" -msgstr "Ansicht erfolgreich aktualisiert" +msgstr "Funktion erfolgreich aktualisiert" #: windows/main/database/routine.py:551 msgid "Procedure created successfully" @@ -1392,37 +1371,36 @@ msgid "Procedure updated successfully" msgstr "Prozedur erfolgreich aktualisiert" #: windows/main/database/routine.py:590 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Error saving routine: {}" -msgstr "Fehler beim Speichern der Ansicht: {}" +msgstr "Fehler beim Speichern der Routine: {}" #: windows/main/database/routine.py:604 -#, fuzzy msgid "Function" -msgstr "Funktionen" +msgstr "Funktion" #: windows/main/database/routine.py:604 msgid "Procedure" -msgstr "Procedure" +msgstr "Prozedur" #: windows/main/database/routine.py:607 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Are you sure you want to delete {} '{}'?" -msgstr "Sind Sie sicher, dass Sie die Ansicht '{}' löschen möchten?" +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 -#, fuzzy, python-brace-format +#, python-brace-format msgid "{} deleted successfully" -msgstr "Ansicht erfolgreich gelöscht" +msgstr "{} erfolgreich gelöscht" #: windows/main/database/routine.py:634 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Error deleting routine: {}" -msgstr "Fehler beim Löschen der Ansicht: {}" +msgstr "Fehler beim Löschen der Routine: {}" #: windows/main/database/view.py:255 msgid "View created successfully" @@ -1489,9 +1467,8 @@ msgid "No active database connection" msgstr "Keine aktive Datenbankverbindung" #: windows/main/query/controller.py:227 -#, fuzzy msgid "Database connection lost" -msgstr "Verbindung verloren" +msgstr "Datenbankverbindung verloren" #: windows/main/query/history.py:55 msgid "(empty query)" @@ -1505,7 +1482,7 @@ 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 "Query {query_number}" +msgstr "Abfrage {query_number}" #: windows/main/query/renderer.py:65 #, python-brace-format diff --git a/locale/en_US/LC_MESSAGES/petersql.po b/locale/en_US/LC_MESSAGES/petersql.po index 3a81cb0..612415d 100644 --- a/locale/en_US/LC_MESSAGES/petersql.po +++ b/locale/en_US/LC_MESSAGES/petersql.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-06-06 10:58+0200\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" @@ -162,7 +162,7 @@ msgstr "Password" #: windows/views.py:203 msgid "Connection timeout" -msgstr "Connection" +msgstr "Connection timeout" #: windows/views.py:221 msgid "Use TLS" @@ -280,7 +280,7 @@ msgstr "Total connection attempts" #: windows/views.py:569 msgid "Average connection time (ms)" -msgstr "Connection" +msgstr "Average connection time (ms)" #: windows/views.py:586 msgid "Most recent connection duration" @@ -771,126 +771,111 @@ msgstr "Data type" #: windows/views.py:2269 msgid "Procedure (doesn't return a result)" -msgstr "" +msgstr "Procedure (doesn't return a result)" #: windows/views.py:2269 msgid "Function (return a result)" -msgstr "" +msgstr "Function (return a result)" #: windows/views.py:2279 -#, fuzzy msgid "Return type" -msgstr "Data type" +msgstr "Return type" #: windows/views.py:2299 -#, fuzzy msgid "Comment" -msgstr "Comments" +msgstr "Comment" #: windows/components/dataview.py:111 windows/views.py:2335 msgid "#" msgstr "#" #: windows/views.py:2337 -#, fuzzy msgid "Datatype" -msgstr "Data type" +msgstr "Datatype" #: windows/views.py:2338 -#, fuzzy msgid "Context" -msgstr "Connect" +msgstr "Context" #: windows/views.py:2345 msgid "Parameters" msgstr "Parameters" #: windows/views.py:2354 -#, fuzzy msgid "Data access" -msgstr "Databases" +msgstr "Data access" #: windows/views.py:2361 msgid "CONTAINS SQL" -msgstr "" +msgstr "CONTAINS SQL" #: windows/views.py:2361 msgid "NO SQL" -msgstr "" +msgstr "NO SQL" #: windows/views.py:2361 msgid "READS SQL DATA" -msgstr "" +msgstr "READS SQL DATA" #: windows/views.py:2361 -#, fuzzy msgid "MODIFIES SQL DATA" -msgstr "Modified at" +msgstr "MODIFIES SQL DATA" #: windows/views.py:2371 -#, fuzzy msgid "Deterministic" -msgstr "Definition" +msgstr "Deterministic" #: windows/views.py:2395 -#, fuzzy msgid "SQL" -msgstr "*.sql" +msgstr "SQL" #: windows/views.py:2395 msgid "PLPGSQL" -msgstr "" +msgstr "PLPGSQL" #: windows/views.py:2405 -#, fuzzy msgid "Volatility" -msgstr "Virtuality" +msgstr "Volatility" #: windows/views.py:2412 msgid "VOLATILE" -msgstr "" +msgstr "VOLATILE" #: windows/views.py:2412 -#, fuzzy msgid "STABLE" -msgstr "Table" +msgstr "STABLE" #: windows/views.py:2412 -#, fuzzy msgid "IMMUTABLE" -msgstr "Table" +msgstr "IMMUTABLE" #: windows/views.py:2422 msgid "Parallel" -msgstr "" +msgstr "Parallel" #: windows/views.py:2429 -#, fuzzy msgid "UNSAFE" -msgstr "Save" +msgstr "UNSAFE" #: windows/views.py:2429 msgid "RESTRICTED" -msgstr "" +msgstr "RESTRICTED" #: windows/views.py:2429 -#, fuzzy msgid "SAFE" -msgstr "Save" +msgstr "SAFE" #: windows/views.py:2441 -#, fuzzy msgid "Cost" -msgstr "Close" +msgstr "Cost" #: windows/views.py:2609 -#, fuzzy msgid "Routine" -msgstr "Uptime" +msgstr "Routine" #: windows/views.py:2617 msgid "Trigger" -msgstr "Delete" +msgstr "Trigger" #: windows/views.py:2634 msgid "Duplicate" @@ -951,7 +936,7 @@ msgstr "Data" #: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" -msgstr "New directory" +msgstr "New query" #: windows/views.py:2800 msgid "Close" @@ -967,7 +952,7 @@ msgstr "Run" #: windows/main/controller.py:321 windows/views.py:2804 msgid "Execute" -msgstr "SSH executable" +msgstr "Execute" #: windows/views.py:2806 msgid "Run all" @@ -1169,7 +1154,7 @@ msgstr "{text} ({shortcut})" #: windows/main/controller.py:322 msgid "Execute all" -msgstr "SSH executable" +msgstr "Execute all" #: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" @@ -1371,14 +1356,12 @@ msgid "Reconnection failed:" msgstr "Reconnection failed:" #: windows/main/database/routine.py:545 -#, fuzzy msgid "Function created successfully" -msgstr "View created successfully" +msgstr "Function created successfully" #: windows/main/database/routine.py:547 -#, fuzzy msgid "Function updated successfully" -msgstr "View updated successfully" +msgstr "Function updated successfully" #: windows/main/database/routine.py:551 msgid "Procedure created successfully" @@ -1389,37 +1372,36 @@ msgid "Procedure updated successfully" msgstr "Procedure updated successfully" #: windows/main/database/routine.py:590 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Error saving routine: {}" -msgstr "Error saving view: {}" +msgstr "Error saving routine: {}" #: windows/main/database/routine.py:604 -#, fuzzy msgid "Function" -msgstr "Connection" +msgstr "Function" #: windows/main/database/routine.py:604 msgid "Procedure" msgstr "Procedure" #: windows/main/database/routine.py:607 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Are you sure you want to delete {} '{}'?" -msgstr "Are you sure you want to delete view '{}'?" +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 -#, fuzzy, python-brace-format +#, python-brace-format msgid "{} deleted successfully" -msgstr "View deleted successfully" +msgstr "{} deleted successfully" #: windows/main/database/routine.py:634 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Error deleting routine: {}" -msgstr "Error deleting view: {}" +msgstr "Error deleting routine: {}" #: windows/main/database/view.py:255 msgid "View created successfully" @@ -1460,7 +1442,7 @@ msgstr "{elapsed_s:.2f} s" #: windows/main/query/controller.py:117 msgid "none" -msgstr "Engine" +msgstr "none" #: windows/main/query/controller.py:123 #, python-brace-format diff --git a/locale/es_ES/LC_MESSAGES/petersql.po b/locale/es_ES/LC_MESSAGES/petersql.po index 97a4654..ff42e00 100644 --- a/locale/es_ES/LC_MESSAGES/petersql.po +++ b/locale/es_ES/LC_MESSAGES/petersql.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-06-06 10:58+0200\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" @@ -162,7 +162,7 @@ msgstr "Contraseña" #: windows/views.py:203 msgid "Connection timeout" -msgstr "Conexión perdida" +msgstr "Tiempo de espera de conexión" #: windows/views.py:221 msgid "Use TLS" @@ -190,7 +190,7 @@ msgstr "Seleccionar un archivo" #: windows/views.py:278 windows/views.py:397 msgid "*.*" -msgstr "*. *" +msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 #: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 @@ -266,7 +266,7 @@ msgstr "Conexiones exitosas" #: windows/views.py:501 msgid "Last successful connection" -msgstr "Conexiones exitosas" +msgstr "Última conexión exitosa" #: windows/views.py:518 msgid "Unsuccessful connections" @@ -278,11 +278,11 @@ msgstr "Último motivo de fallo" #: windows/views.py:552 msgid "Total connection attempts" -msgstr "Última conexión" +msgstr "Total de intentos de conexión" #: windows/views.py:569 msgid "Average connection time (ms)" -msgstr "Reconexión fallida:" +msgstr "Tiempo promedio de conexión (ms)" #: windows/views.py:586 msgid "Most recent connection duration" @@ -773,126 +773,111 @@ msgstr "Tipo de datos" #: windows/views.py:2269 msgid "Procedure (doesn't return a result)" -msgstr "" +msgstr "Procedimiento (no devuelve un resultado)" #: windows/views.py:2269 msgid "Function (return a result)" -msgstr "" +msgstr "Función (devuelve un resultado)" #: windows/views.py:2279 -#, fuzzy msgid "Return type" -msgstr "Tipo de datos" +msgstr "Tipo de retorno" #: windows/views.py:2299 -#, fuzzy msgid "Comment" -msgstr "Comentarios" +msgstr "Comentario" #: windows/components/dataview.py:111 windows/views.py:2335 msgid "#" msgstr "#" #: windows/views.py:2337 -#, fuzzy msgid "Datatype" -msgstr "Tipo de datos" +msgstr "Tipo de dato" #: windows/views.py:2338 -#, fuzzy msgid "Context" -msgstr "Conectar" +msgstr "Contexto" #: windows/views.py:2345 msgid "Parameters" msgstr "Parámetros" #: windows/views.py:2354 -#, fuzzy msgid "Data access" -msgstr "Bases de datos" +msgstr "Acceso a datos" #: windows/views.py:2361 msgid "CONTAINS SQL" -msgstr "" +msgstr "CONTAINS SQL" #: windows/views.py:2361 msgid "NO SQL" -msgstr "" +msgstr "NO SQL" #: windows/views.py:2361 msgid "READS SQL DATA" -msgstr "" +msgstr "READS SQL DATA" #: windows/views.py:2361 -#, fuzzy msgid "MODIFIES SQL DATA" -msgstr "Modificado en" +msgstr "MODIFIES SQL DATA" #: windows/views.py:2371 -#, fuzzy msgid "Deterministic" -msgstr "Definición" +msgstr "Determinista" #: windows/views.py:2395 -#, fuzzy msgid "SQL" -msgstr "*.sql" +msgstr "SQL" #: windows/views.py:2395 msgid "PLPGSQL" -msgstr "" +msgstr "PLPGSQL" #: windows/views.py:2405 -#, fuzzy msgid "Volatility" -msgstr "Virtualidad" +msgstr "Volatilidad" #: windows/views.py:2412 msgid "VOLATILE" -msgstr "" +msgstr "VOLÁTIL" #: windows/views.py:2412 -#, fuzzy msgid "STABLE" -msgstr "Tabla" +msgstr "ESTABLE" #: windows/views.py:2412 -#, fuzzy msgid "IMMUTABLE" -msgstr "Tabla" +msgstr "INMUTABLE" #: windows/views.py:2422 msgid "Parallel" -msgstr "" +msgstr "Paralelo" #: windows/views.py:2429 -#, fuzzy msgid "UNSAFE" -msgstr "Guardar" +msgstr "INSEGURO" #: windows/views.py:2429 msgid "RESTRICTED" -msgstr "" +msgstr "RESTRINGIDO" #: windows/views.py:2429 -#, fuzzy msgid "SAFE" -msgstr "Guardar" +msgstr "SEGURO" #: windows/views.py:2441 -#, fuzzy msgid "Cost" -msgstr "Cerrar" +msgstr "Costo" #: windows/views.py:2609 -#, fuzzy msgid "Routine" -msgstr "Tiempo de actividad" +msgstr "Rutina" #: windows/views.py:2617 msgid "Trigger" -msgstr "Disparadores" +msgstr "Disparador" #: windows/views.py:2634 msgid "Duplicate" @@ -913,7 +898,7 @@ msgstr "" #: windows/views.py:2656 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "{database_name}.{table_name} - filas {from_row} - {to_row} de {total_rows}" #: windows/views.py:2664 msgid "First" @@ -953,7 +938,7 @@ msgstr "Datos" #: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" -msgstr "Consulta" +msgstr "Nueva consulta" #: windows/views.py:2800 msgid "Close" @@ -961,7 +946,7 @@ msgstr "Cerrar" #: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" -msgstr "Consulta" +msgstr "Cerrar consulta" #: windows/views.py:2804 msgid "Run" @@ -1175,12 +1160,12 @@ msgstr "Ejecutar todo" #: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" -msgstr "Consulta" +msgstr "Consulta (1)" #: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" -msgstr "Query ({query_number})" +msgstr "Consulta ({query_number})" #: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" @@ -1374,14 +1359,12 @@ msgid "Reconnection failed:" msgstr "Reconexión fallida:" #: windows/main/database/routine.py:545 -#, fuzzy msgid "Function created successfully" -msgstr "Vista creada exitosamente" +msgstr "Función creada exitosamente" #: windows/main/database/routine.py:547 -#, fuzzy msgid "Function updated successfully" -msgstr "Vista actualizada exitosamente" +msgstr "Función actualizada exitosamente" #: windows/main/database/routine.py:551 msgid "Procedure created successfully" @@ -1392,37 +1375,36 @@ msgid "Procedure updated successfully" msgstr "Procedimiento actualizado exitosamente" #: windows/main/database/routine.py:590 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Error saving routine: {}" -msgstr "Error al guardar vista: {}" +msgstr "Error al guardar rutina: {}" #: windows/main/database/routine.py:604 -#, fuzzy msgid "Function" -msgstr "Funciones" +msgstr "Función" #: windows/main/database/routine.py:604 msgid "Procedure" -msgstr "Procedure" +msgstr "Procedimiento" #: windows/main/database/routine.py:607 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Are you sure you want to delete {} '{}'?" -msgstr "¿Está seguro de que desea eliminar la vista '{}'?" +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 -#, fuzzy, python-brace-format +#, python-brace-format msgid "{} deleted successfully" -msgstr "Vista eliminada exitosamente" +msgstr "{} eliminado exitosamente" #: windows/main/database/routine.py:634 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Error deleting routine: {}" -msgstr "Error al eliminar vista: {}" +msgstr "Error al eliminar rutina: {}" #: windows/main/database/view.py:255 msgid "View created successfully" @@ -1489,9 +1471,8 @@ msgid "No active database connection" msgstr "Sin conexión de base de datos activa" #: windows/main/query/controller.py:227 -#, fuzzy msgid "Database connection lost" -msgstr "Conexión perdida" +msgstr "Conexión de base de datos perdida" #: windows/main/query/history.py:55 msgid "(empty query)" @@ -1505,7 +1486,7 @@ 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 "Query {query_number}" +msgstr "Consulta {query_number}" #: windows/main/query/renderer.py:65 #, python-brace-format diff --git a/locale/fr_FR/LC_MESSAGES/petersql.po b/locale/fr_FR/LC_MESSAGES/petersql.po index cc5ee8d..90b296b 100644 --- a/locale/fr_FR/LC_MESSAGES/petersql.po +++ b/locale/fr_FR/LC_MESSAGES/petersql.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-06-06 10:58+0200\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" @@ -162,11 +162,11 @@ msgstr "Mot de passe" #: windows/views.py:203 msgid "Connection timeout" -msgstr "Connexion perdue" +msgstr "Délai de connexion" #: windows/views.py:221 msgid "Use TLS" -msgstr "Use TLS" +msgstr "Utiliser TLS" #: windows/views.py:232 msgid "Mark read only" @@ -190,7 +190,7 @@ msgstr "Sélectionner un fichier" #: windows/views.py:278 windows/views.py:397 msgid "*.*" -msgstr "*. *" +msgstr "*.*" #: windows/components/dataview.py:70 windows/components/dataview.py:92 #: windows/views.py:295 windows/views.py:1372 windows/views.py:1624 @@ -264,7 +264,7 @@ msgstr "Connexions réussies" #: windows/views.py:501 msgid "Last successful connection" -msgstr "Connexions réussies" +msgstr "Dernière connexion réussie" #: windows/views.py:518 msgid "Unsuccessful connections" @@ -276,11 +276,11 @@ msgstr "Dernière raison de l'échec" #: windows/views.py:552 msgid "Total connection attempts" -msgstr "Dernière connexion" +msgstr "Total des tentatives de connexion" #: windows/views.py:569 msgid "Average connection time (ms)" -msgstr "Échec de la reconnexion :" +msgstr "Temps moyen de connexion (ms)" #: windows/views.py:586 msgid "Most recent connection duration" @@ -771,126 +771,111 @@ msgstr "Type de données" #: windows/views.py:2269 msgid "Procedure (doesn't return a result)" -msgstr "" +msgstr "Procédure (ne renvoie pas de résultat)" #: windows/views.py:2269 msgid "Function (return a result)" -msgstr "" +msgstr "Fonction (renvoie un résultat)" #: windows/views.py:2279 -#, fuzzy msgid "Return type" -msgstr "Type de données" +msgstr "Type de retour" #: windows/views.py:2299 -#, fuzzy msgid "Comment" -msgstr "Commentaires" +msgstr "Commentaire" #: windows/components/dataview.py:111 windows/views.py:2335 msgid "#" msgstr "#" #: windows/views.py:2337 -#, fuzzy msgid "Datatype" msgstr "Type de données" #: windows/views.py:2338 -#, fuzzy msgid "Context" -msgstr "Connecter" +msgstr "Contexte" #: windows/views.py:2345 msgid "Parameters" msgstr "Paramètres" #: windows/views.py:2354 -#, fuzzy msgid "Data access" -msgstr "Bases de données" +msgstr "Accès aux données" #: windows/views.py:2361 msgid "CONTAINS SQL" -msgstr "" +msgstr "CONTAINS SQL" #: windows/views.py:2361 msgid "NO SQL" -msgstr "" +msgstr "NO SQL" #: windows/views.py:2361 msgid "READS SQL DATA" -msgstr "" +msgstr "READS SQL DATA" #: windows/views.py:2361 -#, fuzzy msgid "MODIFIES SQL DATA" -msgstr "Modifié le" +msgstr "MODIFIES SQL DATA" #: windows/views.py:2371 -#, fuzzy msgid "Deterministic" -msgstr "Définition" +msgstr "Déterministe" #: windows/views.py:2395 -#, fuzzy msgid "SQL" -msgstr "*.sql" +msgstr "SQL" #: windows/views.py:2395 msgid "PLPGSQL" -msgstr "" +msgstr "PLPGSQL" #: windows/views.py:2405 -#, fuzzy msgid "Volatility" -msgstr "Virtualité" +msgstr "Volatilité" #: windows/views.py:2412 msgid "VOLATILE" -msgstr "" +msgstr "VOLATILE" #: windows/views.py:2412 -#, fuzzy msgid "STABLE" -msgstr "Table" +msgstr "STABLE" #: windows/views.py:2412 -#, fuzzy msgid "IMMUTABLE" -msgstr "Table" +msgstr "IMMUTABLE" #: windows/views.py:2422 msgid "Parallel" -msgstr "" +msgstr "Parallèle" #: windows/views.py:2429 -#, fuzzy msgid "UNSAFE" -msgstr "Enregistrer" +msgstr "UNSAFE" #: windows/views.py:2429 msgid "RESTRICTED" -msgstr "" +msgstr "RESTRICTED" #: windows/views.py:2429 -#, fuzzy msgid "SAFE" -msgstr "Enregistrer" +msgstr "SAFE" #: windows/views.py:2441 -#, fuzzy msgid "Cost" -msgstr "Fermer" +msgstr "Coût" #: windows/views.py:2609 -#, fuzzy msgid "Routine" -msgstr "Temps de fonctionnement" +msgstr "Routine" #: windows/views.py:2617 msgid "Trigger" -msgstr "Déclencheurs" +msgstr "Déclencheur" #: windows/views.py:2634 msgid "Duplicate" @@ -911,7 +896,7 @@ msgstr "" #: windows/views.py:2656 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "{database_name}.{table_name} - lignes {from_row} - {to_row} sur {total_rows}" #: windows/views.py:2664 msgid "First" @@ -951,7 +936,7 @@ msgstr "Données" #: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" -msgstr "Requête" +msgstr "Nouvelle requête" #: windows/views.py:2800 msgid "Close" @@ -959,7 +944,7 @@ msgstr "Fermer" #: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" -msgstr "Requête" +msgstr "Fermer la requête" #: windows/views.py:2804 msgid "Run" @@ -1175,12 +1160,12 @@ msgstr "Tout exécuter" #: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" -msgstr "Requête" +msgstr "Requête (1)" #: windows/main/controller.py:469 #, python-brace-format msgid "Query ({query_number})" -msgstr "Query ({query_number})" +msgstr "Requête ({query_number})" #: windows/main/controller.py:518 msgid "You have unsaved changes. Save before closing?" @@ -1376,14 +1361,12 @@ msgid "Reconnection failed:" msgstr "Échec de la reconnexion :" #: windows/main/database/routine.py:545 -#, fuzzy msgid "Function created successfully" -msgstr "Vue créée avec succès" +msgstr "Fonction créée avec succès" #: windows/main/database/routine.py:547 -#, fuzzy msgid "Function updated successfully" -msgstr "Vue mise à jour avec succès" +msgstr "Fonction mise à jour avec succès" #: windows/main/database/routine.py:551 msgid "Procedure created successfully" @@ -1394,37 +1377,36 @@ msgid "Procedure updated successfully" msgstr "Procédure mise à jour avec succès" #: windows/main/database/routine.py:590 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Error saving routine: {}" -msgstr "Erreur lors de l'enregistrement de la vue: {}" +msgstr "Erreur lors de l'enregistrement de la routine: {}" #: windows/main/database/routine.py:604 -#, fuzzy msgid "Function" -msgstr "Fonctions" +msgstr "Fonction" #: windows/main/database/routine.py:604 msgid "Procedure" -msgstr "Procedure" +msgstr "Procédure" #: windows/main/database/routine.py:607 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Are you sure you want to delete {} '{}'?" -msgstr "Êtes-vous sûr de vouloir supprimer la vue '{}'?" +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 -#, fuzzy, python-brace-format +#, python-brace-format msgid "{} deleted successfully" -msgstr "Vue supprimée avec succès" +msgstr "{} supprimé avec succès" #: windows/main/database/routine.py:634 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Error deleting routine: {}" -msgstr "Erreur lors de la suppression de la vue: {}" +msgstr "Erreur lors de la suppression de la routine: {}" #: windows/main/database/view.py:255 msgid "View created successfully" @@ -1491,9 +1473,8 @@ msgid "No active database connection" msgstr "Aucune connexion de base de données active" #: windows/main/query/controller.py:227 -#, fuzzy msgid "Database connection lost" -msgstr "Connexion perdue" +msgstr "Connexion de base de données perdue" #: windows/main/query/history.py:55 msgid "(empty query)" @@ -1507,7 +1488,7 @@ 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 "Query {query_number}" +msgstr "Requête {query_number}" #: windows/main/query/renderer.py:65 #, python-brace-format diff --git a/locale/it_IT/LC_MESSAGES/petersql.po b/locale/it_IT/LC_MESSAGES/petersql.po index 0134efd..13f2716 100644 --- a/locale/it_IT/LC_MESSAGES/petersql.po +++ b/locale/it_IT/LC_MESSAGES/petersql.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PeterSQL 0.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-06-06 10:58+0200\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" @@ -276,11 +276,11 @@ msgstr "Ultimo motivo di fallimento" #: windows/views.py:552 msgid "Total connection attempts" -msgstr "Totale connessioni" +msgstr "Totale tentativi di connessione" #: 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:586 msgid "Most recent connection duration" @@ -771,122 +771,107 @@ msgstr "Tipo di dati" #: windows/views.py:2269 msgid "Procedure (doesn't return a result)" -msgstr "" +msgstr "Procedura (non restituisce un risultato)" #: windows/views.py:2269 msgid "Function (return a result)" -msgstr "" +msgstr "Funzione (restituisce un risultato)" #: windows/views.py:2279 -#, fuzzy msgid "Return type" -msgstr "Tipo di dati" +msgstr "Tipo di ritorno" #: windows/views.py:2299 -#, fuzzy msgid "Comment" -msgstr "Commenti" +msgstr "Commento" #: windows/components/dataview.py:111 windows/views.py:2335 msgid "#" msgstr "#" #: windows/views.py:2337 -#, fuzzy msgid "Datatype" msgstr "Tipo di dati" #: windows/views.py:2338 -#, fuzzy msgid "Context" -msgstr "Connetti" +msgstr "Contesto" #: windows/views.py:2345 msgid "Parameters" msgstr "Parametri" #: windows/views.py:2354 -#, fuzzy msgid "Data access" -msgstr "Database" +msgstr "Accesso dati" #: windows/views.py:2361 msgid "CONTAINS SQL" -msgstr "" +msgstr "CONTAINS SQL" #: windows/views.py:2361 msgid "NO SQL" -msgstr "" +msgstr "NO SQL" #: windows/views.py:2361 msgid "READS SQL DATA" -msgstr "" +msgstr "READS SQL DATA" #: windows/views.py:2361 -#, fuzzy msgid "MODIFIES SQL DATA" -msgstr "Modificato il" +msgstr "MODIFIES SQL DATA" #: windows/views.py:2371 -#, fuzzy msgid "Deterministic" -msgstr "Definizione" +msgstr "Deterministico" #: windows/views.py:2395 -#, fuzzy msgid "SQL" -msgstr "*.sql" +msgstr "SQL" #: windows/views.py:2395 msgid "PLPGSQL" -msgstr "" +msgstr "PLPGSQL" #: windows/views.py:2405 -#, fuzzy msgid "Volatility" -msgstr "Virtualità" +msgstr "Volatilità" #: windows/views.py:2412 msgid "VOLATILE" -msgstr "" +msgstr "VOLATILE" #: windows/views.py:2412 -#, fuzzy msgid "STABLE" -msgstr "Tabella" +msgstr "STABILE" #: windows/views.py:2412 -#, fuzzy msgid "IMMUTABLE" -msgstr "Tabella" +msgstr "IMMUTABILE" #: windows/views.py:2422 msgid "Parallel" -msgstr "" +msgstr "Parallelo" #: windows/views.py:2429 -#, fuzzy msgid "UNSAFE" -msgstr "Salva" +msgstr "UNSAFE" #: windows/views.py:2429 msgid "RESTRICTED" -msgstr "" +msgstr "RESTRICTED" #: windows/views.py:2429 -#, fuzzy msgid "SAFE" -msgstr "Salva" +msgstr "SAFE" #: windows/views.py:2441 -#, fuzzy msgid "Cost" -msgstr "Chiudi" +msgstr "Costo" #: windows/views.py:2609 -#, fuzzy msgid "Routine" -msgstr "Tempo di attività" +msgstr "Routine" #: windows/views.py:2617 msgid "Trigger" @@ -911,7 +896,7 @@ msgstr "" #: windows/views.py:2656 #, python-brace-format msgid "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" -msgstr "{database_name}.{table_name} - rows {from_row} - {to_row} of {total_rows}" +msgstr "{database_name}.{table_name} - righe {from_row} - {to_row} di {total_rows}" #: windows/views.py:2664 msgid "First" @@ -951,7 +936,7 @@ msgstr "Dati" #: windows/main/controller.py:319 windows/views.py:2798 msgid "New query" -msgstr "Query" +msgstr "Nuova query" #: windows/views.py:2800 msgid "Close" @@ -959,7 +944,7 @@ msgstr "Chiudi" #: windows/main/controller.py:320 windows/views.py:2800 msgid "Close query" -msgstr "Query" +msgstr "Chiudi query" #: windows/views.py:2804 msgid "Run" @@ -1173,7 +1158,7 @@ msgstr "Esegui tutto" #: windows/main/controller.py:442 windows/main/controller.py:450 msgid "Query (1)" -msgstr "Query" +msgstr "Query (1)" #: windows/main/controller.py:469 #, python-brace-format @@ -1371,14 +1356,12 @@ msgid "Reconnection failed:" msgstr "Riconnessione fallita:" #: windows/main/database/routine.py:545 -#, fuzzy msgid "Function created successfully" -msgstr "Vista creata con successo" +msgstr "Funzione creata con successo" #: windows/main/database/routine.py:547 -#, fuzzy msgid "Function updated successfully" -msgstr "Vista aggiornata con successo" +msgstr "Funzione aggiornata con successo" #: windows/main/database/routine.py:551 msgid "Procedure created successfully" @@ -1389,37 +1372,36 @@ msgid "Procedure updated successfully" msgstr "Procedura aggiornata con successo" #: windows/main/database/routine.py:590 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Error saving routine: {}" -msgstr "Errore nel salvataggio della vista: {}" +msgstr "Errore nel salvataggio della routine: {}" #: windows/main/database/routine.py:604 -#, fuzzy msgid "Function" -msgstr "Funzioni" +msgstr "Funzione" #: windows/main/database/routine.py:604 msgid "Procedure" -msgstr "Procedure" +msgstr "Procedura" #: windows/main/database/routine.py:607 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Are you sure you want to delete {} '{}'?" -msgstr "Sei sicuro di voler eliminare la vista '{}'?" +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 -#, fuzzy, python-brace-format +#, python-brace-format msgid "{} deleted successfully" -msgstr "Vista eliminata con successo" +msgstr "{} eliminato con successo" #: windows/main/database/routine.py:634 -#, fuzzy, python-brace-format +#, python-brace-format msgid "Error deleting routine: {}" -msgstr "Errore nell'eliminazione della vista: {}" +msgstr "Errore nell'eliminazione della routine: {}" #: windows/main/database/view.py:255 msgid "View created successfully" From 1b63c0f81225ea745cbd07d7f0f3b6402c043524 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Mon, 8 Jun 2026 14:56:07 +0200 Subject: [PATCH 88/93] Fix MySQL and MariaDB index alter implementations --- structures/engines/mariadb/database.py | 4 ++++ structures/engines/mysql/database.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/structures/engines/mariadb/database.py b/structures/engines/mariadb/database.py index 57bddcd..606335a 100644 --- a/structures/engines/mariadb/database.py +++ b/structures/engines/mariadb/database.py @@ -274,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/mysql/database.py b/structures/engines/mysql/database.py index 7938470..e6a67b4 100644 --- a/structures/engines/mysql/database.py +++ b/structures/engines/mysql/database.py @@ -275,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() From 59466bb0337f2335bd22acd87b2d644a019fc699 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 9 Jun 2026 16:56:25 +0200 Subject: [PATCH 89/93] Store connection passwords in system keyring --- PROJECT_STATUS.md | 5 +- README.md | 25 ++--- pyproject.toml | 1 + structures/secrets.py | 59 +++++++++++ tests/core/test_connections_repository.py | 118 +++++++++++++++++++--- uv.lock | 92 +++++++++++++++++ windows/dialogs/connections/repository.py | 69 ++++++++++++- 7 files changed, 338 insertions(+), 31 deletions(-) create mode 100644 structures/secrets.py diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 56f2a12..595b1b4 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -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) @@ -182,6 +182,7 @@ ## 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. diff --git a/README.md b/README.md index 40b62a2..10d013a 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-56%25-brightgreen) +![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-tested-lightgrey) -![MySQL](https://img.shields.io/badge/MySQL-5.7%20%7C%208.0%20%7C%20latest-lightgrey) -![MariaDB](https://img.shields.io/badge/MariaDB-5.5%20%7C%2010.11%20%7C%2011.8%20%7C%20latest-lightgrey) -![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15%20%7C%2016%20%7C%20latest-lightgrey) +![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-green) # PeterSQL @@ -38,6 +38,7 @@ For a detailed status snapshot, see: ### Recent updates + - Connection passwords are now stored securely via system keyring 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. @@ -124,13 +125,13 @@ For detailed test coverage matrix, statistics, and architecture, see **[tests/RE | Suite | Passed | Skipped | |-------|--------|---------| -| autocomplete | ![passed](https://img.shields.io/badge/passed-1944-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-59-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-1-lightgrey) | -| mariadb | ![passed](https://img.shields.io/badge/passed-115-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-3-lightgrey) | -| postgresql | ![passed](https://img.shields.io/badge/passed-132-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-0-lightgrey) | -| sqlite | ![passed](https://img.shields.io/badge/passed-21-brightgreen) | ![skipped](https://img.shields.io/badge/skipped-5-lightgrey) | +| 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-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/pyproject.toml b/pyproject.toml index 75450a4..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", diff --git a/structures/secrets.py b/structures/secrets.py new file mode 100644 index 0000000..69eacaa --- /dev/null +++ b/structures/secrets.py @@ -0,0 +1,59 @@ +from typing import Optional + +import keyring + +SERVICE_NAME = "PeterSQL" + + +def _database_password_key(connection_id: int) -> str: + return f"connection:{connection_id}:database_password" + + +def _ssh_password_key(connection_id: int) -> str: + return f"connection:{connection_id}:ssh_password" + + +def get_database_password(connection_id: int) -> Optional[str]: + key = _database_password_key(connection_id) + value = keyring.get_password(SERVICE_NAME, key) + return value if value else None + + +def set_database_password(connection_id: int, password: Optional[str]) -> None: + key = _database_password_key(connection_id) + if password is None or password == "": + delete_database_password(connection_id) + return + + keyring.set_password(SERVICE_NAME, key, password) + + +def delete_database_password(connection_id: int) -> None: + key = _database_password_key(connection_id) + try: + keyring.delete_password(SERVICE_NAME, key) + except keyring.errors.PasswordDeleteError: + pass + + +def get_ssh_password(connection_id: int) -> Optional[str]: + key = _ssh_password_key(connection_id) + value = keyring.get_password(SERVICE_NAME, key) + return value if value else None + + +def set_ssh_password(connection_id: int, password: Optional[str]) -> None: + key = _ssh_password_key(connection_id) + if password is None or password == "": + delete_ssh_password(connection_id) + return + + keyring.set_password(SERVICE_NAME, key, password) + + +def delete_ssh_password(connection_id: int) -> None: + key = _ssh_password_key(connection_id) + try: + keyring.delete_password(SERVICE_NAME, key) + except keyring.errors.PasswordDeleteError: + pass diff --git a/tests/core/test_connections_repository.py b/tests/core/test_connections_repository.py index 9289684..cbced7b 100644 --- a/tests/core/test_connections_repository.py +++ b/tests/core/test_connections_repository.py @@ -1,5 +1,8 @@ import os import tempfile +from collections import OrderedDict +from typing import Any, Dict, Optional + import pytest import yaml @@ -12,6 +15,29 @@ 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] + + +@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 +51,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 +64,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 = [ { @@ -155,7 +181,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 +201,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 +228,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 +259,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 +275,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 +284,75 @@ 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 saved_data[0]["configuration"]["password_keyring_id"] == "1" + assert saved_data[0]["ssh_tunnel"].get("password") is None + assert saved_data[0]["ssh_tunnel"]["password_keyring_id"] == "1" + assert fake_keyring.get_password("PeterSQL", "connection:1:database_password") == "plaintext" + assert fake_keyring.get_password("PeterSQL", "connection:1: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", "connection:1:database_password") == "secret" + assert fake_keyring.get_password("PeterSQL", "connection:1:ssh_password") == "sshsecret" + + repo.delete_connection(connection) + + assert fake_keyring.get_password("PeterSQL", "connection:1:database_password") is None + assert fake_keyring.get_password("PeterSQL", "connection:1:ssh_password") is None diff --git a/uv.lock b/uv.lock index 1fc389d..619e500 100644 --- a/uv.lock +++ b/uv.lock @@ -256,6 +256,48 @@ 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" @@ -268,6 +310,23 @@ 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" @@ -407,6 +466,15 @@ wheels = [ { 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" @@ -498,6 +566,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "babel" }, + { name = "keyring" }, { name = "oracledb" }, { name = "psutil" }, { name = "psycopg2-binary" }, @@ -529,6 +598,7 @@ 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" }, @@ -880,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" @@ -943,6 +1022,19 @@ 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" diff --git a/windows/dialogs/connections/repository.py b/windows/dialogs/connections/repository.py index ddb677f..3f91dd2 100644 --- a/windows/dialogs/connections/repository.py +++ b/windows/dialogs/connections/repository.py @@ -6,6 +6,15 @@ from helpers.observables import ObservableLazyList from helpers.repository import YamlRepository +from structures.secrets import ( + 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 +49,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]]: @@ -90,12 +99,15 @@ def _connection_from_dict( ] = None 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: + config_data["password"] = get_database_password(password_keyring_id) configuration = self._build_credentials_configuration(config_data) elif engine == ConnectionEngine.SQLITE: configuration = SourceConfiguration(**config_data) @@ -151,6 +163,8 @@ def add_connection( if connection.is_new: connection.id = self._next_id() + self._persist_connection_secrets(connection) + if parent: parent.children.append(connection) connection.parent = parent @@ -181,6 +195,8 @@ def _find_and_replace( return True return False + self._persist_connection_secrets(connection) + if _find_and_replace(self.connections.get_value(), connection.id): self._write() self.connections.refresh() @@ -305,6 +321,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() @@ -354,6 +371,54 @@ 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 + + configuration = data.get("configuration") + if configuration: + configuration = dict(configuration) + password = configuration.pop("password", None) + if password not in (None, ""): + configuration["password_keyring_id"] = str(item.id) + set_database_password(item.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["password_keyring_id"] = str(item.id) + set_ssh_password(item.id, ssh_password) + elif "password_keyring_id" in ssh_tunnel: + del ssh_tunnel["password_keyring_id"] + data["ssh_tunnel"] = ssh_tunnel + + return data + + def _persist_connection_secrets(self, connection: Connection) -> None: + if isinstance(connection.configuration, CredentialsConfiguration): + set_database_password( + connection.id, connection.configuration.password + ) + + if connection.ssh_tunnel: + set_ssh_password(connection.id, connection.ssh_tunnel.password) + + def _delete_connection_secrets(self, connection: Connection) -> None: + delete_database_password(connection.id) + delete_ssh_password(connection.id) + @staticmethod def _normalize_ssh_extra_args(extra_args: Any) -> Optional[Union[str, list[str]]]: if isinstance(extra_args, str): From aecdbe614fbc5c6339a656345d4750b6cc149d6c Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 9 Jun 2026 22:53:42 +0200 Subject: [PATCH 90/93] fix(secrets): use stable UUID-based secret_id for keyring keys Replace numeric connection.id with a dedicated secret_id (UUID) as the keyring lookup key. Existing entries stored under numeric IDs are migrated on first load and re-keyed automatically. --- structures/connection.py | 7 ++- structures/secrets.py | 45 +++++++++------- windows/dialogs/connections/repository.py | 62 ++++++++++++++++++----- 3 files changed, 83 insertions(+), 31 deletions(-) diff --git a/structures/connection.py b/structures/connection.py index 8f31fea..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 @@ -85,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): @@ -103,7 +105,7 @@ def copy(self): return dataclasses.replace(self) def to_dict(self): - return { + data = { "id": self.id, "type": "connection", "name": self.name, @@ -124,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/secrets.py b/structures/secrets.py index 69eacaa..c0899cc 100644 --- a/structures/secrets.py +++ b/structures/secrets.py @@ -1,58 +1,67 @@ +import re from typing import Optional import keyring SERVICE_NAME = "PeterSQL" +_LEGACY_NUMERIC_ID_PATTERN = re.compile(r"^[0-9]+$") -def _database_password_key(connection_id: int) -> str: - return f"connection:{connection_id}:database_password" +def _database_password_key(secret_id: str) -> str: + return f"connection:{secret_id}:database_password" -def _ssh_password_key(connection_id: int) -> str: - return f"connection:{connection_id}:ssh_password" +def _ssh_password_key(secret_id: str) -> str: + return f"connection:{secret_id}:ssh_password" -def get_database_password(connection_id: int) -> Optional[str]: - key = _database_password_key(connection_id) + +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(connection_id: int, password: Optional[str]) -> None: - key = _database_password_key(connection_id) +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(connection_id) + delete_database_password(secret_id) return keyring.set_password(SERVICE_NAME, key, password) -def delete_database_password(connection_id: int) -> None: - key = _database_password_key(connection_id) +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(connection_id: int) -> Optional[str]: - key = _ssh_password_key(connection_id) +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(connection_id: int, password: Optional[str]) -> None: - key = _ssh_password_key(connection_id) +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(connection_id) + delete_ssh_password(secret_id) return keyring.set_password(SERVICE_NAME, key, password) -def delete_ssh_password(connection_id: int) -> None: - key = _ssh_password_key(connection_id) +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: diff --git a/windows/dialogs/connections/repository.py b/windows/dialogs/connections/repository.py index 3f91dd2..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 @@ -7,6 +8,7 @@ from helpers.repository import YamlRepository from structures.secrets import ( + _is_legacy_numeric_id, delete_database_password, delete_ssh_password, get_database_password, @@ -98,6 +100,10 @@ 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 = dict(data["configuration"]) if engine in [ @@ -107,12 +113,32 @@ def _connection_from_dict( ]: password_keyring_id = config_data.pop("password_keyring_id", None) if password_keyring_id is not None: - config_data["password"] = get_database_password(password_keyring_id) + 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) @@ -153,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( @@ -163,6 +190,9 @@ 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: @@ -195,6 +225,9 @@ 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): @@ -382,13 +415,15 @@ def _prepare_connection_dict( ] 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["password_keyring_id"] = str(item.id) - set_database_password(item.id, password) + 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 @@ -398,26 +433,29 @@ def _prepare_connection_dict( ssh_tunnel = dict(ssh_tunnel) ssh_password = ssh_tunnel.pop("password", None) if ssh_password not in (None, ""): - ssh_tunnel["password_keyring_id"] = str(item.id) - set_ssh_password(item.id, ssh_password) + 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( - connection.id, connection.configuration.password - ) + set_database_password(secret_id, connection.configuration.password) if connection.ssh_tunnel: - set_ssh_password(connection.id, connection.ssh_tunnel.password) + set_ssh_password(secret_id, connection.ssh_tunnel.password) def _delete_connection_secrets(self, connection: Connection) -> None: - delete_database_password(connection.id) - delete_ssh_password(connection.id) + 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]]]: From dacab42f4c09d13ec1e7b5e97eee285850915a1e Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 9 Jun 2026 22:53:52 +0200 Subject: [PATCH 91/93] fix(query): recreate executor only on session change; renderer created once --- windows/main/query/controller.py | 8 ++++++++ windows/main/query/executor.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/windows/main/query/controller.py b/windows/main/query/controller.py index e145b15..1151251 100644 --- a/windows/main/query/controller.py +++ b/windows/main/query/controller.py @@ -44,6 +44,7 @@ def __init__( 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 @@ -181,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() diff --git a/windows/main/query/executor.py b/windows/main/query/executor.py index 2ba989d..9af752e 100644 --- a/windows/main/query/executor.py +++ b/windows/main/query/executor.py @@ -265,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() From 4513607ba37fff307021e1731dcc203280133eaa Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 9 Jun 2026 22:54:02 +0200 Subject: [PATCH 92/93] fix(ui): set Tables/Database as default tabs; fix notebook expand; add sash gravity --- PeterSQL.fbp | 343 +++++++++++++++++++++++++---------------------- windows/views.py | 51 ++++--- 2 files changed, 219 insertions(+), 175 deletions(-) diff --git a/PeterSQL.fbp b/PeterSQL.fbp index d5b2430..4b75db3 100755 --- a/PeterSQL.fbp +++ b/PeterSQL.fbp @@ -7386,8 +7386,8 @@ 5 wxALL|wxEXPAND - 0 - + 1 + 1 1 1 @@ -7654,7 +7654,7 @@ Load From File; icons/16x16/database.png Database - 0 + 1 1 1 @@ -10542,7 +10542,7 @@ Tables - 0 + 1 1 1 @@ -10701,136 +10701,125 @@ 5 - wxEXPAND + wxALL|wxEXPAND 1 - + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + - bSizer152 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - 0 - 1 - - - 0 - wxID_ANY - - - list_ctrl_database_tables - protected - - - - ; ; forward_declare - - - - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Name - wxDATAVIEW_CELL_INERT - 0 - m_dataViewColumn12 - protected - Text - -1 - - - wxALIGN_RIGHT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Rows - wxDATAVIEW_CELL_INERT - 1 - m_dataViewColumn13 - protected - Text - -1 - - - wxALIGN_RIGHT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Size - wxDATAVIEW_CELL_INERT - 2 - m_dataViewColumn14 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Created at - wxDATAVIEW_CELL_INERT - 3 - m_dataViewColumn15 - protected - Date - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Updated at - wxDATAVIEW_CELL_INERT - 4 - m_dataViewColumn16 - protected - Date - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Engine - wxDATAVIEW_CELL_INERT - 5 - m_dataViewColumn17 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Collation - wxDATAVIEW_CELL_INERT - 6 - m_dataViewColumn19 - protected - Text - -1 - - - wxALIGN_LEFT - - wxDATAVIEW_COL_RESIZABLE|wxDATAVIEW_COL_SORTABLE - Comments - wxDATAVIEW_CELL_INERT - 7 - m_dataViewColumn18 - protected - Text - -1 - - + 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 @@ -11270,7 +11259,7 @@ Functions - 1 + 0 1 1 @@ -11919,17 +11908,6 @@ - - 5 - wxEXPAND - 1 - - - bSizer147 - wxVERTICAL - none - - 5 wxEXPAND @@ -12539,7 +12517,7 @@ 1 Resizable - 0.0 + 0.5 200 -1 1 @@ -17401,11 +17379,11 @@ - + Load From File; icons/16x16/code-folding.png Routine - 1 - + 0 + 1 1 1 @@ -17457,16 +17435,16 @@ wxTAB_TRAVERSAL - + bSizer160 wxVERTICAL none - + 5 wxEXPAND 1 - + 1 1 1 @@ -17524,8 +17502,8 @@ - - + + 1 1 1 @@ -17577,16 +17555,16 @@ wxTAB_TRAVERSAL - + bSizer166 wxVERTICAL none - + 5 wxEXPAND | wxALL 1 - + 1 1 1 @@ -19760,7 +19738,7 @@ - + Security 0 @@ -20343,11 +20321,11 @@ - + 5 wxEXPAND 0 - + bSizer911 wxHORIZONTAL @@ -22786,5 +22764,52 @@ + + 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 + 1 + + + bSizer152 + wxVERTICAL + none + + + + diff --git a/windows/views.py b/windows/views.py index baf2be3..df059a2 100755 --- a/windows/views.py +++ b/windows/views.py @@ -1359,8 +1359,6 @@ def __init__( self, parent ): bSizer154.Add( self.m_toolBar51, 0, wx.EXPAND, 5 ) - bSizer152 = wx.BoxSizer( wx.VERTICAL ) - 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 ) @@ -1370,16 +1368,13 @@ def __init__( self, parent ): 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 ) - - - bSizer154.Add( bSizer152, 1, wx.EXPAND, 5 ) + bSizer154.Add( self.list_ctrl_database_tables, 1, wx.ALL|wx.EXPAND, 5 ) self.m_panel55.SetSizer( bSizer154 ) self.m_panel55.Layout() bSizer154.Fit( self.m_panel55 ) - self.m_notebook10.AddPage( self.m_panel55, _(u"Tables"), False ) + 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 ) @@ -1451,7 +1446,7 @@ def __init__( self, parent ): self.m_panel6521.SetSizer( bSizer148211 ) self.m_panel6521.Layout() bSizer148211.Fit( self.m_panel6521 ) - self.m_notebook10.AddPage( self.m_panel6521, _(u"Functions"), True ) + 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 ) @@ -1510,11 +1505,6 @@ def __init__( self, parent ): self.m_splitter7.SplitHorizontally( self.m_panel54, self.m_panel651, 200 ) bSizer80.Add( self.m_splitter7, 1, wx.EXPAND, 5 ) - bSizer147 = wx.BoxSizer( wx.VERTICAL ) - - - bSizer80.Add( bSizer147, 1, wx.EXPAND, 5 ) - bSizer138 = wx.BoxSizer( wx.HORIZONTAL ) self.btn_cancel_database = wx.Button( self.m_panel30, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 ) @@ -1581,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 ) @@ -1592,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 ) @@ -2606,7 +2597,7 @@ def __init__( self, parent ): self.panel_routine.SetSizer( bSizer160 ) self.panel_routine.Layout() bSizer160.Fit( self.panel_routine ) - self.MainFrameNotebook.AddPage( self.panel_routine, _(u"Routine"), True ) + 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 ) @@ -2928,7 +2919,7 @@ def __init__( self, parent ): MainFrameNotebookIndex += 1 - bSizer25.Add( self.MainFrameNotebook, 0, wx.ALL|wx.EXPAND, 5 ) + bSizer25.Add( self.MainFrameNotebook, 1, wx.ALL|wx.EXPAND, 5 ) self.m_panel15.SetSizer( bSizer25 ) @@ -3319,3 +3310,31 @@ def m_splitter8OnIdle( self, event ): self.m_splitter8.Unbind( wx.EVT_IDLE ) +########################################################################### +## Class Trash +########################################################################### + +class Trash ( wx.Frame ): + + def __init__( self, parent ): + 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 ) + + bSizer147 = wx.BoxSizer( wx.VERTICAL ) + + bSizer152 = wx.BoxSizer( wx.VERTICAL ) + + + bSizer147.Add( bSizer152, 1, wx.EXPAND, 5 ) + + + self.SetSizer( bSizer147 ) + self.Layout() + + self.Centre( wx.BOTH ) + + def __del__( self ): + pass + + From 581789143946d50a02a759a881f22e5c119aa275 Mon Sep 17 00:00:00 2001 From: gtripoli Date: Tue, 9 Jun 2026 22:56:22 +0200 Subject: [PATCH 93/93] fix(tests): update connection repository tests for UUID secret_id migration Update existing assertions to use secret_id-based keyring keys; add test_load_migrates_legacy_numeric_keyring_ids and test_save_connection_persists_uuid_secret_id. --- README.md | 2 +- tests/core/test_connections_repository.py | 93 +++++++++++++++++++++-- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 10d013a..8ef5837 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ For a detailed status snapshot, see: ### Recent updates - - Connection passwords are now stored securely via system keyring instead of plaintext YAML. + - 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. diff --git a/tests/core/test_connections_repository.py b/tests/core/test_connections_repository.py index cbced7b..e2c2359 100644 --- a/tests/core/test_connections_repository.py +++ b/tests/core/test_connections_repository.py @@ -1,5 +1,8 @@ + import os +import re import tempfile +import uuid from collections import OrderedDict from typing import Any, Dict, Optional @@ -31,6 +34,10 @@ def delete_password(self, service: str, username: str) -> None: 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() @@ -108,6 +115,8 @@ def test_load_connections_from_yaml(self, temp_yaml, repo, fake_keyring): 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] @@ -121,6 +130,8 @@ def test_load_connections_from_yaml(self, temp_yaml, repo, fake_keyring): 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.""" @@ -319,11 +330,12 @@ def test_save_connection_moves_plaintext_password_to_keyring( saved_data = yaml.safe_load(f) assert saved_data[0]["configuration"].get("password") is None - assert saved_data[0]["configuration"]["password_keyring_id"] == "1" + assert "password_keyring_id" not in saved_data[0]["configuration"] assert saved_data[0]["ssh_tunnel"].get("password") is None - assert saved_data[0]["ssh_tunnel"]["password_keyring_id"] == "1" - assert fake_keyring.get_password("PeterSQL", "connection:1:database_password") == "plaintext" - assert fake_keyring.get_password("PeterSQL", "connection:1:ssh_password") == "sshplain" + 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 @@ -349,10 +361,75 @@ def test_delete_connection_removes_keyring_entries( ), ) repo.add_connection(connection) - assert fake_keyring.get_password("PeterSQL", "connection:1:database_password") == "secret" - assert fake_keyring.get_password("PeterSQL", "connection:1:ssh_password") == "sshsecret" + 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", "connection:1:database_password") is None - assert fake_keyring.get_password("PeterSQL", "connection:1:ssh_password") is None + 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"