From 10e634754f6f83ea0b6c1486245ecffaa3e324c2 Mon Sep 17 00:00:00 2001
From: gtripoli
Date: Thu, 26 Feb 2026 20:30:56 +0100
Subject: [PATCH 01/72] ci: refactor workflow from pre-commit to pytest with
improved setup
- Rename job from 'pre-commit' to 'test' and remove container configuration
- Replace manual uv installation with astral-sh/setup-uv@v4 action
- Add Python 3.14 setup using actions/setup-python@v5
- Change dependency installation from `uv sync --dev` to `uv sync --extra dev`
- Replace pre-commit execution with pytest running full test suite with coverage
- Update pre-commit hook to use runtest-local.sh script
- Add global constants
---
.github/workflows/ci.yml | 33 +-
.pre-commit-config.yaml | 2 +-
CODE_STYLE.md | 79 +-
README.md | 114 +-
constants.py | 42 +
helpers/__init__.py | 41 +-
helpers/bindings.py | 74 +-
helpers/dataview.py | 2 +-
helpers/repository.py | 24 +
icons/16x16/server-oracle.png | Bin 0 -> 4926 bytes
icons/__init__.py | 19 +-
main.py | 38 +-
pyproject.toml | 11 +
scripts/locales.py | 4 +-
scripts/runtest-local.sh | 19 +
scripts/runtest.sh | 8 +-
settings.py | 19 -
structures/connection.py | 1 +
structures/engines/__init__.py | 10 -
structures/engines/context.py | 5 +
structures/engines/mariadb/context.py | 4 +
structures/engines/mysql/context.py | 4 +
structures/engines/postgresql/context.py | 15 +-
structures/engines/sqlite/context.py | 4 +
tests/autocomplete/autocomplete_adapter.py | 124 +
tests/autocomplete/cases/alias.json | 48 +
.../cases/alias_prefix_disambiguation.json | 199 ++
tests/autocomplete/cases/alx.json | 150 +
tests/autocomplete/cases/curr.json | 59 +
tests/autocomplete/cases/cursor.json | 23 +
.../cases/derived_tables_cte.json | 211 ++
tests/autocomplete/cases/dot.json | 117 +
tests/autocomplete/cases/dot_completion.json | 184 ++
tests/autocomplete/cases/empty.json | 35 +
tests/autocomplete/cases/from.json | 180 ++
.../cases/from_clause_prioritization.json | 85 +
.../cases/from_join_clause_current_table.json | 161 +
tests/autocomplete/cases/fut.json | 23 +
tests/autocomplete/cases/group.json | 105 +
tests/autocomplete/cases/having.json | 117 +
.../cases/having_aggregate_priority.json | 146 +
tests/autocomplete/cases/join.json | 81 +
tests/autocomplete/cases/lex.json | 87 +
tests/autocomplete/cases/limit.json | 35 +
tests/autocomplete/cases/mq.json | 173 ++
.../cases/multi_query_support.json | 360 +++
tests/autocomplete/cases/mw.json | 44 +
tests/autocomplete/cases/on.json | 149 +
.../cases/operator_left_column_filter.json | 198 ++
tests/autocomplete/cases/order.json | 187 ++
.../cases/out_of_scope_hints.json | 165 +
tests/autocomplete/cases/perf.json | 59 +
.../autocomplete/cases/prefix_expansion.json | 236 ++
tests/autocomplete/cases/scope.json | 126 +
.../cases/scope_restriction_join_on.json | 103 +
.../cases/scope_restriction_order_group.json | 104 +
.../cases/scope_restriction_where.json | 84 +
tests/autocomplete/cases/sel.json | 303 ++
tests/autocomplete/cases/select_no_scope.json | 87 +
.../select_qualified_column_whitespace.json | 96 +
.../select_with_scope_current_table.json | 111 +
tests/autocomplete/cases/single.json | 154 +
tests/autocomplete/cases/using.json | 24 +
tests/autocomplete/cases/where.json | 213 ++
.../cases/whitespace_comma_behavior.json | 209 ++
tests/autocomplete/test_autocomplete_basic.py | 230 ++
tests/autocomplete/test_config.json | 81 +
tests/autocomplete/test_golden_cases.py | 65 +
tests/engines/mariadb/conftest.py | 24 +-
tests/engines/mariadb/test_context.py | 1 +
tests/engines/mariadb/test_integration.py | 110 +-
tests/engines/mysql/conftest.py | 24 +-
tests/engines/mysql/test_context.py | 1 +
tests/engines/mysql/test_integration.py | 110 +-
tests/engines/postgresql/conftest.py | 33 +-
tests/engines/postgresql/test_context.py | 1 +
tests/engines/postgresql/test_integration.py | 110 +-
tests/engines/sqlite/test_integration.py | 2 +
tests/test_column_controller.py | 34 +-
tests/test_connections.py | 2 +-
tests/ui/test_column_controller.py | 34 +-
tests/ui/test_connections.py | 4 +-
tests/ui/test_index_controller.py | 14 +-
tests/ui/test_repository.py | 4 +-
themes/README.md | 84 +
windows/__init__.py | 378 ++-
windows/components/dataview.py | 18 +-
windows/components/popup.py | 3 +-
windows/components/stc/__init__.py | 4 +-
windows/components/stc/auto_complete.py | 275 --
.../stc/autocomplete/AUTO_COMPLETE_RULES.md | 2015 ++++++++++++
.../stc/autocomplete/auto_complete.py | 320 ++
.../stc/autocomplete/autocomplete_popup.py | 166 +
.../stc/autocomplete/completion_types.py | 23 +
.../stc/autocomplete/context_detector.py | 262 ++
.../autocomplete/dot_completion_handler.py | 114 +
.../stc/autocomplete/query_scope.py | 29 +
.../stc/autocomplete/sql_context.py | 17 +
.../stc/autocomplete/statement_extractor.py | 45 +
.../stc/autocomplete/suggestion_builder.py | 805 +++++
windows/components/stc/sql_templates.py | 75 +
windows/components/stc/styles.py | 19 +-
windows/components/stc/template_menu.py | 68 +
windows/components/stc/theme_loader.py | 118 +
windows/dialogs/__init__.py | 7 +
.../dialogs/advanced_cell_editor/__init__.py | 3 +
.../advanced_cell_editor/controller.py | 60 +
windows/{ => dialogs}/connections/__init__.py | 0
.../{ => dialogs}/connections/controller.py | 2 +-
windows/{ => dialogs}/connections/model.py | 0
.../{ => dialogs}/connections/repository.py | 38 +-
.../connections/view.py} | 22 +-
windows/dialogs/settings/__init__.py | 3 +
windows/dialogs/settings/controller.py | 223 ++
windows/dialogs/settings/repository.py | 28 +
windows/explorer.bkp.py | 473 ---
windows/main/__init__.py | 57 +-
windows/main/{main_frame.py => controller.py} | 105 +-
windows/main/tabs/__init__.py | 0
windows/main/{ => tabs}/check.py | 2 +-
windows/main/{ => tabs}/column.py | 5 +-
windows/main/{ => tabs}/database.py | 0
windows/main/{ => tabs}/explorer.py | 8 +-
windows/main/{ => tabs}/foreign_key.py | 5 +-
windows/main/{ => tabs}/index.py | 2 +-
windows/main/tabs/query.py | 501 +++
windows/main/{ => tabs}/records.py | 63 +-
windows/main/{ => tabs}/table.py | 4 +-
windows/main/tabs/view.py | 316 ++
windows/state.py | 25 +
windows/views.py | 2739 +++++++++++++++++
131 files changed, 14950 insertions(+), 1390 deletions(-)
create mode 100644 constants.py
create mode 100644 helpers/repository.py
create mode 100644 icons/16x16/server-oracle.png
create mode 100755 scripts/runtest-local.sh
delete mode 100644 settings.py
create mode 100644 tests/autocomplete/autocomplete_adapter.py
create mode 100644 tests/autocomplete/cases/alias.json
create mode 100644 tests/autocomplete/cases/alias_prefix_disambiguation.json
create mode 100644 tests/autocomplete/cases/alx.json
create mode 100644 tests/autocomplete/cases/curr.json
create mode 100644 tests/autocomplete/cases/cursor.json
create mode 100644 tests/autocomplete/cases/derived_tables_cte.json
create mode 100644 tests/autocomplete/cases/dot.json
create mode 100644 tests/autocomplete/cases/dot_completion.json
create mode 100644 tests/autocomplete/cases/empty.json
create mode 100644 tests/autocomplete/cases/from.json
create mode 100644 tests/autocomplete/cases/from_clause_prioritization.json
create mode 100644 tests/autocomplete/cases/from_join_clause_current_table.json
create mode 100644 tests/autocomplete/cases/fut.json
create mode 100644 tests/autocomplete/cases/group.json
create mode 100644 tests/autocomplete/cases/having.json
create mode 100644 tests/autocomplete/cases/having_aggregate_priority.json
create mode 100644 tests/autocomplete/cases/join.json
create mode 100644 tests/autocomplete/cases/lex.json
create mode 100644 tests/autocomplete/cases/limit.json
create mode 100644 tests/autocomplete/cases/mq.json
create mode 100644 tests/autocomplete/cases/multi_query_support.json
create mode 100644 tests/autocomplete/cases/mw.json
create mode 100644 tests/autocomplete/cases/on.json
create mode 100644 tests/autocomplete/cases/operator_left_column_filter.json
create mode 100644 tests/autocomplete/cases/order.json
create mode 100644 tests/autocomplete/cases/out_of_scope_hints.json
create mode 100644 tests/autocomplete/cases/perf.json
create mode 100644 tests/autocomplete/cases/prefix_expansion.json
create mode 100644 tests/autocomplete/cases/scope.json
create mode 100644 tests/autocomplete/cases/scope_restriction_join_on.json
create mode 100644 tests/autocomplete/cases/scope_restriction_order_group.json
create mode 100644 tests/autocomplete/cases/scope_restriction_where.json
create mode 100644 tests/autocomplete/cases/sel.json
create mode 100644 tests/autocomplete/cases/select_no_scope.json
create mode 100644 tests/autocomplete/cases/select_qualified_column_whitespace.json
create mode 100644 tests/autocomplete/cases/select_with_scope_current_table.json
create mode 100644 tests/autocomplete/cases/single.json
create mode 100644 tests/autocomplete/cases/using.json
create mode 100644 tests/autocomplete/cases/where.json
create mode 100644 tests/autocomplete/cases/whitespace_comma_behavior.json
create mode 100644 tests/autocomplete/test_autocomplete_basic.py
create mode 100644 tests/autocomplete/test_config.json
create mode 100644 tests/autocomplete/test_golden_cases.py
create mode 100644 themes/README.md
mode change 100755 => 100644 windows/__init__.py
delete mode 100644 windows/components/stc/auto_complete.py
create mode 100644 windows/components/stc/autocomplete/AUTO_COMPLETE_RULES.md
create mode 100644 windows/components/stc/autocomplete/auto_complete.py
create mode 100644 windows/components/stc/autocomplete/autocomplete_popup.py
create mode 100644 windows/components/stc/autocomplete/completion_types.py
create mode 100644 windows/components/stc/autocomplete/context_detector.py
create mode 100644 windows/components/stc/autocomplete/dot_completion_handler.py
create mode 100644 windows/components/stc/autocomplete/query_scope.py
create mode 100644 windows/components/stc/autocomplete/sql_context.py
create mode 100644 windows/components/stc/autocomplete/statement_extractor.py
create mode 100644 windows/components/stc/autocomplete/suggestion_builder.py
create mode 100644 windows/components/stc/sql_templates.py
create mode 100644 windows/components/stc/template_menu.py
create mode 100644 windows/components/stc/theme_loader.py
create mode 100644 windows/dialogs/__init__.py
create mode 100644 windows/dialogs/advanced_cell_editor/__init__.py
create mode 100644 windows/dialogs/advanced_cell_editor/controller.py
rename windows/{ => dialogs}/connections/__init__.py (100%)
rename windows/{ => dialogs}/connections/controller.py (98%)
rename windows/{ => dialogs}/connections/model.py (100%)
rename windows/{ => dialogs}/connections/repository.py (85%)
rename windows/{connections/manager.py => dialogs/connections/view.py} (95%)
create mode 100644 windows/dialogs/settings/__init__.py
create mode 100644 windows/dialogs/settings/controller.py
create mode 100644 windows/dialogs/settings/repository.py
delete mode 100755 windows/explorer.bkp.py
rename windows/main/{main_frame.py => controller.py} (83%)
create mode 100644 windows/main/tabs/__init__.py
rename windows/main/{ => tabs}/check.py (98%)
rename windows/main/{ => tabs}/column.py (98%)
rename windows/main/{ => tabs}/database.py (100%)
rename windows/main/{ => tabs}/explorer.py (96%)
rename windows/main/{ => tabs}/foreign_key.py (97%)
rename windows/main/{ => tabs}/index.py (98%)
create mode 100644 windows/main/tabs/query.py
rename windows/main/{ => tabs}/records.py (82%)
rename windows/main/{ => tabs}/table.py (94%)
create mode 100644 windows/main/tabs/view.py
create mode 100644 windows/state.py
create mode 100755 windows/views.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9aae4d8..9f2b611 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,36 +7,25 @@ on:
branches: [ main, master ]
jobs:
- pre-commit:
+ test:
runs-on: ubuntu-latest
- container: ubuntu:22.04
steps:
- uses: actions/checkout@v4
-
- - name: Install system dependencies
- run: |
- apt-get update
- apt-get install -y python3 python3-pip curl pkg-config git pkg-config libgtk-3-dev libwebkit2gtk-4.0-dev nodejs npm
- name: Install uv
- run: |
- curl -LsSf https://astral.sh/uv/install.sh | sh
- echo "$HOME/.local/bin" >> $GITHUB_PATH
- echo "$HOME/.cargo/bin" >> $GITHUB_PATH
+ uses: astral-sh/setup-uv@v4
+ with:
+ version: "latest"
+ enable-cache: true
- - name: Set up cache
- uses: actions/cache@v3
+ - name: Set up Python
+ uses: actions/setup-python@v5
with:
- path: ~/.cache/uv
- key: ${{ runner.os }}-uv-${{ hashFiles('**/uv.lock') }}
- restore-keys: |
- ${{ runner.os }}-uv-
+ python-version: "3.14"
- name: Install dependencies
- run: |
- uv sync --dev
+ run: uv sync --extra dev
- - name: Run pre-commit
- run: |
- uv run pre-commit run --all-files
\ No newline at end of file
+ - name: Run all tests (unit + integration)
+ run: uv run pytest tests/ -v --tb=short --cov=. --cov-report=term
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2de1d21..5bbcb03 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,7 +14,7 @@ repos:
hooks:
- id: runtest
name: run tests and update badges
- entry: bash ./scripts/runtest.sh
+ entry: bash ./scripts/runtest-local.sh
language: system
pass_filenames: false
always_run: true
diff --git a/CODE_STYLE.md b/CODE_STYLE.md
index aef0bb1..3f07df5 100644
--- a/CODE_STYLE.md
+++ b/CODE_STYLE.md
@@ -1,4 +1,4 @@
-# Code Style Guidelines (v1.2)
+# Code Style Guidelines (v1.3)
These rules define the expected coding style for this project.
They apply to all contributors, including humans, AI-assisted tools, and automated systems.
@@ -27,7 +27,21 @@ They are mandatory unless explicitly stated otherwise.
---
-## 1. Comments
+## 1. Language
+
+- All code, comments, documentation, commit messages, and free-form text MUST be written in English.
+- This includes:
+ - Comments in code
+ - Docstrings
+ - Documentation files (README, guides, etc.)
+ - Commit messages
+ - Variable and function names
+ - Error messages and user-facing text
+- No exceptions are allowed.
+
+---
+
+## 2. Comments
- Comments MUST be written in English.
- Comments MUST be concise and non-verbose.
@@ -49,7 +63,7 @@ They are mandatory unless explicitly stated otherwise.
---
-## 2. Naming Conventions
+## 3. Naming Conventions
- Variable, attribute, class, function, and parameter names MUST be descriptive.
- Names MUST NOT be aggressively shortened.
@@ -82,7 +96,7 @@ self.par = par
---
-## 3. Python Typing
+## 4. Python Typing
This project targets Python 3.14 and uses PEP 585 generics for standard collections.
@@ -174,7 +188,7 @@ if TYPE_CHECKING:
from pkg.heavy import HeavyType # TYPE_CHECKING: unavoidable circular import
```
-## 4. Import Rules
+## 5. Import Rules
### Submodules vs symbols (`from ... import ...`)
@@ -384,7 +398,7 @@ When importing multiple symbols from the same module:
- each line MUST import as many symbols as possible
- keep the same import group ordering rules
-### Good examples
+#### Good examples
```python
from windows.components.stc.detectors import detect_syntax_id, is_base64, is_csv
@@ -397,7 +411,7 @@ from .detectors import is_regex, is_sql, is_xml
from .detectors import is_base64, is_csv, is_html
```
-### Bad examples
+#### Bad examples
```python
from windows.components.stc.detectors import is_html
@@ -417,9 +431,46 @@ from .detectors import (
)
```
+### Lazy Imports
+
+- Lazy imports (imports inside functions or methods) MUST NOT be used.
+- Lazy imports are allowed ONLY as a last resort when:
+ - There is an unavoidable circular import that cannot be resolved by refactoring
+ - The performance gain is critical and measurable (e.g., avoiding expensive module initialization)
+- When lazy imports are used, they MUST include a clear inline comment explaining why they are necessary.
+
+#### Good examples
+
+```python
+from windows.main import CURRENT_CONNECTION
+
+
+def get_dialect() -> str:
+ connection = CURRENT_CONNECTION.get_value()
+ return connection.engine.value.dialect
+```
+
+#### Bad examples
+
+```python
+def get_dialect() -> str:
+ from windows.main import CURRENT_CONNECTION # Lazy import without justification
+ connection = CURRENT_CONNECTION.get_value()
+ return connection.engine.value.dialect
+```
+
+#### Allowed (last resort)
+
+```python
+def get_dialect() -> str:
+ from windows.main import CURRENT_CONNECTION # Lazy import: unavoidable circular dependency
+ connection = CURRENT_CONNECTION.get_value()
+ return connection.engine.value.dialect
+```
+
---
-## 5. Variable Definition Order
+## 6. Variable Definition Order
When defining multiple variables in sequence, variables MUST be ordered by increasing number of characters in the
variable name (shorter names first).
@@ -445,7 +496,7 @@ pos = self._editor.GetCurrentPos()
---
-## 6. Python Classes
+## 7. Python Classes
### Naming
@@ -518,19 +569,19 @@ class Example:
---
-## 7. Function and Method Size
+## 8. 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.
---
-## 8. Walrus Operator ( := )
+## 9. Walrus Operator ( := )
- Always try to use the walrus operator when it improves clarity and avoids redundant calls.
- Do NOT use it if it reduces readability.
-### Good examples
+#### Good examples
```python
if (user := get_user()) is not None:
@@ -540,7 +591,7 @@ while (line := file.readline()):
handle_line(line)
```
-### Bad examples
+#### Bad examples
```python
user = get_user()
@@ -550,7 +601,7 @@ if user is not None:
---
-## 9. Mypy & Static Analysis
+## 10. Mypy & Static Analysis
- Code MUST be mypy-friendly.
- Do NOT silence errors with `# type: ignore` unless there is no reasonable alternative.
diff --git a/README.md b/README.md
index 84caa0b..acad80d 100644
--- a/README.md
+++ b/README.md
@@ -12,11 +12,15 @@
-> Heidi's (silly?) friend โ a wxPython-based reinterpretation of HeidiSQL
+> Inspired by HeidiSQL โ reimagined in pure Python.
**PeterSQL** is a graphical client for database management, inspired by the
excellent [HeidiSQL](https://www.heidisql.com/), but written entirely in **Python**
-using **wxPython**, with a focus on portability and native look & feel.
+using **wxPython**, with a focus on portability, extensibility, and native look & feel.
+
+PeterSQL is **not a clone and not a port** of HeidiSQL.
+It shares the same spirit โ clarity, speed, practicality โ but follows its own
+path as a Python-native project.
---
@@ -31,24 +35,42 @@ Use at your own risk and **do not rely on this project in production environment
## ๐งญ Why PeterSQL?
-Over the years, I have used **HeidiSQL** as my primary tool for working with
+For years, I have used **HeidiSQL** as my primary tool for working with
MySQL, MariaDB, SQLite, and other databases.
-It is a tool I deeply appreciate: **streamlined**, **intuitive**, and
-**powerful**.
+It is streamlined, intuitive, and powerful.
+
+PeterSQL started as a personal challenge:
+to recreate that same *spirit* in a **pure Python** application.
-Rather than trying to compete with HeidiSQL, PeterSQL started as a personal
-challenge: to recreate the same *spirit* in a **pure Python** application.
+But PeterSQL is not meant to be a 1:1 replacement.
-PeterSQL is not a 1:1 port.
-It is a Python-first reinterpretation, built with different goals in mind.
+Where HeidiSQL is Delphi-based and Windows-centric,
+PeterSQL is:
- ๐ **Written entirely in Python**
-- ๐งฉ **Built entirely in Python to enable easy modification and extension**
-- ๐ฏ **Focused on simplicity and clarity**, inspired by HeidiSQL
+- ๐งฉ **Easily modifiable and extensible**
+- ๐ **Cross-platform**
+- ๐ฏ **Focused on clarity and simplicity**
- ๐ **Free and open source**
-PeterSQL exists for developers who love HeidiSQLโs approach, but want a tool
-that feels native to the Python ecosystem.
+PeterSQL aims to feel natural for developers who live in the Python ecosystem
+and appreciate lightweight, practical tools.
+
+---
+
+## ๐ญ Vision
+
+PeterSQL is evolving beyond a simple SQL client.
+
+Planned directions include:
+
+- ๐ง Smarter, scope-aware SQL autocomplete
+- ๐ Visual schema / diagram viewer (inspired by tools like MySQL Workbench)
+- ๐ Extensible architecture for future tooling
+- ๐ Better integration with Python-based workflows
+
+The goal is not to replicate existing tools,
+but to build a Python-native SQL workbench with its own identity.
---
@@ -56,7 +78,21 @@ that feels native to the Python ecosystem.
- [Python 3.14+](https://www.python.org/)
- [wxPython 4.2.5](https://wxpython.org/) - native cross-platform interface
-- [wxFormBuilder 4.2.1](https://github.com/wxFormBuilder/wxFormBuilder) - for the construction of the interface
+- [wxFormBuilder 4.2.1](https://github.com/wxFormBuilder/wxFormBuilder) - UI construction
+
+---
+
+## ๐ Available Languages
+
+PeterSQL supports the following languages:
+
+- ๐บ๐ธ **English** (en_US)
+- ๐ฎ๐น **Italiano** (it_IT)
+- ๐ซ๐ท **Franรงais** (fr_FR)
+- ๐ช๐ธ **Espaรฑol** (es_ES)
+- ๐ฉ๐ช **Deutsch** (de_DE)
+
+You can change the language in the application settings (Settings โ General โ Language).
---
@@ -74,52 +110,4 @@ PeterSQL uses [uv](https://github.com/astral-sh/uv) for fast and reliable depend
1. Clone the repository:
```bash
git clone https://github.com/gtripoli/petersql.git
- cd petersql
- ```
-
-2. Install dependencies (including dev tools for testing):
- ```bash
- uv sync
- ```
-
-3. Run the application:
- ```bash
- uv run main.py
- ```
-
-### Development
-
-```bash
-uv sync --extra dev
-```
-
-To run tests:
-
-```bash
-uv run pytest
-```
-
-### Troubleshooting installation
-
-#### wxPython
-
-If `uv sync` fails because no compatible wxPython wheel is available for your platform/Python version, reinstall it from source with:
-This forces a source build and usually unblocks the setup.
-
-```bash
-uv pip install -U --reinstall wxPython==4.2.4 --no-binary wxPython
-```
-
-###### Once the build finishes, rerun `uv sync` so the refreshed environment picks up the manually installed wxPython.
-
-## ๐ธ Screenshot
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+ cd petersql
\ No newline at end of file
diff --git a/constants.py b/constants.py
new file mode 100644
index 0000000..45eaa7c
--- /dev/null
+++ b/constants.py
@@ -0,0 +1,42 @@
+"""Global constants for PeterSQL application."""
+
+import os
+from pathlib import Path
+from enum import Enum
+
+
+WORKDIR = Path(os.path.abspath(os.path.dirname(__file__)))
+
+
+class LogLevel(Enum):
+ DEBUG = "DEBUG"
+ INFO = "INFO"
+ WARNING = "WARNING"
+ ERROR = "ERROR"
+
+
+class Language(Enum):
+ EN_US = ("en_US", "English")
+ IT_IT = ("it_IT", "Italiano")
+ FR_FR = ("fr_FR", "Franรงais")
+ ES_ES = ("es_ES", "Espaรฑol")
+ DE_DE = ("de_DE", "Deutsch")
+
+ def __init__(self, code: str, label: str):
+ self.code = code
+ self.label = label
+
+ @classmethod
+ def get_codes(cls) -> list[str]:
+ return [lang.code for lang in cls]
+
+ @classmethod
+ def get_labels(cls) -> list[str]:
+ return [lang.label for lang in cls]
+
+ @classmethod
+ def from_code(cls, code: str) -> "Language":
+ for lang in cls:
+ if lang.code == code:
+ return lang
+ return cls.EN_US
diff --git a/helpers/__init__.py b/helpers/__init__.py
index 6ab07f3..349face 100644
--- a/helpers/__init__.py
+++ b/helpers/__init__.py
@@ -1,11 +1,13 @@
import enum
+import os
+import sys
from typing import Callable
+from pathlib import Path
from gettext import pgettext
-import babel.numbers
-
import wx
+import babel.numbers
from helpers.observables import Observable
@@ -18,7 +20,11 @@ class SizeUnit(enum.Enum):
TERABYTE = pgettext("unit", "TB")
-def wx_colour_to_hex(colour: wx.Colour):
+def wx_colour_to_hex(colour):
+ if isinstance(colour, str):
+ if colour.startswith('#'):
+ return colour
+ return f"#{colour}"
return f"#{colour.Red():02x}{colour.Green():02x}{colour.Blue():02x}"
@@ -57,3 +63,32 @@ def call_and_reset():
for obs in observables:
setattr(obs, '_debounce_callback', _debounced)
obs.subscribe(_debounced)
+
+
+def get_base_path(base_path: Path) -> Path:
+ if getattr(sys, "frozen", False):
+ return Path(sys.executable).parent
+
+ return base_path
+
+
+def get_resource_path(base_path: Path, *paths: str) -> Path:
+ if hasattr(sys, "_MEIPASS"):
+ return Path(sys._MEIPASS).joinpath(*paths)
+
+ return get_base_path(base_path).joinpath(*paths)
+
+
+def get_config_dir() -> Path:
+ base: str = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
+ return Path(base) / "petersql"
+
+
+def get_data_dir() -> Path:
+ base: str = os.environ.get("XDG_DATA_HOME", str(Path.home() / ".local" / "share"))
+ return Path(base) / "petersql"
+
+
+def get_cache_dir() -> Path:
+ base: str = os.environ.get("XDG_CACHE_HOME", str(Path.home() / ".cache"))
+ return Path(base) / "petersql"
\ No newline at end of file
diff --git a/helpers/bindings.py b/helpers/bindings.py
index 45e2adb..2179c4a 100644
--- a/helpers/bindings.py
+++ b/helpers/bindings.py
@@ -4,6 +4,7 @@
from typing import Optional, Union, Any, TypeAlias, Callable
import wx
+import wx.stc
from helpers.observables import Observable, CallbackEvent
@@ -11,7 +12,10 @@
CONTROL_BIND_VALUE: TypeAlias = Union[wx.TextCtrl, wx.SpinCtrl, wx.CheckBox]
CONTROL_BIND_PATH: TypeAlias = Union[wx.FilePickerCtrl, wx.DirPickerCtrl]
CONTROL_BIND_SELECTION: TypeAlias = wx.Choice
-CONTROLS: TypeAlias = Union[CONTROL_BIND_LABEL, CONTROL_BIND_VALUE, CONTROL_BIND_PATH, CONTROL_BIND_SELECTION]
+CONTROL_BIND_COMBO: TypeAlias = wx.ComboBox
+CONTROL_BIND_STC: TypeAlias = wx.stc.StyledTextCtrl
+CONTROL_BIND_RADIO_GROUP: TypeAlias = list[wx.RadioButton]
+CONTROLS: TypeAlias = Union[CONTROL_BIND_LABEL, CONTROL_BIND_VALUE, CONTROL_BIND_PATH, CONTROL_BIND_SELECTION, CONTROL_BIND_COMBO, CONTROL_BIND_STC, CONTROL_BIND_RADIO_GROUP]
class AbstractBindControl(abc.ABC):
@@ -77,6 +81,7 @@ def __init__(self, control: CONTROL_BIND_VALUE, observable: Observable):
event = wx.EVT_SPINCTRL
elif isinstance(control, wx.CheckBox):
event = wx.EVT_CHECKBOX
+
super().__init__(control, observable, event=event)
def clear(self) -> None:
@@ -151,6 +156,67 @@ def set(self, value: Any) -> None:
self.control.SetPath(str(value))
+class BindComboControl(AbstractBindControl):
+ def __init__(self, control: CONTROL_BIND_COMBO, observable: Observable):
+ super().__init__(control, observable, event=wx.EVT_TEXT)
+
+ def clear(self) -> None:
+ self.control.SetValue(self.initial if self.initial is not None else "")
+
+ def get(self) -> str:
+ return self.control.GetValue()
+
+ def set(self, value: Any) -> None:
+ self.control.SetValue(str(value))
+
+
+class BindStyledTextControl(AbstractBindControl):
+ def __init__(self, control: CONTROL_BIND_STC, observable: Observable):
+ super().__init__(control, observable, event=wx.stc.EVT_STC_CHANGE)
+
+ def clear(self) -> None:
+ self.control.SetText(self.initial if self.initial is not None else "")
+
+ def get(self) -> str:
+ return self.control.GetText()
+
+ def set(self, value: Any) -> None:
+ self.control.SetText(str(value))
+
+
+class BindRadioGroupControl(AbstractBindControl):
+ def __init__(self, radios: CONTROL_BIND_RADIO_GROUP, observable: Observable):
+ self.radios = radios
+ self.control = radios[0] if radios else None
+ self.initial = self.get()
+ self.observable = observable
+
+ self.observable.subscribe(self._set_value, CallbackEvent.AFTER_CHANGE)
+
+ for radio in self.radios:
+ radio.Bind(wx.EVT_RADIOBUTTON, self.handle_control_event)
+
+ if (value := self.observable.get_value()) is not None:
+ self.set(value)
+
+ def clear(self) -> None:
+ if self.radios:
+ self.radios[0].SetValue(True)
+
+ def get(self) -> Optional[str]:
+ for radio in self.radios:
+ if radio.GetValue():
+ return radio.GetLabel()
+ return None
+
+ def set(self, value: Any) -> None:
+ value_str = str(value).upper()
+ for radio in self.radios:
+ if radio.GetLabel().upper() == value_str:
+ radio.SetValue(True)
+ return
+
+
class AbstractMetaModel(abc.ABCMeta):
def __init__(cls, name, bases, attrs):
super().__init__(name, bases, attrs)
@@ -171,6 +237,12 @@ def bind_control(self, control: CONTROLS, observable: Observable):
BindPathControl(control, observable)
elif isinstance(control, wx.Choice):
BindSelectionControl(control, observable)
+ elif isinstance(control, wx.ComboBox):
+ BindComboControl(control, observable)
+ elif isinstance(control, wx.stc.StyledTextCtrl):
+ BindStyledTextControl(control, observable)
+ elif isinstance(control, list) and control and isinstance(control[0], wx.RadioButton):
+ BindRadioGroupControl(control, observable)
self.observables.append(observable)
diff --git a/helpers/dataview.py b/helpers/dataview.py
index 38991f4..67a78a9 100644
--- a/helpers/dataview.py
+++ b/helpers/dataview.py
@@ -223,7 +223,7 @@ def __init__(self, column_count: Optional[int] = None):
AbstractBaseDataModel.__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)
diff --git a/helpers/repository.py b/helpers/repository.py
new file mode 100644
index 0000000..ba54976
--- /dev/null
+++ b/helpers/repository.py
@@ -0,0 +1,24 @@
+from pathlib import Path
+from typing import Any, Generic, TypeVar
+
+import yaml
+
+
+T = TypeVar('T')
+
+
+class YamlRepository(Generic[T]):
+ def __init__(self, config_file: Path):
+ self._config_file = config_file
+
+ def _read_yaml(self) -> dict[str, Any]:
+ try:
+ with open(self._config_file, 'r') as file:
+ data = yaml.full_load(file)
+ return data or {}
+ except Exception:
+ return {}
+
+ def _write_yaml(self, data: Any) -> None:
+ with open(self._config_file, 'w') as file:
+ yaml.dump(data, file, sort_keys=False)
diff --git a/icons/16x16/server-oracle.png b/icons/16x16/server-oracle.png
new file mode 100644
index 0000000000000000000000000000000000000000..4a1aea92bbac83b9be77d0a28f69a7f925438192
GIT binary patch
literal 4926
zcmbt$i9b|-)cV2}y}JRH!?
zv)I-Ox*`1x>;eHmli6Pwc$~Hy01jYgY^Be{lOdVKlV>nQw$6o|#gR_<8UM4BX7G@a
zIEN-NA;A!ru@okbIENw36Udvp_D>_w<47zMO`JoKSxE9ChBSvJL&Aa-42g*)EFdwE
z7Ydw3k;Wkx+fH2IpvQt{tSBx>}*WzzheZpcOH*tTcD9a*MB6;
z5GXSk3Y$sxR*+bDG6SLxN(D8^UKNOWHb5u@(xFzNAwo1lJ2QiaTu>AwPzJ&pH<}6Y
zkB24>SqSrZ+&^ZZS`e9J28PH&6Im#zItAhi$}^?}ptWJ)DXVxsHZ+Jv_Ke7Lcqkj0
z#ok7!1SIUW{6{9#2Ahq4h@d%=AqY@)_Duh6^&GUlkOB=0PhvoUXvjr@>OyP$uQ6gZ
z->?Ssf{X=Om{@LaZEgkfZiCJ3tu4d)E!?FDVQwiYBVs>uKQuz$Ge0$;wy_0
zs)F&eCsINO*QRP$bPBvZbzcblPFiXm;qy6nxG|gI*(G9&YWN~aogj*7or>H}dRF3a
z*|;}wvtuQCduzkz&ojGUpQpx~AH`mIUenyNp|x6-zg96hvHop+YT)J4=LN)LOp9oy$p=ym8jb=E%90{IE>|Aestf18t)%LH%yJmEyGodUx$Ss(?F5$+N7jyI1gB{6
zwVcxaRohIkyM$5@@N~AR*Cd>qht%A^JYuKPy@%d5C-)|I4
zA4~?Pq)?k8EQc2dMZ7Y)EdfHiw7LE0cCby>;)BsJ1pUR8u_=w*y0>mUQ}cy;0V~5y
zsd{8f9d~v8pdjYw1#kj{wrg%Hb4GjVUGW!&-Qp73f_IoF->aW1?dJn3WP_g|F5S0|vM;I=EgG7Nlj
z%AOZ4sWa3Tz7jAlZ7%T6XQ^w{@YoyaGB`DEJhX9+a`;TH2p4Dl-*1JK&gT&
zuX}jhEKD00ti&c`;C*pFv~hI7eGHtztKsVj-0@a@x%m(83EEE{SQ%3VDwfZd*Pi^a
zd8-{!=_8{+5I8>(9n4Wz#i7b!-mi&t_BbomvX#hFush{Fw-#OL6xh*YWb)+HdmHJg
z&(&3_bke@^)58l#qndnk8ta8La;`Oxw@*tY1tZKVF4L~A
zr}#CwJ-Mfhjz$d)5$RLAZTExc9wsn?Zrhtky2({|=^W39U5k_fBg$IU?Z81-yS+v~
zD^)DG%=%@$iB#1Ky|(Saw{r6TazO)DVAUL{so~U&NAh{A
zw>{WZrR5v~lI-C5`0DS5;LuQoi}F495OBdB7%Xf2$~*ab)|c0B$p@5Pu;V&nRV2tI
zGGunUMmWchk5*v&uo!K&FHLDR^Hsy&ykO9AUPuGb@9K*Ppw*mimsbeUFK
zYWh{=w{!j)gUebD=3FeepGTIoZt?(VFb>Y|y1{zLLp6#Y#P-NL#*TuFu0AtOGWX;k
z9=c!TA!_~UUvIbEHB$ZFlv72aH3qc>nc^{Luf1v{LNB6uDhNTMky0P4X=
zoC7axBw7EcCK{Cf0jpmNVVLv1=qnOOHP9f@QY#rrxsQ-b;HIW>ZT+?gj~9IL^~VCHjMKtT`Emx!k&wjgNo4WB;5*N0s^
zga#8AIOsaTob=lI4K;1JUw|@juyBIizZN1i{Php4O-fK
zB+d({-dTaIAbrK>I7Ys==GUJi4DB}Bzv`z9j<%UbC&aGUwLJV$`{*35gKtRFU5(m~
z06&`Ht2L}1miWl5*9=b^V%x7+KQx&Vb>z886P)&Pb=3T&q*NB&B)X)`1qSSS=AMX!
zVyQ2}^2+7WfWqMD)}R(l{Hs;|+C{6vgGPO_r*)+`0mK#jaDzLkIE1udB`q#fGj+7o(pFL^y^{>F!>HFIrAi5;}
zKmH3La6j?(%GO8l@D9=4DsYX=RJ
zs5%ijjW2S|#H~zGukxzS{yq&SQxHS&(xqycs639xnl-A2DJcCoA6r+HK6J)~@mxy9
zakAb%Cm&Y&nsK5g#}jM~R|J2*&n1O3tlfSoKYo$IAEe6qS&}=x=)x!{ypgybV)S-w
zZs&Ayq$U4nn4EyY-@L+`>$!G2G42#jd6}Mk7HCh90DksYmXqiKqNVajJ3Bb>)EZ&5
z{Fdq=MakTSx&59-U#7))RL}x6QPEGY#=J+El9QhHrKF#jTygrPQ@|P1=x|t6AD3P=>}Qnm`Ge~Z=~{Q;fzBwKe1q-s
z1kzxI)9IES+XgtHwB?@}Jz=v-b;e6AAmksXI4o-Hb1$-OTG?oL@_>at!)qwRy%Udg;OR`s|H%KABpb
z)IZs|I>h=zrZWojGF7)_MSV@k9{zVVW8JeqE*h@
zj~E3>yq-dbJv6{jHy_YVuUS7X0z1P)eKE(0dj8?0XTB@AXCXOXC+#@*5iEF4>?4?b
zNNTUA6~N;~4!-zGk@qGj#^rSBT+8CQy308~_G;{dEJuMx{GCTCDMHL@vAFD!){N}7
z!M2&%^mpq+l?v}RA{92=hMw4OFMfWz5izF6oEbpvQ4_JRRohDYpr{H$4?H%K+4Ofk
zTy$$_DEWrJiD=cJXKEBwfuD9WQMJ75TsNR0EokjxADlLo_C`RF$tj%nDmlzePN%~CZ
zZ)|St{;~%wsm0+!Y%X=54D2>YY}OpXfq>|QnV^-Rkcg^J$BC71q#bpCjhVu`WNV5C
zJ6Hi|weO!g2R`cL+Fq$;M(H;lBJhG@+fuHk<#QtK)F5z&9Bz4ff)=^8A;G*@Tjh=b
z*9oSf?=#zf_1a%A`nn&a79un1&D<65s+F1^8vrBq93Q&)P#=Q^Ep}b2yeOAVjyXG?
ze{pqr+-8@^esDb2EN@Cl?rOm1(vb!P7}qjbvLK<`ST^#D{aPPglyA#sH|e}yjb9nU
zX#e(3p$wpa;OfQBk1pb@lm9*ZwNNp@H{4n=XleCOYB=g!gs5KmxUGv*j|{Jvsn0<<
zCFIsMrtsI`
z+6Mis7*EyXtgL-z0|Ws7-#^fzx+6il
WLaNJ&Pit!c`WWh)=#}a?Ui}|wDuulO
literal 0
HcmV?d00001
diff --git a/icons/__init__.py b/icons/__init__.py
index 083896c..183e49f 100755
--- a/icons/__init__.py
+++ b/icons/__init__.py
@@ -32,11 +32,12 @@ class IconList:
FUNCTION = Icon("function", "lightning.png")
EVENT = Icon("event", "calendar_view_day.png")
- # Engines
- SQLITE = Icon("engine_sqlite", "server-sqlite.png")
- MYSQL = Icon("engine_mysql", "server-mysql.png")
- MARIADB = Icon("engine_mariadb", "server-mariadb.png")
- POSTGRESQL = Icon("engine_postgresql", "server-postgresql.png")
+ # Servers
+ SQLITE = Icon("server_sqlite", "server-sqlite.png")
+ MYSQL = Icon("server_mysql", "server-mysql.png")
+ MARIADB = Icon("server_mariadb", "server-mariadb.png")
+ POSTGRESQL = Icon("server_postgresql", "server-postgresql.png")
+ ORACLE = Icon("server_oracle", "server-oracle.png")
# Keys
KEY_PRIMARY = Icon("key_primary", "key_primary.png")
@@ -61,8 +62,8 @@ def __init__(self, base_path: str, size: int = 16):
self.base_path = base_path
self._imagelist = wx.ImageList(size, size)
- self._idx_cache: Dict[Hashable, int] = {}
- self._bmp_cache: Dict[Hashable, wx.Bitmap] = {}
+ self._idx_cache: dict[Hashable, int] = {}
+ self._bmp_cache: dict[Hashable, wx.Bitmap] = {}
@property
def imagelist(self) -> wx.ImageList:
@@ -91,7 +92,7 @@ def _combine_bitmaps(*bitmaps: wx.Bitmap) -> wx.Bitmap:
return img.ConvertToBitmap()
@staticmethod
- def _key(*icons: "Icon") -> Tuple[Hashable, ...]:
+ def _key(*icons: "Icon") -> tuple[Hashable, ...]:
# single -> (id,), combo -> (id1, id2, ...)
return tuple(icon.id for icon in icons)
@@ -115,7 +116,7 @@ def get_bitmap(self, *icons: "Icon") -> wx.Bitmap:
return bmp
# combo: ensure single bitmaps exist (and are cached with (id,))
- parts: List[wx.Bitmap] = []
+ parts: list[wx.Bitmap] = []
for icon in icons:
part = self.get_bitmap(icon) # caches (id,)
if part and part.IsOk():
diff --git a/main.py b/main.py
index ea1c74c..0fee084 100755
--- a/main.py
+++ b/main.py
@@ -6,38 +6,43 @@
import wx
-import settings
-
+from constants import WORKDIR
from icons import IconRegistry
from helpers.loader import Loader
from helpers.logger import logger
from helpers.observables import ObservableObject
-from windows.components.stc.styles import apply_stc_theme
+from windows.dialogs.settings.repository import SettingsRepository
+
+from windows.components.stc.styles import apply_stc_theme, set_theme_loader
from windows.components.stc.themes import ThemeManager
from windows.components.stc.registry import SyntaxRegistry
from windows.components.stc.profiles import BASE64, CSV, HTML, JSON, MARKDOWN, REGEX, SQL, TEXT, XML, YAML
-
-WORKDIR = Path(os.path.abspath(os.path.dirname(__file__)))
+from windows.components.stc.theme_loader import ThemeLoader
class PeterSQL(wx.App):
locale: wx.Locale = wx.Locale()
- settings: ObservableObject = settings.load(WORKDIR.joinpath("settings.yml"))
+ settings_repository = SettingsRepository(WORKDIR / "settings.yml")
+ settings: ObservableObject = settings_repository.load()
main_frame: wx.Frame = None
icon_registry_16: IconRegistry
syntax_registry: SyntaxRegistry
+
+ theme_loader: ThemeLoader
def OnInit(self) -> bool:
Loader.loading.subscribe(self._on_loading_change)
- self.icon_registry_16 = IconRegistry(os.path.join(WORKDIR, "icons"), 16)
+ 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])
@@ -46,6 +51,17 @@ def OnInit(self) -> bool:
self.open_session_manager()
return True
+
+ def _init_theme_loader(self) -> None:
+ theme_name = self.settings.get_value("theme", "current") or "petersql"
+ self.theme_loader = ThemeLoader(WORKDIR / "themes")
+ try:
+ self.theme_loader.load_theme(theme_name)
+ set_theme_loader(self.theme_loader)
+ except FileNotFoundError:
+ logger.warning(f"Theme '{theme_name}' not found, using default colors")
+ except Exception as ex:
+ logger.error(f"Error loading theme: {ex}", exc_info=True)
def _init_locale(self):
_locale = self.settings.get_value("locale")
@@ -72,17 +88,17 @@ def gettext_wrapper(message):
locale.setlocale(locale.LC_ALL, _locale)
def open_session_manager(self) -> None:
- from windows.connections.manager import ConnectionsManager
+ from windows.dialogs.connections.view import ConnectionsManager
self.connection_manager = ConnectionsManager(None)
self.connection_manager.SetIcon(
- wx.Icon(os.path.join(WORKDIR, "icons", "petersql.ico"))
+ wx.Icon(str(WORKDIR / "icons" / "petersql.ico"))
)
self.connection_manager.Show()
def open_main_frame(self) -> None:
try:
- from windows.main.main_frame import MainFrameController
+ from windows.main.controller import MainFrameController
self.main_frame = MainFrameController()
size = wx.Size(
@@ -98,7 +114,7 @@ def open_main_frame(self) -> None:
self.main_frame.SetPosition(position)
self.main_frame.Layout()
self.main_frame.SetIcon(
- wx.Icon(os.path.join(WORKDIR, "icons", "petersql.ico"))
+ wx.Icon(str(WORKDIR / "icons" / "petersql.ico"))
)
self.main_frame.Show()
diff --git a/pyproject.toml b/pyproject.toml
index e2b1433..0e18f5a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,6 +29,17 @@ dev = [
]
+[tool.pytest.ini_options]
+markers = [
+ "integration: marks tests as integration tests (testcontainers, slow, deselect with '-m \"not integration\"')",
+]
+addopts = "-m 'not integration'"
+testpaths = ["tests"]
+python_files = ["test_*.py"]
+python_classes = ["Test*"]
+python_functions = ["test_*"]
+cache_dir = ".cache/pytest"
+
[tool.pyright]
typeCheckingMode = "standard"
reportMissingImports = "none"
diff --git a/scripts/locales.py b/scripts/locales.py
index 7267f38..e08ddc8 100755
--- a/scripts/locales.py
+++ b/scripts/locales.py
@@ -4,8 +4,10 @@
import subprocess
from pathlib import Path
+from constants import Language
+
APP_NAME = "petersql"
-LANGUAGES = ["fr_FR", "it_IT", "es_ES", "en_US", "de_DE"]
+LANGUAGES = Language.get_codes()
BASE_DIR = Path(__file__).parent
LOCALE_DIR = BASE_DIR.joinpath("locale")
diff --git a/scripts/runtest-local.sh b/scripts/runtest-local.sh
new file mode 100755
index 0000000..31b1254
--- /dev/null
+++ b/scripts/runtest-local.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+# Local test runner (fast, excludes integration tests)
+# For local development and pre-commit hooks
+# Does NOT update badges - use runtest.sh for that
+
+echo "Running local tests (excluding integration tests)..."
+
+# Run all tests except integration tests (testcontainers)
+# Integration tests are marked with @pytest.mark.integration
+uv run pytest tests/ -m "not integration" --cov=. --cov-report=term --tb=short -v
+
+PYTEST_EXIT_CODE=$?
+
+echo ""
+echo "Local tests completed. Exit code: $PYTEST_EXIT_CODE"
+echo ""
+echo "Note: Integration tests excluded. Run 'scripts/runtest.sh' for full test suite with badge updates."
+
+exit $PYTEST_EXIT_CODE
diff --git a/scripts/runtest.sh b/scripts/runtest.sh
index 6d135a5..8305799 100755
--- a/scripts/runtest.sh
+++ b/scripts/runtest.sh
@@ -1,16 +1,16 @@
#!/bin/bash
# Unified test runner with badge updates
-# Runs all tests once and updates README badges based on results
-# Replaces: pytest, update_coverage_badge, update_version_badges
+# Runs ALL tests (unit + integration) and updates README badges based on results
+# For CI/CD and complete test validation
README="README.md"
TESTS_DIR="tests/engines"
RESULTS_FILE="/tmp/pytest_results.txt"
COVERAGE_FILE="/tmp/pytest_coverage.txt"
-echo "Running all tests with coverage..."
+echo "Running ALL tests (unit + integration) with coverage..."
-# Run all tests once with coverage, save results
+# Run ALL tests including integration tests (testcontainers)
uv run pytest tests/ --cov=. --cov-report=term --tb=no -v 2>&1 | tee "$RESULTS_FILE"
PYTEST_EXIT_CODE=${PIPESTATUS[0]}
diff --git a/settings.py b/settings.py
deleted file mode 100644
index d839825..0000000
--- a/settings.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import copy
-from typing import Any
-
-import yaml
-
-from helpers.observables import ObservableObject
-
-
-def load(settings_file):
- settings = ObservableObject(yaml.full_load(open(settings_file)))
- settings.subscribe(lambda settings: save(settings, settings_file))
- return settings
-
-
-def save(settings: Dict[str, Any], settings_file) -> None:
- settings = copy.copy(settings)
-
- with open(settings_file, "w") as outfile:
- yaml.dump(settings, outfile, sort_keys=False)
diff --git a/structures/connection.py b/structures/connection.py
index c7fe7bb..b8b4bbe 100755
--- a/structures/connection.py
+++ b/structures/connection.py
@@ -20,6 +20,7 @@ class ConnectionEngine(enum.Enum):
MARIADB = Engine("MariaDB", "mysql", IconList.MARIADB)
MYSQL = Engine("MySQL", "mysql", IconList.MYSQL)
POSTGRESQL = Engine("PostgreSQL", "postgres", IconList.POSTGRESQL)
+ ORACLE = Engine("Oracle", "oracle", IconList.ORACLE)
@classmethod
def get_all(cls) -> list["ConnectionEngine"]:
diff --git a/structures/engines/__init__.py b/structures/engines/__init__.py
index 054366c..e69de29 100644
--- a/structures/engines/__init__.py
+++ b/structures/engines/__init__.py
@@ -1,10 +0,0 @@
-import enum
-
-from functools import lru_cache
-from typing import NamedTuple
-
-import wx
-
-from icons import IconList, Icon
-
-
diff --git a/structures/engines/context.py b/structures/engines/context.py
index d3f48bd..9ddfa07 100755
--- a/structures/engines/context.py
+++ b/structures/engines/context.py
@@ -32,6 +32,7 @@ class AbstractContext(abc.ABC):
COLLATIONS: dict[str, str] = {}
IDENTIFIER_QUOTE: str = '"'
+ DEFAULT_STATEMENT_SEPARATOR: str = ";"
databases: ObservableLazyList[SQLDatabase]
@@ -68,6 +69,10 @@ def connect(self, **connect_kwargs) -> None:
"""Establish connection to the database using native driver"""
raise NotImplementedError
+ @abc.abstractmethod
+ def set_database(self, database: SQLDatabase) -> None:
+ raise NotImplementedError
+
@abc.abstractmethod
def get_server_version(self) -> str:
raise NotImplementedError
diff --git a/structures/engines/mariadb/context.py b/structures/engines/mariadb/context.py
index c149f48..dd372a8 100755
--- a/structures/engines/mariadb/context.py
+++ b/structures/engines/mariadb/context.py
@@ -25,6 +25,7 @@ class MariaDBContext(AbstractContext):
INDEXTYPE = MariaDBIndexType
IDENTIFIER_QUOTE = "`"
+ DEFAULT_STATEMENT_SEPARATOR = ";"
def __init__(self, connection: Connection):
super().__init__(connection)
@@ -163,6 +164,9 @@ def disconnect(self) -> None:
self._cursor = None
self._connection = None
+ def set_database(self, database: SQLDatabase) -> None:
+ self.execute(f"USE {database.sql_safe_name}")
+
def get_server_version(self) -> str:
self.execute("SELECT VERSION() as version")
version = self.cursor.fetchone()
diff --git a/structures/engines/mysql/context.py b/structures/engines/mysql/context.py
index 716d170..aa63f23 100644
--- a/structures/engines/mysql/context.py
+++ b/structures/engines/mysql/context.py
@@ -27,6 +27,7 @@ class MySQLContext(AbstractContext):
INDEXTYPE = MySQLIndexType
IDENTIFIER_QUOTE = "`"
+ DEFAULT_STATEMENT_SEPARATOR = ";"
def __init__(self, connection: Connection):
super().__init__(connection)
@@ -142,6 +143,9 @@ def connect(self, **connect_kwargs) -> None:
logger.error(f"Failed to connect to MySQL: {e}")
raise
+ def set_database(self, database: SQLDatabase) -> None:
+ self.execute(f"USE {database.sql_safe_name}")
+
def get_server_version(self) -> str:
self.execute("SELECT VERSION() as version")
version = self.cursor.fetchone()
diff --git a/structures/engines/postgresql/context.py b/structures/engines/postgresql/context.py
index eccb11b..07ca291 100644
--- a/structures/engines/postgresql/context.py
+++ b/structures/engines/postgresql/context.py
@@ -25,6 +25,7 @@ class PostgreSQLContext(AbstractContext):
INDEXTYPE = PostgreSQLIndexType
IDENTIFIER_QUOTE = '"'
+ DEFAULT_STATEMENT_SEPARATOR = ";"
def __init__(self, connection: Connection):
super().__init__(connection)
@@ -151,11 +152,11 @@ def disconnect(self) -> None:
self._connection = None
self._current_database = None
- def _set_database(self, db_name: str) -> None:
+ def set_database(self, database: SQLDatabase) -> None:
"""Switch to a different database by reconnecting."""
- if self._current_database != db_name:
+ if self._current_database != database.name:
self.disconnect()
- self.connect(database=db_name)
+ self.connect(database=database.name)
def get_server_version(self) -> str:
self.execute("SELECT version() as version")
@@ -188,7 +189,7 @@ def get_databases(self) -> list[SQLDatabase]:
return results
def get_views(self, database: SQLDatabase) -> list[PostgreSQLView]:
- self._set_database(database.name)
+ self.set_database(database)
results = []
self.execute(f"SELECT schemaname, viewname, definition FROM pg_views WHERE schemaname NOT IN ('information_schema', 'pg_catalog') ORDER BY schemaname, viewname")
for i, result in enumerate(self.fetchall()):
@@ -202,7 +203,7 @@ def get_views(self, database: SQLDatabase) -> list[PostgreSQLView]:
return results
def get_triggers(self, database: SQLDatabase) -> list[PostgreSQLTrigger]:
- self._set_database(database.name)
+ self.set_database(database)
results = []
self.execute(f"SELECT n.nspname as schemaname, tgname, pg_get_triggerdef(t.oid) as sql FROM pg_trigger t JOIN pg_class c ON t.tgrelid = c.oid JOIN pg_namespace n ON c.relnamespace = n.oid WHERE n.nspname NOT IN ('information_schema', 'pg_catalog') ORDER BY n.nspname, tgname")
for i, result in enumerate(self.fetchall()):
@@ -216,7 +217,7 @@ def get_triggers(self, database: SQLDatabase) -> list[PostgreSQLTrigger]:
return results
def get_tables(self, database: SQLDatabase) -> list[SQLTable]:
- self._set_database(database.name)
+ self.set_database(database)
QUERY_LOGS.append(f"/* get_tables for database={database.name} */")
self.execute(f"""
@@ -347,7 +348,7 @@ def get_foreign_keys(self, table: SQLTable) -> list[SQLForeignKey]:
if table is None or table.is_new:
return []
- self._set_database(table.database.name)
+ self.set_database(table.database)
logger.debug(f"get_foreign_keys for table={table.name}")
diff --git a/structures/engines/sqlite/context.py b/structures/engines/sqlite/context.py
index 8ba2cb3..eadf50a 100755
--- a/structures/engines/sqlite/context.py
+++ b/structures/engines/sqlite/context.py
@@ -32,6 +32,7 @@ class SQLiteContext(AbstractContext):
INDEXTYPE = SQLiteIndexType()
IDENTIFIER_QUOTE = '"'
+ DEFAULT_STATEMENT_SEPARATOR = ";"
_map_sqlite_master = defaultdict(lambda: defaultdict(dict))
@@ -66,6 +67,9 @@ def connect(self, **connect_kwargs) -> None:
self._cursor = self._connection.cursor()
self._on_connect()
+ def set_database(self, database: SQLDatabase) -> None:
+ pass
+
def get_server_version(self) -> str:
self.execute("SELECT sqlite_version()")
version = self.cursor.fetchone()
diff --git a/tests/autocomplete/autocomplete_adapter.py b/tests/autocomplete/autocomplete_adapter.py
new file mode 100644
index 0000000..f80dcc4
--- /dev/null
+++ b/tests/autocomplete/autocomplete_adapter.py
@@ -0,0 +1,124 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any, Dict, List, Optional
+from unittest.mock import Mock
+
+from windows.components.stc.autocomplete.auto_complete import SQLCompletionProvider
+from structures.engines.database import SQLDatabase, SQLTable, SQLColumn, SQLDataType
+
+
+@dataclass(frozen=True)
+class AutocompleteRequest:
+ sql: str
+ dialect: str
+ current_table: Optional[str]
+ schema: Dict[str, Any]
+
+
+@dataclass(frozen=True)
+class AutocompleteResponse:
+ mode: str
+ context: str
+ prefix: Optional[str]
+ suggestions: List[str]
+ extras: Dict[str, Any]
+
+
+def _create_mock_database(schema: Dict[str, Any], vocab: Dict[str, Any] = None) -> SQLDatabase:
+ mock_db = Mock(spec=SQLDatabase)
+ mock_db.name = "test_db"
+
+ mock_context = Mock()
+ if vocab:
+ mock_context.KEYWORDS = vocab.get("keywords_all", [])
+ mock_context.FUNCTIONS = vocab.get("functions_all", [])
+ else:
+ mock_context.KEYWORDS = []
+ mock_context.FUNCTIONS = []
+ mock_db.context = mock_context
+
+ tables = []
+ for table_data in schema.get("tables", []):
+ mock_table = Mock(spec=SQLTable)
+ mock_table.name = table_data["name"]
+ mock_table.database = mock_db
+
+ columns = []
+ for col_data in table_data.get("columns", []):
+ mock_col = Mock(spec=SQLColumn)
+ mock_col.name = col_data["name"]
+ mock_col.datatype = Mock(spec=SQLDataType)
+ mock_col.table = mock_table
+ columns.append(mock_col)
+
+ mock_table.columns = columns
+ tables.append(mock_table)
+
+ mock_db.tables = tables
+ return mock_db
+
+
+def get_suggestions(request: AutocompleteRequest) -> AutocompleteResponse:
+ from windows.components.stc.autocomplete.context_detector import ContextDetector
+ from windows.components.stc.autocomplete.statement_extractor import StatementExtractor
+ import json
+ from pathlib import Path
+
+ config_path = Path(__file__).parent / "test_config.json"
+ with open(config_path) as f:
+ config = json.load(f)
+
+ database = _create_mock_database(request.schema, config.get("vocab", {}))
+
+ current_table = None
+ if request.current_table:
+ for table in database.tables:
+ if table.name == request.current_table:
+ current_table = table
+ break
+
+ provider = SQLCompletionProvider(
+ get_database=lambda: database,
+ get_current_table=lambda: current_table
+ )
+
+ cursor_pos = request.sql.find("|")
+ if cursor_pos == -1:
+ cursor_pos = len(request.sql)
+
+ text = request.sql.replace("|", "")
+
+ from windows.components.stc.autocomplete.dot_completion_handler import DotCompletionHandler
+ from windows.components.stc.autocomplete.sql_context import SQLContext
+
+ extractor = StatementExtractor()
+ statement, relative_pos = extractor.extract_current_statement(text, cursor_pos)
+
+ dot_handler = DotCompletionHandler(database)
+ is_dot = dot_handler.is_dot_completion(statement, relative_pos)
+
+ if is_dot:
+ sql_context = SQLContext.DOT_COMPLETION
+ else:
+ detector = ContextDetector()
+ sql_context, scope, prefix = detector.detect(statement, relative_pos, database)
+
+ result = provider.get(text=text, pos=cursor_pos)
+
+ suggestions = [item.name for item in result.items]
+
+ if sql_context.name == "DOT_COMPLETION":
+ mode = "DOT"
+ elif result.prefix:
+ mode = "PREFIX"
+ else:
+ mode = "CONTEXT"
+
+ return AutocompleteResponse(
+ mode=mode,
+ context=sql_context.name,
+ prefix=result.prefix if result.prefix else None,
+ suggestions=suggestions,
+ extras={}
+ )
diff --git a/tests/autocomplete/cases/alias.json b/tests/autocomplete/cases/alias.json
new file mode 100644
index 0000000..69f2f56
--- /dev/null
+++ b/tests/autocomplete/cases/alias.json
@@ -0,0 +1,48 @@
+{
+ "group": "ALIAS",
+ "cases": [
+ {
+ "case_id": "ALIAS_001",
+ "title": "Alias disambiguation does NOT trigger when prefix startswith alias but not equal",
+ "sql": "SELECT * FROM users u WHERE us|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "us",
+ "alias_exact_match": null,
+ "comment": "Table-name expansion: users.* in schema order. WHERE is scope-restricted, so only users table columns.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at"
+ ]
+ }
+ },
+ {
+ "case_id": "ALIAS_002",
+ "title": "Prefix 'user' should not trigger alias 'u'",
+ "sql": "SELECT * FROM users u WHERE user|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "user",
+ "alias_exact_match": null,
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/alias_prefix_disambiguation.json b/tests/autocomplete/cases/alias_prefix_disambiguation.json
new file mode 100644
index 0000000..8ee66e2
--- /dev/null
+++ b/tests/autocomplete/cases/alias_prefix_disambiguation.json
@@ -0,0 +1,199 @@
+{
+ "group": "ALIAS_PREFIX_DISAMBIGUATION",
+ "cases": [
+ {
+ "case_id": "ALIAS_DISAMBIG_001",
+ "title": "Exact alias match activates alias-prefix mode",
+ "sql": "SELECT * FROM users u WHERE u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "comment": "Prefix 'u' exactly matches alias 'u'. Alias-prefix mode activated in schema order.",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "ALIAS_DISAMBIG_002",
+ "title": "Startswith does NOT activate alias-prefix mode",
+ "sql": "SELECT * FROM users u WHERE us|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "us",
+ "comment": "Prefix 'us' starts with alias 'u' but not exact match. Generic prefix matching. Scope-restricted: only users table in schema order.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at"
+ ]
+ }
+ },
+ {
+ "case_id": "ALIAS_DISAMBIG_003",
+ "title": "Alias-prefix mode with CURRENT_TABLE - alias takes precedence",
+ "sql": "SELECT * FROM users u WHERE u|",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "comment": "Alias-exact-match overrides CURRENT_TABLE priority. Alias 'u' columns shown in schema order.",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "ALIAS_DISAMBIG_004",
+ "title": "Deduplication - no users.id when u.id shown",
+ "sql": "SELECT * FROM users u WHERE u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "comment": "Alias-prefix mode. Only u.* columns in schema order, no users.* duplicates.",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "ALIAS_DISAMBIG_005",
+ "title": "Alias-prefix mode in SELECT_LIST",
+ "sql": "SELECT u| FROM users u",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "comment": "SELECT_LIST with alias-exact-match. Only alias columns + functions in schema order.",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ },
+ {
+ "case_id": "ALIAS_DISAMBIG_006",
+ "title": "Alias-prefix mode in JOIN_ON",
+ "sql": "SELECT * FROM users u JOIN orders o ON u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "JOIN_ON",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "comment": "JOIN_ON with alias-exact-match. Only alias 'u' columns in schema order.",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "ALIAS_DISAMBIG_007",
+ "title": "KILLER: Alias exact-match with multi-scope - only alias columns, zero duplicates",
+ "sql": "SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "comment": "KILLER: Multi-scope [users as u, orders as o]. Alias-exact-match for 'u'. Only u.* columns in schema order, NO users.* duplicates.",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "ALIAS_DISAMBIG_008",
+ "title": "KILLER: Startswith does NOT activate alias-mode - generic prefix matching",
+ "sql": "SELECT * FROM users u WHERE us|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "us",
+ "comment": "KILLER: Prefix 'us' does NOT match alias 'u' exactly. Generic prefix mode. Scope-restricted: only users table in schema order.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/alx.json b/tests/autocomplete/cases/alx.json
new file mode 100644
index 0000000..9e67b4c
--- /dev/null
+++ b/tests/autocomplete/cases/alx.json
@@ -0,0 +1,150 @@
+{
+ "group": "ALX",
+ "cases": [
+ {
+ "case_id": "ALX_001",
+ "title": "Exact alias 'u' triggers alias-prefix mode in WHERE_CLAUSE",
+ "sql": "SELECT * FROM users u WHERE u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "ALX_002",
+ "title": "Exact alias 'o' triggers alias-prefix mode in JOIN_ON",
+ "sql": "SELECT * FROM users u JOIN orders o ON o|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "JOIN_ON",
+ "prefix": "o",
+ "alias_exact_match": "o",
+ "suggestions": [
+ "o.id",
+ "o.user_id",
+ "o.total",
+ "o.status",
+ "o.created_at"
+ ]
+ }
+ },
+ {
+ "case_id": "ALX_003",
+ "title": "Exact alias 'u' triggers alias-prefix mode in ORDER_BY_CLAUSE",
+ "sql": "SELECT * FROM users u ORDER BY u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "ORDER_BY_CLAUSE",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "ALX_004",
+ "title": "Exact alias 'u' triggers alias-prefix mode in GROUP_BY_CLAUSE",
+ "sql": "SELECT * FROM users u GROUP BY u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "GROUP_BY_CLAUSE",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "ALX_005",
+ "title": "Exact alias 'u' triggers alias-prefix mode in HAVING_CLAUSE",
+ "sql": "SELECT status, COUNT(*) FROM users u GROUP BY status HAVING u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "HAVING_CLAUSE",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "ALX_006",
+ "title": "Multi-query: second SELECT doesn't see alias from first",
+ "sql": "SELECT * FROM users u WHERE id = 1; SELECT u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "comment": "Multi-query: second query has no scope, no alias 'u'. Table-name expansion: users.*",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.user_id",
+ "products.unit_price",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/curr.json b/tests/autocomplete/cases/curr.json
new file mode 100644
index 0000000..1325498
--- /dev/null
+++ b/tests/autocomplete/cases/curr.json
@@ -0,0 +1,59 @@
+{
+ "group": "CURR",
+ "cases": [
+ {
+ "case_id": "CURR_001",
+ "title": "CURRENT_TABLE columns prioritized over scope and database",
+ "sql": "SELECT u|",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.user_id",
+ "products.unit_price",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ },
+ {
+ "case_id": "CURR_002",
+ "title": "CURRENT_TABLE uses alias when query defines alias",
+ "sql": "SELECT * FROM users u WHERE id = 1; SELECT u|",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.user_id",
+ "products.unit_price",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/cursor.json b/tests/autocomplete/cases/cursor.json
new file mode 100644
index 0000000..7670e86
--- /dev/null
+++ b/tests/autocomplete/cases/cursor.json
@@ -0,0 +1,23 @@
+{
+ "group": "CURSOR",
+ "cases": [
+ {
+ "case_id": "CURSOR_001",
+ "title": "Cursor in middle of identifier uses left part as prefix",
+ "sql": "SELECT na|me FROM users",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "na",
+ "suggestions": [
+ "users.name",
+ "products.name",
+ "customers.name"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/derived_tables_cte.json b/tests/autocomplete/cases/derived_tables_cte.json
new file mode 100644
index 0000000..ae88004
--- /dev/null
+++ b/tests/autocomplete/cases/derived_tables_cte.json
@@ -0,0 +1,211 @@
+{
+ "group": "DERIVED_TABLES_CTE",
+ "cases": [
+ {
+ "case_id": "DERIVED_001",
+ "title": "Derived table columns resolved from subquery alias",
+ "sql": "SELECT * FROM (SELECT id, total FROM orders) AS o WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "comment": "Scope=[derived table 'o' with columns id, total]. Only derived columns available.",
+ "suggestions": [
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "DERIVED_002",
+ "title": "Derived table with JOIN - both scopes available",
+ "sql": "SELECT * FROM (SELECT id, total FROM orders) AS o JOIN users u ON o.id = u.id WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "comment": "Scope=[derived 'o', users 'u']. Both scopes available.",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "CTE_001",
+ "title": "CTE columns available and qualified by CTE name",
+ "sql": "WITH active_users AS (SELECT id, name FROM users WHERE status='active') SELECT * FROM active_users WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "comment": "Scope=[CTE 'active_users' with columns id, name]. Only CTE columns available.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "CTE_002",
+ "title": "CTE not visible in next statement",
+ "sql": "WITH active_users AS (SELECT id, name FROM users WHERE status='active') SELECT * FROM active_users; SELECT * FROM |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "comment": "Second statement. CTE from first statement not visible. Only physical tables.",
+ "suggestions": [
+ "customers",
+ "orders",
+ "payments",
+ "products",
+ "users"
+ ]
+ }
+ },
+ {
+ "case_id": "CTE_003",
+ "title": "CTE with physical table JOIN",
+ "sql": "WITH au AS (SELECT id, name FROM users) SELECT * FROM au JOIN orders o ON au.id = o.user_id WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "comment": "Scope=[CTE 'au', physical table 'orders' as 'o']. Both scopes available.",
+ "suggestions": [
+ "o.id",
+ "o.user_id",
+ "o.total",
+ "o.status",
+ "o.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "CTE_004",
+ "title": "CTE suggested in FROM_CLAUSE",
+ "sql": "WITH active_users AS (SELECT id, name FROM users WHERE status='active') SELECT * FROM |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "comment": "CTE 'active_users' available as table candidate.",
+ "suggestions": [
+ "active_users",
+ "customers",
+ "orders",
+ "products",
+ "users"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/dot.json b/tests/autocomplete/cases/dot.json
new file mode 100644
index 0000000..899aa09
--- /dev/null
+++ b/tests/autocomplete/cases/dot.json
@@ -0,0 +1,117 @@
+{
+ "group": "DOT",
+ "cases": [
+ {
+ "case_id": "DOT_001",
+ "title": "Dot-completion returns unqualified column names and ignores broader context",
+ "sql": "SELECT users.|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": null,
+ "suggestions": [
+ "id",
+ "name",
+ "email",
+ "status",
+ "created_at"
+ ]
+ }
+ },
+ {
+ "case_id": "DOT_002",
+ "title": "Dot-completion returns unqualified column names and ignores broader context",
+ "sql": "SELECT users.i|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": "i",
+ "suggestions": [
+ "id"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "DOT_003",
+ "title": "Dot-completion returns unqualified column names and ignores broader context",
+ "sql": "SELECT * FROM users u WHERE u.|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": null,
+ "suggestions": [
+ "id",
+ "name",
+ "email",
+ "status",
+ "created_at"
+ ]
+ }
+ },
+ {
+ "case_id": "DOT_004",
+ "title": "Dot-completion returns unqualified column names and ignores broader context",
+ "sql": "SELECT * FROM users u WHERE u.i|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": "i",
+ "suggestions": [
+ "id"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "DOT_005",
+ "title": "Dot-completion returns unqualified column names and ignores broader context",
+ "sql": "SELECT * FROM orders o ON o.|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": null,
+ "suggestions": [
+ "id",
+ "user_id",
+ "total",
+ "status",
+ "created_at"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "DOT_006",
+ "title": "Dot-completion returns unqualified column names and ignores broader context",
+ "sql": "SELECT * FROM users u ORDER BY u.c|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": "c",
+ "suggestions": [
+ "created_at"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/dot_completion.json b/tests/autocomplete/cases/dot_completion.json
new file mode 100644
index 0000000..cf0dbad
--- /dev/null
+++ b/tests/autocomplete/cases/dot_completion.json
@@ -0,0 +1,184 @@
+{
+ "group": "DOT_COMPLETION",
+ "cases": [
+ {
+ "case_id": "DOT_001",
+ "title": "Dot-completion on table name - unqualified columns in schema order",
+ "sql": "SELECT users.|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": null,
+ "comment": "Dot-completion returns unqualified column names in schema order.",
+ "suggestions": [
+ "id",
+ "name",
+ "email",
+ "status",
+ "created_at"
+ ]
+ }
+ },
+ {
+ "case_id": "DOT_002",
+ "title": "Dot-completion on alias - unqualified columns in schema order",
+ "sql": "SELECT * FROM users u WHERE u.|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": null,
+ "comment": "Dot-completion on alias 'u' returns unqualified columns.",
+ "suggestions": [
+ "id",
+ "name",
+ "email",
+ "status",
+ "created_at"
+ ]
+ }
+ },
+ {
+ "case_id": "DOT_003",
+ "title": "Dot-completion with prefix - filtered unqualified columns",
+ "sql": "SELECT users.c|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": "c",
+ "comment": "Dot-completion with prefix 'c' filters to created_at.",
+ "suggestions": [
+ "created_at"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "DOT_004",
+ "title": "Dot-completion - no functions, no keywords, no CURRENT_TABLE logic",
+ "sql": "SELECT orders.|",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": null,
+ "comment": "Dot-completion ignores CURRENT_TABLE. Only orders columns.",
+ "suggestions": [
+ "id",
+ "user_id",
+ "total",
+ "status",
+ "created_at"
+ ]
+ }
+ },
+ {
+ "case_id": "DOT_005",
+ "title": "Dot-completion on CTE alias",
+ "sql": "WITH au AS (SELECT id, name FROM users) SELECT au.|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": null,
+ "comment": "Dot-completion on CTE alias returns CTE columns.",
+ "suggestions": [
+ "id",
+ "name"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "DOT_006",
+ "title": "Dot-completion on derived table alias",
+ "sql": "SELECT * FROM (SELECT id, total FROM orders) AS o WHERE o.|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": null,
+ "comment": "Dot-completion on derived table alias returns derived columns.",
+ "suggestions": [
+ "id",
+ "total"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "DOT_007",
+ "title": "KILLER: Dot-completion in WHERE on alias - zero functions contamination",
+ "sql": "SELECT * FROM users u WHERE u.|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": null,
+ "comment": "KILLER: Dot-completion returns ONLY unqualified columns in schema order. NO functions, NO keywords, NO CURRENT_TABLE logic.",
+ "suggestions": [
+ "id",
+ "name",
+ "email",
+ "status",
+ "created_at"
+ ]
+ }
+ },
+ {
+ "case_id": "DOT_008",
+ "title": "KILLER: Dot-completion with prefix filter - startswith post-dot",
+ "sql": "SELECT users.i|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": "i",
+ "comment": "KILLER: Dot-completion with prefix 'i' filters to 'id' only. Unqualified.",
+ "suggestions": [
+ "id"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "DOT_009",
+ "title": "KILLER: Dot-completion in SELECT - no functions even if context normally allows",
+ "sql": "SELECT u.| FROM users u",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "DOT",
+ "context": "DOT_COMPLETION",
+ "prefix": null,
+ "comment": "KILLER: Even in SELECT_LIST context, dot-completion returns ONLY columns. NO functions like UPPER, UUID, etc.",
+ "suggestions": [
+ "id",
+ "name",
+ "email",
+ "status",
+ "created_at"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/empty.json b/tests/autocomplete/cases/empty.json
new file mode 100644
index 0000000..7bfa2b2
--- /dev/null
+++ b/tests/autocomplete/cases/empty.json
@@ -0,0 +1,35 @@
+{
+ "group": "EMPTY",
+ "cases": [
+ {
+ "case_id": "EMPTY_001",
+ "title": "Empty editor shows primary keywords",
+ "sql": "|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "EMPTY",
+ "context": "EMPTY",
+ "prefix": null,
+ "suggestions": [
+ "SELECT",
+ "INSERT",
+ "UPDATE",
+ "DELETE",
+ "CREATE",
+ "DROP",
+ "ALTER",
+ "TRUNCATE",
+ "SHOW",
+ "DESCRIBE",
+ "EXPLAIN",
+ "WITH",
+ "REPLACE",
+ "MERGE"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/from.json b/tests/autocomplete/cases/from.json
new file mode 100644
index 0000000..03dee95
--- /dev/null
+++ b/tests/autocomplete/cases/from.json
@@ -0,0 +1,180 @@
+{
+ "group": "FROM",
+ "cases": [
+ {
+ "case_id": "FROM_001",
+ "title": "FROM without prefix suggests tables",
+ "sql": "SELECT * FROM |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "customers",
+ "orders",
+ "payments",
+ "products",
+ "users"
+ ]
+ }
+ },
+ {
+ "case_id": "FROM_002",
+ "title": "FROM with prefix filters tables",
+ "sql": "SELECT * FROM u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "FROM_CLAUSE",
+ "prefix": "u",
+ "suggestions": [
+ "users"
+ ]
+ }
+ },
+ {
+ "case_id": "FROM_003",
+ "title": "FROM after comma suggests more tables",
+ "sql": "SELECT * FROM users, |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "customers",
+ "orders",
+ "payments",
+ "products"
+ ]
+ }
+ },
+ {
+ "case_id": "FROM_004",
+ "title": "FROM after table + space suggests JOIN/AS/WHERE etc",
+ "sql": "SELECT * FROM users |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "JOIN",
+ "INNER JOIN",
+ "LEFT JOIN",
+ "RIGHT JOIN",
+ "CROSS JOIN",
+ "AS",
+ "WHERE",
+ "GROUP BY",
+ "ORDER BY",
+ "LIMIT"
+ ]
+ }
+ },
+ {
+ "case_id": "FROM_005",
+ "title": "FROM includes CTE names defined in same statement",
+ "sql": "WITH active_users AS (SELECT id, name FROM users) SELECT * FROM |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "customers",
+ "orders",
+ "payments",
+ "products",
+ "users"
+ ]
+ }
+ },
+ {
+ "case_id": "FROM_006",
+ "title": "CTE not visible across statements",
+ "sql": "WITH a AS (SELECT 1) SELECT * FROM a; SELECT * FROM |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "customers",
+ "orders",
+ "payments",
+ "products",
+ "users"
+ ]
+ }
+ },
+ {
+ "case_id": "FROM_007",
+ "title": "After AS with space, no suggestions (user typing alias)",
+ "sql": "SELECT * FROM users AS |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "comment": "After AS keyword, user is typing an alias name, so no suggestions should be shown",
+ "suggestions": []
+ }
+ },
+ {
+ "case_id": "FROM_008",
+ "title": "After AS with partial alias, no suggestions",
+ "sql": "SELECT * FROM users AS u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "FROM_CLAUSE",
+ "prefix": "u",
+ "comment": "User is typing alias name after AS, so no suggestions should interfere",
+ "suggestions": []
+ }
+ },
+ {
+ "case_id": "FROM_009",
+ "title": "After complete alias with space, suggest clause keywords (no AS since alias exists)",
+ "sql": "SELECT * FROM users AS u |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "comment": "After complete alias definition, suggest next clause keywords. AS is excluded since alias already exists.",
+ "suggestions": [
+ "JOIN",
+ "INNER JOIN",
+ "LEFT JOIN",
+ "RIGHT JOIN",
+ "CROSS JOIN",
+ "WHERE",
+ "GROUP BY",
+ "ORDER BY",
+ "LIMIT"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/from_clause_prioritization.json b/tests/autocomplete/cases/from_clause_prioritization.json
new file mode 100644
index 0000000..f0dd527
--- /dev/null
+++ b/tests/autocomplete/cases/from_clause_prioritization.json
@@ -0,0 +1,85 @@
+{
+ "group": "FROM_CLAUSE_PRIORITIZATION",
+ "cases": [
+ {
+ "case_id": "FROM_PRIO_001",
+ "title": "FROM clause without prefix: prioritize tables referenced in SELECT",
+ "sql": "SELECT users.id FROM ",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "comment": "users is referenced in SELECT (qualified column), should be prioritized first even without prefix",
+ "suggestions": [
+ "users",
+ "customers",
+ "orders",
+ "payments",
+ "products"
+ ]
+ }
+ },
+ {
+ "case_id": "FROM_PRIO_002",
+ "title": "FROM clause with prefix: prioritize referenced table",
+ "sql": "SELECT users.name FROM u",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "FROM_CLAUSE",
+ "prefix": "u",
+ "comment": "users matches prefix 'u' and is referenced in SELECT",
+ "suggestions": [
+ "users"
+ ]
+ }
+ },
+ {
+ "case_id": "FROM_PRIO_003",
+ "title": "FROM clause: multiple tables referenced in SELECT",
+ "sql": "SELECT users.id, orders.total FROM ",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "comment": "Both orders and users are referenced in SELECT, so they appear first (in order of reference), followed by other tables alphabetically",
+ "suggestions": [
+ "orders",
+ "users",
+ "customers",
+ "payments",
+ "products"
+ ]
+ }
+ },
+ {
+ "case_id": "FROM_PRIO_004",
+ "title": "FROM clause: no qualified columns in SELECT",
+ "sql": "SELECT * FROM ",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "comment": "No tables referenced in SELECT, so all tables in alphabetical order",
+ "suggestions": [
+ "customers",
+ "orders",
+ "payments",
+ "products",
+ "users"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/from_join_clause_current_table.json b/tests/autocomplete/cases/from_join_clause_current_table.json
new file mode 100644
index 0000000..1fb4f7e
--- /dev/null
+++ b/tests/autocomplete/cases/from_join_clause_current_table.json
@@ -0,0 +1,161 @@
+{
+ "group": "FROM_JOIN_CLAUSE_CURRENT_TABLE",
+ "cases": [
+ {
+ "case_id": "FROM_CURR_001",
+ "title": "FROM without prefix - CURRENT_TABLE suggested if set and not present",
+ "sql": "SELECT * FROM |",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "comment": "CURRENT_TABLE=users suggested as convenience. All physical tables.",
+ "suggestions": [
+ "customers",
+ "orders",
+ "payments",
+ "products",
+ "users"
+ ]
+ }
+ },
+ {
+ "case_id": "FROM_CURR_002",
+ "title": "FROM with prefix - CURRENT_TABLE filtered by prefix",
+ "sql": "SELECT * FROM u|",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "FROM_CLAUSE",
+ "prefix": "u",
+ "comment": "CURRENT_TABLE=users matches prefix 'u'.",
+ "suggestions": [
+ "users"
+ ]
+ }
+ },
+ {
+ "case_id": "FROM_CURR_003",
+ "title": "FROM - CURRENT_TABLE not suggested if already present",
+ "sql": "SELECT * FROM users, |",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "comment": "CURRENT_TABLE=users already present. Excluded from suggestions.",
+ "suggestions": [
+ "customers",
+ "orders",
+ "payments",
+ "products"
+ ]
+ }
+ },
+ {
+ "case_id": "JOIN_CURR_001",
+ "title": "JOIN_CLAUSE without prefix - CURRENT_TABLE suggested even if scope exists",
+ "sql": "SELECT * FROM orders JOIN |",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "JOIN_CLAUSE",
+ "prefix": null,
+ "comment": "Scope=[orders]. CURRENT_TABLE=users suggested (scope building). Exclude orders (already present).",
+ "suggestions": [
+ "customers",
+ "payments",
+ "products",
+ "users"
+ ]
+ }
+ },
+ {
+ "case_id": "JOIN_CURR_002",
+ "title": "JOIN_CLAUSE with prefix - CURRENT_TABLE filtered by prefix",
+ "sql": "SELECT * FROM orders JOIN u|",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "JOIN_CLAUSE",
+ "prefix": "u",
+ "comment": "CURRENT_TABLE=users matches prefix 'u'.",
+ "suggestions": [
+ "users"
+ ]
+ }
+ },
+ {
+ "case_id": "JOIN_CURR_003",
+ "title": "JOIN_CLAUSE - CURRENT_TABLE not suggested if already in statement",
+ "sql": "SELECT * FROM users JOIN orders ON users.id=orders.user_id JOIN |",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "JOIN_CLAUSE",
+ "prefix": null,
+ "comment": "CURRENT_TABLE=users already present. Excluded. Exclude users and orders.",
+ "suggestions": [
+ "customers",
+ "payments",
+ "products"
+ ]
+ }
+ },
+ {
+ "case_id": "FROM_WITH_CTE_001",
+ "title": "FROM with CTE - CTE names suggested first",
+ "sql": "WITH active_users AS (SELECT * FROM users WHERE status='active') SELECT * FROM |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "FROM_CLAUSE",
+ "prefix": null,
+ "comment": "CTE 'active_users' available. Physical tables also suggested.",
+ "suggestions": [
+ "active_users",
+ "customers",
+ "orders",
+ "products",
+ "users"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "JOIN_CURR_004",
+ "title": "KILLER: JOIN with CURRENT_TABLE already present - NOT suggested again",
+ "sql": "SELECT * FROM users JOIN |",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "JOIN_CLAUSE",
+ "prefix": null,
+ "comment": "KILLER: CURRENT_TABLE=users already present in FROM. Must NOT suggest users again. Only other tables.",
+ "suggestions": [
+ "customers",
+ "orders",
+ "payments",
+ "products"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/fut.json b/tests/autocomplete/cases/fut.json
new file mode 100644
index 0000000..8fcf6fe
--- /dev/null
+++ b/tests/autocomplete/cases/fut.json
@@ -0,0 +1,23 @@
+{
+ "group": "FUT",
+ "cases": [
+ {
+ "case_id": "FUT_001",
+ "title": "Window function OVER context (future enhancement marker)",
+ "sql": "SELECT *, ROW_NUMBER() OVER | FROM users",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WINDOW_OVER",
+ "prefix": null,
+ "suggestions": [
+ "ORDER BY",
+ "PARTITION BY"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/group.json b/tests/autocomplete/cases/group.json
new file mode 100644
index 0000000..139758f
--- /dev/null
+++ b/tests/autocomplete/cases/group.json
@@ -0,0 +1,105 @@
+{
+ "group": "GROUP",
+ "cases": [
+ {
+ "case_id": "GROUP_001",
+ "title": "GROUP BY without prefix suggests columns + functions",
+ "sql": "SELECT COUNT(*) FROM users GROUP BY |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "GROUP_BY_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "GROUP_002",
+ "title": "GROUP BY with prefix filters columns/functions",
+ "sql": "SELECT COUNT(*) FROM users GROUP BY s|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "GROUP_BY_CLAUSE",
+ "prefix": "s",
+ "suggestions": [
+ "users.status",
+ "SUBSTR",
+ "SUM"
+ ]
+ }
+ },
+ {
+ "case_id": "GROUP_003",
+ "title": "GROUP BY after comma suggests more group keys",
+ "sql": "SELECT COUNT(*) FROM users GROUP BY status, |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "GROUP_BY_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/having.json b/tests/autocomplete/cases/having.json
new file mode 100644
index 0000000..98b4a64
--- /dev/null
+++ b/tests/autocomplete/cases/having.json
@@ -0,0 +1,117 @@
+{
+ "group": "HAVING",
+ "cases": [
+ {
+ "case_id": "HAVING_001",
+ "title": "HAVING without prefix: aggregates first then columns then other functions",
+ "sql": "SELECT status, COUNT(*) FROM users GROUP BY status HAVING |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "HAVING_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "AVG",
+ "COUNT",
+ "GROUP_CONCAT",
+ "MAX",
+ "MIN",
+ "SUM",
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "COALESCE",
+ "CONCAT",
+ "DATE",
+ "LENGTH",
+ "LOWER",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "HAVING_002",
+ "title": "HAVING with prefix prioritizes aggregate functions matching prefix",
+ "sql": "SELECT status, COUNT(*) FROM users GROUP BY status HAVING c|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "HAVING_CLAUSE",
+ "prefix": "c",
+ "suggestions": [
+ "COUNT",
+ "users.created_at",
+ "COALESCE",
+ "CONCAT"
+ ]
+ }
+ },
+ {
+ "case_id": "HAVING_003",
+ "title": "HAVING after operator: NULL/TRUE/FALSE + aggregates + columns",
+ "sql": "SELECT status, COUNT(*) FROM users GROUP BY status HAVING COUNT(*) > |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_OPERATOR",
+ "context": "HAVING_AFTER_OPERATOR",
+ "prefix": null,
+ "suggestions": [
+ "NULL",
+ "TRUE",
+ "FALSE",
+ "AVG",
+ "COUNT",
+ "GROUP_CONCAT",
+ "MAX",
+ "MIN",
+ "SUM",
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "HAVING_004",
+ "title": "HAVING after expression suggests logical + clauses",
+ "sql": "SELECT status, COUNT(*) FROM users GROUP BY status HAVING COUNT(*) > 10 |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_EXPRESSION",
+ "context": "HAVING_AFTER_EXPRESSION",
+ "prefix": null,
+ "suggestions": [
+ "AND",
+ "EXISTS",
+ "LIMIT",
+ "NOT",
+ "OR",
+ "ORDER BY"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/having_aggregate_priority.json b/tests/autocomplete/cases/having_aggregate_priority.json
new file mode 100644
index 0000000..96545c8
--- /dev/null
+++ b/tests/autocomplete/cases/having_aggregate_priority.json
@@ -0,0 +1,146 @@
+{
+ "group": "HAVING_AGGREGATE_PRIORITY",
+ "cases": [
+ {
+ "case_id": "HAVING_001",
+ "title": "HAVING without prefix - aggregate functions first, then columns",
+ "sql": "SELECT status, COUNT(*) FROM users GROUP BY status HAVING |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "HAVING",
+ "prefix": null,
+ "comment": "HAVING exception: aggregate functions before columns. Scope=[users].",
+ "suggestions": [
+ "AVG",
+ "COUNT",
+ "GROUP_CONCAT",
+ "MAX",
+ "MIN",
+ "SUM",
+ "users.created_at",
+ "users.email",
+ "users.id",
+ "users.name",
+ "users.status",
+ "COALESCE",
+ "CONCAT",
+ "DATE",
+ "LENGTH",
+ "LOWER",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "HAVING_002",
+ "title": "HAVING with prefix - aggregate functions first",
+ "sql": "SELECT status, COUNT(*) FROM users GROUP BY status HAVING C|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "HAVING",
+ "prefix": "C",
+ "comment": "Aggregate functions first: COUNT. Then columns: users.created_at. Then other functions: COALESCE, CONCAT.",
+ "suggestions": [
+ "COUNT",
+ "users.created_at",
+ "COALESCE",
+ "CONCAT",
+ "CURRENT_DATE",
+ "CURRENT_TIME",
+ "CURRENT_TIMESTAMP"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "HAVING_003",
+ "title": "HAVING with scope restriction - no DB-wide columns",
+ "sql": "SELECT status, COUNT(*) FROM users GROUP BY status HAVING u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "HAVING",
+ "prefix": "u",
+ "comment": "Scope=[users]. DB-wide excluded. Aggregate functions alphabetically, then columns, then other functions.",
+ "suggestions": [
+ "users.created_at",
+ "users.email",
+ "users.id",
+ "users.name",
+ "users.status",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "HAVING_004",
+ "title": "HAVING after comparison operator - literals first, then columns",
+ "sql": "SELECT status, COUNT(*) as cnt FROM users GROUP BY status HAVING cnt > |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_OPERATOR",
+ "context": "HAVING_AFTER_OPERATOR",
+ "prefix": null,
+ "comment": "Literals first, then scope columns, then functions.",
+ "suggestions": [
+ "NULL",
+ "TRUE",
+ "FALSE",
+ "CURRENT_DATE",
+ "CURRENT_TIME",
+ "CURRENT_TIMESTAMP",
+ "users.created_at",
+ "users.email",
+ "users.id",
+ "users.name",
+ "users.status",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/join.json b/tests/autocomplete/cases/join.json
new file mode 100644
index 0000000..4519fb9
--- /dev/null
+++ b/tests/autocomplete/cases/join.json
@@ -0,0 +1,81 @@
+{
+ "group": "JOIN",
+ "cases": [
+ {
+ "case_id": "JOIN_001",
+ "title": "JOIN without prefix suggests tables",
+ "sql": "SELECT * FROM users JOIN |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "JOIN_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "customers",
+ "orders",
+ "payments",
+ "products"
+ ]
+ }
+ },
+ {
+ "case_id": "JOIN_002",
+ "title": "JOIN with prefix filters tables",
+ "sql": "SELECT * FROM users JOIN o|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "JOIN_CLAUSE",
+ "prefix": "o",
+ "suggestions": [
+ "orders"
+ ]
+ }
+ },
+ {
+ "case_id": "JOIN_003",
+ "title": "JOIN after table + space suggests AS/ON/USING",
+ "sql": "SELECT * FROM users JOIN orders |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_JOIN_TABLE",
+ "context": "JOIN_AFTER_TABLE",
+ "prefix": null,
+ "suggestions": [
+ "AS",
+ "ON",
+ "USING"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "JOIN_004",
+ "title": "JOIN includes CTE names",
+ "sql": "WITH au AS (SELECT id FROM users) SELECT * FROM users u JOIN |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "JOIN_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "au",
+ "customers",
+ "orders",
+ "payments",
+ "products",
+ "users"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/lex.json b/tests/autocomplete/cases/lex.json
new file mode 100644
index 0000000..4feb596
--- /dev/null
+++ b/tests/autocomplete/cases/lex.json
@@ -0,0 +1,87 @@
+{
+ "group": "LEX",
+ "cases": [
+ {
+ "case_id": "LEX_001",
+ "title": "Semicolon in string should not split statement",
+ "sql": "SELECT * FROM users WHERE name = 'a;b' AND |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "LEX_002",
+ "title": "Dot-like sequence inside string should not trigger dot-completion",
+ "sql": "SELECT * FROM users WHERE name = 'u.x' AND |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/limit.json b/tests/autocomplete/cases/limit.json
new file mode 100644
index 0000000..03972fc
--- /dev/null
+++ b/tests/autocomplete/cases/limit.json
@@ -0,0 +1,35 @@
+{
+ "group": "LIMIT",
+ "cases": [
+ {
+ "case_id": "LIMIT_001",
+ "title": "LIMIT offers no suggestions",
+ "sql": "SELECT * FROM users LIMIT |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "NO_SUGGESTIONS",
+ "context": "LIMIT_OFFSET_CLAUSE",
+ "prefix": null,
+ "suggestions": [],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "LIMIT_002",
+ "title": "OFFSET offers no suggestions",
+ "sql": "SELECT * FROM users LIMIT 10 OFFSET |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "NO_SUGGESTIONS",
+ "context": "LIMIT_OFFSET_CLAUSE",
+ "prefix": null,
+ "suggestions": [],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/mq.json b/tests/autocomplete/cases/mq.json
new file mode 100644
index 0000000..da55442
--- /dev/null
+++ b/tests/autocomplete/cases/mq.json
@@ -0,0 +1,173 @@
+{
+ "group": "MQ",
+ "cases": [
+ {
+ "case_id": "MQ_001",
+ "title": "Multi-query separation extracts only current statement around cursor",
+ "sql": "SELECT * FROM users; SELECT * FROM orders WHERE |; SELECT * FROM products;",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "MULTI_QUERY",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "users.created_at",
+ "users.email",
+ "users.id",
+ "users.name",
+ "users.status",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "MQ_002",
+ "title": "Multi-query separation extracts only current statement around cursor",
+ "sql": "SELECT 'O''Reilly;' AS x; SELECT * FROM users WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "MULTI_QUERY",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "users.created_at",
+ "users.email",
+ "users.id",
+ "users.name",
+ "users.status",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "MQ_003",
+ "title": "Multi-query separation extracts only current statement around cursor",
+ "sql": "SELECT 1; -- comment ; still comment\nSELECT * FROM users WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "MULTI_QUERY",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "users.created_at",
+ "users.email",
+ "users.id",
+ "users.name",
+ "users.status",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "MQ_004",
+ "title": "Multi-query separation extracts only current statement around cursor",
+ "sql": "SELECT 1; /* block ; comment */ SELECT * FROM users WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "MULTI_QUERY",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "users.created_at",
+ "users.email",
+ "users.id",
+ "users.name",
+ "users.status",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/multi_query_support.json b/tests/autocomplete/cases/multi_query_support.json
new file mode 100644
index 0000000..34e9f04
--- /dev/null
+++ b/tests/autocomplete/cases/multi_query_support.json
@@ -0,0 +1,360 @@
+{
+ "group": "MULTI_QUERY_SUPPORT",
+ "cases": [
+ {
+ "case_id": "MQ_EXTRACT_001",
+ "title": "Multi-query - cursor in second statement, analyze only second",
+ "sql": "SELECT * FROM users; SELECT |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "SELECT_LIST",
+ "prefix": null,
+ "comment": "Cursor in second statement. First statement ignored. No scope in second statement.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.id",
+ "orders.user_id",
+ "orders.total",
+ "orders.status",
+ "orders.created_at",
+ "products.id",
+ "products.name",
+ "products.price",
+ "products.unit_price",
+ "products.stock",
+ "customers.id",
+ "customers.name",
+ "customers.email",
+ "payments.id",
+ "payments.order_id",
+ "payments.amount",
+ "payments.method",
+ "payments.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR",
+ "FROM",
+ "WHERE",
+ "LIMIT",
+ "ORDER BY",
+ "GROUP BY"
+ ]
+ }
+ },
+ {
+ "case_id": "MQ_EXTRACT_002",
+ "title": "Multi-query - cursor in first statement WHERE clause",
+ "sql": "SELECT * FROM users WHERE |; SELECT * FROM orders",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "comment": "Cursor in first statement. Second statement ignored. Scope=[users].",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "MQ_EXTRACT_003",
+ "title": "Multi-query - second statement has scope from its own FROM",
+ "sql": "SELECT * FROM users; SELECT * FROM orders WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "comment": "Cursor in second statement. Scope=[orders] from second statement only.",
+ "suggestions": [
+ "orders.id",
+ "orders.user_id",
+ "orders.total",
+ "orders.status",
+ "orders.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "MQ_EXTRACT_004",
+ "title": "Multi-query - cursor on separator position",
+ "sql": "SELECT * FROM users;|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "STATEMENT_END",
+ "prefix": null,
+ "comment": "Cursor on separator. Treat as end of first statement or beginning of empty second statement.",
+ "suggestions": [
+ "ALTER",
+ "CREATE",
+ "DELETE",
+ "DESCRIBE",
+ "DROP",
+ "EXPLAIN",
+ "INSERT",
+ "MERGE",
+ "REPLACE",
+ "SELECT",
+ "SHOW",
+ "TRUNCATE",
+ "UPDATE",
+ "WITH"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "MQ_EXTRACT_005",
+ "title": "Multi-query - empty statement between separators",
+ "sql": "SELECT * FROM users; ; SELECT |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "SELECT_LIST",
+ "prefix": null,
+ "comment": "Empty statement ignored. Cursor in third statement.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.id",
+ "orders.user_id",
+ "orders.total",
+ "orders.status",
+ "orders.created_at",
+ "products.id",
+ "products.name",
+ "products.price",
+ "products.unit_price",
+ "products.stock",
+ "customers.id",
+ "customers.name",
+ "customers.email",
+ "payments.id",
+ "payments.order_id",
+ "payments.amount",
+ "payments.method",
+ "payments.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR",
+ "FROM",
+ "WHERE",
+ "LIMIT",
+ "ORDER BY",
+ "GROUP BY"
+ ]
+ }
+ },
+ {
+ "case_id": "MQ_EXTRACT_006",
+ "title": "KILLER: Cursor exactly on separator - end of previous statement",
+ "sql": "SELECT * FROM users;|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "STATEMENT_END",
+ "prefix": null,
+ "comment": "KILLER: Cursor on separator. Treat as end of first statement or beginning of new statement. Primary keywords.",
+ "suggestions": [
+ "ALTER",
+ "CREATE",
+ "DELETE",
+ "DESCRIBE",
+ "DROP",
+ "EXPLAIN",
+ "INSERT",
+ "MERGE",
+ "REPLACE",
+ "SELECT",
+ "SHOW",
+ "TRUNCATE",
+ "UPDATE",
+ "WITH"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "MQ_EXTRACT_007",
+ "title": "KILLER: Separator inside string literal - NOT a statement separator",
+ "sql": "SELECT 'test;value' FROM users WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "comment": "KILLER: Semicolon inside string literal is NOT a separator. Single statement. Scope=[users].",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "MQ_EXTRACT_008",
+ "title": "KILLER: Separator inside comment - NOT a statement separator",
+ "sql": "SELECT * FROM users /* comment; with semicolon */ WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "comment": "KILLER: Semicolon inside comment is NOT a separator. Single statement. Scope=[users].",
+ "suggestions": [
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/mw.json b/tests/autocomplete/cases/mw.json
new file mode 100644
index 0000000..b933f81
--- /dev/null
+++ b/tests/autocomplete/cases/mw.json
@@ -0,0 +1,44 @@
+{
+ "group": "MW",
+ "cases": [
+ {
+ "case_id": "MW_001",
+ "title": "Multi-word keyword ORDER BY inserted as single completion item",
+ "sql": "ORD|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "SINGLE_TOKEN",
+ "context": "SINGLE_TOKEN",
+ "prefix": "ORD",
+ "suggestions": [
+ "ORDER BY"
+ ],
+ "insertion": {
+ "text": "ORDER BY ",
+ "append_space": true
+ },
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "MW_002",
+ "title": "IS NULL and IS NOT NULL suggested as whole items",
+ "sql": "SELECT * FROM users WHERE status IS |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_AFTER_EXPRESSION",
+ "prefix": null,
+ "suggestions": [
+ "IS NOT NULL",
+ "IS NULL"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/on.json b/tests/autocomplete/cases/on.json
new file mode 100644
index 0000000..c1a3571
--- /dev/null
+++ b/tests/autocomplete/cases/on.json
@@ -0,0 +1,149 @@
+{
+ "group": "ON",
+ "cases": [
+ {
+ "case_id": "ON_001",
+ "title": "ON without prefix suggests columns in scope + functions",
+ "sql": "SELECT * FROM users u JOIN orders o ON |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "JOIN_ON",
+ "prefix": null,
+ "suggestions": [
+ "o.created_at",
+ "o.id",
+ "o.status",
+ "o.total",
+ "o.user_id",
+ "u.created_at",
+ "u.email",
+ "u.id",
+ "u.name",
+ "u.status",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "ON_002",
+ "title": "ON alias exact match suggests alias columns + matching functions",
+ "sql": "SELECT * FROM users u JOIN orders o ON u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "JOIN_ON",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "ON_003",
+ "title": "ON after operator prioritizes other-side table columns",
+ "sql": "SELECT * FROM users u JOIN orders o ON u.id = |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_OPERATOR",
+ "context": "JOIN_ON_AFTER_OPERATOR",
+ "prefix": null,
+ "suggestions": [
+ "o.id",
+ "o.user_id",
+ "o.total",
+ "o.status",
+ "o.created_at",
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "NULL",
+ "TRUE",
+ "FALSE",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "ON_004",
+ "title": "ON after complete expression suggests logical + next clauses",
+ "sql": "SELECT * FROM users u JOIN orders o ON u.id = o.user_id |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_EXPRESSION",
+ "context": "JOIN_ON_AFTER_EXPRESSION",
+ "prefix": null,
+ "suggestions": [
+ "AND",
+ "NOT",
+ "OR",
+ "GROUP BY",
+ "LIMIT",
+ "ORDER BY",
+ "WHERE"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/operator_left_column_filter.json b/tests/autocomplete/cases/operator_left_column_filter.json
new file mode 100644
index 0000000..5ae6c25
--- /dev/null
+++ b/tests/autocomplete/cases/operator_left_column_filter.json
@@ -0,0 +1,198 @@
+{
+ "group": "OPERATOR_LEFT_COLUMN_FILTER",
+ "cases": [
+ {
+ "case_id": "OP_FILTER_001",
+ "title": "WHERE with = operator: do NOT suggest left column",
+ "sql": "SELECT * FROM users WHERE users.id = ",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "comment": "users.id is on left of =, should NOT be suggested. Other columns + functions should be suggested.",
+ "suggestions": [
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "OP_FILTER_002",
+ "title": "WHERE with != operator: do NOT suggest left column",
+ "sql": "SELECT * FROM users WHERE users.name != ",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "comment": "users.name is on left of !=, should NOT be suggested",
+ "suggestions": [
+ "users.id",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "OP_FILTER_003",
+ "title": "JOIN ON with = operator: do NOT suggest left column",
+ "sql": "SELECT * FROM users JOIN orders ON users.id = ",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "JOIN_ON",
+ "prefix": null,
+ "comment": "users.id is on left of =, should NOT be suggested. orders.user_id should be suggested (common join pattern).",
+ "suggestions": [
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.id",
+ "orders.user_id",
+ "orders.total",
+ "orders.status",
+ "orders.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "OP_FILTER_004",
+ "title": "WHERE with < operator: do NOT suggest left column",
+ "sql": "SELECT * FROM orders WHERE orders.total < ",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "comment": "orders.total is on left of <, should NOT be suggested",
+ "suggestions": [
+ "orders.id",
+ "orders.user_id",
+ "orders.status",
+ "orders.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "OP_FILTER_005",
+ "title": "WHERE with prefix: normal behavior (no filtering)",
+ "sql": "SELECT * FROM users WHERE users.id = u",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "u",
+ "comment": "With prefix, no left-column filtering applied. Normal prefix matching includes columns + functions.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/order.json b/tests/autocomplete/cases/order.json
new file mode 100644
index 0000000..a88ea93
--- /dev/null
+++ b/tests/autocomplete/cases/order.json
@@ -0,0 +1,187 @@
+{
+ "group": "ORDER",
+ "cases": [
+ {
+ "case_id": "ORDER_001",
+ "title": "ORDER BY without prefix: columns, functions, then sort keywords",
+ "sql": "SELECT * FROM users ORDER BY |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "ORDER_BY_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR",
+ "ASC",
+ "DESC",
+ "NULLS FIRST",
+ "NULLS LAST"
+ ]
+ }
+ },
+ {
+ "case_id": "ORDER_002",
+ "title": "ORDER BY with prefix filters columns/functions/keywords",
+ "sql": "SELECT * FROM users ORDER BY c|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "ORDER_BY_CLAUSE",
+ "prefix": "c",
+ "suggestions": [
+ "users.created_at",
+ "CONCAT",
+ "COUNT",
+ "CURRENT_DATE",
+ "CURRENT_TIME",
+ "CURRENT_TIMESTAMP"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "ORDER_003",
+ "title": "ORDER BY after column + space suggests sort direction keywords",
+ "sql": "SELECT * FROM users ORDER BY created_at |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_ORDER_COLUMN",
+ "context": "ORDER_BY_AFTER_COLUMN",
+ "prefix": null,
+ "suggestions": [
+ "ASC",
+ "DESC",
+ "NULLS FIRST",
+ "NULLS LAST"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "ORDER_004",
+ "title": "ORDER BY after comma suggests more sort keys",
+ "sql": "SELECT * FROM users ORDER BY created_at DESC, |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "ORDER_BY_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR",
+ "ASC",
+ "DESC",
+ "NULLS FIRST",
+ "NULLS LAST"
+ ]
+ }
+ },
+ {
+ "case_id": "ORDER_005",
+ "title": "ORDER BY scope includes join table columns (aliases)",
+ "sql": "SELECT * FROM users u JOIN orders o ON u.id=o.user_id ORDER BY |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "ORDER_BY_CLAUSE",
+ "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",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR",
+ "ASC",
+ "DESC",
+ "NULLS FIRST",
+ "NULLS LAST"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/out_of_scope_hints.json b/tests/autocomplete/cases/out_of_scope_hints.json
new file mode 100644
index 0000000..e0b7d89
--- /dev/null
+++ b/tests/autocomplete/cases/out_of_scope_hints.json
@@ -0,0 +1,165 @@
+{
+ "group": "OUT_OF_SCOPE_HINTS",
+ "cases": [
+ {
+ "case_id": "HINT_001",
+ "title": "Out-of-scope table hint when prefix matches only DB-wide tables",
+ "sql": "SELECT c| FROM orders",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "c",
+ "comment": "Scope=[orders]. Prefix 'c' matches scope column orders.created_at and DB-wide table customers. Functions before table hints.",
+ "suggestions": [
+ "customers.id",
+ "customers.name",
+ "customers.email",
+ "orders.created_at",
+ "users.created_at",
+ "payments.created_at",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "CREATE",
+ "CROSS JOIN",
+ "CURRENT_DATE",
+ "CURRENT_TIME",
+ "CURRENT_TIMESTAMP"
+ ]
+ }
+ },
+ {
+ "case_id": "HINT_002",
+ "title": "KILLER: Out-of-scope hint when prefix matches NO scope columns, only DB table",
+ "sql": "SELECT u| FROM products",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "comment": "Scope=[products]. Prefix 'u' matches NO scope columns (products has: id,name,price,unit_price,stock). Matches DB table 'users'. HINT MODE: functions first, then table hint, NO DB-wide columns.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "products.unit_price",
+ "orders.user_id",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ },
+ {
+ "case_id": "HINT_003",
+ "title": "No out-of-scope hint when prefix matches scope columns",
+ "sql": "SELECT t| FROM orders",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "t",
+ "comment": "Scope=[orders]. Prefix 't' matches scope column orders.total. No DB-wide table starts with 't'. No hint.",
+ "suggestions": [
+ "orders.total",
+ "TRIM",
+ "TRUE",
+ "TRUNCATE"
+ ]
+ }
+ },
+ {
+ "case_id": "HINT_004",
+ "title": "No out-of-scope hint in WHERE context",
+ "sql": "SELECT * FROM orders WHERE c|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "c",
+ "comment": "WHERE is scope-restricted. No out-of-scope hints. Only scope columns + functions.",
+ "suggestions": [
+ "orders.created_at",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "CURRENT_DATE",
+ "CURRENT_TIME",
+ "CURRENT_TIMESTAMP"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "HINT_005",
+ "title": "Case A: SELECT_LIST with scope - prefix matches scope column - DB-wide allowed",
+ "sql": "SELECT u| FROM orders",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "comment": "Case A: Prefix 'u' matches scope column orders.user_id. DB-wide columns allowed (table-name expansion + column-name matching). Table hint also shown.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.user_id",
+ "products.unit_price",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ },
+ {
+ "case_id": "HINT_006",
+ "title": "Case B: SELECT_LIST with scope - prefix matches NO scope, only DB table - hint mode",
+ "sql": "SELECT c| FROM orders",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "c",
+ "comment": "Case B: Prefix 'c' matches scope column orders.created_at AND DB table customers. Functions first, then table hint. DB-wide columns for table-name expansion allowed.",
+ "suggestions": [
+ "customers.id",
+ "customers.name",
+ "customers.email",
+ "orders.created_at",
+ "users.created_at",
+ "payments.created_at",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "CREATE",
+ "CROSS JOIN",
+ "CURRENT_DATE",
+ "CURRENT_TIME",
+ "CURRENT_TIMESTAMP"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/perf.json b/tests/autocomplete/cases/perf.json
new file mode 100644
index 0000000..7d8a4ab
--- /dev/null
+++ b/tests/autocomplete/cases/perf.json
@@ -0,0 +1,59 @@
+{
+ "group": "PERF",
+ "cases": [
+ {
+ "case_id": "PERF_001",
+ "title": "Guardrail: no prefix, huge schema => skip non-scope database columns",
+ "sql": "SELECT * FROM users WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "big",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "guardrail_expected": true,
+ "suggestions": [
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "PERF_002",
+ "title": "Guardrail: with prefix includes filtered db-wide columns even in big schema",
+ "sql": "SELECT * FROM users WHERE col_0|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "big",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "col_0",
+ "suggestions_contains": [
+ "t01_big.col_001"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/prefix_expansion.json b/tests/autocomplete/cases/prefix_expansion.json
new file mode 100644
index 0000000..9397f45
--- /dev/null
+++ b/tests/autocomplete/cases/prefix_expansion.json
@@ -0,0 +1,236 @@
+{
+ "group": "PREFIX_EXPANSION",
+ "cases": [
+ {
+ "case_id": "PREFIX_EXP_001",
+ "title": "SELECT_LIST: prefix 'u' expands users table + matches user_id columns",
+ "sql": "SELECT u| FROM orders",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "comment": "Table-name expansion: users.* in schema order. Column-name matching: orders.user_id, products.unit_price",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.user_id",
+ "products.unit_price",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ },
+ {
+ "case_id": "PREFIX_EXP_002",
+ "title": "WHERE: prefix 'us' matches table name, uses table name not alias",
+ "sql": "SELECT * FROM users u WHERE us|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "us",
+ "comment": "Alias 'u' exists but 'us' != 'u' so NOT alias-exact-match. Table-name expansion: users.* in schema order. WHERE is scope-restricted so no orders.user_id.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at"
+ ]
+ }
+ },
+ {
+ "case_id": "PREFIX_EXP_003",
+ "title": "JOIN ON: prefix 'u' expands users + matches user_id",
+ "sql": "SELECT * FROM users u JOIN orders o ON u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "JOIN_ON",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "comment": "Alias exact match: 'u' == alias 'u' -> show only u.* columns in schema order",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "PREFIX_EXP_004",
+ "title": "ORDER BY: prefix 'u' expands users table",
+ "sql": "SELECT * FROM orders ORDER BY u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "ORDER_BY_CLAUSE",
+ "prefix": "u",
+ "comment": "ORDER BY is scope-restricted. Only orders table in scope. Column-name matching: orders.user_id",
+ "suggestions": [
+ "orders.user_id",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "PREFIX_EXP_005",
+ "title": "GROUP BY: prefix 's' matches status column",
+ "sql": "SELECT COUNT(*) FROM users GROUP BY s|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "GROUP_BY_CLAUSE",
+ "prefix": "s",
+ "comment": "GROUP BY is scope-restricted. Only users table in scope. Column-name matching: users.status",
+ "suggestions": [
+ "users.status",
+ "SUBSTR",
+ "SUM"
+ ]
+ }
+ },
+ {
+ "case_id": "PREFIX_EXP_006",
+ "title": "HAVING: prefix 'u' expands users table",
+ "sql": "SELECT status, COUNT(*) FROM orders GROUP BY status HAVING u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "HAVING_CLAUSE",
+ "prefix": "u",
+ "comment": "HAVING is scope-restricted. Only orders table in scope. Column-name matching: orders.user_id",
+ "suggestions": [
+ "orders.user_id",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "PREFIX_EXP_007",
+ "title": "Deduplication: column appears in both table expansion and column match",
+ "sql": "SELECT u| FROM users",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "comment": "Table-name expansion: users.* in schema order. Column-name matching: orders.user_id, products.unit_price. Deduplication applied.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.user_id",
+ "products.unit_price",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ },
+ {
+ "case_id": "PREFIX_EXP_008",
+ "title": "Multi-query: second query context isolated from first",
+ "sql": "SELECT * FROM orders; SELECT u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "comment": "Multi-query separation: second query has no scope. Table-name expansion: users.* in schema order. Column-name matching: orders.user_id, products.unit_price",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.user_id",
+ "products.unit_price",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ },
+ {
+ "case_id": "PREFIX_EXP_009",
+ "title": "WHERE after operator: prefix 'u' expands users table",
+ "sql": "SELECT * FROM orders WHERE status = u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "u",
+ "comment": "WHERE is scope-restricted. Only orders table in scope. Column-name matching: orders.user_id",
+ "suggestions": [
+ "orders.user_id",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "PREFIX_EXP_010",
+ "title": "JOIN ON after operator: prefix 'o' with alias exact match",
+ "sql": "SELECT * FROM users u JOIN orders o ON u.id = o|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "JOIN_ON",
+ "prefix": "o",
+ "alias_exact_match": "o",
+ "comment": "Alias exact match: 'o' == alias 'o' -> show only o.* columns in schema order",
+ "suggestions": [
+ "o.id",
+ "o.user_id",
+ "o.total",
+ "o.status",
+ "o.created_at"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/scope.json b/tests/autocomplete/cases/scope.json
new file mode 100644
index 0000000..ca2e50e
--- /dev/null
+++ b/tests/autocomplete/cases/scope.json
@@ -0,0 +1,126 @@
+{
+ "group": "SCOPE",
+ "cases": [
+ {
+ "case_id": "SCOPE_001",
+ "title": "Derived table columns available with alias in WHERE",
+ "sql": "SELECT * FROM (SELECT id, total FROM orders) AS o WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "SCOPE_002",
+ "title": "CTE columns available and qualified by CTE name",
+ "sql": "WITH active_users AS (SELECT id, name FROM users WHERE status='active') SELECT * FROM active_users WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "SCOPE_003",
+ "title": "ON other-side prioritization works with derived tables and CTEs",
+ "sql": "WITH au AS (SELECT id, name FROM users WHERE status='active') SELECT * FROM (SELECT id, total FROM orders) AS o JOIN au ON o.id = |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_OPERATOR",
+ "context": "JOIN_ON_AFTER_OPERATOR",
+ "prefix": null,
+ "suggestions": [
+ "au.id",
+ "au.name",
+ "o.id",
+ "o.total",
+ "NULL",
+ "TRUE",
+ "FALSE",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/scope_restriction_join_on.json b/tests/autocomplete/cases/scope_restriction_join_on.json
new file mode 100644
index 0000000..cfe260f
--- /dev/null
+++ b/tests/autocomplete/cases/scope_restriction_join_on.json
@@ -0,0 +1,103 @@
+{
+ "group": "SCOPE_RESTRICTION_JOIN_ON",
+ "cases": [
+ {
+ "case_id": "JOIN_ON_SCOPE_001",
+ "title": "JOIN_ON with scope - no DB-wide columns",
+ "sql": "SELECT * FROM users u JOIN orders o ON u.id = o.user_id JOIN products p ON p|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "JOIN_ON",
+ "prefix": "p",
+ "comment": "Scope=[users as u, orders as o, products as p]. Only scope columns starting with 'p' in schema order. DB-wide excluded.",
+ "suggestions": [
+ "p.id",
+ "p.name",
+ "p.price",
+ "p.unit_price",
+ "p.stock"
+ ]
+ }
+ },
+ {
+ "case_id": "JOIN_ON_SCOPE_002",
+ "title": "JOIN_ON generic prefix - only scope table columns",
+ "sql": "SELECT * FROM orders o JOIN users u ON u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "JOIN_ON",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "comment": "Scope=[orders as o, users as u]. Alias-exact-match for 'u' in schema order.",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "JOIN_ON_SCOPE_003",
+ "title": "JOIN_ON with CURRENT_TABLE not in scope - CURRENT_TABLE ignored",
+ "sql": "SELECT * FROM orders o JOIN products p ON p.id = |",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "JOIN_ON",
+ "prefix": null,
+ "comment": "Scope=[orders (alias o), products (alias p)]. CURRENT_TABLE=users not in scope. p.id is on left of =, so filtered out.",
+ "suggestions_contains": [
+ "o.id",
+ "o.user_id",
+ "p.name",
+ "COUNT",
+ "SUM"
+ ],
+ "suggestions_not_contains": [
+ "p.id",
+ "users.id",
+ "users.name",
+ "customers.id"
+ ]
+ }
+ },
+ {
+ "case_id": "JOIN_ON_SCOPE_004",
+ "title": "JOIN_ON with CURRENT_TABLE in scope - CURRENT_TABLE included",
+ "sql": "SELECT * FROM users u JOIN orders o ON u|",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "JOIN_ON",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "comment": "Scope=[users as u, orders as o]. CURRENT_TABLE=users in scope. Alias-exact-match in schema order.",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/scope_restriction_order_group.json b/tests/autocomplete/cases/scope_restriction_order_group.json
new file mode 100644
index 0000000..36719a2
--- /dev/null
+++ b/tests/autocomplete/cases/scope_restriction_order_group.json
@@ -0,0 +1,104 @@
+{
+ "group": "SCOPE_RESTRICTION_ORDER_GROUP",
+ "cases": [
+ {
+ "case_id": "ORDER_SCOPE_001",
+ "title": "ORDER_BY with scope - no DB-wide columns",
+ "sql": "SELECT * FROM users ORDER BY c|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "ORDER_BY_CLAUSE",
+ "prefix": "c",
+ "comment": "Scope=[users]. Only scope column users.created_at. DB-wide excluded.",
+ "suggestions": [
+ "users.created_at",
+ "COALESCE",
+ "CONCAT",
+ "COUNT"
+ ]
+ }
+ },
+ {
+ "case_id": "ORDER_SCOPE_002",
+ "title": "ORDER_BY after column + space suggests ASC/DESC only",
+ "sql": "SELECT * FROM users ORDER BY created_at |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "ORDER_BY_CLAUSE",
+ "prefix": null,
+ "comment": "Whitespace after column - shows all columns, functions, and keywords.",
+ "suggestions_contains": [
+ "users.id",
+ "ASC",
+ "DESC"
+ ]
+ }
+ },
+ {
+ "case_id": "ORDER_SCOPE_003",
+ "title": "ORDER_BY after comma suggests columns again",
+ "sql": "SELECT * FROM users u JOIN orders o ON u.id=o.user_id ORDER BY u.name, |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "ORDER_BY_CLAUSE",
+ "prefix": null,
+ "comment": "Comma = next-item rules. Scope columns + functions.",
+ "suggestions_contains": [
+ "u.id",
+ "u.name",
+ "o.id",
+ "o.total",
+ "AVG",
+ "COUNT"
+ ]
+ }
+ },
+ {
+ "case_id": "GROUP_SCOPE_001",
+ "title": "GROUP_BY with scope - no DB-wide columns",
+ "sql": "SELECT * FROM users GROUP BY s|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "GROUP_BY_CLAUSE",
+ "prefix": "s",
+ "comment": "Scope=[users]. Only scope column users.status. DB-wide excluded.",
+ "suggestions": [
+ "users.status",
+ "SUBSTR",
+ "SUM"
+ ]
+ }
+ },
+ {
+ "case_id": "GROUP_SCOPE_002",
+ "title": "GROUP_BY with CURRENT_TABLE not in scope - CURRENT_TABLE ignored",
+ "sql": "SELECT * FROM orders GROUP BY s|",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "GROUP_BY_CLAUSE",
+ "prefix": "s",
+ "comment": "Scope=[orders]. CURRENT_TABLE=users not in scope. Only scope column orders.status.",
+ "suggestions": [
+ "orders.status",
+ "SUBSTR",
+ "SUM"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/scope_restriction_where.json b/tests/autocomplete/cases/scope_restriction_where.json
new file mode 100644
index 0000000..e6301f3
--- /dev/null
+++ b/tests/autocomplete/cases/scope_restriction_where.json
@@ -0,0 +1,84 @@
+{
+ "group": "SCOPE_RESTRICTION_WHERE",
+ "cases": [
+ {
+ "case_id": "WHERE_SCOPE_001",
+ "title": "WHERE with scope - no DB-wide columns, only scope tables",
+ "sql": "SELECT * FROM users WHERE p|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "p",
+ "comment": "Scope=[users]. No scope column starts with 'p'. DB-wide columns excluded. Only functions.",
+ "suggestions": []
+ }
+ },
+ {
+ "case_id": "WHERE_SCOPE_002",
+ "title": "WHERE with JOIN scope - only scope table columns",
+ "sql": "SELECT * FROM users u JOIN orders o ON u.id=o.user_id WHERE s|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "s",
+ "comment": "Scope=[users as u, orders as o]. Scope columns: u.status, o.status. DB-wide excluded.",
+ "suggestions": [
+ "u.status",
+ "o.status",
+ "SUBSTR",
+ "SUM"
+ ]
+ }
+ },
+ {
+ "case_id": "WHERE_SCOPE_003",
+ "title": "WHERE with CURRENT_TABLE not in scope - CURRENT_TABLE ignored",
+ "sql": "SELECT * FROM orders WHERE u|",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "u",
+ "comment": "Scope=[orders]. CURRENT_TABLE=users not in scope. Only scope column orders.user_id.",
+ "suggestions": [
+ "orders.user_id",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "WHERE_SCOPE_004",
+ "title": "WHERE with CURRENT_TABLE in scope - CURRENT_TABLE included",
+ "sql": "SELECT * FROM users WHERE u|",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "u",
+ "comment": "Scope=[users]. CURRENT_TABLE=users in scope. Table-name expansion: users.* in schema order.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/sel.json b/tests/autocomplete/cases/sel.json
new file mode 100644
index 0000000..e9fe5f5
--- /dev/null
+++ b/tests/autocomplete/cases/sel.json
@@ -0,0 +1,303 @@
+{
+ "group": "SEL",
+ "cases": [
+ {
+ "case_id": "SEL_LIST_001",
+ "title": "SELECT_LIST without FROM/JOIN shows functions + clause keywords",
+ "sql": "SELECT |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "SELECT_LIST",
+ "prefix": null,
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.id",
+ "orders.user_id",
+ "orders.total",
+ "orders.status",
+ "orders.created_at",
+ "products.id",
+ "products.name",
+ "products.price",
+ "products.unit_price",
+ "products.stock",
+ "customers.id",
+ "customers.name",
+ "customers.email",
+ "payments.id",
+ "payments.order_id",
+ "payments.amount",
+ "payments.method",
+ "payments.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR",
+ "FROM",
+ "WHERE",
+ "LIMIT",
+ "ORDER BY",
+ "GROUP BY"
+ ]
+ }
+ },
+ {
+ "case_id": "SEL_LIST_002",
+ "title": "SELECT_LIST with FROM (multi-statement) suggests columns + functions",
+ "sql": "SELECT * FROM users WHERE id=1; SELECT |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "SELECT_LIST",
+ "prefix": null,
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.id",
+ "orders.user_id",
+ "orders.total",
+ "orders.status",
+ "orders.created_at",
+ "products.id",
+ "products.name",
+ "products.price",
+ "products.unit_price",
+ "products.stock",
+ "customers.id",
+ "customers.name",
+ "customers.email",
+ "payments.id",
+ "payments.order_id",
+ "payments.amount",
+ "payments.method",
+ "payments.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR",
+ "FROM",
+ "WHERE",
+ "LIMIT",
+ "ORDER BY",
+ "GROUP BY"
+ ]
+ }
+ },
+ {
+ "case_id": "SEL_LIST_003",
+ "title": "SELECT_LIST with FROM+JOIN suggests alias-qualified columns + functions",
+ "sql": "SELECT * FROM users u JOIN orders o ON u.id = o.user_id; SELECT |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "SELECT_LIST",
+ "prefix": null,
+ "suggestions": [
+ "o.created_at",
+ "o.id",
+ "o.status",
+ "o.total",
+ "o.user_id",
+ "u.created_at",
+ "u.email",
+ "u.id",
+ "u.name",
+ "u.status",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "SEL_LIST_004",
+ "title": "SELECT_LIST prefix equals alias => alias columns + matching functions",
+ "sql": "SELECT * FROM users u WHERE id=1; SELECT u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "suggestions": [
+ "u.created_at",
+ "u.email",
+ "u.id",
+ "u.name",
+ "u.status",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "SEL_LIST_005",
+ "title": "SELECT_LIST with scope - prefix matches scope table only",
+ "sql": "SELECT u| FROM users",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "comment": "Scope exists. Table-name expansion: users.* (scope table starts with 'u'). DB-wide columns: orders.user_id, products.unit_price. Functions before keywords.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.user_id",
+ "products.unit_price",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ },
+ {
+ "case_id": "SEL_LIST_006",
+ "title": "SELECT_LIST after comma suggests columns in scope + functions",
+ "sql": "SELECT id, | FROM users u JOIN orders o ON u.id = o.user_id",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "SELECT_LIST",
+ "prefix": null,
+ "suggestions": [
+ "o.created_at",
+ "o.id",
+ "o.status",
+ "o.total",
+ "o.user_id",
+ "u.created_at",
+ "u.email",
+ "u.id",
+ "u.name",
+ "u.status",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "SEL_LIST_007",
+ "title": "After complete column + space suggests clause keywords",
+ "sql": "SELECT users.id |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_COLUMN",
+ "context": "SELECT_LIST_AFTER_COLUMN",
+ "prefix": null,
+ "suggestions": [
+ "AS",
+ "FROM",
+ "GROUP BY",
+ "HAVING",
+ "LIMIT",
+ "ORDER BY",
+ "WHERE"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/select_no_scope.json b/tests/autocomplete/cases/select_no_scope.json
new file mode 100644
index 0000000..4501c72
--- /dev/null
+++ b/tests/autocomplete/cases/select_no_scope.json
@@ -0,0 +1,87 @@
+{
+ "group": "SELECT_NO_SCOPE",
+ "cases": [
+ {
+ "case_id": "SEL_NO_SCOPE_001",
+ "title": "SELECT without scope and CURRENT_TABLE set - CURRENT_TABLE columns first",
+ "sql": "SELECT u|",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "comment": "No scope. CURRENT_TABLE=users. Table-name expansion + column-name matching + functions in schema order.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.user_id",
+ "products.unit_price",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ },
+ {
+ "case_id": "SEL_NO_SCOPE_002",
+ "title": "SELECT without scope and no CURRENT_TABLE - DB-wide columns allowed",
+ "sql": "SELECT p|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "p",
+ "comment": "No scope. Table-name expansion: products.* and payments.* in schema order (both match prefix 'p').",
+ "suggestions": [
+ "products.id",
+ "products.name",
+ "products.price",
+ "products.unit_price",
+ "products.stock",
+ "payments.id",
+ "payments.order_id",
+ "payments.amount",
+ "payments.method",
+ "payments.created_at"
+ ]
+ }
+ },
+ {
+ "case_id": "SEL_NO_SCOPE_003",
+ "title": "SELECT without scope - deduplication between table expansion and column match",
+ "sql": "SELECT u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "comment": "Table-name expansion: users.* in schema order. Column-name matching: orders.user_id, products.unit_price.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.user_id",
+ "products.unit_price",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/select_qualified_column_whitespace.json b/tests/autocomplete/cases/select_qualified_column_whitespace.json
new file mode 100644
index 0000000..5538983
--- /dev/null
+++ b/tests/autocomplete/cases/select_qualified_column_whitespace.json
@@ -0,0 +1,96 @@
+{
+ "group": "SELECT_QUALIFIED_COLUMN_WHITESPACE",
+ "cases": [
+ {
+ "case_id": "QUAL_WS_001",
+ "title": "After qualified column + space: suggest ONLY keywords, NOT functions",
+ "sql": "SELECT users.id ",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "SELECT_LIST",
+ "prefix": null,
+ "comment": "Whitespace after qualified column = selection complete. Show clause keywords ONLY, NOT functions.",
+ "suggestions": [
+ "FROM",
+ "WHERE",
+ "LIMIT",
+ "ORDER BY",
+ "GROUP BY"
+ ]
+ }
+ },
+ {
+ "case_id": "QUAL_WS_002",
+ "title": "After qualified column + space: no COUNT, SUM, UPPER, etc.",
+ "sql": "SELECT orders.total ",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "SELECT_LIST",
+ "prefix": null,
+ "comment": "Functions should NOT be suggested after qualified column + space",
+ "suggestions": [
+ "FROM",
+ "WHERE",
+ "LIMIT",
+ "ORDER BY",
+ "GROUP BY"
+ ]
+ }
+ },
+ {
+ "case_id": "QUAL_WS_003",
+ "title": "After qualified column + comma: suggest columns and functions",
+ "sql": "SELECT users.id, ",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "SELECT_LIST",
+ "prefix": null,
+ "comment": "Comma = next item. Show columns and functions (not keywords).",
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "QUAL_WS_004",
+ "title": "After qualified column + space + prefix: suggest ONLY matching keywords",
+ "sql": "SELECT users.id F",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "F",
+ "comment": "Whitespace after qualified column = selection complete. Even with prefix, show ONLY keywords (not functions/columns).",
+ "suggestions": [
+ "FROM"
+ ]
+ }
+ },
+ {
+ "case_id": "QUAL_WS_005",
+ "title": "After qualified column + space + prefix W: suggest WHERE",
+ "sql": "SELECT orders.total W",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "W",
+ "comment": "Only keywords matching 'W'",
+ "suggestions": [
+ "WHERE"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/select_with_scope_current_table.json b/tests/autocomplete/cases/select_with_scope_current_table.json
new file mode 100644
index 0000000..0520aa9
--- /dev/null
+++ b/tests/autocomplete/cases/select_with_scope_current_table.json
@@ -0,0 +1,111 @@
+{
+ "group": "SELECT_WITH_SCOPE_CURRENT_TABLE",
+ "cases": [
+ {
+ "case_id": "SEL_SCOPE_CURR_001",
+ "title": "SELECT with scope - CURRENT_TABLE not in scope must be ignored",
+ "sql": "SELECT u| FROM orders",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "comment": "Scope=[orders]. CURRENT_TABLE=users not in scope. DB-wide columns allowed. No hints (scope column orders.user_id matches).",
+ "suggestions": [
+ "orders.user_id",
+ "products.unit_price",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ },
+ {
+ "case_id": "SEL_SCOPE_CURR_002",
+ "title": "SELECT with scope - CURRENT_TABLE in scope via table name",
+ "sql": "SELECT u| FROM users",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "comment": "Scope=[users]. CURRENT_TABLE=users in scope. Table-name expansion: users.* in schema order. DB-wide columns: orders.user_id, products.unit_price.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.user_id",
+ "products.unit_price",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ },
+ {
+ "case_id": "SEL_SCOPE_CURR_003",
+ "title": "SELECT with scope - CURRENT_TABLE in scope via alias (alias-exact-match)",
+ "sql": "SELECT u| FROM users u",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "comment": "Scope=[users as u]. CURRENT_TABLE=users in scope. Alias-exact-match activated in schema order.",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "UPDATE",
+ "USING"
+ ]
+ }
+ },
+ {
+ "case_id": "SEL_SCOPE_CURR_004",
+ "title": "SELECT with scope - CURRENT_TABLE not in scope, prefix matches no scope columns",
+ "sql": "SELECT c| FROM orders",
+ "dialect": "generic",
+ "current_table": "users",
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "c",
+ "comment": "Scope=[orders]. Prefix 'c' matches scope column orders.created_at and DB table customers. DB-wide table expansion + scope columns + functions.",
+ "suggestions_contains": [
+ "customers.id",
+ "customers.name",
+ "customers.email",
+ "payments.created_at",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "CREATE",
+ "CROSS JOIN",
+ "CURRENT_DATE",
+ "CURRENT_TIME",
+ "CURRENT_TIMESTAMP"
+ ]
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/single.json b/tests/autocomplete/cases/single.json
new file mode 100644
index 0000000..f834baf
--- /dev/null
+++ b/tests/autocomplete/cases/single.json
@@ -0,0 +1,154 @@
+{
+ "group": "SINGLE",
+ "cases": [
+ {
+ "case_id": "SINGLE_001",
+ "title": "SINGLE_TOKEN keyword completion for SEL|",
+ "sql": "SEL|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SINGLE_TOKEN",
+ "prefix": "SEL",
+ "suggestions": [
+ "SELECT"
+ ]
+ }
+ },
+ {
+ "case_id": "SINGLE_002",
+ "title": "SINGLE_TOKEN keyword completion for INS|",
+ "sql": "INS|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SINGLE_TOKEN",
+ "prefix": "INS",
+ "suggestions": [
+ "INSERT"
+ ]
+ }
+ },
+ {
+ "case_id": "SINGLE_003",
+ "title": "SINGLE_TOKEN keyword completion for UPD|",
+ "sql": "UPD|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SINGLE_TOKEN",
+ "prefix": "UPD",
+ "suggestions": [
+ "UPDATE"
+ ]
+ }
+ },
+ {
+ "case_id": "SINGLE_004",
+ "title": "SINGLE_TOKEN keyword completion for DELE|",
+ "sql": "DELE|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SINGLE_TOKEN",
+ "prefix": "DELE",
+ "suggestions": [
+ "DELETE"
+ ]
+ }
+ },
+ {
+ "case_id": "SINGLE_005",
+ "title": "SINGLE_TOKEN keyword completion for CRE|",
+ "sql": "CRE|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SINGLE_TOKEN",
+ "prefix": "CRE",
+ "suggestions": [
+ "CREATE"
+ ]
+ }
+ },
+ {
+ "case_id": "SINGLE_006",
+ "title": "SINGLE_TOKEN keyword completion for WIT|",
+ "sql": "WIT|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SINGLE_TOKEN",
+ "prefix": "WIT",
+ "suggestions": [
+ "WITH"
+ ]
+ }
+ },
+ {
+ "case_id": "SINGLE_007",
+ "title": "SINGLE_TOKEN keyword completion for ORD|",
+ "sql": "ORD|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SINGLE_TOKEN",
+ "prefix": "ORD",
+ "suggestions": [
+ "ORDER BY"
+ ]
+ }
+ },
+ {
+ "case_id": "SINGLE_008",
+ "title": "Not SINGLE_TOKEN because whitespace exists",
+ "sql": "IS N|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "UNKNOWN",
+ "prefix": "N",
+ "suggestions": [
+ "NOT",
+ "NULL",
+ "NULLS FIRST",
+ "NULLS LAST"
+ ],
+ "note": "Context UNKNOWN but still provides keyword suggestions."
+ }
+ },
+ {
+ "case_id": "SINGLE_009",
+ "title": "Not SINGLE_TOKEN across newline (two tokens total)",
+ "sql": "SELECT\nSEL|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "SEL",
+ "suggestions": [
+ "SELECT"
+ ],
+ "note": "Must be context-based."
+ }
+ }
+ ]
+}
diff --git a/tests/autocomplete/cases/using.json b/tests/autocomplete/cases/using.json
new file mode 100644
index 0000000..8b16239
--- /dev/null
+++ b/tests/autocomplete/cases/using.json
@@ -0,0 +1,24 @@
+{
+ "group": "USING",
+ "cases": [
+ {
+ "case_id": "USING_001",
+ "title": "USING keyword suggested after JOIN table name",
+ "sql": "SELECT * FROM users u JOIN orders o |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_JOIN_TABLE",
+ "context": "JOIN_AFTER_TABLE",
+ "prefix": null,
+ "suggestions": [
+ "AS",
+ "ON",
+ "USING"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/where.json b/tests/autocomplete/cases/where.json
new file mode 100644
index 0000000..13f753a
--- /dev/null
+++ b/tests/autocomplete/cases/where.json
@@ -0,0 +1,213 @@
+{
+ "group": "WHERE",
+ "cases": [
+ {
+ "case_id": "WHERE_001",
+ "title": "WHERE without prefix suggests columns + functions",
+ "sql": "SELECT * FROM users WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "prefix": null,
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "WHERE_002",
+ "title": "WHERE alias exact match suggests alias columns + matching functions",
+ "sql": "SELECT * FROM users u WHERE u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "u",
+ "alias_exact_match": "u",
+ "suggestions": [
+ "u.id",
+ "u.name",
+ "u.email",
+ "u.status",
+ "u.created_at",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ },
+ {
+ "case_id": "WHERE_003",
+ "title": "WHERE after operator suggests literals + columns + functions",
+ "sql": "SELECT * FROM users WHERE status = |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_OPERATOR",
+ "context": "WHERE_AFTER_OPERATOR",
+ "prefix": null,
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "NULL",
+ "TRUE",
+ "FALSE",
+ "CURRENT_DATE",
+ "CURRENT_TIME",
+ "CURRENT_TIMESTAMP",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "WHERE_004",
+ "title": "WHERE after expression suggests logical/expression keywords + clauses",
+ "sql": "SELECT * FROM users WHERE id = 1 |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_EXPRESSION",
+ "context": "WHERE_AFTER_EXPRESSION",
+ "prefix": null,
+ "suggestions": [
+ "AND",
+ "BETWEEN",
+ "EXISTS",
+ "GROUP BY",
+ "HAVING",
+ "IN",
+ "IS NOT NULL",
+ "IS NULL",
+ "LIKE",
+ "LIMIT",
+ "NOT",
+ "OR",
+ "ORDER BY"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "WHERE_005",
+ "title": "WHERE with JOIN scope includes both tables and uses aliases",
+ "sql": "SELECT * FROM users u JOIN orders o ON u.id=o.user_id WHERE |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "WHERE_CLAUSE",
+ "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",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR"
+ ]
+ }
+ },
+ {
+ "case_id": "WHERE_006",
+ "title": "WHERE generic prefix with scope restriction - no DB-wide columns",
+ "sql": "SELECT * FROM orders WHERE u|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "WHERE_CLAUSE",
+ "prefix": "u",
+ "comment": "Scope-restricted: only scope table columns. Column-name matching: orders.user_id (scope table column starts with 'u'). DB-wide columns excluded.",
+ "suggestions": [
+ "orders.user_id",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/cases/whitespace_comma_behavior.json b/tests/autocomplete/cases/whitespace_comma_behavior.json
new file mode 100644
index 0000000..a1c1557
--- /dev/null
+++ b/tests/autocomplete/cases/whitespace_comma_behavior.json
@@ -0,0 +1,209 @@
+{
+ "group": "WHITESPACE_COMMA_BEHAVIOR",
+ "cases": [
+ {
+ "case_id": "WS_COMMA_001",
+ "title": "After column + space suggests clause keywords only",
+ "sql": "SELECT id |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_COLUMN",
+ "context": "SELECT_LIST_AFTER_COLUMN",
+ "prefix": null,
+ "comment": "Whitespace after column = selection complete. Only clause keywords.",
+ "suggestions": [
+ "AS",
+ "FROM",
+ "GROUP BY",
+ "HAVING",
+ "LIMIT",
+ "ORDER BY",
+ "WHERE"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "WS_COMMA_002",
+ "title": "After column + comma suggests next-item (columns + functions)",
+ "sql": "SELECT id, |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "SELECT_LIST",
+ "prefix": null,
+ "comment": "Comma = next-item rules. No scope, so functions + clause keywords.",
+ "suggestions": [
+ "users.id",
+ "users.name",
+ "users.email",
+ "users.status",
+ "users.created_at",
+ "orders.id",
+ "orders.user_id",
+ "orders.total",
+ "orders.status",
+ "orders.created_at",
+ "products.id",
+ "products.name",
+ "products.price",
+ "products.unit_price",
+ "products.stock",
+ "customers.id",
+ "customers.name",
+ "customers.email",
+ "payments.id",
+ "payments.order_id",
+ "payments.amount",
+ "payments.method",
+ "payments.created_at",
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "UUID",
+ "YEAR",
+ "FROM",
+ "WHERE",
+ "LIMIT",
+ "ORDER BY",
+ "GROUP BY"
+ ]
+ }
+ },
+ {
+ "case_id": "WS_COMMA_003",
+ "title": "ORDER BY after column + space suggests ASC/DESC only",
+ "sql": "SELECT * FROM users ORDER BY created_at |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_COLUMN",
+ "context": "ORDER_BY_AFTER_COLUMN",
+ "prefix": null,
+ "comment": "Whitespace after column in ORDER BY = selection complete. Only sorting keywords.",
+ "suggestions": [
+ "ASC",
+ "DESC",
+ "NULLS FIRST",
+ "NULLS LAST"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "WS_COMMA_004",
+ "title": "ORDER BY after comma suggests columns again",
+ "sql": "SELECT * FROM users ORDER BY created_at, |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "CONTEXT",
+ "context": "ORDER_BY",
+ "prefix": null,
+ "comment": "Comma = next-item rules. Scope columns + functions.",
+ "suggestions": [
+ "AVG",
+ "COALESCE",
+ "CONCAT",
+ "COUNT",
+ "DATE",
+ "GROUP_CONCAT",
+ "LENGTH",
+ "LOWER",
+ "MAX",
+ "MIN",
+ "MONTH",
+ "NOW",
+ "NULLIF",
+ "ROW_NUMBER",
+ "SUBSTR",
+ "SUM",
+ "TRIM",
+ "UNIX_TIMESTAMP",
+ "UPPER",
+ "users.created_at",
+ "users.email",
+ "users.id",
+ "users.name",
+ "users.status",
+ "UUID",
+ "YEAR"
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "WS_COMMA_005",
+ "title": "Comma never appears as suggestion",
+ "sql": "SELECT id|",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "PREFIX",
+ "context": "SELECT_LIST",
+ "prefix": "id",
+ "comment": "Comma never suggested. Only matching items.",
+ "suggestions_contains": [
+ "FROM"
+ ],
+ "suggestions_not_contains": [
+ ","
+ ],
+ "xfail": true
+ }
+ },
+ {
+ "case_id": "WS_COMMA_006",
+ "title": "WHERE after expression + space suggests logical keywords",
+ "sql": "SELECT * FROM users WHERE id = 1 |",
+ "dialect": "generic",
+ "current_table": null,
+ "schema_variant": "small",
+ "expected": {
+ "mode": "AFTER_EXPRESSION",
+ "context": "WHERE_AFTER_EXPRESSION",
+ "prefix": null,
+ "comment": "Whitespace after expression = selection complete. Logical operators + clause keywords.",
+ "suggestions": [
+ "AND",
+ "BETWEEN",
+ "EXISTS",
+ "GROUP BY",
+ "HAVING",
+ "IN",
+ "IS NOT NULL",
+ "IS NULL",
+ "LIKE",
+ "LIMIT",
+ "NOT",
+ "OR",
+ "ORDER BY"
+ ],
+ "xfail": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/autocomplete/test_autocomplete_basic.py b/tests/autocomplete/test_autocomplete_basic.py
new file mode 100644
index 0000000..0d0e232
--- /dev/null
+++ b/tests/autocomplete/test_autocomplete_basic.py
@@ -0,0 +1,230 @@
+from typing import Optional
+from unittest.mock import Mock
+
+from windows.components.stc.autocomplete.auto_complete import SQLCompletionProvider
+from windows.components.stc.autocomplete.completion_types import CompletionItemType
+
+
+def create_mock_column(col_id: int, name: str, table):
+ column = Mock()
+ column.id = col_id
+ column.name = name
+ column.table = table
+ column.datatype = None
+ return column
+
+
+def create_mock_table(table_id: int, name: str, database, columns_data):
+ table = Mock()
+ table.id = table_id
+ table.name = name
+ table.database = database
+
+ columns = [create_mock_column(i, col_name, table) for i, col_name in enumerate(columns_data, 1)]
+ table.columns = columns
+
+ return table
+
+
+def create_mock_database():
+ context = Mock()
+ context.KEYWORDS = ["SELECT", "FROM", "WHERE", "INSERT", "UPDATE", "DELETE", "JOIN", "ORDER BY", "GROUP BY", "HAVING", "LIMIT", "ASC", "DESC"]
+ context.FUNCTIONS = ["COUNT", "SUM", "AVG", "MAX", "MIN", "UPPER", "LOWER", "CONCAT"]
+
+ database = Mock()
+ database.id = 1
+ database.name = "test_db"
+ database.context = context
+
+ users_table = create_mock_table(
+ 1, "users", database,
+ ["id", "name", "email", "created_at", "status"]
+ )
+
+ orders_table = create_mock_table(
+ 2, "orders", database,
+ ["id", "user_id", "total", "status", "created_at"]
+ )
+
+ database.tables = [users_table, orders_table]
+
+ return database
+
+
+def test_empty_context():
+ database = create_mock_database()
+ provider = SQLCompletionProvider(
+ get_database=lambda: database,
+ get_current_table=lambda: None
+ )
+
+ result = provider.get(text="", pos=0)
+
+ assert result is not None
+ assert len(result.items) > 0
+
+ item_names = [item.name for item in result.items]
+ assert "SELECT" in item_names
+ 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()
+ provider = SQLCompletionProvider(
+ get_database=lambda: database,
+ get_current_table=lambda: None
+ )
+
+ result = provider.get(text="SEL", pos=3)
+
+ assert result is not None
+ assert len(result.items) > 0
+ assert result.prefix == "SEL"
+
+ 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()
+ provider = SQLCompletionProvider(
+ get_database=lambda: database,
+ get_current_table=lambda: None
+ )
+
+ result = provider.get(text="SELECT ", pos=7)
+
+ assert result is not None
+ assert len(result.items) > 0
+
+ item_names = [item.name for item in result.items]
+ assert "COUNT" in item_names
+ assert "SUM" in item_names
+ assert "FROM" in item_names
+
+ print("โ GT-020 SELECT without FROM test passed")
+
+
+def test_select_with_from():
+ database = create_mock_database()
+ provider = SQLCompletionProvider(
+ get_database=lambda: database,
+ get_current_table=lambda: None
+ )
+
+ result = provider.get(text="SELECT FROM users", pos=7)
+
+ assert result is not None
+ assert len(result.items) > 0
+
+ item_names = [item.name for item in result.items]
+
+ has_users_columns = any("users." in name for name in item_names)
+ assert has_users_columns
+ assert "COUNT" in item_names
+
+ print("โ GT-021 SELECT with FROM test passed")
+
+
+def test_where_basic():
+ database = create_mock_database()
+ provider = SQLCompletionProvider(
+ get_database=lambda: database,
+ get_current_table=lambda: None
+ )
+
+ result = provider.get(text="SELECT * FROM users WHERE ", pos=27)
+
+ assert result is not None
+ assert len(result.items) > 0
+
+ item_names = [item.name for item in result.items]
+
+ has_users_columns = any("users." in name for name in item_names)
+ assert has_users_columns
+ assert "COUNT" in item_names
+
+ print("โ GT-030 WHERE basic test passed")
+
+
+def test_from_clause():
+ database = create_mock_database()
+ provider = SQLCompletionProvider(
+ get_database=lambda: database,
+ get_current_table=lambda: None
+ )
+
+ result = provider.get(text="SELECT * FROM ", pos=14)
+
+ assert result is not None
+ assert len(result.items) > 0
+
+ item_names = [item.name for item in result.items]
+ assert "users" in item_names
+ assert "orders" in item_names
+
+ print("โ FROM clause test passed")
+
+
+def test_dot_completion():
+ database = create_mock_database()
+ provider = SQLCompletionProvider(
+ get_database=lambda: database,
+ get_current_table=lambda: None
+ )
+
+ result = provider.get(text="SELECT users.", pos=13)
+
+ assert result is not None
+ assert len(result.items) > 0
+
+ item_names = [item.name for item in result.items]
+ assert "id" in item_names
+ assert "name" in item_names
+ assert "email" in item_names
+
+ 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()
+ provider = SQLCompletionProvider(
+ get_database=lambda: database,
+ get_current_table=lambda: None
+ )
+
+ text = "SELECT * FROM users;\nSELECT * FROM orders WHERE "
+ pos = len(text)
+
+ result = provider.get(text=text, pos=pos)
+
+ assert result is not None
+ assert len(result.items) > 0
+
+ item_names = [item.name for item in result.items]
+
+ has_orders_columns = any("orders." in name for name in item_names)
+ assert has_orders_columns
+
+ print("โ GT-001 Multi-query test passed")
+
+
+if __name__ == "__main__":
+ test_empty_context()
+ test_single_token()
+ test_select_without_from()
+ test_select_with_from()
+ test_where_basic()
+ test_from_clause()
+ test_dot_completion()
+ test_multi_query()
+
+ print("\nโ
All basic autocomplete tests passed!")
diff --git a/tests/autocomplete/test_config.json b/tests/autocomplete/test_config.json
new file mode 100644
index 0000000..483430d
--- /dev/null
+++ b/tests/autocomplete/test_config.json
@@ -0,0 +1,81 @@
+{
+ "vocab": {
+ "primary_keywords": [
+ "SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER",
+ "TRUNCATE", "SHOW", "DESCRIBE", "EXPLAIN", "WITH", "REPLACE", "MERGE"
+ ],
+ "keywords_all": [
+ "ALTER", "AND", "AS", "ASC", "BETWEEN", "CREATE", "CROSS JOIN",
+ "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DELETE", "DESC",
+ "DESCRIBE", "DROP", "EXISTS", "EXPLAIN", "FALSE", "FROM", "FULL JOIN",
+ "GROUP BY", "HAVING", "IN", "INNER JOIN", "INSERT", "IS NOT NULL",
+ "IS NULL", "JOIN", "LEFT JOIN", "LIKE", "LIMIT", "MERGE", "NOT", "NULL",
+ "NULLS FIRST", "NULLS LAST", "OFFSET", "ON", "OR", "ORDER BY", "REPLACE",
+ "RIGHT JOIN", "SELECT", "SHOW", "TRUE", "TRUNCATE", "UPDATE", "USING",
+ "WHERE", "WITH"
+ ],
+ "functions_all": [
+ "AVG", "COALESCE", "CONCAT", "COUNT", "DATE", "GROUP_CONCAT", "LENGTH",
+ "LOWER", "MAX", "MIN", "MONTH", "NOW", "NULLIF", "ROW_NUMBER", "SUBSTR",
+ "SUM", "TRIM", "UNIX_TIMESTAMP", "UPPER", "UUID", "YEAR"
+ ],
+ "aggregate_functions": [
+ "AVG", "COUNT", "GROUP_CONCAT", "MAX", "MIN", "SUM"
+ ]
+ },
+ "schema_small": {
+ "tables": [
+ {
+ "name": "users",
+ "columns": [
+ {"name": "id", "type": "INTEGER"},
+ {"name": "name", "type": "VARCHAR"},
+ {"name": "email", "type": "VARCHAR"},
+ {"name": "status", "type": "VARCHAR"},
+ {"name": "created_at", "type": "TIMESTAMP"}
+ ]
+ },
+ {
+ "name": "orders",
+ "columns": [
+ {"name": "id", "type": "INTEGER"},
+ {"name": "user_id", "type": "INTEGER"},
+ {"name": "total", "type": "DECIMAL"},
+ {"name": "status", "type": "VARCHAR"},
+ {"name": "created_at", "type": "TIMESTAMP"}
+ ]
+ },
+ {
+ "name": "products",
+ "columns": [
+ {"name": "id", "type": "INTEGER"},
+ {"name": "name", "type": "VARCHAR"},
+ {"name": "price", "type": "DECIMAL"},
+ {"name": "unit_price", "type": "DECIMAL"},
+ {"name": "stock", "type": "INTEGER"}
+ ]
+ },
+ {
+ "name": "customers",
+ "columns": [
+ {"name": "id", "type": "INTEGER"},
+ {"name": "name", "type": "VARCHAR"},
+ {"name": "email", "type": "VARCHAR"}
+ ]
+ },
+ {
+ "name": "payments",
+ "columns": [
+ {"name": "id", "type": "INTEGER"},
+ {"name": "order_id", "type": "INTEGER"},
+ {"name": "amount", "type": "DECIMAL"},
+ {"name": "method", "type": "VARCHAR"},
+ {"name": "created_at", "type": "TIMESTAMP"}
+ ]
+ }
+ ]
+ },
+ "schema_big": {
+ "tables": []
+ }
+}
diff --git a/tests/autocomplete/test_golden_cases.py b/tests/autocomplete/test_golden_cases.py
new file mode 100644
index 0000000..c25322c
--- /dev/null
+++ b/tests/autocomplete/test_golden_cases.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Any, Dict, Iterable, Tuple
+
+import pytest
+
+from tests.autocomplete.autocomplete_adapter import AutocompleteRequest, get_suggestions
+
+
+ROOT = Path(__file__).resolve().parent
+CASES_DIR = ROOT / "cases"
+CONFIG_PATH = ROOT / "test_config.json"
+
+
+def _load_json(path: Path) -> Dict[str, Any]:
+ return json.loads(path.read_text(encoding="utf-8"))
+
+
+def _iter_cases() -> Iterable[Tuple[str, Dict[str, Any]]]:
+ for path in sorted(CASES_DIR.glob("*.json")):
+ payload = _load_json(path)
+ for case in payload["cases"]:
+ yield (path.name, case)
+
+
+def _schema_for_variant(config: Dict[str, Any], schema_variant: str) -> Dict[str, Any]:
+ if schema_variant == "small":
+ return config["schema_small"]
+ if schema_variant == "big":
+ return config["schema_big"]
+ raise ValueError(f"Unknown schema_variant: {schema_variant}")
+
+
+@pytest.mark.parametrize("file_name,case", list(_iter_cases()))
+def test_golden_case(file_name: str, case: Dict[str, Any]) -> None:
+ config = _load_json(CONFIG_PATH)
+ expected = case["expected"]
+
+ if bool(expected.get("xfail", False)):
+ pytest.xfail("Marked as future enhancement")
+
+ schema = _schema_for_variant(config, case.get("schema_variant", "small"))
+
+ request = AutocompleteRequest(
+ sql=case["sql"],
+ dialect=case.get("dialect", "generic"),
+ current_table=case.get("current_table"),
+ schema=schema,
+ )
+
+ response = get_suggestions(request)
+
+ assert response.mode == expected["mode"], (file_name, case["case_id"])
+ assert response.context == expected["context"], (file_name, case["case_id"])
+ assert response.prefix == expected.get("prefix"), (file_name, case["case_id"])
+
+ if "suggestions" in expected:
+ assert response.suggestions == expected["suggestions"], (file_name, case["case_id"])
+ elif "suggestions_contains" in expected:
+ for needle in expected["suggestions_contains"]:
+ assert needle in response.suggestions, (file_name, case["case_id"], needle)
+ else:
+ raise AssertionError("Case must define 'suggestions' or 'suggestions_contains'")
diff --git a/tests/engines/mariadb/conftest.py b/tests/engines/mariadb/conftest.py
index a733528..41cdb3a 100644
--- a/tests/engines/mariadb/conftest.py
+++ b/tests/engines/mariadb/conftest.py
@@ -22,12 +22,32 @@ def pytest_generate_tests(metafunc):
@pytest.fixture(scope="module")
def mariadb_container(mariadb_version):
- with MySqlContainer(mariadb_version, name=f"petersql_test_{mariadb_version.replace(":", "_")}",
+ container = MySqlContainer(mariadb_version, name=f"petersql_test_{mariadb_version.replace(":", "_")}",
mem_limit="768m",
memswap_limit="1g",
nano_cpus=1_000_000_000,
shm_size="256m",
- ) as container:
+ )
+ # Expose SSH port
+ container.with_exposed_ports(22)
+
+ with container:
+ # Install and configure SSH in the container
+ install_ssh_commands = [
+ "apt-get update",
+ "apt-get install -y openssh-server",
+ "mkdir -p /var/run/sshd",
+ "echo 'root:testpassword' | chpasswd",
+ "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
+ "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
+ "/usr/sbin/sshd",
+ ]
+
+ for cmd in install_ssh_commands:
+ exit_code, output = container.exec(cmd)
+ if exit_code != 0 and "sshd" not in cmd:
+ raise RuntimeError(f"Failed to execute: {cmd}\nOutput: {output}")
+
yield container
diff --git a/tests/engines/mariadb/test_context.py b/tests/engines/mariadb/test_context.py
index 1fc1ac6..f74812b 100644
--- a/tests/engines/mariadb/test_context.py
+++ b/tests/engines/mariadb/test_context.py
@@ -1,6 +1,7 @@
import pytest
+@pytest.mark.integration
class TestMariaDBContext:
"""Tests for MariaDB context methods."""
diff --git a/tests/engines/mariadb/test_integration.py b/tests/engines/mariadb/test_integration.py
index 1872795..f5c480f 100644
--- a/tests/engines/mariadb/test_integration.py
+++ b/tests/engines/mariadb/test_integration.py
@@ -56,57 +56,71 @@ def create_users_table(mariadb_database, mariadb_session) -> MariaDBTable:
@pytest.fixture(scope="function")
def ssh_mariadb_session(mariadb_container, mariadb_session):
"""Create SSH tunnel session for testing."""
+ # Verify SSH is accessible on the container
+ import socket
+ ssh_host = mariadb_container.get_container_host_ip()
+ ssh_port = mariadb_container.get_exposed_port(22)
+
try:
- # Create SSH tunnel to MariaDB container
- tunnel = SSHTunnel(
- mariadb_container.get_container_host_ip(),
- 22, # Assuming SSH access to host
- ssh_username=None,
- ssh_password=None,
- remote_port=mariadb_container.get_exposed_port(3306),
- local_bind_address=('localhost', 0)
- )
-
- tunnel.start(timeout=5)
-
- # Create connection using tunnel
- from structures.session import Session
- from structures.connection import Connection, ConnectionEngine
- from structures.configurations import CredentialsConfiguration
-
- config = CredentialsConfiguration(
- hostname="localhost",
- username="root",
- password=mariadb_container.root_password,
- port=tunnel.local_port,
- )
-
- connection = Connection(
- id=1,
- name="ssh_mariadb_test",
- engine=ConnectionEngine.MARIADB,
- configuration=config,
- )
-
- session = Session(connection=connection)
- session.connect()
-
- yield session, tunnel
-
- except Exception:
- pytest.skip("SSH tunnel not available")
-
- finally:
- try:
- session.disconnect()
- except:
- pass
- try:
- tunnel.stop()
- except:
- pass
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(2)
+ result = sock.connect_ex((ssh_host, ssh_port))
+ sock.close()
+ if result != 0:
+ pytest.skip(f"SSH port {ssh_port} not accessible on container")
+ except Exception as e:
+ pytest.skip(f"Cannot verify SSH accessibility: {e}")
+
+ # Create SSH tunnel to MariaDB container
+ tunnel = SSHTunnel(
+ ssh_host,
+ ssh_port,
+ ssh_username="root",
+ ssh_password="testpassword",
+ remote_port=3306, # MariaDB inside container
+ local_bind_address=('localhost', 0)
+ )
+
+ try:
+ tunnel.start(timeout=10)
+ except Exception as e:
+ pytest.skip(f"SSH tunnel failed to start: {e}")
+
+ # Create connection using tunnel
+ from structures.session import Session
+ from structures.connection import Connection, ConnectionEngine
+ from structures.configurations import CredentialsConfiguration
+
+ config = CredentialsConfiguration(
+ hostname="localhost",
+ username="root",
+ password=mariadb_container.root_password,
+ port=tunnel.local_port,
+ )
+
+ connection = Connection(
+ id=1,
+ name="ssh_mariadb_test",
+ engine=ConnectionEngine.MARIADB,
+ configuration=config,
+ )
+
+ session = Session(connection=connection)
+ session.connect()
+
+ yield session, tunnel
+
+ try:
+ session.disconnect()
+ except:
+ pass
+ try:
+ tunnel.stop()
+ except:
+ pass
+@pytest.mark.integration
class TestMariaDBIntegration:
"""Integration tests for MariaDB engine using build_empty_* API."""
diff --git a/tests/engines/mysql/conftest.py b/tests/engines/mysql/conftest.py
index d73171e..77b8f2e 100644
--- a/tests/engines/mysql/conftest.py
+++ b/tests/engines/mysql/conftest.py
@@ -22,12 +22,32 @@ def pytest_generate_tests(metafunc):
@pytest.fixture(scope="module")
def mysql_container(mysql_version):
- with MySqlContainer(mysql_version, name=f"petersql_test_{mysql_version.replace(':', '_')}",
+ container = MySqlContainer(mysql_version, name=f"petersql_test_{mysql_version.replace(':', '_')}",
mem_limit="768m",
memswap_limit="1g",
nano_cpus=1_000_000_000,
shm_size="256m",
- ) as container:
+ )
+ # Expose SSH port
+ container.with_exposed_ports(22)
+
+ with container:
+ # Install and configure SSH in the container
+ install_ssh_commands = [
+ "apt-get update",
+ "apt-get install -y openssh-server",
+ "mkdir -p /var/run/sshd",
+ "echo 'root:testpassword' | chpasswd",
+ "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
+ "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
+ "/usr/sbin/sshd",
+ ]
+
+ for cmd in install_ssh_commands:
+ exit_code, output = container.exec(cmd)
+ if exit_code != 0 and "sshd" not in cmd:
+ raise RuntimeError(f"Failed to execute: {cmd}\nOutput: {output}")
+
yield container
diff --git a/tests/engines/mysql/test_context.py b/tests/engines/mysql/test_context.py
index e1d7c05..babc65c 100644
--- a/tests/engines/mysql/test_context.py
+++ b/tests/engines/mysql/test_context.py
@@ -1,6 +1,7 @@
import pytest
+@pytest.mark.integration
class TestMySQLContext:
"""Tests for MySQL context methods."""
diff --git a/tests/engines/mysql/test_integration.py b/tests/engines/mysql/test_integration.py
index 3cd2980..d101c11 100644
--- a/tests/engines/mysql/test_integration.py
+++ b/tests/engines/mysql/test_integration.py
@@ -55,57 +55,71 @@ def create_users_table(mysql_database, mysql_session) -> MySQLTable:
@pytest.fixture(scope="function")
def ssh_mysql_session(mysql_container, mysql_session):
"""Create SSH tunnel session for testing."""
+ # Verify SSH is accessible on the container
+ import socket
+ ssh_host = mysql_container.get_container_host_ip()
+ ssh_port = mysql_container.get_exposed_port(22)
+
try:
- # Create SSH tunnel to MySQL container
- tunnel = SSHTunnel(
- mysql_container.get_container_host_ip(),
- 22, # Assuming SSH access to host
- ssh_username=None,
- ssh_password=None,
- remote_port=mysql_container.get_exposed_port(3306),
- local_bind_address=('localhost', 0)
- )
-
- tunnel.start(timeout=5)
-
- # Create connection using tunnel
- from structures.session import Session
- from structures.connection import Connection, ConnectionEngine
- from structures.configurations import CredentialsConfiguration
-
- config = CredentialsConfiguration(
- hostname="localhost",
- username="root",
- password=mysql_container.root_password,
- port=tunnel.local_port,
- )
-
- connection = Connection(
- id=1,
- name="ssh_mysql_test",
- engine=ConnectionEngine.MYSQL,
- configuration=config,
- )
-
- session = Session(connection=connection)
- session.connect()
-
- yield session, tunnel
-
- except Exception:
- pytest.skip("SSH tunnel not available")
-
- finally:
- try:
- session.disconnect()
- except:
- pass
- try:
- tunnel.stop()
- except:
- pass
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(2)
+ result = sock.connect_ex((ssh_host, ssh_port))
+ sock.close()
+ if result != 0:
+ pytest.skip(f"SSH port {ssh_port} not accessible on container")
+ except Exception as e:
+ pytest.skip(f"Cannot verify SSH accessibility: {e}")
+
+ # Create SSH tunnel to MySQL container
+ tunnel = SSHTunnel(
+ ssh_host,
+ ssh_port,
+ ssh_username="root",
+ ssh_password="testpassword",
+ remote_port=3306, # MySQL inside container
+ local_bind_address=('localhost', 0)
+ )
+
+ try:
+ tunnel.start(timeout=10)
+ except Exception as e:
+ pytest.skip(f"SSH tunnel failed to start: {e}")
+
+ # Create connection using tunnel
+ from structures.session import Session
+ from structures.connection import Connection, ConnectionEngine
+ from structures.configurations import CredentialsConfiguration
+
+ config = CredentialsConfiguration(
+ hostname="localhost",
+ username="root",
+ password=mysql_container.root_password,
+ port=tunnel.local_port,
+ )
+
+ connection = Connection(
+ id=1,
+ name="ssh_mysql_test",
+ engine=ConnectionEngine.MYSQL,
+ configuration=config,
+ )
+
+ session = Session(connection=connection)
+ session.connect()
+
+ yield session, tunnel
+
+ try:
+ session.disconnect()
+ except:
+ pass
+ try:
+ tunnel.stop()
+ except:
+ pass
+@pytest.mark.integration
class TestMySQLIntegration:
"""Integration tests for MySQL engine using build_empty_* API."""
diff --git a/tests/engines/postgresql/conftest.py b/tests/engines/postgresql/conftest.py
index c4ba837..9a7ea89 100644
--- a/tests/engines/postgresql/conftest.py
+++ b/tests/engines/postgresql/conftest.py
@@ -21,14 +21,43 @@ def pytest_generate_tests(metafunc):
@pytest.fixture(scope="module")
def postgresql_container(postgresql_version):
- with PostgresContainer(
+ container = PostgresContainer(
postgresql_version,
name=f"petersql_test_{postgresql_version.replace(':', '_')}",
mem_limit="512m",
memswap_limit="768m",
nano_cpus=1_000_000_000,
shm_size="128m",
- ) as container:
+ )
+ # Expose SSH port
+ container.with_exposed_ports(22)
+
+ with container:
+ # Install and configure SSH in the container
+ import logging
+ logger = logging.getLogger(__name__)
+
+ install_ssh_commands = [
+ ["sh", "-c", "apt-get update"],
+ ["sh", "-c", "DEBIAN_FRONTEND=noninteractive apt-get install -y openssh-server"],
+ ["sh", "-c", "mkdir -p /var/run/sshd"],
+ ["sh", "-c", "echo 'root:testpassword' | chpasswd"],
+ ["sh", "-c", "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config"],
+ ["sh", "-c", "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config"],
+ ["sh", "-c", "nohup /usr/sbin/sshd -D > /dev/null 2>&1 &"],
+ ]
+
+ for cmd in install_ssh_commands:
+ logger.info(f"Executing: {cmd}")
+ exit_code, output = container.exec(cmd)
+ logger.info(f"Exit code: {exit_code}, Output: {output}")
+ if exit_code != 0 and "sshd" not in cmd:
+ raise RuntimeError(f"Failed to execute: {cmd}\nExit code: {exit_code}\nOutput: {output}")
+
+ # Verify SSH is running
+ exit_code, output = container.exec("ps aux | grep sshd")
+ logger.info(f"SSH processes: {output}")
+
yield container
diff --git a/tests/engines/postgresql/test_context.py b/tests/engines/postgresql/test_context.py
index 12e14e5..dea82dd 100644
--- a/tests/engines/postgresql/test_context.py
+++ b/tests/engines/postgresql/test_context.py
@@ -1,6 +1,7 @@
import pytest
+@pytest.mark.integration
class TestPostgreSQLContext:
"""Tests for PostgreSQL context - focus on reading database structures."""
diff --git a/tests/engines/postgresql/test_integration.py b/tests/engines/postgresql/test_integration.py
index 70bf5e0..c1fe720 100644
--- a/tests/engines/postgresql/test_integration.py
+++ b/tests/engines/postgresql/test_integration.py
@@ -52,57 +52,71 @@ def create_users_table(postgresql_database, postgresql_session) -> PostgreSQLTab
@pytest.fixture(scope="function")
def ssh_postgresql_session(postgresql_container, postgresql_session):
"""Create SSH tunnel session for testing."""
+ # Verify SSH is accessible on the container
+ import socket
+ ssh_host = postgresql_container.get_container_host_ip()
+ ssh_port = postgresql_container.get_exposed_port(22)
+
try:
- # Create SSH tunnel to PostgreSQL container
- tunnel = SSHTunnel(
- postgresql_container.get_container_host_ip(),
- 22, # Assuming SSH access to host
- ssh_username=None,
- ssh_password=None,
- remote_port=postgresql_container.get_exposed_port(5432),
- local_bind_address=('localhost', 0)
- )
-
- tunnel.start(timeout=5)
-
- # Create connection using tunnel
- from structures.session import Session
- from structures.connection import Connection, ConnectionEngine
- from structures.configurations import CredentialsConfiguration
-
- config = CredentialsConfiguration(
- hostname="localhost",
- username=postgresql_container.username,
- password=postgresql_container.password,
- port=tunnel.local_port,
- )
-
- connection = Connection(
- id=1,
- name="ssh_postgresql_test",
- engine=ConnectionEngine.POSTGRESQL,
- configuration=config,
- )
-
- session = Session(connection=connection)
- session.connect()
-
- yield session, tunnel
-
- except Exception:
- pytest.skip("SSH tunnel not available")
-
- finally:
- try:
- session.disconnect()
- except:
- pass
- try:
- tunnel.stop()
- except:
- pass
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(2)
+ result = sock.connect_ex((ssh_host, ssh_port))
+ sock.close()
+ if result != 0:
+ pytest.skip(f"SSH port {ssh_port} not accessible on container")
+ except Exception as e:
+ pytest.skip(f"Cannot verify SSH accessibility: {e}")
+
+ # Create SSH tunnel to PostgreSQL container
+ tunnel = SSHTunnel(
+ ssh_host,
+ ssh_port,
+ ssh_username="root",
+ ssh_password="testpassword",
+ remote_port=5432, # PostgreSQL inside container
+ local_bind_address=('localhost', 0)
+ )
+
+ try:
+ tunnel.start(timeout=10)
+ except Exception as e:
+ pytest.skip(f"SSH tunnel failed to start: {e}")
+
+ # Create connection using tunnel
+ from structures.session import Session
+ from structures.connection import Connection, ConnectionEngine
+ from structures.configurations import CredentialsConfiguration
+
+ config = CredentialsConfiguration(
+ hostname="localhost",
+ username=postgresql_container.username,
+ password=postgresql_container.password,
+ port=tunnel.local_port,
+ )
+
+ connection = Connection(
+ id=1,
+ name="ssh_postgresql_test",
+ engine=ConnectionEngine.POSTGRESQL,
+ configuration=config,
+ )
+
+ session = Session(connection=connection)
+ session.connect()
+
+ yield session, tunnel
+
+ try:
+ session.disconnect()
+ except:
+ pass
+ try:
+ tunnel.stop()
+ except:
+ pass
+@pytest.mark.integration
class TestPostgreSQLIntegration:
"""Integration tests for PostgreSQL engine using build_empty_* API."""
diff --git a/tests/engines/sqlite/test_integration.py b/tests/engines/sqlite/test_integration.py
index 9cd7246..eea366d 100644
--- a/tests/engines/sqlite/test_integration.py
+++ b/tests/engines/sqlite/test_integration.py
@@ -1,3 +1,4 @@
+import pytest
from structures.engines.sqlite.database import (
SQLiteTable,
SQLiteColumn,
@@ -55,6 +56,7 @@ def create_users_table(sqlite_database, sqlite_session) -> SQLiteTable:
return next(t for t in sqlite_database.tables.get_value() if t.name == "users")
+@pytest.mark.integration
class TestSQLiteIntegration:
"""Integration tests for SQLite engine."""
diff --git a/tests/test_column_controller.py b/tests/test_column_controller.py
index dba0509..bc475cf 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.column import TableColumnsController
+from windows.main.tabs.column import TableColumnsController
@pytest.fixture
@@ -37,9 +37,9 @@ def mock_table(mock_session):
@patch('wx.GetApp')
-@patch('windows.main.column.CURRENT_SESSION')
-@patch('windows.main.column.CURRENT_TABLE')
-@patch('windows.main.column.NEW_TABLE')
+@patch('windows.main.tabs.column.CURRENT_SESSION')
+@patch('windows.main.tabs.column.CURRENT_TABLE')
+@patch('windows.main.tabs.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.column.CURRENT_SESSION')
-@patch('windows.main.column.CURRENT_TABLE')
-@patch('windows.main.column.NEW_TABLE')
+@patch('windows.main.tabs.column.CURRENT_SESSION')
+@patch('windows.main.tabs.column.CURRENT_TABLE')
+@patch('windows.main.tabs.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.column.CURRENT_SESSION')
-@patch('windows.main.column.CURRENT_TABLE')
-@patch('windows.main.column.NEW_TABLE')
+@patch('windows.main.tabs.column.CURRENT_SESSION')
+@patch('windows.main.tabs.column.CURRENT_TABLE')
+@patch('windows.main.tabs.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.column.CURRENT_SESSION')
-@patch('windows.main.column.CURRENT_TABLE')
-@patch('windows.main.column.CURRENT_COLUMN')
-@patch('windows.main.column.NEW_TABLE')
+@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')
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.column.CURRENT_SESSION')
-@patch('windows.main.column.CURRENT_TABLE')
-@patch('windows.main.column.NEW_TABLE')
+@patch('windows.main.tabs.column.CURRENT_SESSION')
+@patch('windows.main.tabs.column.CURRENT_TABLE')
+@patch('windows.main.tabs.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/test_connections.py b/tests/test_connections.py
index cda20f9..62b5989 100644
--- a/tests/test_connections.py
+++ b/tests/test_connections.py
@@ -5,7 +5,7 @@
from structures.connection import Connection, ConnectionEngine, ConnectionDirectory
from structures.configurations import CredentialsConfiguration, SourceConfiguration, SSHTunnelConfiguration
-from windows.connections.repository import ConnectionsRepository
+from windows.dialogs.connections.repository import ConnectionsRepository
class TestConnectionsRepository:
diff --git a/tests/ui/test_column_controller.py b/tests/ui/test_column_controller.py
index 7913396..209f234 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.column import TableColumnsController
+from windows.main.tabs.column import TableColumnsController
@pytest.fixture
@@ -27,9 +27,9 @@ def mock_table(sqlite_session):
@patch('wx.GetApp')
-@patch('windows.main.column.CURRENT_SESSION')
-@patch('windows.main.column.CURRENT_TABLE')
-@patch('windows.main.column.NEW_TABLE')
+@patch('windows.main.tabs.column.CURRENT_SESSION')
+@patch('windows.main.tabs.column.CURRENT_TABLE')
+@patch('windows.main.tabs.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.column.CURRENT_SESSION')
-@patch('windows.main.column.CURRENT_TABLE')
-@patch('windows.main.column.NEW_TABLE')
+@patch('windows.main.tabs.column.CURRENT_SESSION')
+@patch('windows.main.tabs.column.CURRENT_TABLE')
+@patch('windows.main.tabs.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.column.CURRENT_SESSION')
-@patch('windows.main.column.CURRENT_TABLE')
-@patch('windows.main.column.NEW_TABLE')
+@patch('windows.main.tabs.column.CURRENT_SESSION')
+@patch('windows.main.tabs.column.CURRENT_TABLE')
+@patch('windows.main.tabs.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.column.CURRENT_SESSION')
-@patch('windows.main.column.CURRENT_TABLE')
-@patch('windows.main.column.CURRENT_COLUMN')
-@patch('windows.main.column.NEW_TABLE')
+@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')
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.column.CURRENT_SESSION')
-@patch('windows.main.column.CURRENT_TABLE')
-@patch('windows.main.column.NEW_TABLE')
+@patch('windows.main.tabs.column.CURRENT_SESSION')
+@patch('windows.main.tabs.column.CURRENT_TABLE')
+@patch('windows.main.tabs.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_connections.py b/tests/ui/test_connections.py
index 58c6661..6ae6f5b 100644
--- a/tests/ui/test_connections.py
+++ b/tests/ui/test_connections.py
@@ -3,8 +3,8 @@
from structures.connection import Connection, ConnectionEngine
from structures.configurations import CredentialsConfiguration, SourceConfiguration
-from windows.connections.model import ConnectionModel
-from windows.connections import CURRENT_CONNECTION, PENDING_CONNECTION
+from windows.dialogs.connections.model import ConnectionModel
+from windows.dialogs.connections import CURRENT_CONNECTION, PENDING_CONNECTION
class TestConnectionModel:
diff --git a/tests/ui/test_index_controller.py b/tests/ui/test_index_controller.py
index 57463cf..d241349 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.index import TableIndexController
+from windows.main.tabs.index import TableIndexController
@pytest.fixture
@@ -24,9 +24,9 @@ def mock_table(sqlite_session):
@patch('wx.GetApp')
-@patch('windows.main.index.CURRENT_TABLE')
-@patch('windows.main.index.CURRENT_INDEX')
-@patch('windows.main.index.NEW_TABLE')
+@patch('windows.main.tabs.index.CURRENT_TABLE')
+@patch('windows.main.tabs.index.CURRENT_INDEX')
+@patch('windows.main.tabs.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.index.CURRENT_TABLE')
-@patch('windows.main.index.CURRENT_INDEX')
-@patch('windows.main.index.NEW_TABLE')
+@patch('windows.main.tabs.index.CURRENT_TABLE')
+@patch('windows.main.tabs.index.CURRENT_INDEX')
+@patch('windows.main.tabs.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/tests/ui/test_repository.py b/tests/ui/test_repository.py
index 8049e59..02e5381 100644
--- a/tests/ui/test_repository.py
+++ b/tests/ui/test_repository.py
@@ -5,8 +5,8 @@
from structures.connection import Connection, ConnectionEngine
from structures.configurations import CredentialsConfiguration, SourceConfiguration
-from windows.connections import ConnectionDirectory
-from windows.connections.repository import ConnectionsRepository
+from windows.dialogs.connections import ConnectionDirectory
+from windows.dialogs.connections.repository import ConnectionsRepository
class TestConnectionsRepository:
diff --git a/themes/README.md b/themes/README.md
new file mode 100644
index 0000000..0b4fad3
--- /dev/null
+++ b/themes/README.md
@@ -0,0 +1,84 @@
+# PeterSQL Themes
+
+This directory contains theme files for PeterSQL. Each theme defines colors for the editor and autocomplete components, with support for both dark and light modes.
+
+## Theme Structure
+
+Each theme is a YAML file with the following structure:
+
+```yaml
+name: Theme Name
+version: 1.0
+
+editor:
+ dark:
+ # Colors for dark mode
+ background: auto # 'auto' uses system color
+ foreground: auto
+ keyword: '#569cd6'
+ string: '#ce9178'
+ # ... more colors
+
+ light:
+ # Colors for light mode
+ background: auto
+ foreground: auto
+ keyword: '#0000ff'
+ # ... more colors
+
+autocomplete:
+ dark:
+ keyword: '#569cd6'
+ function: '#dcdcaa'
+ table: '#4ec9b0'
+ column: '#9cdcfe'
+
+ light:
+ keyword: '#0000ff'
+ function: '#800080'
+ table: '#008000'
+ column: '#000000'
+```
+
+## Available Colors
+
+### Editor Colors
+- `background` - Editor background
+- `foreground` - Default text color
+- `line_number_background` - Line number margin background
+- `line_number_foreground` - Line number text color
+- `keyword` - SQL keywords (SELECT, FROM, etc.)
+- `string` - String literals
+- `comment` - Comments
+- `number` - Numeric literals
+- `operator` - Operators (+, -, *, etc.)
+- `property` - JSON properties
+- `error` - Error highlighting
+- `uri` - URI/URL highlighting
+- `reference` - Reference highlighting
+- `document` - Document markers
+
+### Autocomplete Colors
+- `keyword` - SQL keywords
+- `function` - SQL functions
+- `table` - Table names
+- `column` - Column names
+
+## Using 'auto' Color
+
+Set a color to `auto` to use the system color. This is useful for background and foreground colors to ensure the editor adapts to the system theme.
+
+## Creating a New Theme
+
+1. Create a new YAML file in this directory (e.g., `mytheme.yml`)
+2. Copy the structure from `petersql.yml`
+3. Customize the colors
+4. Update `settings.yml` to use your theme:
+ ```yaml
+ theme:
+ current: mytheme
+ ```
+
+## Default Theme
+
+The default theme is `petersql.yml`, which provides VS Code-like colors for both dark and light modes.
diff --git a/windows/__init__.py b/windows/__init__.py
old mode 100755
new mode 100644
index fed60d3..3defec6
--- a/windows/__init__.py
+++ b/windows/__init__.py
@@ -665,7 +665,7 @@ def on_syntax_changed( self, event ):
class MainFrameView ( wx.Frame ):
def __init__( self, parent ):
- wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"PeterSQL"), pos = wx.DefaultPosition, size = wx.Size( 1024,762 ), style = wx.DEFAULT_FRAME_STYLE|wx.MAXIMIZE_BOX|wx.TAB_TRAVERSAL )
+ wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"PeterSQL"), pos = wx.DefaultPosition, size = wx.Size( 1280,1024 ), style = wx.DEFAULT_FRAME_STYLE|wx.MAXIMIZE_BOX|wx.TAB_TRAVERSAL )
self.SetSizeHints( wx.Size( 800,600 ), wx.DefaultSize )
@@ -1258,41 +1258,128 @@ def __init__( self, parent ):
self.m_panel34 = wx.Panel( self.m_notebook7, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
bSizer85 = wx.BoxSizer( wx.VERTICAL )
- bSizer86 = wx.BoxSizer( wx.VERTICAL )
-
- bSizer89 = wx.BoxSizer( wx.HORIZONTAL )
-
bSizer87 = wx.BoxSizer( wx.HORIZONTAL )
self.m_staticText40 = wx.StaticText( self.m_panel34, wx.ID_ANY, _(u"Name"), wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_staticText40.Wrap( -1 )
+ self.m_staticText40.SetMinSize( wx.Size( 150,-1 ) )
+
bSizer87.Add( self.m_staticText40, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
- self.m_textCtrl22 = wx.TextCtrl( self.m_panel34, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
- bSizer87.Add( self.m_textCtrl22, 1, wx.ALL|wx.EXPAND, 5 )
+ self.txt_view_name = wx.TextCtrl( self.m_panel34, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer87.Add( self.txt_view_name, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
- bSizer89.Add( bSizer87, 1, wx.EXPAND, 5 )
+ bSizer85.Add( bSizer87, 0, wx.ALL|wx.EXPAND, 5 )
- bSizer871 = wx.BoxSizer( wx.HORIZONTAL )
+ bSizer89 = wx.BoxSizer( wx.HORIZONTAL )
- self.m_staticText401 = wx.StaticText( self.m_panel34, wx.ID_ANY, _(u"Temporary"), wx.DefaultPosition, wx.DefaultSize, 0 )
- self.m_staticText401.Wrap( -1 )
+ bSizer116 = wx.BoxSizer( wx.VERTICAL )
- bSizer871.Add( self.m_staticText401, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+ bSizer87211 = wx.BoxSizer( wx.HORIZONTAL )
- self.m_checkBox5 = wx.CheckBox( self.m_panel34, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
- bSizer871.Add( self.m_checkBox5, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+ self.m_staticText40211 = wx.StaticText( self.m_panel34, wx.ID_ANY, _(u"Schema"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_staticText40211.Wrap( -1 )
+
+ self.m_staticText40211.SetMinSize( wx.Size( 150,-1 ) )
+
+ bSizer87211.Add( self.m_staticText40211, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ cho_view_schemaChoices = []
+ self.cho_view_schema = wx.Choice( self.m_panel34, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, cho_view_schemaChoices, 0 )
+ self.cho_view_schema.SetSelection( 0 )
+ bSizer87211.Add( self.cho_view_schema, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+
+ bSizer116.Add( bSizer87211, 0, wx.ALL|wx.EXPAND, 5 )
+
+ bSizer872 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText402 = wx.StaticText( self.m_panel34, wx.ID_ANY, _(u"Definer"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_staticText402.Wrap( -1 )
+
+ self.m_staticText402.SetMinSize( wx.Size( 150,-1 ) )
+
+ bSizer872.Add( self.m_staticText402, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ cmb_view_definerChoices = []
+ self.cmb_view_definer = wx.ComboBox( self.m_panel34, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, cmb_view_definerChoices, 0 )
+ bSizer872.Add( self.cmb_view_definer, 1, wx.ALIGN_CENTER|wx.ALL|wx.EXPAND, 5 )
+
+
+ bSizer116.Add( bSizer872, 0, wx.ALL|wx.EXPAND, 5 )
+
+
+ bSizer89.Add( bSizer116, 1, wx.EXPAND, 5 )
+
+ bSizer8711 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer8721 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText4021 = wx.StaticText( self.m_panel34, wx.ID_ANY, _(u"SQL security"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_staticText4021.Wrap( -1 )
+
+ self.m_staticText4021.SetMinSize( wx.Size( 150,-1 ) )
+
+ bSizer8721.Add( self.m_staticText4021, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ cho_view_sql_securityChoices = [ _(u"DEFINER"), _(u"INVOKER") ]
+ self.cho_view_sql_security = wx.Choice( self.m_panel34, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, cho_view_sql_securityChoices, 0 )
+ self.cho_view_sql_security.SetSelection( 0 )
+ bSizer8721.Add( self.cho_view_sql_security, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
- bSizer89.Add( bSizer871, 1, wx.EXPAND, 5 )
+ bSizer8711.Add( bSizer8721, 0, wx.ALL|wx.EXPAND, 5 )
+ sbSizer1 = wx.StaticBoxSizer( wx.VERTICAL, self.m_panel34, _(u"Algorithm") )
- bSizer86.Add( bSizer89, 0, wx.EXPAND, 5 )
+ self.rad_view_algorithm_undefined = wx.RadioButton( sbSizer1.GetStaticBox(), wx.ID_ANY, _(u"UNDEFINED"), wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP )
+ sbSizer1.Add( self.rad_view_algorithm_undefined, 0, wx.ALL, 5 )
+ self.rad_view_algorithm_merge = wx.RadioButton( sbSizer1.GetStaticBox(), wx.ID_ANY, _(u"MERGE"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer1.Add( self.rad_view_algorithm_merge, 0, wx.ALL, 5 )
- bSizer85.Add( bSizer86, 1, wx.EXPAND, 5 )
+ self.rad_view_algorithm_temptable = wx.RadioButton( sbSizer1.GetStaticBox(), wx.ID_ANY, _(u"TEMPTABLE"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer1.Add( self.rad_view_algorithm_temptable, 0, wx.ALL, 5 )
+
+ self.m_radioBtn10 = wx.RadioButton( sbSizer1.GetStaticBox(), wx.ID_ANY, _(u"RadioBtn"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer1.Add( self.m_radioBtn10, 0, wx.ALL, 5 )
+
+
+ bSizer8711.Add( sbSizer1, 0, wx.EXPAND, 5 )
+
+ sbSizer11 = wx.StaticBoxSizer( wx.VERTICAL, self.m_panel34, _(u"View constraint") )
+
+ self.rad_view_constraint_none = wx.RadioButton( sbSizer11.GetStaticBox(), wx.ID_ANY, _(u"None"), wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP )
+ sbSizer11.Add( self.rad_view_constraint_none, 0, wx.ALL, 5 )
+
+ self.rad_view_constraint_local = wx.RadioButton( sbSizer11.GetStaticBox(), wx.ID_ANY, _(u"LOCAL"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer11.Add( self.rad_view_constraint_local, 0, wx.ALL, 5 )
+
+ self.rad_view_constraint_cascaded = wx.RadioButton( sbSizer11.GetStaticBox(), wx.ID_ANY, _(u"CASCADE"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer11.Add( self.rad_view_constraint_cascaded, 0, wx.ALL, 5 )
+
+ self.rad_view_constraint_check_only = wx.RadioButton( sbSizer11.GetStaticBox(), wx.ID_ANY, _(u"CHECK ONLY"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer11.Add( self.rad_view_constraint_check_only, 0, wx.ALL, 5 )
+
+ self.rad_view_constraint_read_only = wx.RadioButton( sbSizer11.GetStaticBox(), wx.ID_ANY, _(u"READ ONLY"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer11.Add( self.rad_view_constraint_read_only, 0, wx.ALL, 5 )
+
+
+ bSizer8711.Add( sbSizer11, 0, wx.EXPAND, 5 )
+
+ self.chk_view_security_barrier = wx.CheckBox( self.m_panel34, wx.ID_ANY, _(u"Security barrier"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer8711.Add( self.chk_view_security_barrier, 0, wx.ALL, 5 )
+
+ self.chk_view_force = wx.CheckBox( self.m_panel34, wx.ID_ANY, _(u"Force"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer8711.Add( self.chk_view_force, 0, wx.ALL, 5 )
+
+
+ bSizer89.Add( bSizer8711, 1, wx.EXPAND, 5 )
+
+
+ bSizer85.Add( bSizer89, 0, wx.EXPAND, 5 )
self.m_panel34.SetSizer( bSizer85 )
@@ -1302,38 +1389,38 @@ def __init__( self, parent ):
bSizer84.Add( self.m_notebook7, 1, wx.EXPAND | wx.ALL, 5 )
- self.sql_view = wx.stc.StyledTextCtrl( self.panel_views, wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,200 ), 0)
- self.sql_view.SetUseTabs ( True )
- self.sql_view.SetTabWidth ( 4 )
- self.sql_view.SetIndent ( 4 )
- self.sql_view.SetTabIndents( True )
- self.sql_view.SetBackSpaceUnIndents( True )
- self.sql_view.SetViewEOL( False )
- self.sql_view.SetViewWhiteSpace( False )
- self.sql_view.SetMarginWidth( 2, 0 )
- self.sql_view.SetIndentationGuides( True )
- self.sql_view.SetReadOnly( False )
- self.sql_view.SetMarginWidth( 1, 0 )
- self.sql_view.SetMarginType( 0, wx.stc.STC_MARGIN_NUMBER )
- self.sql_view.SetMarginWidth( 0, self.sql_view.TextWidth( wx.stc.STC_STYLE_LINENUMBER, "_99999" ) )
- self.sql_view.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS )
- self.sql_view.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK)
- self.sql_view.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE)
- self.sql_view.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS )
- self.sql_view.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK )
- self.sql_view.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE )
- self.sql_view.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY )
- self.sql_view.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS )
- self.sql_view.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK )
- self.sql_view.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE )
- self.sql_view.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUS )
- self.sql_view.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.BLACK)
- self.sql_view.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.WHITE)
- self.sql_view.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY )
- self.sql_view.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY )
- self.sql_view.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) )
- self.sql_view.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) )
- bSizer84.Add( self.sql_view, 1, wx.EXPAND | wx.ALL, 5 )
+ self.stc_view_select = wx.stc.StyledTextCtrl( self.panel_views, wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,200 ), 0)
+ self.stc_view_select.SetUseTabs ( True )
+ self.stc_view_select.SetTabWidth ( 4 )
+ self.stc_view_select.SetIndent ( 4 )
+ self.stc_view_select.SetTabIndents( True )
+ self.stc_view_select.SetBackSpaceUnIndents( True )
+ self.stc_view_select.SetViewEOL( False )
+ self.stc_view_select.SetViewWhiteSpace( False )
+ self.stc_view_select.SetMarginWidth( 2, 0 )
+ self.stc_view_select.SetIndentationGuides( True )
+ self.stc_view_select.SetReadOnly( False )
+ self.stc_view_select.SetMarginWidth( 1, 0 )
+ self.stc_view_select.SetMarginType( 0, wx.stc.STC_MARGIN_NUMBER )
+ self.stc_view_select.SetMarginWidth( 0, self.stc_view_select.TextWidth( wx.stc.STC_STYLE_LINENUMBER, "_99999" ) )
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS )
+ self.stc_view_select.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK)
+ self.stc_view_select.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE)
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS )
+ self.stc_view_select.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK )
+ self.stc_view_select.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE )
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY )
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS )
+ self.stc_view_select.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK )
+ self.stc_view_select.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE )
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUS )
+ self.stc_view_select.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.BLACK)
+ self.stc_view_select.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.WHITE)
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY )
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY )
+ self.stc_view_select.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) )
+ self.stc_view_select.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) )
+ bSizer84.Add( self.stc_view_select, 1, wx.EXPAND | wx.ALL, 5 )
bSizer91 = wx.BoxSizer( wx.HORIZONTAL )
@@ -1516,49 +1603,22 @@ def __init__( self, parent ):
self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex )
MainFrameNotebookIndex += 1
- self.panel_query = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
- self.panel_query.Enable( False )
+ self.QueryPanel = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ self.QueryPanel.Enable( False )
bSizer26 = wx.BoxSizer( wx.VERTICAL )
- self.sql_query = wx.stc.StyledTextCtrl( self.panel_query, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0)
- self.sql_query.SetUseTabs ( True )
- self.sql_query.SetTabWidth ( 4 )
- self.sql_query.SetIndent ( 4 )
- self.sql_query.SetTabIndents( True )
- self.sql_query.SetBackSpaceUnIndents( True )
- self.sql_query.SetViewEOL( False )
- self.sql_query.SetViewWhiteSpace( False )
- self.sql_query.SetMarginWidth( 2, 0 )
- self.sql_query.SetIndentationGuides( True )
- self.sql_query.SetReadOnly( False )
- self.sql_query.SetMarginWidth( 1, 0 )
- self.sql_query.SetMarginType( 0, wx.stc.STC_MARGIN_NUMBER )
- self.sql_query.SetMarginWidth( 0, self.sql_query.TextWidth( wx.stc.STC_STYLE_LINENUMBER, "_99999" ) )
- self.sql_query.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS )
- self.sql_query.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK)
- self.sql_query.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE)
- self.sql_query.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS )
- self.sql_query.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK )
- self.sql_query.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE )
- self.sql_query.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY )
- self.sql_query.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS )
- self.sql_query.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK )
- self.sql_query.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE )
- self.sql_query.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUS )
- self.sql_query.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.BLACK)
- self.sql_query.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.WHITE)
- self.sql_query.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY )
- self.sql_query.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY )
- self.sql_query.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) )
- self.sql_query.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) )
- bSizer26.Add( self.sql_query, 1, wx.EXPAND | wx.ALL, 5 )
-
-
- self.panel_query.SetSizer( bSizer26 )
- self.panel_query.Layout()
- bSizer26.Fit( self.panel_query )
- self.MainFrameNotebook.AddPage( self.panel_query, _(u"Query"), False )
+ self.m_textCtrl10 = wx.TextCtrl( self.QueryPanel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE|wx.TE_RICH|wx.TE_RICH2 )
+ bSizer26.Add( self.m_textCtrl10, 1, wx.ALL|wx.EXPAND, 5 )
+
+ self.m_button12 = wx.Button( self.QueryPanel, wx.ID_ANY, _(u"New"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer26.Add( self.m_button12, 0, wx.ALIGN_RIGHT|wx.ALL, 5 )
+
+
+ self.QueryPanel.SetSizer( bSizer26 )
+ self.QueryPanel.Layout()
+ bSizer26.Fit( self.QueryPanel )
+ self.MainFrameNotebook.AddPage( self.QueryPanel, _(u"Query"), False )
MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/arrow_right.png", wx.BITMAP_TYPE_ANY )
if ( MainFrameNotebookBitmap.IsOk() ):
MainFrameNotebookImages.Add( MainFrameNotebookBitmap )
@@ -1652,7 +1712,7 @@ def __init__( self, parent ):
self.LogSQLPanel.SetSizer( sizer_log_sql )
self.LogSQLPanel.Layout()
sizer_log_sql.Fit( self.LogSQLPanel )
- self.m_splitter51.SplitHorizontally( self.m_panel22, self.LogSQLPanel, -150 )
+ self.m_splitter51.SplitHorizontally( self.m_panel22, self.LogSQLPanel, 432 )
bSizer21.Add( self.m_splitter51, 1, wx.EXPAND, 5 )
@@ -1788,7 +1848,7 @@ def on_apply_filters( self, event ):
event.Skip()
def m_splitter51OnIdle( self, event ):
- self.m_splitter51.SetSashPosition( -150 )
+ self.m_splitter51.SetSashPosition( 432 )
self.m_splitter51.Unbind( wx.EVT_IDLE )
def m_splitter4OnIdle( self, event ):
@@ -1836,6 +1896,53 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.
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 )
+ 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 )
+
+ bSizer86 = wx.BoxSizer( wx.HORIZONTAL )
+
+
+ bSizer129.Add( bSizer86, 0, 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 )
+ bSizer129.Add( self.rad_view_constraint, 0, wx.ALL|wx.EXPAND, 5 )
+
+
+ bSizer93.Add( bSizer129, 1, wx.EXPAND, 5 )
+
bSizer90.Add( bSizer93, 1, wx.EXPAND, 5 )
@@ -1911,6 +2018,11 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.
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 )
@@ -1929,16 +2041,6 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.
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 )
- bSizer12211 = wx.BoxSizer( wx.HORIZONTAL )
-
- self.m_staticText2211 = wx.StaticText( self, wx.ID_ANY, _(u"Port"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 )
- self.m_staticText2211.Wrap( -1 )
-
- bSizer12211.Add( self.m_staticText2211, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
-
-
- bSizer90.Add( bSizer12211, 0, wx.EXPAND, 5 )
-
self.tree_ctrl_sessions2 = wx.TreeCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TR_DEFAULT_STYLE )
self.tree_ctrl_sessions2.Hide()
@@ -2013,11 +2115,77 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.
self.m_listBox1 = wx.ListBox( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_listBox1Choices, 0 )
bSizer90.Add( self.m_listBox1, 0, wx.ALL, 5 )
- self.m_textCtrl10 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE|wx.TE_RICH|wx.TE_RICH2 )
- bSizer90.Add( self.m_textCtrl10, 1, wx.ALL|wx.EXPAND, 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 )
+
- self.m_button12 = wx.Button( self, wx.ID_ANY, _(u"New"), wx.DefaultPosition, wx.DefaultSize, 0 )
- bSizer90.Add( self.m_button12, 0, wx.ALIGN_RIGHT|wx.ALL, 5 )
+ bSizer90.Add( gSizer3, 1, wx.EXPAND, 5 )
self.SetSizer( bSizer90 )
diff --git a/windows/components/dataview.py b/windows/components/dataview.py
index 9dc4dd7..618b748 100644
--- a/windows/components/dataview.py
+++ b/windows/components/dataview.py
@@ -12,13 +12,12 @@
from structures.engines.database import SQLColumn, SQLTable, SQLIndex
from structures.engines.datatype import DataTypeCategory
from structures.engines.indextype import SQLIndexType, StandardIndexType
-from windows.components import BaseDataViewCtrl
+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.main import CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE
-from windows.main.table import NEW_TABLE
+from windows.state import CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE, NEW_TABLE
class _SQLiteTableColumnsDataViewCtrl:
@@ -330,18 +329,21 @@ def _load_table_columns(self, popup, column2_render: PopupRenderer) -> list[str]
class TableRecordsDataViewCtrl(BaseDataViewCtrl):
on_record_insert: Callable[[...], Optional[bool]]
on_record_delete: Callable[[...], Optional[bool]]
- make_advanced_dialog: Callable[[wx.Window, str], 'AdvancedCellEditorDialog']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
CURRENT_TABLE.subscribe(self._load_table)
+ def make_advanced_dialog(self, parent, value: str):
+ from windows.dialogs.advanced_cell_editor import AdvancedCellEditorController
+ return AdvancedCellEditorController(parent, value)
+
def _get_column_renderer(self, column: SQLColumn) -> wx.dataview.DataViewRenderer:
for foreign_key in column.table.foreign_keys:
if column.name in foreign_key.columns:
- session = CURRENT_SESSION()
- database = CURRENT_DATABASE()
+ session = CURRENT_SESSION.get_value()
+ database = CURRENT_DATABASE.get_value()
records = []
references = []
@@ -446,6 +448,10 @@ def autosize_columns_from_content(self, sample_rows: int = 30):
col.SetWidth(width)
+class QueryEditorResultsDataViewCtrl(TableRecordsDataViewCtrl):
+ pass
+
+
class DatabaseTablesDataViewCtrl(BaseDataViewCtrl):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
diff --git a/windows/components/popup.py b/windows/components/popup.py
index 3111df7..3078c11 100644
--- a/windows/components/popup.py
+++ b/windows/components/popup.py
@@ -13,7 +13,8 @@
from windows.components import BasePopup
from structures.engines.datatype import SQLDataType, DataTypeCategory, StandardDataType
-from windows.main import CURRENT_SESSION
+
+from windows.state import CURRENT_SESSION
class PopupColumnDefault(BasePopup):
diff --git a/windows/components/stc/__init__.py b/windows/components/stc/__init__.py
index e9a00ce..6c5504c 100644
--- a/windows/components/stc/__init__.py
+++ b/windows/components/stc/__init__.py
@@ -1,5 +1,5 @@
-from windows.components.stc.auto_complete import CompletionResult
-from windows.components.stc.auto_complete import SQLAutoCompleteController, SQLCompletionProvider
+from windows.components.stc.autocomplete.completion_types import CompletionResult
+from windows.components.stc.autocomplete.auto_complete import SQLAutoCompleteController, SQLCompletionProvider
from windows.components.stc.detectors import detect_syntax_id
from windows.components.stc.profiles import BASE64, CSV, HTML, JSON, MARKDOWN, REGEX, SQL, TEXT, XML, YAML
from windows.components.stc.profiles import Detector, Formatter, SyntaxProfile
diff --git a/windows/components/stc/auto_complete.py b/windows/components/stc/auto_complete.py
deleted file mode 100644
index 10b52a8..0000000
--- a/windows/components/stc/auto_complete.py
+++ /dev/null
@@ -1,275 +0,0 @@
-import re
-
-from dataclasses import dataclass
-from typing import Callable, Optional
-
-import wx
-import wx.stc
-
-from structures.engines.database import SQLDatabase, SQLTable
-
-
-@dataclass(frozen=True, slots=True)
-class CompletionResult:
- prefix: str
- prefix_length: int
- items: tuple[str, ...]
-
-
-class SQLCompletionProvider:
- _word_at_caret_pattern = re.compile(r"[A-Za-z_][A-Za-z0-9_]*$")
- _token_pattern = re.compile(r"[A-Za-z_][A-Za-z0-9_]*|[.,()*=]")
-
- _from_like_keywords: set[str] = {"FROM", "JOIN", "UPDATE", "INTO"}
-
- def __init__(
- self,
- get_database: Callable[[], Optional[SQLDatabase]],
- get_current_table: Optional[Callable[[], Optional[SQLTable]]] = None,
- *,
- is_filter_editor: bool = False,
- ) -> None:
- self._get_database = get_database
- self._get_current_table = get_current_table or (lambda: None)
- self._is_filter_editor = is_filter_editor
-
- def get(self, text: str, pos: int) -> Optional[CompletionResult]:
- if (database := self._get_database()) is None:
- return None
-
- safe_pos = self._clamp_position(pos=pos, text=text)
- prefix = self._extract_prefix(pos=safe_pos, text=text)
- previous_token = self._previous_token(pos=safe_pos, text=text)
- previous_keyword = previous_token.upper() if previous_token and previous_token[0].isalpha() else None
-
- if self._is_dot_trigger(pos=safe_pos, text=text):
- owner = self._word_before_dot(dot_pos=safe_pos - 1, text=text)
- items = self._columns_for_owner(database=database, owner=owner)
- return CompletionResult(items=tuple(items), prefix="", prefix_length=0)
-
- if previous_keyword in self._from_like_keywords:
- items = self._tables(database=database)
- return CompletionResult(items=tuple(items), prefix=prefix, prefix_length=len(prefix))
-
- if self._is_filter_editor:
- items = self._filter_items(database=database)
- return CompletionResult(items=tuple(items), prefix=prefix, prefix_length=len(prefix))
-
- if self._should_suggest_select_items(previous_token=previous_token, pos=safe_pos, text=text):
- items = self._columns_prioritized(database=database) + self._functions(database=database)
- return CompletionResult(items=tuple(items), prefix=prefix, prefix_length=len(prefix))
-
- items = self._keywords(database=database)
- return CompletionResult(items=tuple(items), prefix=prefix, prefix_length=len(prefix))
-
- @staticmethod
- def _clamp_position(*, pos: int, text: str) -> int:
- if pos < 0:
- return 0
- if pos > len(text):
- return len(text)
- return pos
-
- def _extract_prefix(self, *, pos: int, text: str) -> str:
- left_text = text[:pos]
- if (match := self._word_at_caret_pattern.search(left_text)) is None:
- return ""
- return match.group(0)
-
- def _previous_token(self, *, pos: int, text: str) -> Optional[str]:
- tokens = self._token_pattern.findall(text[:pos])
- if not tokens:
- return None
- return tokens[-1]
-
- @staticmethod
- def _is_dot_trigger(*, pos: int, text: str) -> bool:
- return pos > 0 and text[pos - 1] == "."
-
- def _word_before_dot(self, *, dot_pos: int, text: str) -> str:
- if dot_pos <= 0:
- return ""
- left_text = text[:dot_pos]
- if (match := self._word_at_caret_pattern.search(left_text)) is None:
- return ""
- return match.group(0)
-
- def _should_suggest_select_items(self, *, previous_token: Optional[str], pos: int, text: str) -> bool:
- if not self._is_in_select_list(pos=pos, text=text):
- return False
- if not previous_token:
- return False
- if previous_token == ",":
- return True
- return previous_token.upper() == "SELECT"
-
- @staticmethod
- def _is_in_select_list(*, pos: int, text: str) -> bool:
- left_upper = text[:pos].upper()
- select_index = left_upper.rfind("SELECT")
- if select_index == -1:
- return False
- from_index = left_upper.rfind("FROM")
- return from_index == -1 or from_index < select_index
-
- def _columns_for_owner(self, *, database: SQLDatabase, owner: str) -> list[str]:
- if not owner:
- return []
- for table in database.tables:
- if table.name.lower() == owner.lower():
- return [column.name for column in table.columns if column.name]
- return []
-
- def _columns_prioritized(self, *, database: SQLDatabase) -> list[str]:
- items: list[str] = []
- current_table = self._get_current_table()
-
- if current_table is not None:
- items.extend([column.name for column in current_table.columns if column.name])
-
- for table in database.tables:
- if table is current_table:
- continue
- for column in table.columns:
- if column.name:
- items.append(f"{table.name}.{column.name}")
-
- return items
-
- def _filter_items(self, *, database: SQLDatabase) -> list[str]:
- return self._columns_prioritized(database=database) + self._functions(database=database)
-
- def _functions(self, *, database: SQLDatabase) -> list[str]:
- functions = database.context.FUNCTIONS
- return [str(function_name).upper() for function_name in functions]
-
- def _keywords(self, *, database: SQLDatabase) -> list[str]:
- keywords = database.context.KEYWORDS
- return [str(keyword).upper() for keyword in keywords]
-
- def _tables(self, *, database: SQLDatabase) -> list[str]:
- return [table.name for table in database.tables]
-
-
-class SQLAutoCompleteController:
- def __init__(
- self,
- editor: wx.stc.StyledTextCtrl,
- provider: SQLCompletionProvider,
- *,
- debounce_ms: int = 80,
- is_enabled: bool = True,
- min_prefix_length: int = 1,
- ) -> None:
- self._editor = editor
- self._provider = provider
- self._debounce_ms = debounce_ms
- self._is_enabled = is_enabled
- self._min_prefix_length = min_prefix_length
-
- self._is_showing = False
- self._pending_call: Optional[wx.CallLater] = None
-
- self._configure_autocomp()
-
- self._editor.Bind(wx.stc.EVT_STC_CHARADDED, self._on_char_added)
- self._editor.Bind(wx.EVT_KEY_DOWN, self._on_key_down)
-
- def set_enabled(self, is_enabled: bool) -> None:
- self._is_enabled = is_enabled
- if not is_enabled:
- self._cancel_pending()
- self._cancel_if_active()
-
- def show(self, *, force: bool) -> None:
- if not self._is_enabled:
- return
- if self._is_showing:
- return
-
- self._is_showing = True
- try:
- pos = self._editor.GetCurrentPos()
- text = self._editor.GetText()
-
- result = self._provider.get(pos=pos, text=text)
- if result is None:
- self._cancel_if_active()
- return
-
- if not force and result.prefix_length < self._min_prefix_length:
- self._cancel_if_active()
- return
-
- if not result.items:
- self._cancel_if_active()
- return
-
- items = self._unique_sorted_items(items=result.items)
-
- self._editor.AutoCompShow(result.prefix_length, "\n".join(items))
- if result.prefix:
- self._editor.AutoCompSelect(result.prefix)
- finally:
- self._is_showing = False
-
- def _configure_autocomp(self) -> None:
- # Use newline separator to support a wide range of identifiers.
- self._editor.AutoCompSetSeparator(ord("\n"))
- self._editor.AutoCompSetIgnoreCase(True)
- self._editor.AutoCompSetAutoHide(True)
- self._editor.AutoCompSetDropRestOfWord(True)
-
- def _on_key_down(self, event: wx.KeyEvent) -> None:
- if not self._is_enabled:
- event.Skip()
- return
-
- key_code = event.GetKeyCode()
-
- if event.ControlDown() and key_code == wx.WXK_SPACE:
- self._cancel_pending()
- self.show(force=True)
- return
-
- if key_code == wx.WXK_TAB and self._editor.AutoCompActive():
- self._cancel_pending()
- self._editor.AutoCompComplete()
- return
-
- if key_code == wx.WXK_ESCAPE and self._editor.AutoCompActive():
- self._cancel_pending()
- self._editor.AutoCompCancel()
- return
-
- event.Skip()
-
- def _on_char_added(self, event: wx.stc.StyledTextEvent) -> None:
- if not self._is_enabled:
- return
-
- key_code = event.GetKey()
- character = chr(key_code)
-
- if character.isalnum() or character in {"_", ".", ","}:
- self._schedule_show(force=False)
-
- def _schedule_show(self, *, force: bool) -> None:
- self._cancel_pending()
- self._pending_call = wx.CallLater(self._debounce_ms, self.show, force=force)
-
- def _cancel_pending(self) -> None:
- if self._pending_call is None:
- return
- if self._pending_call.IsRunning():
- self._pending_call.Stop()
- self._pending_call = None
-
- def _cancel_if_active(self) -> None:
- if self._editor.AutoCompActive():
- self._editor.AutoCompCancel()
-
- @staticmethod
- def _unique_sorted_items(*, items: tuple[str, ...]) -> list[str]:
- unique_items: set[str] = set(items)
- return sorted(unique_items, key=str.upper)
diff --git a/windows/components/stc/autocomplete/AUTO_COMPLETE_RULES.md b/windows/components/stc/autocomplete/AUTO_COMPLETE_RULES.md
new file mode 100644
index 0000000..39d8cf2
--- /dev/null
+++ b/windows/components/stc/autocomplete/AUTO_COMPLETE_RULES.md
@@ -0,0 +1,2015 @@
+# SQL Autocomplete Rules
+
+This document defines the autocomplete behavior for each SQL context.
+
+---
+
+## Context Detection
+
+The autocomplete system uses `sqlglot` to parse the SQL query and determine the current context.
+Contexts are defined in the `SQLContext` enum.
+
+---
+
+## Key Examples: Scope Restriction in Action
+
+These examples demonstrate the strict separation between table-selection and expression contexts.
+
+**Assume:** `CURRENT_TABLE = users` (set in table editor)
+
+### Example 1: SELECT with no FROM โ CURRENT_TABLE + DB-wide allowed
+
+```sql
+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')
+- `UPPER, UUID, UNIX_TIMESTAMP` (functions)
+
+**Rationale:** No scope tables exist, so CURRENT_TABLE and DB-wide columns are allowed.
+
+---
+
+### Example 2: FROM/JOIN suggests CURRENT_TABLE as table candidate
+
+```sql
+SELECT * FROM orders JOIN |
+```
+
+**Context:** JOIN_CLAUSE (table-selection)
+
+**Suggestions:**
+- `users` (CURRENT_TABLE as convenience shortcut)
+- `products, customers, ...` (other physical tables)
+- CTEs (if any)
+
+**Rationale:** JOIN_CLAUSE is table-selection. CURRENT_TABLE may be suggested even though scope tables (orders) already exist. This is how the user brings it into scope.
+
+---
+
+### Example 3: WHERE/JOIN_ON shows only scoped columns (CURRENT_TABLE excluded unless in scope)
+
+```sql
+-- Case A: CURRENT_TABLE not in scope
+SELECT * FROM orders WHERE u|
+```
+
+**Context:** WHERE (expression context), scope = [orders]
+
+**Suggestions:**
+- `orders.user_id` (scope table column matching 'u')
+- `UPPER, UUID, UNIX_TIMESTAMP` (functions)
+
+**NOT suggested:**
+- โ `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.
+
+```sql
+-- Case B: CURRENT_TABLE in scope
+SELECT * FROM users u JOIN orders o WHERE u|
+```
+
+**Context:** WHERE (expression context), scope = [users (alias u), orders (alias o)]
+
+**Suggestions:**
+- `u.id, u.name, u.email, ...` (CURRENT_TABLE in scope via alias 'u')
+- `UPPER, UUID, UNIX_TIMESTAMP` (functions)
+
+**Rationale:** CURRENT_TABLE (users) is in scope via alias 'u', so its columns are included.
+
+---
+
+## Precedence Chain
+
+The autocomplete resolution follows this strict precedence order:
+
+1. **Multi-Query Separation**
+ - If multiple queries in editor (separated by the effective statement separator), extract the current statement where the cursor is
+ - All subsequent rules apply only to current statement
+
+2. **Dot-Completion** (`table.` or `alias.`)
+ - If token immediately before cursor contains `.` โ Dot-Completion mode
+ - 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)
+ - 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
+ - Check for exact alias match first (Alias Prefix Disambiguation)
+ - Otherwise generic prefix matching (tables, columns, functions, keywords)
+
+**Example resolution:**
+```sql
+-- Multi-query: extract current statement
+SELECT * FROM users; SELECT * FROM orders WHERE u|
+
+-- No dot โ not Dot-Completion
+-- Context: WHERE clause
+-- Prefix: "u"
+-- Check aliases: no alias "u" in this statement
+-- Generic prefix: show users.*, orders.user_id, UPPER, etc.
+```
+
+**Example with dot:**
+```sql
+SELECT * FROM users u WHERE u.i|
+
+-- Dot detected โ Dot-Completion (precedence 2)
+-- Show columns of "u" starting with "i"
+-- Context (WHERE) is ignored for this specific resolution
+```
+
+This precedence eliminates ambiguity: **Dot-Completion always wins**, then context, then prefix rules.
+
+---
+
+## Universal Rules (Apply to All Contexts)
+
+### Prefix Definition
+
+**Prefix** = the identifier token immediately before the cursor, composed of `[A-Za-z0-9_]+` (or equivalent for dialect).
+
+**Rules:**
+- Match is case-insensitive
+- Output preserves original form (alias/table/column name)
+- If token contains `.` โ **not a prefix**: triggers Dot-Completion instead
+
+**Examples:**
+```sql
+SELECT u|
+โ Prefix: "u" (triggers prefix matching)
+
+SELECT u.i|
+โ NOT a prefix (contains dot)
+โ Triggers Dot-Completion on table/alias "u"
+
+SELECT ui|
+โ Prefix: "ui" (triggers prefix matching)
+```
+
+**This distinction is critical:**
+- `u.i|` โ Dot-Completion (show columns of table/alias `u` starting with `i`)
+- `ui|` โ Prefix matching (show items starting with `ui`)
+
+---
+
+### Column Qualification (table.column vs alias.column)
+
+**Important:** Always prefer `alias.column` format when an alias is defined, otherwise use `table.column`.
+
+**Examples:**
+```sql
+-- No alias: use table.column
+SELECT * FROM users WHERE |
+โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at
+
+-- With alias: use alias.column
+SELECT * FROM users u WHERE |
+โ u.id, u.name, u.email
+
+-- Multiple tables with aliases
+SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE |
+โ u.id, u.name, o.user_id, o.total
+```
+
+**Note:** This rule applies to all contexts: SELECT_LIST, WHERE, JOIN ON, ORDER BY, GROUP BY, HAVING.
+
+---
+
+### Comma and Whitespace Behavior
+
+**Universal rule for all contexts:**
+
+- **Comma is never suggested** as an autocomplete item
+- If the user types `,` โ they want another item โ apply "next-item" rules for that context (e.g., after comma in SELECT list, show columns/functions)
+- If the user types **whitespace** after a completed identifier/expression โ treat it as "selection complete" โ show only keywords valid in that position (clause keywords or context modifiers like `ASC`, `DESC`, `NULLS FIRST`, etc.)
+
+**Rationale:** Whitespace signals intentional pause/completion. Comma signals continuation. This distinction applies consistently across all SQL contexts.
+
+**Examples:**
+```sql
+SELECT id, |
+โ Comma typed โ next-item rules โ show columns/functions
+
+SELECT id |
+โ Whitespace typed โ selection complete โ show clause keywords (FROM, WHERE, AS, ...)
+
+ORDER BY created_at, |
+โ Comma typed โ next-item rules โ show columns/functions
+
+ORDER BY created_at |
+โ Whitespace typed โ selection complete โ show ASC, DESC, NULLS FIRST, NULLS LAST
+```
+
+---
+
+### Scope-Restricted Expression Contexts
+
+**Definition:** The following contexts are **scope-restricted expression contexts**:
+- WHERE
+- JOIN_ON
+- ORDER_BY
+- GROUP_BY
+- HAVING
+
+**Scope restriction rules for these contexts:**
+- Column suggestions MUST be limited to scope tables only
+- Database-wide columns MUST NOT be suggested
+- Table-name expansion MUST be limited to scope tables only
+- Column-name matching MUST be limited to scope tables only
+- `CURRENT_TABLE` columns MUST NOT be suggested unless `CURRENT_TABLE` is in scope
+
+**Operator context rule (WHERE, JOIN_ON):**
+- When cursor is after a comparison operator (`=`, `!=`, `<>`, `<`, `>`, `<=`, `>=`, `LIKE`, `IN`, etc.)
+- The column on the LEFT side of the operator MUST NOT be suggested
+- **Rationale:** Suggesting the same column on both sides (e.g., `WHERE users.id = users.id`) is redundant and not useful
+- **Example:** `WHERE users.id = |` should suggest `users.name`, `users.email`, etc., but NOT `users.id`
+
+**Rationale:** These clauses cannot legally reference tables not present in scope.
+
+---
+
+### CURRENT_TABLE Scope Restriction
+
+**Definition:** `CURRENT_TABLE` = UI-selected table from table editor context (optional, may be `None`).
+
+**Scope tables** = tables/CTEs/derived tables that appear in the current statement's FROM/JOIN clauses:
+- Physical tables in FROM/JOIN
+- CTEs referenced in FROM/JOIN
+- Derived tables (subquery aliases) in FROM/JOIN
+
+---
+
+#### Table-Selection Contexts (FROM_CLAUSE, JOIN_CLAUSE)
+
+These contexts suggest **tables**, not columns.
+
+**Rules:**
+- `CURRENT_TABLE` MAY be suggested as a table candidate
+- Allowed even if scope tables already exist (this is how user brings it into scope)
+- MUST NOT suggest `CURRENT_TABLE` if it is already present in the statement
+- Purpose: convenience shortcut for selecting the current table
+
+---
+
+#### Expression Contexts (JOIN_ON, WHERE, ORDER_BY, GROUP_BY, HAVING)
+
+These are **scope-restricted expression contexts** (see **Scope-Restricted Expression Contexts** section).
+
+These contexts suggest **columns** from scope tables only.
+
+---
+
+#### SELECT_LIST Context (Special Case)
+
+**If statement has NO scope tables (no FROM/JOIN yet):**
+- `CURRENT_TABLE` columns MUST be included first (if set)
+- Database-wide columns MAY be included (guardrail applies when no prefix)
+- Functions and keywords are included
+
+**If statement HAS 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 MAY still be included (for table-name expansion and column-name matching)
+- Scope table columns are included with alias-first qualification
+
+---
+
+### Dot-Completion (table.column or alias.column)
+
+**Trigger:** After `table.` or `alias.` in any SQL context
+
+**Show:**
+- Columns of the specific table (filtered by prefix if present)
+
+**Output format:** Unqualified column names (e.g., `id`, `name`) NOT qualified (e.g., `u.id`, `u.name`). This is an exception to the alias-first rule used elsewhere.
+
+**Ordering:** Dot-completion bypasses global ordering rules and returns only the selected table's columns (table definition order). Columns preserve their ordinal position in the table schema. No functions, keywords, or other tables.
+
+**Filtering:** When a prefix is present after the dot (e.g., `users.i|`), filtering uses `startswith(prefix)` on column name (case-insensitive). NOT contains or fuzzy matching.
+
+**Examples:**
+```sql
+SELECT users.|
+โ id, name, email, password, is_enabled, created_at (schema order, NOT alphabetical)
+โ NOT users.id, users.name, ...
+
+SELECT users.i|
+โ id, is_enabled (columns starting with 'i', in schema order)
+โ NOT users.id
+
+WHERE u.| (where u is alias of users)
+โ id, name, email, password, is_enabled, created_at (schema order)
+โ NOT u.id, u.name, ...
+
+ON orders.|
+โ id, user_id, total, created_at
+
+ORDER BY users.|
+โ id, name, email, password, is_enabled, created_at (schema order)
+```
+
+**Note:** This rule takes precedence over context-specific rules when a dot is detected.
+
+---
+
+## Autocomplete Rules by Context
+
+### 1. EMPTY (Empty editor)
+
+**Trigger:** Completely empty editor
+
+**Show:**
+- Primary keywords: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `CREATE`, `DROP`, `ALTER`, `TRUNCATE`, `SHOW`, `DESCRIBE`, `EXPLAIN`, `WITH`, `REPLACE`, `MERGE`
+
+**Examples:**
+```sql
+| โ SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ...
+```
+
+---
+
+### 2. SINGLE_TOKEN (Single token without spaces)
+
+**Trigger:** Single partial token, no spaces
+
+**Important:** Applies only if the entire current statement contains exactly one token (no whitespace). Whitespace includes newline. This avoids misinterpretation when splitting statements/lines.
+
+**Token definition:** A valid identifier matching the pattern `^[A-Za-z_][A-Za-z0-9_]*$` (or dialect-equivalent). This excludes symbols like `(`, `)`, `,`, `.`, etc. Examples: `SEL` โ
, `SEL(` โ, `SELECT,` โ.
+
+**Note:** Token matching is dialect-aware; the pattern above is the default baseline. Some dialects may support `$`, `#`, or unicode characters in identifiers.
+
+**Show:**
+- All keywords (filtered by prefix)
+
+**Examples:**
+```sql
+SEL| โ SELECT
+INS| โ INSERT
+UPD| โ UPDATE
+```
+
+**Not SINGLE_TOKEN:**
+```sql
+SELECT
+SEL|
+โ This is two tokens (SELECT + SEL), not SINGLE_TOKEN context
+โ Use context detection instead
+```
+
+---
+
+### 3. SELECT_LIST (Inside SELECT, before FROM)
+
+**Trigger:** After `SELECT` and before `FROM`
+
+**Important:** Column suggestions depend on whether FROM/JOIN are present in the query.
+
+#### 3a. Without prefix (after SELECT, no FROM/JOIN in query)
+
+**Show:**
+- Functions
+- Keywords (FROM, WHERE, etc.)
+
+**Examples:**
+```sql
+SELECT |
+โ COUNT, SUM, AVG, MAX, MIN, UPPER, LOWER, ...
+โ FROM, WHERE, LIMIT, ...
+```
+
+#### 3a-bis. Without prefix (after SELECT, with FROM/JOIN in query)
+
+**Show:**
+- Columns in scope (qualified, alias-first)
+- All functions
+
+**Examples:**
+```sql
+SELECT * FROM users WHERE id = 1; SELECT |
+โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at, ...
+โ COUNT, SUM, AVG, ...
+
+SELECT * FROM users u JOIN orders o ON u.id = o.user_id; SELECT |
+โ u.id, u.name, o.user_id, o.total, ...
+โ COUNT, SUM, AVG, ...
+```
+
+#### 3b. With prefix
+
+**Show:**
+- Columns matching the prefix (see **Generic Prefix Matching for Column Contexts** section)
+- Functions matching the prefix
+
+**Matching priority:**
+1. If prefix exactly equals an alias โ Alias-exact-match mode (see **Alias Prefix Disambiguation** section)
+2. Otherwise โ Generic prefix matching (see **Generic Prefix Matching for Column Contexts** section)
+
+**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
+ - Functions are included
+
+- **When scope tables exist (FROM/JOIN present):**
+ - `CURRENT_TABLE` columns MUST be included ONLY if `CURRENT_TABLE` is in scope
+ - If `CURRENT_TABLE` is not in scope, it MUST be ignored
+ - Scope table columns are included with alias-first qualification
+ - **Out-of-Scope Table Hints:** If prefix matches DB-wide tables but no scope tables/columns, suggest table names as hints (see **Out-of-Scope Table Hints** section)
+
+**Examples:**
+
+**No FROM/JOIN (CURRENT_TABLE included):**
+```sql
+-- Assume CURRENT_TABLE = users
+SELECT u|
+โ users.id, users.name, users.email, ... (CURRENT_TABLE columns first)
+โ Table-name expansion: (other tables starting with 'u')
+โ Column-name matching: orders.user_id, products.unit_price (DB-wide columns starting with 'u')
+โ Functions: UPPER, UUID, UNIX_TIMESTAMP
+```
+
+**FROM/JOIN exists, CURRENT_TABLE not in scope (CURRENT_TABLE ignored):**
+```sql
+-- Assume CURRENT_TABLE = users
+SELECT u| FROM orders
+โ Table-name expansion: users.* (DB-wide table starting with 'u')
+โ Column-name matching: orders.user_id (scope table column starting with 'u')
+โ Functions: UPPER, UUID, UNIX_TIMESTAMP
+โ (CURRENT_TABLE ignored - not in scope)
+```
+
+**FROM/JOIN exists, CURRENT_TABLE in scope (CURRENT_TABLE included):**
+```sql
+-- Assume CURRENT_TABLE = users
+SELECT u| FROM users u JOIN orders o
+โ Alias-exact-match mode activated (u == alias 'u')
+โ u.id, u.name, u.email (CURRENT_TABLE in scope via alias 'u')
+โ UPPER, UUID, UNIX_TIMESTAMP (functions)
+```
+
+#### 3c. After comma (next column)
+
+**Trigger:** After comma in SELECT list
+
+**Show:**
+- All columns (qualified, alias-first) (filtered by prefix if present)
+- All functions (filtered by prefix if present)
+
+**Examples:**
+```sql
+SELECT col1, |
+โ users.id, users.name, users.email, orders.total, ...
+โ COUNT, SUM, AVG, ...
+
+SELECT * FROM users u WHERE id = 1; SELECT id, |
+โ u.id, u.name, u.email, ...
+โ COUNT, SUM, AVG, ...
+
+SELECT id, n|
+โ users.name, orders.name, ...
+```
+
+#### 3d. After complete column (space after column)
+
+**Trigger:** After a complete column name (with or without table prefix) followed by space
+
+**Show:**
+- Keywords ONLY: `FROM`, `WHERE`, `AS`, `LIMIT`, `ORDER BY`, `GROUP BY`, `HAVING`
+- `AS` only if the current select item has no alias yet
+- **IMPORTANT:** NO functions (COUNT, SUM, UPPER, etc.) - user has completed selection
+
+**Note:** If alias presence cannot be reliably detected in incomplete SQL, default to offering `AS` (non-breaking UX).
+
+**Rationale:** Whitespace after a complete column indicates "selection complete" - user wants to move to next clause, not continue with functions.
+
+**Examples:**
+```sql
+SELECT id |
+โ FROM, WHERE, AS, LIMIT, ORDER BY, GROUP BY, HAVING
+โ NOT: COUNT, SUM, UPPER, etc.
+
+SELECT id AS user_id |
+โ FROM, WHERE, LIMIT, ORDER BY, GROUP BY, HAVING (AS excluded - alias already present)
+
+SELECT users.id |
+โ FROM, WHERE, AS, LIMIT, ORDER BY, GROUP BY, HAVING
+โ NOT: COUNT, SUM, UPPER, etc.
+
+SELECT table.column |
+โ FROM, WHERE, AS, LIMIT, ORDER BY, GROUP BY, HAVING
+โ NOT: functions
+```
+
+---
+
+### 4. FROM_CLAUSE (After FROM)
+
+**Trigger:** After `FROM` and before `WHERE`/`JOIN`
+
+#### 4a. Without prefix
+
+**Show:**
+- CTE names (if available from WITH clause)
+- All physical tables
+- `CURRENT_TABLE` (if set and not already in statement)
+
+**Prioritization:** If SELECT list contains qualified columns (e.g., `SELECT users.id`), prioritize those tables first in suggestions, even without prefix.
+
+**Examples:**
+```sql
+SELECT * FROM |
+โ customers, orders, products, users (alphabetical - no prioritization)
+
+SELECT users.id FROM |
+โ users, customers, orders, products (users FIRST - referenced in SELECT)
+
+SELECT orders.total, users.name FROM |
+โ orders, users, customers, products (orders and users FIRST - both referenced)
+
+WITH active_users AS (SELECT * FROM users WHERE status = 'active')
+SELECT * FROM |
+โ active_users, users, orders, products, ...
+```
+
+**Note:** Derived tables are not suggested as candidates to type in FROM/JOIN in v1 (they are inline subqueries, not selectable by name); but if present in the query, their alias contributes columns to scope.
+
+**CURRENT_TABLE handling:**
+
+`CURRENT_TABLE` may be suggested if:
+- It is set
+- It is not already present in the current statement
+
+**Rationale:** FROM_CLAUSE is a table-selection context (scope construction). Prioritizing tables already referenced in SELECT improves UX since users typically want to use the same tables. Even if scope already exists elsewhere in the statement (e.g., user editing a completed query), the only restriction is to avoid suggesting tables already present.
+
+#### 4b. With prefix
+
+**Show:**
+- CTE names starting with the prefix
+- Physical tables starting with the prefix
+- `CURRENT_TABLE` (if set, matches prefix, and not already in statement)
+
+**Prioritization:** Same as 4a - if SELECT list contains qualified columns, prioritize those tables first (among matching tables).
+
+**Examples:**
+```sql
+SELECT * FROM u|
+โ users
+
+SELECT users.column FROM u|
+โ users (prioritized - already referenced in SELECT)
+
+SELECT products.price FROM u|
+โ users (no prioritization - products referenced, not users)
+
+WITH active_users AS (...)
+SELECT * FROM a|
+โ active_users
+```
+
+#### 4c. After comma (multiple tables)
+
+**Trigger:** After comma in FROM clause
+
+**Show:**
+- CTE names (if available)
+- All physical tables (filtered by prefix if present)
+
+**Examples:**
+```sql
+SELECT * FROM users, |
+โ orders, products, customers, ...
+
+WITH active_users AS (...)
+SELECT * FROM users, |
+โ active_users, orders, products, ...
+
+SELECT * FROM users, o|
+โ orders
+```
+
+#### 4d. After table name (space after table)
+
+**Trigger:** After a complete table name followed by space
+
+**Show:**
+- Keywords: `JOIN`, `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `CROSS JOIN`, `WHERE`, `GROUP BY`, `ORDER BY`, `LIMIT`
+- `AS` (only if the table doesn't already have an alias)
+
+**Examples:**
+```sql
+SELECT * FROM users |
+โ JOIN, INNER JOIN, LEFT JOIN, RIGHT JOIN, CROSS JOIN, AS, WHERE, GROUP BY, ORDER BY, LIMIT
+ (AS included because no alias defined yet)
+
+SELECT * FROM users AS u |
+โ JOIN, INNER JOIN, LEFT JOIN, RIGHT JOIN, CROSS JOIN, WHERE, GROUP BY, ORDER BY, LIMIT
+ (AS excluded because alias 'u' already exists)
+```
+
+#### 4e. After AS (alias definition)
+
+**Trigger:** After `AS` keyword in FROM clause
+
+**Show:**
+- Nothing (empty list)
+
+**Rationale:** User is typing a custom alias name. No suggestions should interfere with free-form text input.
+
+**Examples:**
+```sql
+SELECT * FROM users AS |
+โ (no suggestions - user types alias freely)
+
+SELECT * FROM users AS u|
+โ (no suggestions - user is typing alias name)
+
+SELECT * FROM users AS u |
+โ JOIN, INNER JOIN, LEFT JOIN, RIGHT JOIN, WHERE, GROUP BY, ORDER BY, LIMIT
+ (alias complete, suggest next clauses)
+```
+
+**Note:** Once the alias is complete (followed by space), normal clause keyword suggestions resume.
+
+---
+
+### 5. JOIN_CLAUSE (After JOIN)
+
+**JOIN_CLAUSE is a table-selection context (like FROM).**
+
+It suggests tables, not columns.
+
+**Allowed suggestions:**
+- CTE names
+- Physical tables
+- `CURRENT_TABLE` (as a convenience table candidate, if not already present in the statement)
+
+**Important:** JOIN_CLAUSE is part of scope construction. It may include `CURRENT_TABLE` even if other scope tables already exist. Column suggestion logic must NOT run in this context.
+
+**Trigger:** After `JOIN`, `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, etc.
+
+#### 5a. Without prefix
+
+**Show:**
+- CTE names (if available from WITH clause)
+- All physical tables
+- `CURRENT_TABLE` (if set and not already in statement)
+
+**Examples:**
+```sql
+SELECT * FROM users JOIN |
+โ orders, products, customers, ...
+
+WITH active_users AS (...)
+SELECT * FROM users JOIN |
+โ active_users, orders, products, ...
+```
+
+**Note:** Derived tables are not suggested as candidates to type in FROM/JOIN in v1 (they are inline subqueries, not selectable by name); but if present in the query, their alias contributes columns to scope.
+
+**CURRENT_TABLE handling:**
+
+`CURRENT_TABLE` may be suggested if:
+- It is set
+- It is not already present in the current statement
+
+**Rationale:** JOIN_CLAUSE is a table-selection context (scope extension). It follows the same rule as FROM_CLAUSE. The only restriction is to avoid suggesting tables already present.
+
+#### 5b. With prefix
+
+**Show:**
+- CTE names starting with the prefix
+- Physical tables starting with the prefix
+- `CURRENT_TABLE` (if set, matches prefix, and not already in statement)
+
+**Examples:**
+```sql
+SELECT * FROM users JOIN o|
+โ orders
+
+WITH active_users AS (...)
+SELECT * FROM users u LEFT JOIN a|
+โ active_users
+```
+
+#### 5c. After table name (space after table)
+
+**Trigger:** After a complete table name in JOIN clause followed by space
+
+**Show:**
+- Keywords: `AS`, `ON`, `USING`
+
+**Examples:**
+```sql
+SELECT * FROM users JOIN orders |
+โ AS, ON, USING
+
+SELECT * FROM users u LEFT JOIN products p |
+โ ON, USING
+```
+
+---
+
+### 5-JOIN_ON. JOIN_ON (Expression Context)
+
+**JOIN_ON is an expression context.**
+
+It suggests columns and functions.
+
+**Column suggestions MUST be restricted strictly to tables in scope:**
+- FROM tables
+- JOIN tables
+- CTEs referenced in the statement
+- Derived tables (subquery aliases)
+
+**Critical restrictions:**
+
+See **Scope-Restricted Expression Contexts** section for complete rules.
+
+---
+
+#### 5d. After ON (without prefix)
+
+**Trigger:** After `ON` keyword in JOIN clause
+
+**Show:**
+- Columns from scope tables only (qualified, alias-first)
+- All functions
+
+**Examples:**
+```sql
+SELECT * FROM users JOIN orders ON |
+โ users.id, orders.user_id, orders.total, ...
+โ COUNT, SUM, AVG, ...
+
+SELECT * FROM users u JOIN orders o ON |
+โ u.id, o.user_id, o.total, ...
+โ COUNT, SUM, AVG, ...
+```
+
+#### 5e. After ON (with prefix)
+
+**Trigger:** After `ON` keyword with prefix in JOIN clause
+
+**Show:**
+- Columns matching the prefix (see **Generic Prefix Matching for Column Contexts** section)
+- Functions matching the prefix
+
+**Examples:**
+
+**Generic prefix (no alias exact match):**
+```sql
+SELECT * FROM users JOIN orders ON u|
+โ Context: JOIN_ON (scope-restricted)
+โ Table-name expansion: users.* (all columns from scope table 'users')
+โ Column-name matching: orders.user_id (scope table column only)
+โ Functions: UPPER, UUID, UNIX_TIMESTAMP
+โ (Database-wide columns excluded - scope restriction active)
+```
+
+**Alias exact match:**
+```sql
+SELECT * FROM users u JOIN orders o ON u|
+โ Context: JOIN_ON (scope-restricted)
+โ Alias-exact-match mode (u == alias 'u')
+โ u.id, u.name, u.email, u.password, u.is_enabled, u.created_at
+โ UPPER, UUID, UNIX_TIMESTAMP
+```
+
+#### 5f. After comparison operator
+
+**Trigger:** After `=`, `!=`, `<`, `>`, etc. in ON clause
+
+**Show:**
+- Literal keywords: `NULL`, `TRUE`, `FALSE`
+- Columns from scope tables only (qualified, alias-first) (filtered by prefix if present)
+- All functions (filtered by prefix if present)
+
+**Column ranking (HeidiSQL-like UX):**
+- Prioritize columns from the **other-side table** (typically the table being joined)
+- Then columns from other tables in scope
+- This helps users quickly find the matching column
+
+**Other-side table determination:**
+- If left side of operator has qualified column (e.g., `u.id = |`) โ other-side = all other tables in scope, prioritizing tables introduced by current JOIN
+- If left side is from derived table/CTE โ other-side = same logic
+- If left side is not recognizable โ fallback to scope table ordering (FROM > JOIN)
+
+**Critical:** Database-wide columns and `CURRENT_TABLE` are excluded (scope restriction active).
+
+**Examples:**
+```sql
+SELECT * FROM users JOIN orders ON users.id = |
+โ NULL, TRUE, FALSE
+โ orders.user_id, orders.id, ... (orders columns prioritized - other-side table)
+โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at, ... (users columns after)
+
+SELECT * FROM users u JOIN orders o ON u.id = |
+โ NULL, TRUE, FALSE
+โ o.user_id, o.id, ... (orders columns prioritized - other-side table)
+โ u.id, u.name, ... (users columns after)
+
+SELECT * FROM users u JOIN orders o ON u.id = o|
+โ o.user_id, o.id
+```
+
+#### 5g. After complete expression (logical operators)
+
+**Trigger:** After a complete condition/expression followed by space in ON clause
+
+**Show:**
+- Logical keywords: `AND`, `OR`, `NOT`
+- Other keywords: `WHERE`, `ORDER BY`, `GROUP BY`, `LIMIT`
+
+**Examples:**
+```sql
+SELECT * FROM users JOIN orders ON users.id = orders.user_id |
+โ AND, OR, WHERE, ORDER BY, LIMIT
+
+SELECT * FROM users u JOIN orders o ON u.id = o.user_id |
+โ AND, OR, NOT, WHERE, ORDER BY, LIMIT
+```
+
+---
+
+### 6. WHERE_CLAUSE (After WHERE)
+
+**Trigger:** After `WHERE`, `AND`, `OR`
+
+**Important:** Only show columns from tables specified in FROM and JOIN clauses (using alias if defined, otherwise table name).
+
+#### 6a. Without prefix
+
+**Show:**
+- All columns (qualified, alias-first)
+- All functions
+
+**Examples:**
+```sql
+SELECT * FROM users WHERE |
+โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at, ... (schema order)
+โ COUNT, SUM, AVG, ...
+
+SELECT * FROM users u WHERE |
+โ u.id, u.name, u.email, u.password, u.is_enabled, u.created_at, ... (schema order)
+โ COUNT, SUM, AVG, ...
+```
+
+#### 6b. With prefix
+
+**Show:**
+- Columns matching the prefix (see **Generic Prefix Matching for Column Contexts** section)
+- Functions matching the prefix
+
+**Examples:**
+```sql
+SELECT * FROM users WHERE u|
+โ Context: WHERE (scope-restricted)
+โ Scope: [users]
+โ Table-name expansion: users.* (scope table only)
+โ Column-name matching: (none - no scope table columns start with 'u' except from table expansion)
+โ Functions: UPPER, UUID, UNIX_TIMESTAMP
+โ (DB-wide columns excluded - scope restriction active)
+
+SELECT * FROM users u WHERE u|
+โ Context: WHERE (scope-restricted)
+โ Alias-exact-match mode (u == alias 'u')
+โ u.id, u.name, u.email, u.password, u.is_enabled, u.created_at
+โ UPPER, UUID, UNIX_TIMESTAMP
+```
+
+#### 6c. After comparison operator
+
+**Trigger:** After `=`, `!=`, `<`, `>`, `<=`, `>=`, `LIKE`, `IN`, etc. in WHERE clause
+
+**Show:**
+- Literal keywords: `NULL`, `TRUE`, `FALSE`, `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`
+- All columns (qualified, alias-first) (filtered by prefix if present)
+- All functions (filtered by prefix if present)
+
+**Examples:**
+```sql
+SELECT * FROM users WHERE id = |
+โ NULL, TRUE, FALSE
+โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at, ...
+โ COUNT, SUM, ...
+
+SELECT * FROM users WHERE is_enabled = |
+โ NULL, TRUE, FALSE
+โ users.is_enabled, ...
+
+SELECT * FROM users WHERE created_at > |
+โ CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP
+โ users.created_at, ...
+```
+
+**Note:** User can also type string literals (`'...'`) or numbers directly. Future enhancement: suggest `'...'` as snippet.
+
+#### 6d. After complete expression (logical operators)
+
+**Trigger:** After a complete condition/expression followed by space
+
+**Show:**
+- Logical keywords: `AND`, `OR`, `NOT`, `EXISTS`, `IN`, `BETWEEN`, `LIKE`, `IS NULL`, `IS NOT NULL`
+- Other keywords: `ORDER BY`, `GROUP BY`, `LIMIT`, `HAVING`
+
+**Examples:**
+```sql
+SELECT * FROM users WHERE id = 1 |
+โ AND, OR, ORDER BY, GROUP BY, LIMIT
+
+SELECT * FROM users WHERE status = 'active' |
+โ AND, OR, NOT, ORDER BY, LIMIT
+
+SELECT * FROM users WHERE id > 10 |
+โ AND, OR, BETWEEN, ORDER BY, LIMIT
+```
+
+---
+
+### 7. ORDER_BY_CLAUSE (After ORDER BY)
+
+**Trigger:** After `ORDER BY`
+
+**Important:** Only show columns from tables specified in FROM and JOIN clauses (using alias if defined, otherwise table name).
+
+#### 7a. Without prefix
+
+**Show:**
+- Columns in scope (qualified, alias-first)
+- Functions
+- Keywords: `ASC`, `DESC`, `NULLS FIRST`, `NULLS LAST`
+
+**Ordering:** Columns first, then functions, then keywords (ASC/DESC). Users typically need to choose the column before specifying sort direction.
+
+**Examples:**
+```sql
+SELECT * FROM users ORDER BY |
+โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at, ... (columns first)
+โ COUNT, SUM, AVG, ... (functions)
+โ ASC, DESC, NULLS FIRST, NULLS LAST (keywords last)
+
+SELECT * FROM users u JOIN orders o ON u.id = o.user_id ORDER BY |
+โ u.id, u.name, o.total, o.created_at, ... (columns first)
+โ COUNT, SUM, AVG, ... (functions)
+โ ASC, DESC (keywords last)
+```
+
+#### 7b. With prefix
+
+**Show:**
+- Columns matching the prefix (see **Generic Prefix Matching for Column Contexts** section)
+- Functions matching the prefix
+- Keywords matching the prefix
+
+**Examples:**
+```sql
+SELECT * FROM users ORDER BY c|
+โ Context: ORDER_BY (scope-restricted)
+โ Scope: [users]
+โ Table-name expansion: (none - no scope table starts with 'c')
+โ Column-name matching: users.created_at (scope table column only)
+โ Functions: COUNT, CONCAT, COALESCE
+โ Keywords: (none starting with 'c')
+โ (DB-wide columns excluded - scope restriction active)
+```
+
+#### 7c. After column (space after column)
+
+**Show:**
+- Keywords: `ASC`, `DESC`, `NULLS FIRST`, `NULLS LAST`
+
+**Examples:**
+```sql
+SELECT * FROM users ORDER BY created_at |
+โ ASC, DESC, NULLS FIRST, NULLS LAST
+```
+
+#### 7d. After comma (multiple sort keys)
+
+**Trigger:** After comma in ORDER BY clause
+
+**Show:**
+- Columns in scope (qualified, alias-first) (filtered by prefix if present)
+- Functions (filtered by prefix if present)
+
+**Examples:**
+```sql
+SELECT * FROM users ORDER BY created_at DESC, |
+โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at, ...
+โ COUNT, SUM, AVG, ...
+
+SELECT * FROM users u ORDER BY u.created_at DESC, n|
+โ u.name
+```
+
+---
+
+### 8. GROUP_BY_CLAUSE (After GROUP BY)
+
+**Trigger:** After `GROUP BY`
+
+**Important:** Only show columns from tables specified in FROM and JOIN clauses (using alias if defined, otherwise table name).
+
+#### 8a. Without prefix
+
+**Show:**
+- Columns in scope (qualified, alias-first)
+- Functions
+
+**Examples:**
+```sql
+SELECT COUNT(*) FROM users GROUP BY |
+โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at, ...
+โ DATE, YEAR, MONTH, ...
+
+SELECT COUNT(*) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY |
+โ u.id, u.name, u.email, o.status, ...
+โ DATE, YEAR, MONTH, ...
+```
+
+#### 8b. With prefix
+
+**Show:**
+- Columns matching the prefix (see **Generic Prefix Matching for Column Contexts** section)
+- Functions matching the prefix
+
+**Examples:**
+```sql
+SELECT COUNT(*) FROM users GROUP BY s|
+โ Table-name expansion: (none - no tables starting with 's')
+โ Column-name matching: (none - no columns starting with 's' in users table)
+โ Functions: SUM, SUBSTR
+```
+
+#### 8c. After comma (multiple group keys)
+
+**Trigger:** After comma in GROUP BY clause
+
+**Show:**
+- Columns in scope (qualified, alias-first) (filtered by prefix if present)
+- Functions (filtered by prefix if present)
+
+**Examples:**
+```sql
+SELECT COUNT(*) FROM users GROUP BY status, |
+โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at, ...
+โ DATE, YEAR, MONTH, ...
+
+SELECT COUNT(*) FROM users u GROUP BY u.is_enabled, c|
+โ u.created_at
+```
+
+---
+
+### 9. HAVING_CLAUSE (After HAVING)
+
+**Trigger:** After `HAVING`
+
+**Important:** Only show columns from tables specified in FROM and JOIN clauses. Focus on aggregate functions.
+
+**Aggregate functions definition:** Predefined set of functions per SQL dialect that perform aggregation operations. Standard set includes: `COUNT`, `SUM`, `AVG`, `MAX`, `MIN`. Vendor-specific additions: `GROUP_CONCAT` (MySQL), `STRING_AGG` (PostgreSQL), `LISTAGG` (Oracle), `ARRAY_AGG`, etc. This list is dialect-dependent and should be maintained as a constant set in the implementation.
+
+#### 9a. Without prefix
+
+**Show:**
+- Aggregate functions (prioritized): from the predefined aggregate functions set for current dialect
+- Columns in scope (qualified, alias-first)
+- Other functions (non-aggregate)
+
+**Ordering:** Aggregate functions first (alphabetical), then columns (schema order - NOT alphabetical), then other functions (alphabetical).
+
+**Rationale:** HAVING typically filters aggregates; prioritizing aggregate functions reduces keystrokes and improves UX.
+
+**Note:** Columns preserve their table definition order (ordinal_position), consistent with global ordering rules.
+
+**Examples:**
+```sql
+SELECT status, COUNT(*) FROM users GROUP BY status HAVING |
+โ COUNT, SUM, AVG, MAX, MIN, ... (aggregate functions first, alphabetical)
+โ users.id, users.name, users.email, ... (columns in schema order, NOT alphabetical)
+โ CONCAT, UPPER, LOWER, ... (other functions, alphabetical)
+```
+
+#### 9b. With prefix
+
+**Show:**
+- Aggregate functions matching the prefix (prioritized): from the predefined aggregate functions set for current dialect
+- Columns matching the prefix (see **Generic Prefix Matching for Column Contexts** section)
+- Other functions matching the prefix (non-aggregate)
+
+**Ordering:** Aggregate functions first (alphabetical), then columns (schema order - NOT alphabetical), then other functions (alphabetical).
+
+**Note:** Columns preserve their table definition order (ordinal_position), consistent with global ordering rules.
+
+**Examples:**
+```sql
+SELECT status, COUNT(*) FROM users GROUP BY status HAVING c|
+โ COUNT (aggregate function first, alphabetical)
+โ Table-name expansion: customers.* (columns in schema order)
+โ Column-name matching: users.created_at
+โ CONCAT, COALESCE (other functions, alphabetical)
+```
+
+#### 9c. After comparison operator
+
+**Show:**
+- Literal keywords: `NULL`, `TRUE`, `FALSE`
+- Aggregate functions
+- Columns
+- Numbers (user types directly)
+
+**Examples:**
+```sql
+SELECT status, COUNT(*) FROM users GROUP BY status HAVING COUNT(*) > |
+โ NULL, TRUE, FALSE
+โ COUNT, SUM, AVG, ...
+โ (user can type number)
+```
+
+#### 9d. After complete expression (logical operators)
+
+**Trigger:** After a complete condition/expression followed by space
+
+**Show:**
+- Logical keywords: `AND`, `OR`, `NOT`, `EXISTS`
+- Other keywords: `ORDER BY`, `LIMIT`
+
+**Examples:**
+```sql
+SELECT status, COUNT(*) FROM users GROUP BY status HAVING COUNT(*) > 10 |
+โ AND, OR, ORDER BY, LIMIT
+
+SELECT status, COUNT(*) FROM users GROUP BY status HAVING SUM(total) > 1000 |
+โ AND, OR, NOT, ORDER BY, LIMIT
+```
+
+---
+
+### 10. LIMIT_OFFSET_CLAUSE (After LIMIT or OFFSET)
+
+**Trigger:** After `LIMIT` or `OFFSET`
+
+**Show:**
+- Nothing (user types number directly)
+
+**Examples:**
+```sql
+SELECT * FROM users LIMIT |
+โ (no suggestions - user types number)
+
+SELECT * FROM users LIMIT 10 OFFSET |
+โ (no suggestions - user types number)
+```
+
+**Note:** No autocomplete suggestions in this context. User types numeric values freely. This avoids noise and keeps the implementation simple.
+
+---
+
+## Ordering Rules
+
+Suggestions are always ordered by priority:
+
+**Ordering Rules apply after applying scope restrictions.**
+
+**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 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)
+
+**Column display format:** Inside every column group, use `alias.column` when the table has an alias in the current statement; otherwise use `table.column`. (Dot-completion returns unqualified column names.)
+
+**Exception:** In HAVING clause context, aggregate functions are prioritized before columns (see section 9a, 9b for details). This is the only context where functions appear before columns.
+
+**Important:** Examples throughout this document may show columns in alphabetical order for readability, but the actual implementation must return columns in their table definition order (ordinal_position from schema). When in doubt, the rule is: preserve schema order, NOT alphabetical order.
+
+1. **Columns from CURRENT_TABLE** (if set in context, e.g., table editor)
+ - Use `alias.column` format if the table has an alias in the current query, otherwise `table.column`
+ - 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)
+ - Use `alias.column` format if the table has an alias, otherwise `table.column`
+ - Columns preserve their definition order (ordinal position in the table schema). They must NOT be reordered alphabetically.
+ - When multiple FROM tables exist, follow their appearance order in the query; within each table, preserve column definition order.
+
+3. **Columns from tables in JOIN clause** (if any)
+ - Use `alias.column` format if the table has an alias, otherwise `table.column`
+ - Columns preserve their definition order (ordinal position in the table schema). They must NOT be reordered alphabetically.
+ - When multiple JOIN tables exist, follow their appearance order in the query; within each table, preserve column definition order.
+
+4. **All table.column from database** (all other tables not in FROM/JOIN)
+ - 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):** 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 (filtered results are manageable)
+
+5. **Functions**
+ - Alphabetically within this group
+
+6. **Out-of-Scope Table Hints** (SELECT_LIST with scope only)
+ - Format: `table_name (+ Add via FROM or JOIN)`
+ - Only when prefix matches DB-wide tables but no scope tables/columns
+ - See **Out-of-Scope Table Hints** section for details
+
+7. **Keywords**
+ - Alphabetically within this group
+
+---
+
+### Alias Prefix Disambiguation
+
+**Applies in expression contexts** (WHERE, ON, ORDER BY, GROUP BY, HAVING, SELECT_LIST)
+
+**Note:** In SELECT_LIST, alias-prefix disambiguation applies only when FROM/JOIN tables are available in the current statement. Without FROM/JOIN, SELECT_LIST shows functions + keywords (see section 3a).
+
+**Note:** In ORDER BY / GROUP BY, exact alias match still activates alias-prefix mode (same as other contexts). However, this is most relevant when the prefix is immediately followed by `.` (dot-completion) or when the typed token exactly equals an alias. Otherwise generic prefix matching applies (column names are common in these contexts).
+
+**Critical: Exact Match Rule**
+
+Alias-prefix mode activates **only if the token exactly equals an alias** (not startswith). This avoids ambiguity with multiple aliases.
+
+**Rule:**
+- `token == alias` โ alias-prefix mode โ
+- `token.startswith(alias)` โ generic prefix mode โ
+
+**Why exact match?**
+- Avoids ambiguity with multiple aliases (e.g., `u` and `us`)
+- Prevents false positives (e.g., `user|` should not trigger alias `u`)
+
+**Behavior:**
+- If prefix **exactly equals** an alias: show only that alias' columns first (e.g., `u.id`, `u.name`)
+- If prefix does NOT exactly match an alias: treat as generic prefix (match table name, column name, or function name)
+
+**Deduplication in alias-exact-match mode:**
+- When alias-exact-match mode is active, do NOT also emit the same columns qualified with the base table name
+- Deduplicate by underlying column identity (e.g., if showing `u.id`, do not also show `users.id`)
+- This avoids redundancy and keeps suggestions clean
+
+**Interaction with CURRENT_TABLE:**
+- In alias-prefix mode, CURRENT_TABLE priority is ignored; alias columns are always ranked first
+- This avoids unexpected behavior in table editor when using aliases
+
+**Examples:**
+
+**Exact match - Alias-prefix mode:**
+```sql
+SELECT * FROM users u JOIN orders o WHERE u|
+โ token = "u"
+โ alias "u" exists โ exact match โ
+โ u.id, u.name, u.email, ... (alias 'u' columns prioritized)
+โ UPPER, UUID (functions matching 'u')
+โ (do not show users.id, users.name - avoid redundancy)
+```
+
+**No exact match - Generic prefix mode:**
+```sql
+SELECT * FROM users u JOIN orders o WHERE us|
+โ token = "us"
+โ aliases: u, o
+โ "us" != "u" and "us" != "o" โ no exact match โ
+โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at, ... (table starts with 'us')
+โ (generic prefix matching)
+
+SELECT * FROM users u WHERE user|
+โ token = "user"
+โ alias "u" exists but "user" != "u" โ no exact match โ
+โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at, ... (table starts with 'user')
+โ orders.user_id (column starts with 'user')
+โ (generic prefix matching, NOT alias-prefix)
+```
+
+**No alias in query - Generic prefix mode:**
+```sql
+SELECT * FROM users JOIN orders ON u|
+โ token = "u"
+โ Context: JOIN_ON (scope-restricted)
+โ no aliases defined โ generic prefix
+โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at (scope table starts with 'u')
+โ orders.user_id (scope table column starts with 'u')
+โ UPPER, UUID (functions start with 'u')
+โ (Database-wide columns excluded - scope restriction active)
+```
+
+**Note:** This rule applies only to tokens without dot. `u.|` triggers Dot-Completion, not alias-prefix disambiguation.
+
+---
+
+### Generic Prefix Matching for Column Contexts
+
+**Applies to all column-expression contexts:** SELECT_LIST, WHERE_CLAUSE, JOIN_ON, ORDER_BY, GROUP_BY, HAVING, and any additional expression contexts where columns can be inserted.
+
+**When NOT in dot-completion and NOT in alias-exact-match mode:**
+
+Given a prefix P (token immediately before cursor, without '.'):
+
+**Return qualified column suggestions that include BOTH:**
+
+**A) Table-name match expansion:**
+- For EVERY table T whose name startswith(P), return ALL columns of T as qualified column suggestions
+- Qualification: use `alias.column` if table is in current statement scope and has alias, otherwise `table.column`
+
+**B) Column-name match:**
+- For EVERY column C (from all tables in scope and all other database tables) whose column name startswith(P), return it as qualified column suggestion
+- Qualification: use `alias.column` if table is in current statement scope and has alias, otherwise `table.column`
+
+**Scope restriction:**
+
+**Scope-restricted expression contexts (WHERE, JOIN_ON, ORDER_BY, GROUP_BY, HAVING):**
+
+**Hard line:** In scope-restricted expression contexts, both table-name expansion and column-name matching MUST be computed over scope tables only.
+
+See **Scope-Restricted Expression Contexts** section for complete rules.
+
+**SELECT_LIST without scope tables:**
+- `CURRENT_TABLE` columns MUST be included first (if set)
+- Database-wide table-name expansion and column-name matching are included
+
+**SELECT_LIST with scope tables:**
+- `CURRENT_TABLE` columns MUST be included ONLY if `CURRENT_TABLE` is in scope
+- Database-wide table-name expansion and column-name matching are included
+- Scope table columns are included with alias-first qualification
+
+**Important rules:**
+- Do NOT suggest bare table names in column-expression contexts; only columns (qualified)
+- Deduplicate identical suggestions (if a column appears via both A and B, show it once)
+- Apply global Ordering Rules (CURRENT_TABLE > FROM > JOIN > DB > FUNCTIONS > KEYWORDS)
+- Performance guardrail: see Ordering Rules group 4 (applies only to DB-wide columns when no prefix)
+
+**Examples:**
+
+**SELECT_LIST with scope tables (database-wide columns included):**
+```sql
+SELECT u| FROM orders
+โ Prefix: "u"
+โ Context: SELECT_LIST (database-wide columns allowed)
+โ Table-name expansion: users table starts with 'u' โ users.id, users.name, users.email, users.password, users.is_enabled, users.created_at
+โ Column-name matching: orders.user_id (scope table column starts with 'u'), products.unit_price (database-wide column starts with 'u')
+โ Functions: UPPER, UUID, UNIX_TIMESTAMP
+โ Combined (deduplicated): users.id, users.name, users.email, users.password, users.is_enabled, users.created_at, orders.user_id, products.unit_price, UPPER, UUID, UNIX_TIMESTAMP
+```
+
+**WHERE with scope tables (database-wide columns excluded):**
+```sql
+SELECT * FROM users u WHERE us|
+โ Prefix: "us"
+โ Context: WHERE (scope tables exist โ database-wide columns disabled)
+โ Alias "u" exists but "us" != "u" โ NOT alias-exact-match mode
+โ Table-name expansion: users table starts with 'us' โ u.id, u.name, u.email, u.password, u.is_enabled, u.created_at (uses alias)
+โ Column-name matching: restricted to scope tables only (none in this example)
+โ Combined: u.id, u.name, u.email, u.password, u.is_enabled, u.created_at
+```
+
+**Deduplication example:**
+```sql
+SELECT u| FROM users
+โ Table-name expansion: users.* (all columns)
+โ Column-name matching: users.updated_at (if such column exists and starts with 'u')
+โ Deduplication: users.updated_at appears in both โ show once
+```
+
+**Applies to all column contexts:**
+- SELECT_LIST: `SELECT u|` or `SELECT u| FROM users`
+- WHERE: `SELECT * FROM users WHERE u|`
+- JOIN ON: `SELECT * FROM users u JOIN orders o ON u.id = o.u|`
+- ORDER BY: `SELECT * FROM users ORDER BY u|`
+- GROUP BY: `SELECT * FROM users GROUP BY u|`
+- HAVING: `SELECT status, COUNT(*) FROM users GROUP BY status HAVING u|`
+
+**Example - Alias-prefix overrides CURRENT_TABLE:**
+```sql
+-- CURRENT_TABLE = users (in table editor)
+SELECT * FROM users u JOIN orders o WHERE u|
+โ token = "u"
+โ exact match with alias "u" โ alias-prefix mode โ
+โ u.id, u.name, u.email, ... (alias columns first)
+โ CURRENT_TABLE priority ignored in this case
+```
+
+**Example in table editor context (CURRENT_TABLE = users, no alias):**
+```sql
+SELECT u|
+โ users.id (CURRENT_TABLE column)
+โ users.name (CURRENT_TABLE column)
+โ orders.user_id (database column)
+โ products.unit (database column)
+โ UPPER (FUNCTION)
+โ UUID (FUNCTION)
+โ UPDATE (KEYWORD)
+```
+
+**Example in table editor context (CURRENT_TABLE = users, with alias 'u'):**
+```sql
+SELECT * FROM users u WHERE id = 1; SELECT u|
+โ u.id (CURRENT_TABLE column with alias)
+โ u.name (CURRENT_TABLE column with alias)
+โ orders.user_id (database column)
+โ products.unit (database column)
+โ UPPER (FUNCTION)
+โ UUID (FUNCTION)
+โ UPDATE (KEYWORD)
+```
+
+**Example in query with FROM:**
+```sql
+SELECT * FROM users WHERE u|
+โ users.id (FROM table column)
+โ users.name (FROM table column)
+โ orders.user_id (database column)
+โ UPPER (FUNCTION)
+โ UPDATE (KEYWORD)
+```
+
+**Example in query with JOIN:**
+```sql
+SELECT * FROM users u JOIN orders o WHERE u|
+โ u.id (FROM table column with alias)
+โ u.name (FROM table column with alias)
+โ o.user_id (JOIN table column with alias)
+โ products.price (database column)
+โ UPPER (FUNCTION)
+โ UPDATE (KEYWORD)
+```
+
+---
+
+### Out-of-Scope Table Hints (SELECT_LIST with Scope)
+
+**Applies ONLY in SELECT_LIST when scope tables already exist (FROM/JOIN present).**
+
+**Purpose:** Keep SELECT scope-safe (no DB-wide columns), while still allowing controlled table discovery.
+
+---
+
+#### Trigger Conditions
+
+In SELECT_LIST with scope tables:
+
+If prefix P satisfies ALL of:
+- No alias-exact-match
+- No scope table startswith(P)
+- No scope column startswith(P)
+- BUT one or more physical tables in the database startswith(P)
+
+Then:
+- DO NOT suggest DB-wide columns
+- Instead, suggest each matching table as an individual hint item
+
+---
+
+#### Suggestion Format
+
+Each table is a separate suggestion item:
+
+```
+users + Add via FROM/JOIN
+customers + Add via FROM/JOIN
+```
+
+---
+
+#### Behavior Rules
+
+- Each table is a separate suggestion item
+- Suggestion kind: `TABLE_HINT_OUT_OF_SCOPE`
+- No column suggestions for out-of-scope tables
+- Selecting this item MUST NOT auto-insert JOIN type
+- **Minimal v1 behavior:**
+ - Either insert just the table name
+ - Or act as a non-insert hint (implementation choice)
+- JOIN type (INNER/LEFT/RIGHT) remains user decision
+- **No badges:** Badges are reserved for column data types (INT, VARCHAR, etc.)
+
+---
+
+#### Ordering (within SELECT_LIST with scope)
+
+1. Scope columns
+2. Functions
+3. Out-of-scope table hints
+4. Keywords
+
+**Important:** Functions MUST appear before table hints.
+
+**Rationale:** When typing `SELECT c| FROM orders`, the user most likely intends `COUNT, COALESCE, CONCAT`, not `customers` (new table). Therefore, functions are prioritized over discovery hints.
+
+---
+
+#### Example
+
+**Assume:**
+- Scope = [orders]
+- Database tables = [orders, users, customers]
+
+**Query:**
+```sql
+SELECT u| FROM orders
+```
+
+**Suggestions:**
+```
+โ UPPER
+โ UUID
+โ users + Add via FROM/JOIN
+โ UPDATE
+```
+
+**NOT suggested:**
+```
+โ users.id
+โ customers.name
+โ any DB-wide columns
+```
+
+---
+
+#### Important Constraints
+
+- Applies ONLY to SELECT_LIST with existing scope
+- Does NOT apply to WHERE, JOIN_ON, GROUP_BY, HAVING, ORDER_BY
+- Does NOT apply when no scope exists (normal DB-wide allowed case)
+- Dot-completion behavior remains unchanged
+- Badges are reserved for column data types (INT, VARCHAR, etc.)
+
+---
+
+## Context Rules Summary Matrix
+
+**In case of ambiguity, detailed context sections override this summary matrix.**
+
+This table provides a quick reference for implementers to understand the behavior of each context.
+
+| Context | Scope Required | DB-wide Columns | CURRENT_TABLE | Table Hints |
+|---------|---------------|-----------------|---------------|-------------|
+| **SELECT_LIST (no scope)** | No | Yes | Yes (if set) | No |
+| **SELECT_LIST (with scope)** | Yes | Conditional* | 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 |
+| **JOIN_ON** | Yes | No | Only if in scope | No |
+| **WHERE** | Yes | No | Only if in scope | No |
+| **ORDER_BY** | Yes | No | Only if in scope | No |
+| **GROUP_BY** | Yes | No | Only if in scope | No |
+| **HAVING** | Yes | No | Only if in scope | No |
+
+**Legend:**
+- **Scope Required:** Whether the context requires scope tables to exist
+- **DB-wide Columns:** Whether columns from tables outside scope can be suggested
+- **CURRENT_TABLE:** Whether CURRENT_TABLE columns can be suggested
+- **Table Hints:** Whether out-of-scope table hints can be suggested
+
+**Notes:**
+- *SELECT_LIST with scope allows DB-wide columns for table-name expansion and column-name matching, but applies Out-of-Scope Table Hints when prefix matches only DB-wide tables
+- 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)
+
+---
+
+## Implementation Notes
+
+- 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
+
+---
+
+### 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 in statement) - convenience shortcut
+
+ Filtering:
+ - If prefix provided, filter by startswith(prefix)
+ - Exclude tables already present in the statement
+
+ 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
+ - If scope tables exist:
+ * CURRENT_TABLE included only if in scope; otherwise ignored
+ * Include scope table columns
+ * Include database-wide columns (for table-name expansion and column-name matching)
+
+ 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):**
+
+For cleaner architecture, consider using a `QueryScope` object instead of passing multiple parameters:
+
+```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
+
+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.
+
+---
+
+**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:
+
+```
+tables_in_scope = [
+ 1. Derived tables (subquery alias) in FROM/JOIN
+ 2. CTEs referenced in FROM/JOIN
+ 3. Physical tables in FROM/JOIN
+]
+```
+
+**Column resolution follows this order:**
+
+**Important:** Include CURRENT_TABLE columns only when allowed by context rules:
+- SELECT_LIST with no scope tables, OR
+- CURRENT_TABLE is in scope (present in FROM/JOIN)
+
+Otherwise omit CURRENT_TABLE entirely.
+
+1. **CURRENT_TABLE columns** (if allowed by context rules) - use alias if table has alias in query
+2. **Derived table columns** - use alias (only sensible name)
+3. **CTE columns** - use CTE name (acts as alias)
+4. **Physical table columns from FROM** - use alias if defined
+5. **Physical table columns from JOIN** - use alias if defined
+6. **Database columns** (all other tables, with guardrail - only in SELECT_LIST or when no scope restriction)
+
+**Example:**
+```sql
+WITH active_users AS (SELECT id, name FROM users WHERE status = 'active')
+SELECT * FROM (SELECT id, total FROM orders) AS o
+JOIN active_users au ON o.id = au.id
+WHERE |
+โ o.id, o.total (derived table, priority 2)
+โ au.id, au.name (CTE, priority 3)
+โ (no physical tables in this query)
+```
+
+**Note:** Alias-first is fundamental here - for derived tables and CTEs, the alias/CTE name is often the **only** sensible name (no underlying physical table name).
+
+### Scope Handling (Subqueries and CTEs)
+
+**Important:** Scope handling for subqueries and CTEs may be simplified initially.
+
+**Current approach:**
+- Use the nearest FROM/JOIN scope of the current statement
+- For simple subqueries in WHERE clause (e.g., `WHERE x IN (SELECT ...)`), context detection operates on the outer query scope
+- Full nested scope resolution (tracking which tables are available in inner vs outer queries) is a future enhancement
+
+**Examples:**
+```sql
+-- Simple case: outer query scope
+SELECT * FROM users WHERE id IN (SELECT user_id FROM orders) AND |
+โ Suggests columns from 'users' (outer query scope)
+
+-- Future enhancement: inner query scope
+SELECT * FROM users WHERE id IN (SELECT | FROM orders)
+โ Should suggest columns from 'orders' (inner query scope)
+โ Initial implementation may use outer scope or simplified logic
+```
+
+**CTE Support (WITH clauses):**
+
+CTEs are increasingly common and should be considered for v1 implementation:
+
+```sql
+WITH active_users AS (SELECT * FROM users WHERE status = 'active')
+SELECT * FROM active_users WHERE |
+โ active_users.id, active_users.name, ... (CTE columns)
+```
+
+**Basic CTE support:**
+- Treat CTEs as available tables in the query scope
+- CTE name acts like a table name in FROM/JOIN contexts
+- Columns from CTE should be resolved (if CTE definition is parseable)
+- **CTE visibility is limited to the statement where they are defined**
+
+**Example - CTE scope:**
+```sql
+WITH a AS (...)
+SELECT * FROM a;
+SELECT * FROM |
+โ CTE 'a' is NOT visible here (different statement)
+โ Show only physical tables
+```
+
+**Advanced CTE features (future enhancement):**
+- Recursive CTEs
+- Multiple CTEs with dependencies
+- CTE column aliasing
+
+**Subquery Aliases (Derived Tables):**
+
+Subqueries with aliases (derived tables) should also be considered for v1:
+
+```sql
+SELECT * FROM (SELECT id, name FROM users) AS u WHERE |
+โ u.id, u.name (derived table columns)
+```
+
+**Basic support:**
+- Treat aliased subquery as a table in scope
+- Resolve columns from the subquery SELECT list (if parseable)
+- Alias acts like a table name
+
+**Note:** This is similar to CTE support but inline. If CTEs are supported, derived tables should follow the same pattern.
+
+**Window Functions (Future Enhancement):**
+
+Window functions with OVER clause are common in modern SQL:
+
+```sql
+SELECT *, ROW_NUMBER() OVER |
+โ (PARTITION BY, ORDER BY)
+
+SELECT *, ROW_NUMBER() OVER (PARTITION BY |
+โ Columns in scope (qualified, alias-first)
+
+SELECT *, ROW_NUMBER() OVER (PARTITION BY status ORDER BY |
+โ Columns in scope (qualified, alias-first)
+```
+
+**Future support:**
+- Detect OVER clause context
+- After `OVER (` suggest keywords: `PARTITION BY`, `ORDER BY`
+- After `PARTITION BY` suggest columns in scope
+- After `ORDER BY` suggest columns in scope + `ASC`, `DESC`
+
+**Note:** This is a specialized context that can be added after core functionality is stable.
+
+---
+
+### 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
+
+The statement separator is NOT hardcoded.
+
+It is determined at runtime using:
+
+```
+effective_separator = user_override or engine_default
+```
+
+Constraints:
+- The separator MUST be a single character.
+- Multi-character separators (e.g., "GO") are NOT supported.
+- Default for all supported engines (MySQL, MariaDB, PostgreSQL, SQLite) is `";"`
+
+Validation:
+- If `user_override` is set, it MUST be exactly 1 character after trimming (e.g., `" ; "` is invalid).
+- If invalid โ ignore override and fallback to `engine_default`.
+
+All multi-query splitting logic MUST use the effective separator.
+Hardcoding `";"` is forbidden.
+
+---
+
+### Multi-Query Support
+
+**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
+
+**Edge cases:**
+- If cursor is on the separator, treat it as "end of previous statement".
+- Empty statements are ignored (no context); fallback to EMPTY.
+
+**Example:**
+```sql
+SELECT * FROM users WHERE id = 1;
+SELECT * FROM orders WHERE | โ cursor here
+SELECT * FROM products;
+```
+
+Context detection should analyze only: `SELECT * FROM orders WHERE |`
+
+**Critical:**
+- Do NOT use simple `text.split(effective_separator)`
+- The separator must be ignored inside:
+ - Strings (`'...'`, `"..."`)
+ - Comments (`--`, `/* */`)
+ - Dollar-quoted strings (PostgreSQL: `$$...$$`)
+
+**Recommended approach:**
+- Use sqlglot lexer/tokenizer to find statement boundaries (handles strings/comments correctly)
+- 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.
+
+**Matching rule:** Use `startswith()` on normalized text (single spaces, case-insensitive). Normalize both the user input and the keyword before matching.
+
+**Examples:**
+- User types `ORDE|` โ normalized input: `"orde"` โ matches `ORDER BY` (normalized: `"order by"`) โ
+- User types `ORDER B|` โ normalized input: `"order b"` โ matches `ORDER BY` (normalized: `"order by"`) โ
+- User types `NULLS L|` โ normalized input: `"nulls l"` โ matches `NULLS LAST` (normalized: `"nulls last"`) โ
+- User types `IS N|` โ normalized input: `"is n"` โ matches `IS NULL`, `IS NOT NULL` โ
+
+---
+
+### Spacing After Completion
+
+- **Keywords:** Space added after keywords (e.g., `SELECT `, `FROM `, `WHERE `, `JOIN `, `AS `)
+ - Multi-word keywords are treated as keywords for spacing (space appended after `ORDER BY`, `GROUP BY`, `IS NULL`, etc.)
+- **Columns:** No space added (e.g., `users.id|` allows immediate `,` or space)
+- **Tables:** No space added (e.g., `users|` allows immediate space or alias)
+- **Functions:** No space added by default
+ - **Future enhancement:** Consider function snippets with cursor positioning (e.g., `COUNT(|)` where `|` is cursor position)
+ - This would require snippet support in the autocomplete system
diff --git a/windows/components/stc/autocomplete/auto_complete.py b/windows/components/stc/autocomplete/auto_complete.py
new file mode 100644
index 0000000..4d75bfb
--- /dev/null
+++ b/windows/components/stc/autocomplete/auto_complete.py
@@ -0,0 +1,320 @@
+from typing import Callable, Optional
+
+import wx
+import wx.stc
+
+from helpers.logger import logger
+
+from structures.engines.database import SQLDatabase, SQLTable
+
+from windows.components.stc.autocomplete.autocomplete_popup import AutoCompletePopup
+from windows.components.stc.autocomplete.completion_types import CompletionItem, CompletionItemType, CompletionResult
+from windows.components.stc.autocomplete.context_detector import ContextDetector
+from windows.components.stc.autocomplete.dot_completion_handler import DotCompletionHandler
+from windows.components.stc.autocomplete.statement_extractor import StatementExtractor
+from windows.components.stc.autocomplete.suggestion_builder import SuggestionBuilder
+
+from windows.state import CURRENT_SESSION
+
+
+class SQLCompletionProvider:
+ def __init__(
+ self,
+ get_database: Callable[[], Optional[SQLDatabase]],
+ get_current_table: Optional[Callable[[], Optional[SQLTable]]] = None,
+ *,
+ is_filter_editor: bool = False,
+ ) -> None:
+ self._get_database = get_database
+ self._get_current_table = get_current_table or (lambda: None)
+ self._is_filter_editor = is_filter_editor
+ self._cached_database_id: Optional[int] = None
+
+ self._context_detector: Optional[ContextDetector] = None
+ self._dot_handler: Optional[DotCompletionHandler] = None
+ self._statement_extractor = StatementExtractor()
+
+ 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]:
+ try:
+ database = self._get_database()
+ if database is None:
+ return None
+
+ self._update_cache(database=database)
+
+ safe_pos = self._clamp_position(pos=pos, text=text)
+
+ statement, relative_pos = self._statement_extractor.extract_current_statement(text, safe_pos)
+
+ if not self._context_detector:
+ return None
+
+ context, scope, prefix = self._context_detector.detect(statement, relative_pos, database)
+ scope.current_table = self._get_current_table()
+
+ if self._dot_handler:
+ self._dot_handler.refresh(database, scope)
+ if self._dot_handler.is_dot_completion(statement, relative_pos):
+ items, prefix = self._dot_handler.get_completions(statement, relative_pos)
+ if items is not None:
+ return CompletionResult(items=tuple(items), prefix=prefix or "", prefix_length=len(prefix) if prefix else 0)
+
+ builder = SuggestionBuilder(database, scope.current_table)
+ items = builder.build(context, scope, prefix, statement)
+
+ return CompletionResult(items=tuple(items), prefix=prefix, prefix_length=len(prefix))
+ except Exception as ex:
+ logger.error(ex, exc_info=True)
+ return None
+
+ @staticmethod
+ def _clamp_position(*, pos: int, text: str) -> int:
+ if pos < 0:
+ return 0
+ if pos > len(text):
+ return len(text)
+ return pos
+
+ def _update_cache(self, *, database: SQLDatabase) -> None:
+ database_id = id(database)
+ if self._cached_database_id != database_id:
+ self._cached_database_id = database_id
+
+ dialect = self._get_current_dialect()
+ self._context_detector = ContextDetector(dialect)
+ self._dot_handler = DotCompletionHandler(database, None)
+
+
+class SQLAutoCompleteController:
+ def __init__(
+ self,
+ editor: wx.stc.StyledTextCtrl,
+ provider: SQLCompletionProvider,
+ *,
+ settings: Optional[object] = None,
+ theme_loader: Optional[object] = None,
+ debounce_ms: int = 80,
+ is_enabled: bool = True,
+ min_prefix_length: int = 1,
+ ) -> None:
+ self._editor = editor
+ self._provider = provider
+ self._settings = settings
+ self._theme_loader = theme_loader
+
+ if settings:
+ self._debounce_ms = settings.get_value("settings", "autocomplete", "debounce_ms") or debounce_ms
+ self._min_prefix_length = settings.get_value("settings", "autocomplete", "min_prefix_length") or min_prefix_length
+ self._add_space_after_completion = settings.get_value("settings", "autocomplete", "add_space_after_completion")
+ if self._add_space_after_completion is None:
+ self._add_space_after_completion = True
+ else:
+ self._debounce_ms = debounce_ms
+ self._min_prefix_length = min_prefix_length
+ self._add_space_after_completion = True
+
+ self._is_enabled = is_enabled
+
+ self._is_showing = False
+ self._pending_call: Optional[wx.CallLater] = None
+ self._popup: Optional[AutoCompletePopup] = None
+ self._current_result: Optional[CompletionResult] = None
+
+ self._editor.Bind(wx.stc.EVT_STC_CHARADDED, self._on_char_added)
+ self._editor.Bind(wx.EVT_KEY_DOWN, self._on_key_down)
+
+ def set_enabled(self, is_enabled: bool) -> None:
+ self._is_enabled = is_enabled
+ if not is_enabled:
+ self._cancel_pending()
+ self._hide_popup()
+
+ def get_effective_separator(self) -> str:
+ if self._settings:
+ separator = self._settings.get_value("query_editor", "statement_separator")
+ if separator:
+ return separator
+
+ session = CURRENT_SESSION.get_value()
+ if session and hasattr(session, 'context'):
+ return session.context.DEFAULT_STATEMENT_SEPARATOR
+
+ return ";"
+
+ def show(self, *, force: bool) -> None:
+ if not self._is_enabled:
+ return
+ if self._is_showing:
+ return
+
+ self._is_showing = True
+ try:
+ pos = self._editor.GetCurrentPos()
+ text = self._editor.GetText()
+
+ result = self._provider.get(pos=pos, text=text)
+
+ if result is None:
+ self._hide_popup()
+ return
+
+ if not result.items:
+ self._hide_popup()
+ return
+
+ self._current_result = result
+ items = self._unique_sorted_items(items=result.items)
+ self._show_popup(items)
+ except Exception as ex:
+ logger.error(f"Error in show(): {ex}", exc_info=True)
+ finally:
+ self._is_showing = False
+
+ def _show_popup(self, items: list[CompletionItem]) -> None:
+ if not self._popup:
+ self._popup = AutoCompletePopup(
+ self._editor,
+ settings=self._settings,
+ theme_loader=self._theme_loader
+ )
+ self._popup.set_on_item_selected(self._on_item_completed)
+
+ caret_pos = self._editor.GetCurrentPos()
+ point = self._editor.PointFromPosition(caret_pos)
+ screen_point = self._editor.ClientToScreen(point)
+
+ line_height = self._editor.TextHeight(self._editor.GetCurrentLine())
+ popup_position = wx.Point(screen_point.x, screen_point.y + line_height)
+
+ self._popup.show_items(items, popup_position)
+
+ def _hide_popup(self) -> None:
+ if self._popup and self._popup.IsShown():
+ self._popup.Hide()
+
+ def _on_item_completed(self, item: CompletionItem) -> None:
+ if not self._current_result:
+ return
+
+ current_pos = self._editor.GetCurrentPos()
+ start_pos = current_pos - self._current_result.prefix_length
+
+ self._editor.SetSelection(start_pos, current_pos)
+
+ should_add_space = 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)
+
+ self._current_result = None
+ self._hide_popup()
+
+ if should_add_space:
+ trigger_keywords = ['SELECT', 'FROM', 'JOIN', 'UPDATE', 'INTO', 'WHERE', 'AND', 'OR']
+ if item.name.upper() in trigger_keywords:
+ wx.CallAfter(lambda: self._schedule_show(force=False))
+
+ def _on_key_down(self, event: wx.KeyEvent) -> None:
+ if not self._is_enabled:
+ event.Skip()
+ return
+
+ key_code = event.GetKeyCode()
+
+ if key_code == wx.WXK_SPACE:
+ if self._popup and self._popup.IsShown():
+ self._cancel_pending()
+ self._hide_popup()
+ if not event.ControlDown():
+ event.Skip()
+ return
+
+ if event.ControlDown() and key_code == wx.WXK_SPACE:
+ self._cancel_pending()
+ self.show(force=True)
+ return
+
+ if key_code == wx.WXK_TAB and self._popup and self._popup.IsShown():
+ self._cancel_pending()
+ selected_item = self._popup.get_selected_item()
+ if selected_item:
+ self._on_item_completed(selected_item)
+ return
+
+ if key_code == wx.WXK_ESCAPE and self._popup and self._popup.IsShown():
+ self._cancel_pending()
+ self._hide_popup()
+ return
+
+ if key_code == wx.WXK_BACK and self._popup and self._popup.IsShown():
+ event.Skip()
+ wx.CallAfter(self._schedule_show, force=False)
+ return
+
+ if key_code == wx.WXK_RETURN and self._popup and self._popup.IsShown():
+ self._cancel_pending()
+ selected_item = self._popup.get_selected_item()
+ if selected_item:
+ self._on_item_completed(selected_item)
+ return
+
+ event.Skip()
+
+ def _on_char_added(self, event: wx.stc.StyledTextEvent) -> None:
+ if not self._is_enabled:
+ return
+
+ key_code = event.GetKey()
+ character = chr(key_code)
+
+ if character == " ":
+ self._schedule_show(force=False)
+ return
+
+ if character.isalnum() or character in {"_", "."}:
+ self._schedule_show(force=False)
+
+ def _schedule_show(self, *, force: bool) -> None:
+ self._cancel_pending()
+ self._pending_call = wx.CallLater(self._debounce_ms, self.show, force=force)
+
+ def _cancel_pending(self) -> None:
+ if self._pending_call is None:
+ return
+ if self._pending_call.IsRunning():
+ self._pending_call.Stop()
+ self._pending_call = None
+
+ @staticmethod
+ def _unique_sorted_items(*, items: tuple[CompletionItem, ...]) -> list[CompletionItem]:
+ seen_names: set[str] = set()
+ unique_items: list[CompletionItem] = []
+
+ for item in items:
+ if item.name not in seen_names:
+ seen_names.add(item.name)
+ unique_items.append(item)
+
+ type_priority = {
+ CompletionItemType.COLUMN: 0,
+ CompletionItemType.TABLE: 1,
+ CompletionItemType.FUNCTION: 2,
+ CompletionItemType.KEYWORD: 3,
+ }
+
+ # Sort by type priority, but preserve order within same type
+ # This is important for TABLE items which have custom prioritization (e.g., referenced tables first)
+ def sort_key(item):
+ priority = type_priority.get(item.item_type, 999)
+ # For TABLE items, preserve the order from backend (don't sort alphabetically)
+ # For other types, sort alphabetically within the type
+ if item.item_type == CompletionItemType.TABLE:
+ # Use original index to preserve order
+ return (priority, items.index(item))
+ else:
+ return (priority, item.name.upper())
+
+ return sorted(unique_items, key=sort_key)
diff --git a/windows/components/stc/autocomplete/autocomplete_popup.py b/windows/components/stc/autocomplete/autocomplete_popup.py
new file mode 100644
index 0000000..e7ada99
--- /dev/null
+++ b/windows/components/stc/autocomplete/autocomplete_popup.py
@@ -0,0 +1,166 @@
+import wx
+import wx.dataview
+
+from windows.components.stc.autocomplete.completion_types import CompletionItem, CompletionItemType
+from windows.components.stc.theme_loader import ThemeLoader
+
+
+class AutoCompletePopup(wx.PopupWindow):
+ def __init__(self, parent: wx.Window, settings: object = None, theme_loader: ThemeLoader = None) -> None:
+ super().__init__(parent, wx.BORDER_SIMPLE)
+
+ self._selected_index: int = 0
+ self._items: list[CompletionItem] = []
+ self._on_item_selected: callable = None
+ self._settings = settings
+ self._theme_loader = theme_loader
+
+ if settings:
+ self._popup_width = settings.get_value("settings", "autocomplete", "popup_width") or 300
+ self._popup_max_height = settings.get_value("settings", "autocomplete", "popup_max_height") or 10
+ 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,
+ CompletionItemType.FUNCTION: wx.ART_EXECUTABLE_FILE,
+ 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),
+ CompletionItemType.TABLE: wx.Colour(0, 128, 0),
+ 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:
+ 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)
+ return
+
+ if key_code == wx.WXK_PAGEUP:
+ 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)
+ return
+
+ event.Skip()
+
+ def _on_kill_focus(self, event: wx.FocusEvent) -> None:
+ 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
+
+ def get_selected_item(self) -> CompletionItem:
+ row = self._list_ctrl.GetFirstSelected()
+ if row != wx.NOT_FOUND and row < len(self._items):
+ return self._items[row]
+ return None
diff --git a/windows/components/stc/autocomplete/completion_types.py b/windows/components/stc/autocomplete/completion_types.py
new file mode 100644
index 0000000..5b30034
--- /dev/null
+++ b/windows/components/stc/autocomplete/completion_types.py
@@ -0,0 +1,23 @@
+from dataclasses import dataclass
+from enum import Enum
+
+
+class CompletionItemType(Enum):
+ KEYWORD = "keyword"
+ FUNCTION = "function"
+ TABLE = "table"
+ COLUMN = "column"
+
+
+@dataclass(frozen=True, slots=True)
+class CompletionItem:
+ name: str
+ item_type: CompletionItemType
+ description: str = ""
+
+
+@dataclass(frozen=True, slots=True)
+class CompletionResult:
+ prefix: str
+ prefix_length: int
+ items: tuple[CompletionItem, ...]
diff --git a/windows/components/stc/autocomplete/context_detector.py b/windows/components/stc/autocomplete/context_detector.py
new file mode 100644
index 0000000..8e1f2e6
--- /dev/null
+++ b/windows/components/stc/autocomplete/context_detector.py
@@ -0,0 +1,262 @@
+import re
+
+from typing import Optional
+
+import sqlglot
+
+from helpers.logger import logger
+
+from windows.components.stc.autocomplete.query_scope import QueryScope, TableReference
+from windows.components.stc.autocomplete.sql_context import SQLContext
+
+from structures.engines.database import SQLDatabase
+
+
+class ContextDetector:
+ _prefix_pattern = re.compile(r"[A-Za-z_][A-Za-z0-9_]*$")
+
+ def __init__(self, dialect: Optional[str] = None):
+ self._dialect = dialect
+
+ def detect(self, text: str, cursor_pos: int, database: Optional[SQLDatabase]) -> tuple[SQLContext, QueryScope, str]:
+ left_text = text[:cursor_pos]
+ left_text_stripped = left_text.strip()
+
+ if not left_text_stripped:
+ return SQLContext.EMPTY, QueryScope.empty(), ""
+
+ if " " not in left_text and "\n" not in left_text:
+ return SQLContext.SINGLE_TOKEN, QueryScope.empty(), left_text_stripped
+
+ prefix = self._extract_prefix(text, cursor_pos)
+
+ try:
+ context = self._detect_context_with_regex(left_text_stripped)
+ scope = self._extract_scope_from_text(text, database)
+ return context, scope, prefix
+ except Exception as ex:
+ logger.debug(f"context detection error: {ex}")
+ return SQLContext.UNKNOWN, QueryScope.empty(), prefix
+
+ def _extract_prefix(self, text: str, cursor_pos: int) -> str:
+ if cursor_pos == 0:
+ return ""
+
+ left_text = text[:cursor_pos]
+
+ if left_text and left_text[-1] in (' ', '\t', '\n'):
+ return ""
+
+ match = self._prefix_pattern.search(left_text)
+ if match is None:
+ return ""
+ return match.group(0)
+
+ def _detect_context_with_regex(self, left_text: str) -> SQLContext:
+ left_upper = left_text.upper()
+
+ select_pos = left_upper.rfind("SELECT")
+ from_pos = left_upper.rfind("FROM")
+ where_pos = left_upper.rfind("WHERE")
+ join_pos = left_upper.rfind("JOIN")
+ on_pos = left_upper.rfind(" ON ")
+ order_by_pos = left_upper.rfind("ORDER BY")
+ group_by_pos = left_upper.rfind("GROUP BY")
+ having_pos = left_upper.rfind("HAVING")
+ limit_pos = left_upper.rfind("LIMIT")
+ offset_pos = left_upper.rfind("OFFSET")
+
+ if select_pos == -1:
+ return SQLContext.UNKNOWN
+
+ max_pos = max(limit_pos, offset_pos)
+ if max_pos > select_pos and max_pos != -1:
+ return SQLContext.LIMIT_OFFSET_CLAUSE
+
+ if having_pos > select_pos and having_pos != -1:
+ if having_pos > max(group_by_pos, order_by_pos, -1):
+ return SQLContext.HAVING_CLAUSE
+
+ if group_by_pos > select_pos and group_by_pos != -1:
+ if group_by_pos > max(where_pos, order_by_pos, having_pos, -1):
+ return SQLContext.GROUP_BY_CLAUSE
+
+ if order_by_pos > select_pos and order_by_pos != -1:
+ if order_by_pos > max(where_pos, group_by_pos, having_pos, -1):
+ return SQLContext.ORDER_BY_CLAUSE
+
+ if on_pos > select_pos and on_pos != -1:
+ if on_pos > max(join_pos, from_pos, where_pos, -1):
+ return SQLContext.JOIN_ON
+
+ if join_pos > select_pos and join_pos != -1:
+ if join_pos > max(from_pos, where_pos, -1):
+ return SQLContext.JOIN_CLAUSE
+
+ if where_pos > select_pos and where_pos != -1:
+ if where_pos > max(from_pos, order_by_pos, group_by_pos, -1):
+ return SQLContext.WHERE_CLAUSE
+
+ if from_pos > select_pos and from_pos != -1:
+ if from_pos > max(where_pos, join_pos, order_by_pos, group_by_pos, -1):
+ return SQLContext.FROM_CLAUSE
+
+ return SQLContext.SELECT_LIST
+
+ def _extract_scope_from_select(self, parsed: sqlglot.exp.Select, database: Optional[SQLDatabase]) -> QueryScope:
+ from_tables = []
+ join_tables = []
+ aliases = {}
+
+ if from_clause := parsed.args.get("from"):
+ if isinstance(from_clause, sqlglot.exp.From):
+ for table_exp in from_clause.find_all(sqlglot.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(sqlglot.exp.Join):
+ if table_exp := join_exp.this:
+ if isinstance(table_exp, sqlglot.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:
+ sql_keywords = {
+ 'WHERE', 'ORDER', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET', 'UNION',
+ 'INTERSECT', 'EXCEPT', 'ON', 'USING', 'AND', 'OR', 'NOT', 'IN',
+ 'EXISTS', 'BETWEEN', 'LIKE', 'IS', 'NULL', 'ASC', 'DESC'
+ }
+
+ from_pattern = re.compile(r'\bFROM\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:(?:AS\s+)?([A-Za-z_][A-Za-z0-9_]*))?\s*(?:,|\bJOIN\b|\bWHERE\b|\bORDER\b|\bGROUP\b|\bLIMIT\b|$)', re.IGNORECASE)
+ 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|$)', re.IGNORECASE)
+
+ from_tables = []
+ join_tables = []
+ aliases = {}
+
+ for match in from_pattern.finditer(text):
+ table_name = match.group(1)
+ alias = match.group(2) if match.group(2) else None
+
+ if table_name.upper() in sql_keywords:
+ continue
+ if alias and alias.upper() in sql_keywords:
+ alias = 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 match in join_pattern.finditer(text):
+ table_name = match.group(1)
+ alias = match.group(2) if match.group(2) else None
+
+ if table_name.upper() in sql_keywords:
+ continue
+ if alias and alias.upper() in sql_keywords:
+ alias = 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 _find_table_in_database(self, table_name: str, database: SQLDatabase) -> Optional:
+ try:
+ for table in database.tables:
+ if table.name.lower() == table_name.lower():
+ return table
+ except Exception:
+ pass
+ return None
+
+ def _is_in_where(self, text: str) -> bool:
+ upper = text.upper()
+ where_pos = upper.rfind("WHERE")
+ if where_pos == -1:
+ return False
+
+ after_where = upper[where_pos:]
+ return "ORDER BY" not in after_where and "GROUP BY" not in after_where and "LIMIT" not in after_where
+
+ def _is_after_from(self, text: str) -> bool:
+ upper = text.upper()
+ from_pos = upper.rfind("FROM")
+ if from_pos == -1:
+ return False
+
+ after_from = upper[from_pos + 4:].strip()
+ return len(after_from) == 0 or (len(after_from) > 0 and after_from[-1] in [' ', '\n', '\t'])
+
+ def _is_after_on(self, text: str) -> bool:
+ upper = text.upper()
+ on_pos = upper.rfind(" ON ")
+ if on_pos == -1:
+ return False
+
+ after_on = upper[on_pos + 4:].strip()
+ return len(after_on) == 0 or (len(after_on) > 0 and not after_on.endswith(('WHERE', 'ORDER', 'GROUP', 'LIMIT')))
+
+ def _is_after_order_by(self, text: str) -> bool:
+ upper = text.upper()
+ order_by_pos = upper.rfind("ORDER BY")
+ if order_by_pos == -1:
+ return False
+
+ after_order_by = upper[order_by_pos + 8:].strip()
+ return "LIMIT" not in after_order_by
+
+ def _is_after_group_by(self, text: str) -> bool:
+ upper = text.upper()
+ group_by_pos = upper.rfind("GROUP BY")
+ if group_by_pos == -1:
+ return False
+
+ after_group_by = upper[group_by_pos + 8:].strip()
+ return "HAVING" not in after_group_by and "ORDER BY" not in after_group_by and "LIMIT" not in after_group_by
+
+ def _is_in_having(self, text: str) -> bool:
+ upper = text.upper()
+ having_pos = upper.rfind("HAVING")
+ if having_pos == -1:
+ return False
+
+ after_having = upper[having_pos:]
+ return "ORDER BY" not in after_having and "LIMIT" not in after_having
diff --git a/windows/components/stc/autocomplete/dot_completion_handler.py b/windows/components/stc/autocomplete/dot_completion_handler.py
new file mode 100644
index 0000000..4ab4c65
--- /dev/null
+++ b/windows/components/stc/autocomplete/dot_completion_handler.py
@@ -0,0 +1,114 @@
+import re
+
+from typing import Optional
+
+from windows.components.stc.autocomplete.completion_types import CompletionItem, CompletionItemType
+
+from structures.engines.database import SQLDatabase, SQLTable
+
+
+class DotCompletionHandler:
+ _token_pattern = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
+
+ def __init__(self, database: Optional[SQLDatabase], scope: Optional[object] = None):
+ self._database = database
+ self._scope = scope
+ self._table_index: dict[str, SQLTable] = {}
+ self._build_table_index()
+
+ def is_dot_completion(self, text: str, cursor_pos: int) -> bool:
+ if cursor_pos < 1:
+ return False
+
+ left_text = text[:cursor_pos]
+
+ if not left_text or left_text[-1] != '.':
+ tokens = self._token_pattern.findall(left_text)
+ if not tokens:
+ return False
+
+ last_part = left_text[left_text.rfind(tokens[-1]):]
+ return '.' in last_part
+
+ return True
+
+ def get_completions(self, text: str, cursor_pos: int) -> tuple[Optional[list[CompletionItem]], str]:
+ if not self.is_dot_completion(text, cursor_pos):
+ return None, ""
+
+ left_text = text[:cursor_pos]
+
+ tokens = self._token_pattern.findall(left_text)
+ if not tokens:
+ return None, ""
+
+ if left_text.rstrip().endswith('.'):
+ table_or_alias = tokens[-1] if tokens else None
+ prefix = ""
+ else:
+ if len(tokens) < 2:
+ return None, ""
+
+ last_part = left_text[left_text.rfind(tokens[-2]):]
+ if '.' not in last_part:
+ return None, ""
+
+ table_or_alias = tokens[-2]
+ prefix = tokens[-1]
+
+ if not table_or_alias:
+ return None, ""
+
+ table = self._find_table(table_or_alias)
+ if not table:
+ return None, ""
+
+ try:
+ columns = [
+ CompletionItem(
+ name=col.name,
+ item_type=CompletionItemType.COLUMN,
+ description=table.name
+ )
+ for col in table.columns
+ if col.name
+ ]
+ except (AttributeError, TypeError):
+ return None, prefix
+
+ if prefix:
+ prefix_lower = prefix.lower()
+ columns = [c for c in columns if c.name.lower().startswith(prefix_lower)]
+
+ return columns, prefix
+
+ def _find_table(self, name: str) -> Optional[SQLTable]:
+ return self._table_index.get(name.lower())
+
+ def _build_table_index(self) -> None:
+ self._table_index.clear()
+
+ if self._scope:
+ try:
+ for ref in self._scope.from_tables + self._scope.join_tables:
+ if ref.table:
+ self._table_index[ref.name.lower()] = ref.table
+ if ref.alias:
+ self._table_index[ref.alias.lower()] = ref.table
+ except (AttributeError, TypeError):
+ pass
+
+ if not self._database:
+ return
+
+ try:
+ for table in self._database.tables:
+ if table.name.lower() not in self._table_index:
+ self._table_index[table.name.lower()] = table
+ except (AttributeError, TypeError):
+ pass
+
+ def refresh(self, database: Optional[SQLDatabase], scope: Optional[object] = None) -> None:
+ self._database = database
+ self._scope = scope
+ self._build_table_index()
diff --git a/windows/components/stc/autocomplete/query_scope.py b/windows/components/stc/autocomplete/query_scope.py
new file mode 100644
index 0000000..3684219
--- /dev/null
+++ b/windows/components/stc/autocomplete/query_scope.py
@@ -0,0 +1,29 @@
+from dataclasses import dataclass
+
+from typing import Optional
+
+from structures.engines.database import SQLTable
+
+
+@dataclass
+class TableReference:
+ name: str
+ alias: Optional[str] = None
+ table: Optional[SQLTable] = None
+
+
+@dataclass
+class QueryScope:
+ from_tables: list[TableReference]
+ join_tables: list[TableReference]
+ current_table: Optional[SQLTable]
+ aliases: dict[str, TableReference]
+
+ @staticmethod
+ def empty(current_table: Optional[SQLTable] = None) -> "QueryScope":
+ return QueryScope(
+ from_tables=[],
+ join_tables=[],
+ current_table=current_table,
+ aliases={}
+ )
diff --git a/windows/components/stc/autocomplete/sql_context.py b/windows/components/stc/autocomplete/sql_context.py
new file mode 100644
index 0000000..635f5cf
--- /dev/null
+++ b/windows/components/stc/autocomplete/sql_context.py
@@ -0,0 +1,17 @@
+from enum import Enum
+
+
+class SQLContext(Enum):
+ EMPTY = "EMPTY"
+ SINGLE_TOKEN = "SINGLE_TOKEN"
+ DOT_COMPLETION = "DOT_COMPLETION"
+ SELECT_LIST = "SELECT_LIST"
+ FROM_CLAUSE = "FROM_CLAUSE"
+ JOIN_CLAUSE = "JOIN_CLAUSE"
+ JOIN_ON = "JOIN_ON"
+ WHERE_CLAUSE = "WHERE_CLAUSE"
+ ORDER_BY_CLAUSE = "ORDER_BY"
+ GROUP_BY_CLAUSE = "GROUP_BY"
+ HAVING_CLAUSE = "HAVING"
+ LIMIT_OFFSET_CLAUSE = "LIMIT_OFFSET"
+ UNKNOWN = "UNKNOWN"
diff --git a/windows/components/stc/autocomplete/statement_extractor.py b/windows/components/stc/autocomplete/statement_extractor.py
new file mode 100644
index 0000000..82d3804
--- /dev/null
+++ b/windows/components/stc/autocomplete/statement_extractor.py
@@ -0,0 +1,45 @@
+import re
+
+from typing import Optional
+
+
+class StatementExtractor:
+ _string_pattern = re.compile(r"'(?:[^'\\]|\\.)*'|\"(?:[^\"\\]|\\.)*\"")
+ _comment_pattern = re.compile(r"--[^\n]*|/\*.*?\*/", re.DOTALL)
+
+ @staticmethod
+ def extract_current_statement(text: str, cursor_pos: int) -> tuple[str, int]:
+ 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))
+
+ for i in range(len(statement_boundaries) - 1):
+ start = statement_boundaries[i]
+ end = statement_boundaries[i + 1]
+
+ 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
+
+ return statement, relative_pos
+
+ return text, cursor_pos
+
+ @staticmethod
+ def _remove_strings_and_comments(text: str) -> str:
+ text = StatementExtractor._string_pattern.sub(lambda m: ' ' * len(m.group(0)), text)
+ text = StatementExtractor._comment_pattern.sub(lambda m: ' ' * len(m.group(0)), text)
+ return text
diff --git a/windows/components/stc/autocomplete/suggestion_builder.py b/windows/components/stc/autocomplete/suggestion_builder.py
new file mode 100644
index 0000000..05a2409
--- /dev/null
+++ b/windows/components/stc/autocomplete/suggestion_builder.py
@@ -0,0 +1,805 @@
+from typing import Optional
+
+from windows.components.stc.autocomplete.completion_types import CompletionItem, CompletionItemType
+from windows.components.stc.autocomplete.query_scope import QueryScope, TableReference
+from windows.components.stc.autocomplete.sql_context import SQLContext
+
+from structures.engines.database import SQLDatabase, SQLTable
+
+
+class SuggestionBuilder:
+ _primary_keywords = {
+ "SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER",
+ "TRUNCATE", "SHOW", "DESCRIBE", "EXPLAIN", "WITH", "REPLACE", "MERGE"
+ }
+
+ _aggregate_functions = {
+ "COUNT", "SUM", "AVG", "MAX", "MIN", "GROUP_CONCAT"
+ }
+
+ _max_database_columns = 400
+
+ _scope_restricted_contexts = {
+ SQLContext.WHERE_CLAUSE,
+ SQLContext.JOIN_ON,
+ SQLContext.ORDER_BY_CLAUSE,
+ SQLContext.GROUP_BY_CLAUSE,
+ SQLContext.HAVING_CLAUSE
+ }
+
+ def __init__(self, database: Optional[SQLDatabase], current_table: Optional[SQLTable]):
+ self._database = database
+ self._current_table = current_table
+
+ def _is_scope_restricted_context(self, context: SQLContext) -> bool:
+ return context in self._scope_restricted_contexts
+
+ def _is_current_table_in_scope(self, scope: QueryScope) -> bool:
+ if not self._current_table:
+ return False
+ current_table_name_lower = self._current_table.name.lower()
+ for ref in scope.from_tables + scope.join_tables:
+ if ref.name.lower() == current_table_name_lower:
+ return True
+ return False
+
+ def build(self, context: SQLContext, scope: QueryScope, prefix: str, statement: str = "") -> list[CompletionItem]:
+ if context == SQLContext.EMPTY:
+ return self._build_empty(prefix)
+
+ if context == SQLContext.SINGLE_TOKEN:
+ return self._build_single_token(prefix)
+
+ if context == SQLContext.SELECT_LIST:
+ return self._build_select_list(scope, prefix, statement)
+
+ 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 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: x.name.lower())
+ except (AttributeError, TypeError):
+ return []
+ else:
+ keywords = ["JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "CROSS JOIN", "WHERE", "GROUP BY", "ORDER BY", "LIMIT"]
+
+ has_alias = any(ref.alias for ref in scope.from_tables)
+ if not has_alias:
+ keywords.insert(5, "AS")
+
+ return [CompletionItem(name=kw, item_type=CompletionItemType.KEYWORD) for kw in keywords]
+
+ return self._build_from_clause(prefix, statement)
+
+ if context == SQLContext.JOIN_CLAUSE:
+ if not prefix and scope.join_tables:
+ if statement.rstrip().endswith(scope.join_tables[-1].name):
+ keywords = ["AS", "ON", "USING"]
+ return [CompletionItem(name=kw, item_type=CompletionItemType.KEYWORD) for kw in keywords]
+ return self._build_join_clause(prefix, scope)
+
+ if context == SQLContext.JOIN_ON:
+ return self._build_join_on(scope, prefix, statement)
+
+ if context == SQLContext.WHERE_CLAUSE:
+ return self._build_where_clause(scope, prefix, statement)
+
+ if context == SQLContext.ORDER_BY_CLAUSE:
+ return self._build_order_by(scope, prefix)
+
+ if context == SQLContext.GROUP_BY_CLAUSE:
+ return self._build_group_by(scope, prefix)
+
+ if context == SQLContext.HAVING_CLAUSE:
+ return self._build_having(scope, prefix)
+
+ if context == SQLContext.LIMIT_OFFSET_CLAUSE:
+ return []
+
+ return self._build_keywords(prefix)
+
+ def _build_empty(self, prefix: str) -> list[CompletionItem]:
+ keywords = [
+ CompletionItem(name=kw, item_type=CompletionItemType.KEYWORD)
+ for kw in self._primary_keywords
+ ]
+
+ if prefix:
+ prefix_upper = prefix.upper()
+ keywords = [kw for kw in keywords if kw.name.startswith(prefix_upper)]
+
+ return sorted(keywords, key=lambda x: x.name)
+
+ def _build_single_token(self, prefix: str) -> list[CompletionItem]:
+ if not self._database:
+ return []
+
+ try:
+ all_keywords = self._database.context.KEYWORDS
+ keywords = [
+ CompletionItem(name=str(kw).upper(), item_type=CompletionItemType.KEYWORD)
+ for kw in all_keywords
+ ]
+ except (AttributeError, TypeError):
+ return []
+
+ if prefix:
+ prefix_upper = prefix.upper()
+ keywords = [kw for kw in keywords if kw.name.startswith(prefix_upper)]
+
+ return sorted(keywords, key=lambda x: x.name)
+
+ def _build_select_list(self, scope: QueryScope, prefix: str, statement: str = "") -> list[CompletionItem]:
+ items = []
+
+ has_scope = bool(scope.from_tables or scope.join_tables)
+
+ # Check if we're after a complete qualified column (table.column + space)
+ # In this case, suggest ONLY keywords (FROM, WHERE, AS, etc.), NOT functions/columns
+ # This applies both with and without prefix: "SELECT table.column F" should suggest only FROM, not functions
+ if statement:
+ import re
+ # Match: table.column followed by whitespace (and optionally a prefix)
+ # Examples: "SELECT users.id " or "SELECT users.id F"
+ if re.search(r'\b\w+\.\w+\s+', statement):
+ return self._build_select_keywords(prefix)
+
+ columns = self._resolve_columns_in_scope(scope, prefix, SQLContext.SELECT_LIST)
+ items.extend(columns)
+
+ items.extend(self._build_functions(prefix))
+
+ if prefix:
+ items.extend(self._build_keywords(prefix))
+
+ if has_scope and prefix:
+ hints = self._get_out_of_scope_table_hints(scope, prefix, columns)
+ items.extend(hints)
+
+ if not has_scope and not prefix:
+ items.extend(self._build_select_keywords(prefix))
+
+ return items
+
+ def _build_from_clause(self, prefix: str, statement: str = "") -> list[CompletionItem]:
+ if not self._database:
+ return []
+
+ try:
+ tables = [
+ CompletionItem(name=table.name, item_type=CompletionItemType.TABLE)
+ for table in self._database.tables
+ ]
+ except (AttributeError, TypeError):
+ return []
+
+ # Extract tables referenced in SELECT list (e.g., SELECT users.id FROM | โ prioritize users)
+ # This applies ALWAYS, with or without prefix
+ referenced_tables = set()
+ if statement:
+ import re
+ # Find qualified columns in SELECT: table.column
+ # Match both "FROM " (with space) and "FROM" (at end of statement)
+ select_match = re.search(r'SELECT\s+(.*?)\s+FROM\s*', statement, re.IGNORECASE | re.DOTALL)
+ if select_match:
+ select_list = select_match.group(1)
+ # Extract table names from qualified columns
+ qualified_refs = re.findall(r'\b(\w+)\.\w+', select_list)
+ referenced_tables = {ref.lower() for ref in qualified_refs}
+
+ # Filter by prefix if present
+ if prefix:
+ prefix_lower = prefix.lower()
+ tables = [t for t in tables if t.name.lower().startswith(prefix_lower)]
+
+ # Sort: referenced tables first, then alphabetically
+ # This ensures SELECT users.id FROM | shows users first
+ def sort_key(table):
+ is_referenced = table.name.lower() in referenced_tables
+ return (not is_referenced, table.name.lower())
+
+ return sorted(tables, key=sort_key)
+
+ def _build_join_clause(self, prefix: str, scope: QueryScope) -> list[CompletionItem]:
+ if not self._database:
+ return []
+
+ in_scope_table_names = {ref.name.lower() for ref in scope.from_tables + scope.join_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
+ ]
+ except (AttributeError, TypeError):
+ return []
+
+ if prefix:
+ prefix_lower = prefix.lower()
+ tables = [t for t in tables if t.name.lower().startswith(prefix_lower)]
+
+ return sorted(tables, key=lambda x: x.name.lower())
+
+ def _build_join_on(self, scope: QueryScope, prefix: str, statement: str = "") -> list[CompletionItem]:
+ items = []
+ columns = self._resolve_columns_in_scope(scope, prefix, SQLContext.JOIN_ON)
+
+ # Filter out the column on the left side of the operator (same logic as WHERE)
+ if not prefix and statement:
+ import re
+ match = re.search(r'(\w+\.?\w*)\s*(?:=|!=|<>|<|>|<=|>=)\s*$', statement, re.IGNORECASE)
+ if match:
+ left_column = match.group(1).strip()
+ columns = [c for c in columns if c.name.lower() != left_column.lower()]
+
+ items.extend(columns)
+ items.extend(self._build_functions(prefix))
+ return items
+
+ def _build_where_clause(self, scope: QueryScope, prefix: str, statement: str = "") -> list[CompletionItem]:
+ items = []
+
+ columns = self._resolve_columns_in_scope(scope, prefix, SQLContext.WHERE_CLAUSE)
+
+ # Filter out the column on the left side of the operator
+ # e.g., "WHERE users.id = |" should NOT suggest users.id
+ if not prefix and statement:
+ import re
+ # Match: column_name (qualified or not) followed by operator and whitespace
+ # Operators: =, !=, <>, <, >, <=, >=, LIKE, IN, etc.
+ match = re.search(r'(\w+\.?\w*)\s*(?:=|!=|<>|<|>|<=|>=|LIKE|IN|NOT\s+IN)\s*$', statement, re.IGNORECASE)
+ if match:
+ left_column = match.group(1).strip()
+ # Remove the left column from suggestions
+ columns = [c for c in columns if c.name.lower() != left_column.lower()]
+
+ functions = self._build_functions(prefix)
+
+ items.extend(columns)
+ items.extend(functions)
+
+ return items
+
+ def _build_order_by(self, scope: QueryScope, prefix: str) -> list[CompletionItem]:
+ items = []
+ items.extend(self._resolve_columns_in_scope(scope, prefix, SQLContext.ORDER_BY_CLAUSE))
+ items.extend(self._build_functions(prefix))
+
+ order_keywords = ["ASC", "DESC", "NULLS FIRST", "NULLS LAST"]
+ if prefix:
+ prefix_upper = prefix.upper()
+ order_keywords = [kw for kw in order_keywords if kw.startswith(prefix_upper)]
+
+ items.extend([
+ CompletionItem(name=kw, item_type=CompletionItemType.KEYWORD)
+ for kw in order_keywords
+ ])
+
+ return items
+
+ def _build_group_by(self, scope: QueryScope, prefix: str) -> list[CompletionItem]:
+ items = []
+ items.extend(self._resolve_columns_in_scope(scope, prefix, SQLContext.GROUP_BY_CLAUSE))
+ items.extend(self._build_functions(prefix))
+ return items
+
+ def _build_having(self, scope: QueryScope, prefix: str) -> list[CompletionItem]:
+ items = []
+
+ aggregate_funcs = self._build_aggregate_functions(prefix)
+ items.extend(aggregate_funcs)
+
+ items.extend(self._resolve_columns_in_scope(scope, prefix, SQLContext.HAVING_CLAUSE))
+
+ other_funcs = [f for f in self._build_functions(prefix) if f.name not in self._aggregate_functions]
+ items.extend(other_funcs)
+
+ return items
+
+ def _build_keywords(self, prefix: str) -> list[CompletionItem]:
+ if not self._database:
+ return []
+
+ try:
+ all_keywords = self._database.context.KEYWORDS
+ keywords = [
+ CompletionItem(name=str(kw).upper(), item_type=CompletionItemType.KEYWORD)
+ for kw in all_keywords
+ ]
+ except (AttributeError, TypeError):
+ return []
+
+ if prefix:
+ prefix_upper = prefix.upper()
+ keywords = [kw for kw in keywords if kw.name.startswith(prefix_upper)]
+
+ return sorted(keywords, key=lambda x: x.name)
+
+ def _build_select_keywords(self, prefix: str) -> list[CompletionItem]:
+ keywords = ["FROM", "WHERE", "LIMIT", "ORDER BY", "GROUP BY"]
+
+ if prefix:
+ prefix_upper = prefix.upper()
+ keywords = [kw for kw in keywords if kw.startswith(prefix_upper)]
+
+ return [
+ CompletionItem(name=kw, item_type=CompletionItemType.KEYWORD)
+ for kw in keywords
+ ]
+
+ def _build_functions(self, prefix: str) -> list[CompletionItem]:
+ if not self._database:
+ return []
+
+ try:
+ functions = self._database.context.FUNCTIONS
+ function_list = [
+ CompletionItem(name=str(func).upper(), item_type=CompletionItemType.FUNCTION)
+ for func in functions
+ ]
+ except (AttributeError, TypeError):
+ return []
+
+ if prefix:
+ prefix_upper = prefix.upper()
+ function_list = [f for f in function_list if f.name.startswith(prefix_upper)]
+
+ return sorted(function_list, key=lambda x: x.name)
+
+ def _build_aggregate_functions(self, prefix: str) -> list[CompletionItem]:
+ if not self._database:
+ return []
+
+ try:
+ functions = self._database.context.FUNCTIONS
+ aggregate_list = [
+ CompletionItem(name=str(func).upper(), item_type=CompletionItemType.FUNCTION)
+ for func in functions
+ if str(func).upper() in self._aggregate_functions
+ ]
+ except (AttributeError, TypeError):
+ return []
+
+ if prefix:
+ prefix_upper = prefix.upper()
+ aggregate_list = [f for f in aggregate_list if f.name.startswith(prefix_upper)]
+
+ return sorted(aggregate_list, key=lambda x: x.name)
+
+ def _resolve_columns_in_scope(self, scope: QueryScope, prefix: str, context: Optional[SQLContext] = None) -> list[CompletionItem]:
+ if prefix and self._is_exact_alias_match(prefix, scope):
+ return self._get_alias_columns(prefix, scope)
+
+ if prefix:
+ return self._resolve_columns_with_prefix(scope, prefix, context)
+
+ return self._resolve_columns_without_prefix(scope, context)
+
+ def _resolve_columns_without_prefix(self, scope: QueryScope, context: Optional[SQLContext] = None) -> list[CompletionItem]:
+ columns = []
+
+ is_scope_restricted = context and self._is_scope_restricted_context(context)
+ has_scope = bool(scope.from_tables or scope.join_tables)
+
+ if context == SQLContext.SELECT_LIST:
+ if not has_scope and self._current_table:
+ columns.extend(self._get_current_table_columns(scope, None))
+ elif has_scope and self._current_table and self._is_current_table_in_scope(scope):
+ columns.extend(self._get_current_table_columns(scope, None))
+ elif not is_scope_restricted and self._current_table:
+ columns.extend(self._get_current_table_columns(scope, None))
+
+ columns.extend(self._get_from_table_columns(scope, None))
+ columns.extend(self._get_join_table_columns(scope, None))
+
+ if context == SQLContext.SELECT_LIST or not is_scope_restricted:
+ if len(columns) < self._max_database_columns:
+ columns.extend(self._get_database_columns(scope, None))
+
+ return columns
+
+ def _resolve_columns_with_prefix(self, scope: QueryScope, prefix: str, context: Optional[SQLContext] = None) -> list[CompletionItem]:
+ seen = set()
+ columns = []
+
+ is_scope_restricted = context and self._is_scope_restricted_context(context)
+ has_scope = bool(scope.from_tables or scope.join_tables)
+
+ include_current_table = True
+ if context == SQLContext.SELECT_LIST and has_scope:
+ include_current_table = self._is_current_table_in_scope(scope)
+ elif is_scope_restricted:
+ include_current_table = False
+
+ table_expansion_columns = self._get_table_name_expansion_columns(scope, prefix, is_scope_restricted, include_current_table)
+ for col in table_expansion_columns:
+ if col.name.lower() not in seen:
+ seen.add(col.name.lower())
+ columns.append(col)
+
+ column_name_match_columns = self._get_column_name_match_columns(scope, prefix, is_scope_restricted, include_current_table)
+ for col in column_name_match_columns:
+ if col.name.lower() not in seen:
+ seen.add(col.name.lower())
+ columns.append(col)
+
+ return columns
+
+ def _get_table_name_expansion_columns(self, scope: QueryScope, prefix: str, is_scope_restricted: bool, include_current_table: bool = True) -> list[CompletionItem]:
+ columns = []
+ prefix_lower = prefix.lower()
+
+ if include_current_table and self._current_table and self._current_table.name.lower().startswith(prefix_lower):
+ qualifier = self._get_table_qualifier(self._current_table.name, scope)
+ try:
+ for col in self._current_table.columns:
+ if col.name:
+ columns.append(CompletionItem(
+ name=f"{qualifier}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=self._current_table.name
+ ))
+ except (AttributeError, TypeError):
+ pass
+
+ if self._is_exact_alias_match(prefix, scope):
+ columns.extend(self._get_alias_columns(prefix, scope))
+ return columns
+
+ for ref in scope.from_tables:
+ if not ref.table:
+ continue
+
+ if ref.name.lower().startswith(prefix_lower):
+ qualifier = ref.name
+
+ try:
+ for col in ref.table.columns:
+ if col.name:
+ columns.append(CompletionItem(
+ name=f"{qualifier}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=ref.name
+ ))
+ except (AttributeError, TypeError):
+ pass
+
+ for ref in scope.join_tables:
+ if not ref.table:
+ continue
+
+ if ref.name.lower().startswith(prefix_lower):
+ qualifier = ref.name
+
+ try:
+ for col in ref.table.columns:
+ if col.name:
+ columns.append(CompletionItem(
+ name=f"{qualifier}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=ref.name
+ ))
+ except (AttributeError, TypeError):
+ pass
+
+ if not is_scope_restricted and self._database:
+ in_scope_table_names = set()
+ if self._current_table:
+ in_scope_table_names.add(self._current_table.name.lower())
+ for ref in scope.from_tables + scope.join_tables:
+ in_scope_table_names.add(ref.name.lower())
+
+ try:
+ for table in self._database.tables:
+ if table.name.lower().startswith(prefix_lower) and table.name.lower() not in in_scope_table_names:
+ try:
+ for col in table.columns:
+ if col.name:
+ columns.append(CompletionItem(
+ name=f"{table.name}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=table.name
+ ))
+ except (AttributeError, TypeError):
+ pass
+ except (AttributeError, TypeError):
+ pass
+
+ return columns
+
+ def _get_column_name_match_columns(self, scope: QueryScope, prefix: str, is_scope_restricted: bool, include_current_table: bool = True) -> list[CompletionItem]:
+ columns = []
+ prefix_lower = prefix.lower()
+
+ if include_current_table and self._current_table:
+ qualifier = self._get_table_qualifier(self._current_table.name, scope)
+ try:
+ for col in self._current_table.columns:
+ if col.name and col.name.lower().startswith(prefix_lower):
+ columns.append(CompletionItem(
+ name=f"{qualifier}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=self._current_table.name
+ ))
+ except (AttributeError, TypeError):
+ pass
+
+ for ref in scope.from_tables:
+ if ref.table:
+ qualifier = ref.alias if ref.alias else ref.name
+ try:
+ for col in ref.table.columns:
+ if col.name and col.name.lower().startswith(prefix_lower):
+ columns.append(CompletionItem(
+ name=f"{qualifier}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=ref.name
+ ))
+ except (AttributeError, TypeError):
+ pass
+
+ for ref in scope.join_tables:
+ if ref.table:
+ qualifier = ref.alias if ref.alias else ref.name
+ try:
+ for col in ref.table.columns:
+ if col.name and col.name.lower().startswith(prefix_lower):
+ columns.append(CompletionItem(
+ name=f"{qualifier}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=ref.name
+ ))
+ except (AttributeError, TypeError):
+ pass
+
+ if not is_scope_restricted and self._database:
+ in_scope_table_names = set()
+ if self._current_table:
+ in_scope_table_names.add(self._current_table.name.lower())
+ for ref in scope.from_tables + scope.join_tables:
+ in_scope_table_names.add(ref.name.lower())
+
+ try:
+ for table in self._database.tables:
+ if table.name.lower() not in in_scope_table_names:
+ try:
+ for col in table.columns:
+ if col.name and col.name.lower().startswith(prefix_lower):
+ columns.append(CompletionItem(
+ name=f"{table.name}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=table.name
+ ))
+ except (AttributeError, TypeError):
+ pass
+ except (AttributeError, TypeError):
+ pass
+
+ return columns
+
+ def _get_out_of_scope_table_hints(self, scope: QueryScope, prefix: str, existing_columns: list[CompletionItem]) -> list[CompletionItem]:
+ if not self._database or not prefix:
+ return []
+
+ prefix_lower = prefix.lower()
+
+ has_scope_table_match = any(
+ ref.name.lower().startswith(prefix_lower)
+ for ref in scope.from_tables + scope.join_tables
+ )
+ if has_scope_table_match:
+ return []
+
+ in_scope_table_names = {ref.name.lower() for ref in scope.from_tables + scope.join_tables}
+
+ has_scope_column_match = False
+ for col in existing_columns:
+ if col.item_type == CompletionItemType.COLUMN and '.' in col.name:
+ parts = col.name.split('.')
+ if len(parts) == 2:
+ table_part, col_part = parts
+ if table_part.lower() in in_scope_table_names and col_part.lower().startswith(prefix_lower):
+ has_scope_column_match = True
+ break
+
+ if has_scope_column_match:
+ return []
+
+ hints = []
+ try:
+ for table in self._database.tables:
+ if (table.name.lower().startswith(prefix_lower) and
+ table.name.lower() not in in_scope_table_names):
+ hints.append(CompletionItem(
+ name=f"{table.name} (+ Add via FROM/JOIN)",
+ item_type=CompletionItemType.TABLE,
+ description=""
+ ))
+ except (AttributeError, TypeError):
+ pass
+
+ return hints
+
+ def _is_exact_alias_match(self, prefix: str, scope: QueryScope) -> bool:
+ return prefix.lower() in scope.aliases
+
+ def _get_alias_columns(self, alias: str, scope: QueryScope) -> list[CompletionItem]:
+ ref = scope.aliases.get(alias.lower())
+ if not ref or not ref.table:
+ return []
+
+ qualifier = ref.alias if ref.alias else ref.name
+
+ try:
+ columns = [
+ CompletionItem(
+ name=f"{qualifier}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=ref.name
+ )
+ for col in ref.table.columns
+ if col.name
+ ]
+ return columns
+ except (AttributeError, TypeError):
+ return []
+
+ def _get_current_table_columns(self, scope: QueryScope, prefix: str) -> list[CompletionItem]:
+ if not self._current_table:
+ return []
+
+ qualifier = self._get_table_qualifier(self._current_table.name, scope)
+
+ try:
+ columns = [
+ CompletionItem(
+ name=f"{qualifier}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=self._current_table.name
+ )
+ for col in self._current_table.columns
+ if col.name
+ ]
+ except (AttributeError, TypeError):
+ return []
+
+ if prefix:
+ columns = self._filter_columns_by_prefix(columns, prefix)
+
+ return columns
+
+ def _get_from_table_columns(self, scope: QueryScope, prefix: str) -> list[CompletionItem]:
+ columns = []
+
+ for ref in scope.from_tables:
+ if not ref.table:
+ continue
+
+ qualifier = ref.alias if ref.alias else ref.name
+
+ try:
+ table_columns = [
+ CompletionItem(
+ name=f"{qualifier}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=ref.name
+ )
+ for col in ref.table.columns
+ if col.name
+ ]
+ columns.extend(table_columns)
+ except (AttributeError, TypeError):
+ continue
+
+ if prefix:
+ columns = self._filter_columns_by_prefix(columns, prefix)
+
+ return columns
+
+ def _get_join_table_columns(self, scope: QueryScope, prefix: str) -> list[CompletionItem]:
+ columns = []
+
+ for ref in scope.join_tables:
+ if not ref.table:
+ continue
+
+ qualifier = ref.alias if ref.alias else ref.name
+
+ try:
+ table_columns = [
+ CompletionItem(
+ name=f"{qualifier}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=ref.name
+ )
+ for col in ref.table.columns
+ if col.name
+ ]
+ columns.extend(table_columns)
+ except (AttributeError, TypeError):
+ continue
+
+ if prefix:
+ columns = self._filter_columns_by_prefix(columns, prefix)
+
+ return columns
+
+ def _get_database_columns(self, scope: QueryScope, prefix: str) -> list[CompletionItem]:
+ if not self._database:
+ return []
+
+ in_scope_table_names = set()
+ if self._current_table:
+ in_scope_table_names.add(self._current_table.name.lower())
+
+ for ref in scope.from_tables + scope.join_tables:
+ in_scope_table_names.add(ref.name.lower())
+
+ columns = []
+
+ try:
+ for table in self._database.tables:
+ if table.name.lower() in in_scope_table_names:
+ continue
+
+ try:
+ table_columns = [
+ CompletionItem(
+ name=f"{table.name}.{col.name}",
+ item_type=CompletionItemType.COLUMN,
+ description=table.name
+ )
+ for col in table.columns
+ if col.name
+ ]
+ columns.extend(table_columns)
+ except (AttributeError, TypeError):
+ continue
+ except (AttributeError, TypeError):
+ return []
+
+ if prefix:
+ columns = self._filter_columns_by_prefix(columns, prefix)
+
+ return columns
+
+ def _get_table_qualifier(self, table_name: str, scope: QueryScope) -> str:
+ table_lower = table_name.lower()
+
+ if table_lower in scope.aliases:
+ ref = scope.aliases[table_lower]
+ return ref.alias if ref.alias else ref.name
+
+ return table_name
+
+ def _filter_columns_by_prefix(self, columns: list[CompletionItem], prefix: str) -> list[CompletionItem]:
+ prefix_lower = prefix.lower()
+ filtered = []
+
+ for col in columns:
+ col_name_lower = col.name.lower()
+
+ if col_name_lower.startswith(prefix_lower):
+ filtered.append(col)
+ elif "." in col_name_lower:
+ parts = col_name_lower.split(".", 1)
+ if parts[0].startswith(prefix_lower) or parts[1].startswith(prefix_lower):
+ filtered.append(col)
+
+ return filtered
diff --git a/windows/components/stc/sql_templates.py b/windows/components/stc/sql_templates.py
new file mode 100644
index 0000000..16ea97d
--- /dev/null
+++ b/windows/components/stc/sql_templates.py
@@ -0,0 +1,75 @@
+from typing import Optional
+
+from structures.engines.database import SQLDatabase, SQLTable
+
+
+class SQLTemplate:
+ def __init__(self, name: str, template: str, description: str = ""):
+ self.name = name
+ self.template = template
+ self.description = description
+
+ def render(
+ self,
+ database: Optional[SQLDatabase] = None,
+ table: Optional[SQLTable] = None
+ ) -> str:
+ text = self.template
+
+ if table:
+ text = text.replace("{table}", table.name)
+ if table.columns:
+ columns = ", ".join(col.name for col in table.columns[:5])
+ text = text.replace("{columns}", columns)
+ else:
+ text = text.replace("{table}", "table_name")
+ text = text.replace("{columns}", "column1, column2")
+
+ text = text.replace("{condition}", "condition")
+ text = text.replace("{values}", "value1, value2")
+
+ return text
+
+
+SQL_TEMPLATES = [
+ SQLTemplate(
+ name="SELECT *",
+ template="SELECT * FROM {table}",
+ description="Select all columns from table"
+ ),
+ SQLTemplate(
+ name="SELECT with WHERE",
+ template="SELECT {columns}\nFROM {table}\nWHERE {condition}",
+ description="Select specific columns with condition"
+ ),
+ SQLTemplate(
+ name="INSERT",
+ template="INSERT INTO {table} ({columns})\nVALUES ({values})",
+ description="Insert new row"
+ ),
+ SQLTemplate(
+ name="UPDATE",
+ template="UPDATE {table}\nSET column = value\nWHERE {condition}",
+ description="Update rows"
+ ),
+ SQLTemplate(
+ name="DELETE",
+ template="DELETE FROM {table}\nWHERE {condition}",
+ description="Delete rows"
+ ),
+ SQLTemplate(
+ name="SELECT with JOIN",
+ template="SELECT t1.*, t2.*\nFROM {table} t1\nJOIN table2 t2 ON t1.id = t2.id",
+ description="Select with JOIN"
+ ),
+ SQLTemplate(
+ name="SELECT with GROUP BY",
+ template="SELECT {columns}, COUNT(*)\nFROM {table}\nGROUP BY {columns}",
+ description="Select with grouping"
+ ),
+ SQLTemplate(
+ name="CREATE TABLE",
+ template="CREATE TABLE {table} (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL\n)",
+ description="Create new table"
+ ),
+]
diff --git a/windows/components/stc/styles.py b/windows/components/stc/styles.py
index 7b69500..32ada92 100644
--- a/windows/components/stc/styles.py
+++ b/windows/components/stc/styles.py
@@ -4,9 +4,20 @@
from helpers import wx_colour_to_hex
from windows.components.stc.profiles import SyntaxProfile
+from windows.components.stc.theme_loader import ThemeLoader
+
+_theme_loader: ThemeLoader = None
+
+
+def set_theme_loader(theme_loader: ThemeLoader) -> None:
+ global _theme_loader
+ _theme_loader = theme_loader
def get_palette() -> dict[str, str]:
+ if _theme_loader:
+ return _theme_loader.get_palette()
+
background = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
foreground = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)
line_number_background = wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE)
@@ -14,10 +25,10 @@ def get_palette() -> dict[str, str]:
is_dark = wx.SystemSettings.GetAppearance().IsDark()
base = {
- "background": background,
- "foreground": foreground,
- "line_number_background": line_number_background,
- "line_number_foreground": line_number_foreground,
+ "background": wx_colour_to_hex(background),
+ "foreground": wx_colour_to_hex(foreground),
+ "line_number_background": wx_colour_to_hex(line_number_background),
+ "line_number_foreground": wx_colour_to_hex(line_number_foreground),
}
if is_dark:
diff --git a/windows/components/stc/template_menu.py b/windows/components/stc/template_menu.py
new file mode 100644
index 0000000..e964fc2
--- /dev/null
+++ b/windows/components/stc/template_menu.py
@@ -0,0 +1,68 @@
+from typing import Callable, Optional
+
+import wx
+import wx.stc
+
+from structures.engines.database import SQLDatabase, SQLTable
+from windows.components.stc.sql_templates import SQL_TEMPLATES, SQLTemplate
+
+
+class SQLTemplateMenuController:
+ def __init__(
+ self,
+ editor: wx.stc.StyledTextCtrl,
+ get_database: Callable[[], Optional[SQLDatabase]],
+ get_current_table: Callable[[], Optional[SQLTable]]
+ ):
+ self._editor = editor
+ self._get_database = get_database
+ self._get_current_table = get_current_table
+
+ self._editor.Bind(wx.EVT_CONTEXT_MENU, self._on_context_menu)
+
+ def _on_context_menu(self, event: wx.ContextMenuEvent):
+ menu = wx.Menu()
+
+ template_menu = wx.Menu()
+
+ for template in SQL_TEMPLATES:
+ item = template_menu.Append(wx.ID_ANY, template.name, template.description)
+ self._editor.Bind(
+ wx.EVT_MENU,
+ lambda evt, t=template: self._insert_template(t),
+ item
+ )
+
+ menu.AppendSubMenu(template_menu, "Insert Template")
+ menu.AppendSeparator()
+
+ menu.Append(wx.ID_UNDO, "Undo\tCtrl+Z")
+ menu.Append(wx.ID_REDO, "Redo\tCtrl+Y")
+ menu.AppendSeparator()
+ menu.Append(wx.ID_CUT, "Cut\tCtrl+X")
+ menu.Append(wx.ID_COPY, "Copy\tCtrl+C")
+ menu.Append(wx.ID_PASTE, "Paste\tCtrl+V")
+ menu.AppendSeparator()
+ menu.Append(wx.ID_SELECTALL, "Select All\tCtrl+A")
+
+ self._editor.Bind(wx.EVT_MENU, lambda e: self._editor.Undo(), id=wx.ID_UNDO)
+ self._editor.Bind(wx.EVT_MENU, lambda e: self._editor.Redo(), id=wx.ID_REDO)
+ self._editor.Bind(wx.EVT_MENU, lambda e: self._editor.Cut(), id=wx.ID_CUT)
+ self._editor.Bind(wx.EVT_MENU, lambda e: self._editor.Copy(), id=wx.ID_COPY)
+ self._editor.Bind(wx.EVT_MENU, lambda e: self._editor.Paste(), id=wx.ID_PASTE)
+ self._editor.Bind(wx.EVT_MENU, lambda e: self._editor.SelectAll(), id=wx.ID_SELECTALL)
+
+ self._editor.PopupMenu(menu)
+ menu.Destroy()
+
+ def _insert_template(self, template: SQLTemplate):
+ database = self._get_database()
+ table = self._get_current_table()
+
+ text = template.render(database=database, table=table)
+
+ pos = self._editor.GetCurrentPos()
+ self._editor.InsertText(pos, text)
+
+ self._editor.SetSelection(pos, pos + len(text))
+ self._editor.SetCurrentPos(pos + len(text))
diff --git a/windows/components/stc/theme_loader.py b/windows/components/stc/theme_loader.py
new file mode 100644
index 0000000..73a8926
--- /dev/null
+++ b/windows/components/stc/theme_loader.py
@@ -0,0 +1,118 @@
+from pathlib import Path
+from typing import Optional
+
+import wx
+import yaml
+
+
+class ThemeLoader:
+ def __init__(self, themes_dir: Path) -> None:
+ self._themes_dir = themes_dir
+ self._current_theme: Optional[dict] = None
+ self._theme_name: Optional[str] = None
+
+ def load_theme(self, theme_name: str) -> None:
+ theme_file = self._themes_dir / f"{theme_name}.yml"
+ if not theme_file.exists():
+ raise FileNotFoundError(f"Theme file not found: {theme_file}")
+
+ with open(theme_file, 'r') as f:
+ self._current_theme = yaml.safe_load(f)
+ self._theme_name = theme_name
+
+ def get_palette(self) -> dict[str, str]:
+ if not self._current_theme:
+ return self._get_default_palette()
+
+ is_dark = wx.SystemSettings.GetAppearance().IsDark()
+ mode = "dark" if is_dark else "light"
+
+ editor_colors = self._current_theme.get("editor", {}).get(mode, {})
+
+ palette = {}
+ for key, value in editor_colors.items():
+ if value == "auto":
+ palette[key] = self._get_system_color(key)
+ else:
+ palette[key] = value
+
+ return palette
+
+ def get_autocomplete_colors(self) -> dict[str, str]:
+ if not self._current_theme:
+ return self._get_default_autocomplete_colors()
+
+ is_dark = wx.SystemSettings.GetAppearance().IsDark()
+ mode = "dark" if is_dark else "light"
+
+ return self._current_theme.get("autocomplete", {}).get(mode, {})
+
+ def _get_system_color(self, key: str) -> str:
+ color_map = {
+ "background": wx.SYS_COLOUR_WINDOW,
+ "foreground": wx.SYS_COLOUR_WINDOWTEXT,
+ "line_number_background": wx.SYS_COLOUR_3DFACE,
+ "line_number_foreground": wx.SYS_COLOUR_GRAYTEXT,
+ }
+
+ if key in color_map:
+ color = wx.SystemSettings.GetColour(color_map[key])
+ return f"#{color.Red():02x}{color.Green():02x}{color.Blue():02x}"
+
+ return "#000000"
+
+ def _get_default_palette(self) -> dict[str, str]:
+ is_dark = wx.SystemSettings.GetAppearance().IsDark()
+
+ if is_dark:
+ return {
+ "background": self._get_system_color("background"),
+ "foreground": self._get_system_color("foreground"),
+ "line_number_background": self._get_system_color("line_number_background"),
+ "line_number_foreground": self._get_system_color("line_number_foreground"),
+ "keyword": "#569cd6",
+ "string": "#ce9178",
+ "comment": "#6a9955",
+ "number": "#b5cea8",
+ "operator": self._get_system_color("foreground"),
+ "property": "#9cdcfe",
+ "error": "#f44747",
+ "uri": "#4ec9b0",
+ "reference": "#4ec9b0",
+ "document": "#c586c0",
+ }
+
+ return {
+ "background": self._get_system_color("background"),
+ "foreground": self._get_system_color("foreground"),
+ "line_number_background": self._get_system_color("line_number_background"),
+ "line_number_foreground": self._get_system_color("line_number_foreground"),
+ "keyword": "#0000ff",
+ "string": "#990099",
+ "comment": "#007f00",
+ "number": "#ff6600",
+ "operator": "#000000",
+ "property": "#0033aa",
+ "error": "#cc0000",
+ "uri": "#006666",
+ "reference": "#006666",
+ "document": "#7a1fa2",
+ }
+
+ def _get_default_autocomplete_colors(self) -> dict[str, str]:
+ is_dark = wx.SystemSettings.GetAppearance().IsDark()
+
+ if is_dark:
+ return {
+ "keyword": "#569cd6",
+ "function": "#dcdcaa",
+ "table": "#4ec9b0",
+ "column": "#9cdcfe",
+ }
+
+ return {
+ "keyword": "#0000ff",
+ "function": "#800080",
+ "table": "#008000",
+ "column": "#000000",
+ }
diff --git a/windows/dialogs/__init__.py b/windows/dialogs/__init__.py
new file mode 100644
index 0000000..622e1d3
--- /dev/null
+++ b/windows/dialogs/__init__.py
@@ -0,0 +1,7 @@
+from windows.dialogs.connections.view import ConnectionsManager
+from windows.dialogs.settings.controller import SettingsController
+
+__all__ = [
+ "ConnectionsManager",
+ "SettingsController",
+]
diff --git a/windows/dialogs/advanced_cell_editor/__init__.py b/windows/dialogs/advanced_cell_editor/__init__.py
new file mode 100644
index 0000000..c844cb4
--- /dev/null
+++ b/windows/dialogs/advanced_cell_editor/__init__.py
@@ -0,0 +1,3 @@
+from windows.dialogs.advanced_cell_editor.controller import AdvancedCellEditorController
+
+__all__ = ["AdvancedCellEditorController"]
diff --git a/windows/dialogs/advanced_cell_editor/controller.py b/windows/dialogs/advanced_cell_editor/controller.py
new file mode 100644
index 0000000..15ceb12
--- /dev/null
+++ b/windows/dialogs/advanced_cell_editor/controller.py
@@ -0,0 +1,60 @@
+import wx
+
+from windows.components.stc.detectors import detect_syntax_id
+from windows.components.stc.profiles import SyntaxProfile
+from windows.components.stc.styles import apply_stc_theme
+from windows.views import AdvancedCellEditorDialog
+
+
+class AdvancedCellEditorController(AdvancedCellEditorDialog):
+ app = wx.GetApp()
+
+ def __init__(self, parent, value: str):
+ super().__init__(parent)
+
+ self.syntax_choice.AppendItems(self.app.syntax_registry.labels())
+ self.advanced_stc_editor.SetText(value or "")
+ self.advanced_stc_editor.EmptyUndoBuffer()
+
+ self.app.theme_manager.register(self.advanced_stc_editor, self._get_current_syntax_profile)
+
+ self.syntax_choice.SetStringSelection(self._auto_syntax_profile().label)
+
+ self.do_apply_syntax(do_format=True)
+
+ def _auto_syntax_profile(self) -> SyntaxProfile:
+ text = self.advanced_stc_editor.GetText()
+
+ syntax_id = detect_syntax_id(text)
+ return self.app.syntax_registry.get(syntax_id)
+
+ def _get_current_syntax_profile(self) -> SyntaxProfile:
+ label = self.syntax_choice.GetStringSelection()
+ return self.app.syntax_registry.get(label)
+
+ def on_syntax_changed(self, _evt):
+ label = self.syntax_choice.GetStringSelection()
+ self.do_apply_syntax(label)
+
+ def do_apply_syntax(self, do_format: bool = True):
+ label = self.syntax_choice.GetStringSelection()
+ syntax_profile = self.app.syntax_registry.by_label(label)
+
+ apply_stc_theme(self.advanced_stc_editor, syntax_profile)
+
+ if do_format and syntax_profile.formatter:
+ old = self.advanced_stc_editor.GetText()
+ try:
+ formatted = syntax_profile.formatter(old)
+ except Exception:
+ return
+
+ if formatted != old:
+ self._replace_text_undo_friendly(formatted)
+
+ def _replace_text_undo_friendly(self, new_text: str):
+ self.advanced_stc_editor.BeginUndoAction()
+ try:
+ self.advanced_stc_editor.SetText(new_text)
+ finally:
+ self.advanced_stc_editor.EndUndoAction()
diff --git a/windows/connections/__init__.py b/windows/dialogs/connections/__init__.py
similarity index 100%
rename from windows/connections/__init__.py
rename to windows/dialogs/connections/__init__.py
diff --git a/windows/connections/controller.py b/windows/dialogs/connections/controller.py
similarity index 98%
rename from windows/connections/controller.py
rename to windows/dialogs/connections/controller.py
index 4c916f6..8c5774f 100644
--- a/windows/connections/controller.py
+++ b/windows/dialogs/connections/controller.py
@@ -8,7 +8,7 @@
from structures.connection import Connection
from . import CURRENT_CONNECTION, ConnectionDirectory, CURRENT_DIRECTORY
-from windows.connections.repository import ConnectionsRepository
+from windows.dialogs.connections.repository import ConnectionsRepository
class ConnectionsTreeModel(BaseDataViewTreeModel):
diff --git a/windows/connections/model.py b/windows/dialogs/connections/model.py
similarity index 100%
rename from windows/connections/model.py
rename to windows/dialogs/connections/model.py
diff --git a/windows/connections/repository.py b/windows/dialogs/connections/repository.py
similarity index 85%
rename from windows/connections/repository.py
rename to windows/dialogs/connections/repository.py
index c2b178c..e87a2f8 100644
--- a/windows/connections/repository.py
+++ b/windows/dialogs/connections/repository.py
@@ -1,11 +1,12 @@
-import os
+from pathlib import Path
from typing import Any, Optional, Union
-import yaml
+from constants import WORKDIR
+from helpers.logger import logger
+from helpers.observables import ObservableLazyList
+from helpers.repository import YamlRepository
-from helpers.observables import ObservableList, ObservableLazyList
-
-from windows.connections import ConnectionDirectory
+from windows.dialogs.connections import ConnectionDirectory
from structures.connection import (
Connection,
@@ -15,16 +16,13 @@
SSHTunnelConfiguration,
)
-WORKDIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-CONNECTIONS_CONFIG_FILE = os.path.join(WORKDIR, "connections.yml")
+CONNECTIONS_CONFIG_FILE = WORKDIR / "connections.yml"
-class ConnectionsRepository:
+class ConnectionsRepository(YamlRepository[list[Union[ConnectionDirectory, Connection]]]):
def __init__(self, config_file: Optional[str] = None):
- self._config_file = config_file or CONNECTIONS_CONFIG_FILE
+ super().__init__(Path(config_file or CONNECTIONS_CONFIG_FILE))
self._id_counter = 0
-
self.connections = ObservableLazyList(self.load)
def _next_id(self):
@@ -33,20 +31,22 @@ def _next_id(self):
return id
def _read(self) -> list[dict[str, Any]]:
- try:
- connections = yaml.full_load(open(self._config_file))
- return connections or []
- except Exception:
- return []
+ data = self._read_yaml()
+ if isinstance(data, list):
+ return data
+ return []
def _write(self) -> None:
connections = self.connections.get_value()
payload = [item.to_dict() for item in connections]
- with open(self._config_file, 'w') as file_handler:
- yaml.dump(payload, file_handler, sort_keys=False)
+ self._write_yaml(payload)
def load(self) -> list[Union[ConnectionDirectory, Connection]]:
- return [self._item_from_dict(data) for data in self._read()]
+ data = self._read()
+ logger.debug(f"ConnectionsRepository.load: loading {len(data)} items from {self._config_file}")
+ result = [self._item_from_dict(item) for item in data]
+ logger.debug(f"ConnectionsRepository.load: loaded {len(result)} connections/directories")
+ return result
def _item_from_dict(self, data: dict[str, Any], parent: Optional[ConnectionDirectory] = None) -> Union[ConnectionDirectory, Connection]:
if data.get('type') == 'directory':
diff --git a/windows/connections/manager.py b/windows/dialogs/connections/view.py
similarity index 95%
rename from windows/connections/manager.py
rename to windows/dialogs/connections/view.py
index c446c61..8962de5 100644
--- a/windows/connections/manager.py
+++ b/windows/dialogs/connections/view.py
@@ -1,29 +1,29 @@
-from typing import Optional
from gettext import gettext as _
+from typing import Optional
import wx
-from helpers.logger import logger
from helpers.loader import Loader
+from helpers.logger import logger
from structures.session import Session
from structures.connection import Connection, ConnectionEngine
-from windows import ConnectionsDialog
-from windows.main import SESSIONS_LIST, CURRENT_SESSION
+from windows.views import ConnectionsDialog
-from windows.connections import CURRENT_CONNECTION, PENDING_CONNECTION, ConnectionDirectory, CURRENT_DIRECTORY
-from windows.connections.model import ConnectionModel
-from windows.connections.controller import ConnectionsTreeController
-from windows.connections.repository import ConnectionsRepository
+from windows.main import CURRENT_SESSION, SESSIONS_LIST
+from windows.dialogs.connections import CURRENT_CONNECTION, CURRENT_DIRECTORY, PENDING_CONNECTION, ConnectionDirectory
+from windows.dialogs.connections.model import ConnectionModel
+from windows.dialogs.connections.controller import ConnectionsTreeController
+from windows.dialogs.connections.repository import ConnectionsRepository
-class ConnectionsManager(ConnectionsDialog):
- _app = wx.GetApp()
- _repository = ConnectionsRepository()
+class ConnectionsManager(ConnectionsDialog):
def __init__(self, parent):
super().__init__(parent)
+ self._app = wx.GetApp()
+ self._repository = ConnectionsRepository()
self.engine.SetItems([e.name for e in ConnectionEngine.get_all()])
self.connections_tree_controller = ConnectionsTreeController(self.connections_tree_ctrl, self._repository)
diff --git a/windows/dialogs/settings/__init__.py b/windows/dialogs/settings/__init__.py
new file mode 100644
index 0000000..0b61f6d
--- /dev/null
+++ b/windows/dialogs/settings/__init__.py
@@ -0,0 +1,3 @@
+from windows.dialogs.settings.controller import SettingsController
+
+__all__ = ["SettingsController"]
diff --git a/windows/dialogs/settings/controller.py b/windows/dialogs/settings/controller.py
new file mode 100644
index 0000000..f608583
--- /dev/null
+++ b/windows/dialogs/settings/controller.py
@@ -0,0 +1,223 @@
+from pathlib import Path
+
+import wx
+import wx.dataview
+
+from constants import Language, LogLevel
+
+from windows.dialogs.settings.repository import Settings
+from windows.views import SettingsDialog
+
+
+class SettingsController:
+ def __init__(self, parent: wx.Window, settings: Settings) -> None:
+ self.settings = settings
+ self.dialog = SettingsDialog(parent)
+
+ self._load_settings()
+ self._populate_controls()
+ self._bind_events()
+
+ def _bind_events(self) -> None:
+ self.dialog.apply.Bind(wx.EVT_BUTTON, self._on_apply)
+ self.dialog.cancel.Bind(wx.EVT_BUTTON, self._on_cancel)
+ self.dialog.shortcuts_filter.Bind(wx.EVT_SEARCH, self._on_filter_shortcuts)
+
+ def _populate_controls(self) -> None:
+ self._populate_languages()
+ self._populate_themes()
+ self._populate_advanced_settings()
+ self._populate_shortcuts()
+
+ def _populate_languages(self) -> None:
+ self.dialog.language.Clear()
+ for lang in Language:
+ self.dialog.language.Append(lang.label)
+
+ 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"
+
+ self.dialog.shortcuts_list.AppendItem([action, shortcut, context])
+
+ def _populate_themes(self) -> None:
+ themes_dir = Path(wx.GetApp().GetAppName()).parent / "themes"
+ if not themes_dir.exists():
+ themes_dir = Path.cwd() / "themes"
+
+ self.dialog.theme.Clear()
+
+ if themes_dir.exists():
+ for theme_file in sorted(themes_dir.glob("*.yml")):
+ theme_name = theme_file.stem
+ self.dialog.theme.Append(theme_name)
+
+ if self.dialog.theme.GetCount() > 0:
+ self.dialog.theme.SetSelection(0)
+
+ def _load_settings(self) -> None:
+ self._load_general_settings()
+ self._load_appearance_settings()
+ self._load_query_editor_settings()
+ self._load_advanced_settings()
+
+ def _load_advanced_settings(self) -> None:
+ self.dialog.advanced_connection_timeout.SetValue(
+ self.settings.get_value("advanced", "connection_timeout") or 60
+ )
+ self.dialog.advanced_query_timeout.SetValue(
+ self.settings.get_value("advanced", "query_timeout") or 60
+ )
+
+ levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARNING, LogLevel.ERROR]
+ logging_level = self.settings.get_value("advanced", "logging_level") or "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 ""
+ 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"
+ 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"
+ 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.dialog.query_editor_trim_whitespace.SetValue(
+ self.settings.get_value("query_editor", "trim_whitespace") or False
+ )
+ self.dialog.query_editor_execute_selected_only.SetValue(
+ self.settings.get_value("query_editor", "execute_selected_only") or False
+ )
+
+ autocomplete = self.settings.get_value("query_editor", "autocomplete")
+ self.dialog.query_editor_autocomplete.SetValue(
+ autocomplete if autocomplete is not None else True
+ )
+
+ autoformat = self.settings.get_value("query_editor", "autoformat")
+ self.dialog.query_editor_format.SetValue(
+ autoformat if autoformat is not None else True
+ )
+
+ def _save_settings(self) -> None:
+ self._save_general_settings()
+ self._save_appearance_settings()
+ self._save_query_editor_settings()
+ 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",
+ value=self.dialog.advanced_connection_timeout.GetValue()
+ )
+ self.settings.set_value("advanced", "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",
+ 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={})
+
+ 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))
+
+ 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)
+
+ 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",
+ value=self.dialog.query_editor_statement_separator.GetValue()
+ )
+ self.settings.set_value("query_editor", "trim_whitespace",
+ value=self.dialog.query_editor_trim_whitespace.GetValue()
+ )
+ self.settings.set_value("query_editor", "execute_selected_only",
+ value=self.dialog.query_editor_execute_selected_only.GetValue()
+ )
+ self.settings.set_value("query_editor", "autocomplete",
+ value=self.dialog.query_editor_autocomplete.GetValue()
+ )
+ self.settings.set_value("query_editor", "autoformat",
+ value=self.dialog.query_editor_format.GetValue()
+ )
+
+ def _on_apply(self, event: wx.Event) -> None:
+ self._save_settings()
+ self.dialog.EndModal(wx.ID_OK)
+
+ def _on_cancel(self, event: wx.Event) -> None:
+ self.dialog.EndModal(wx.ID_CANCEL)
+
+ 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
+ filter_text in context.lower()):
+ self.dialog.shortcuts_list.AppendItem([action, shortcut, context])
+
+ def show_modal(self) -> int:
+ return self.dialog.ShowModal()
diff --git a/windows/dialogs/settings/repository.py b/windows/dialogs/settings/repository.py
new file mode 100644
index 0000000..b3ee7b1
--- /dev/null
+++ b/windows/dialogs/settings/repository.py
@@ -0,0 +1,28 @@
+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
diff --git a/windows/explorer.bkp.py b/windows/explorer.bkp.py
deleted file mode 100755
index 70f4b20..0000000
--- a/windows/explorer.bkp.py
+++ /dev/null
@@ -1,473 +0,0 @@
-import dataclasses
-from typing import Callable, List
-
-import wx
-import wx.dataview
-import wx.lib.agw.hypertreelist
-
-from helpers import bytes_to_human
-from helpers.dataview import BaseDataViewTreeModel
-from icons import IconList, ImageList, BitmapList
-from structures.connection import Connection
-
-from structures.engines.database import SQLDatabase, SQLTable, SQLView, SQLTrigger, SQLProcedure, SQLFunction, SQLEvent
-
-from windows.main import CURRENT_DATABASE, CURRENT_TABLE, CURRENT_CONNECTION, CURRENT_VIEW, CURRENT_TRIGGER, CONNECTIONS_LIST, CURRENT_EVENT, CURRENT_FUNCTION, CURRENT_PROCEDURE
-from windows.main.table import NEW_TABLE
-
-
-@dataclasses.dataclass
-class TreeCategory:
- name : str
- icon : wx.BitmapBundle
- database : SQLDatabase
-
-
-# class GaugeWithLabel(wx.Panel):
-# def __init__(self, parent, max_range=100, size=(100, 20)):
-# super().__init__(parent, size=size)
-# self.SetBackgroundStyle(wx.BG_STYLE_PAINT)
-#
-# self.gauge = wx.Gauge(self, range=max_range, size=size)
-# self.label = wx.StaticText(self, label="0%", style=wx.ALIGN_CENTER)
-#
-# self.sizer = wx.BoxSizer(wx.VERTICAL)
-# self.sizer.Add(self.gauge, 1, wx.EXPAND, 5)
-# self.sizer.Add(self.label, 0, wx.ALIGN_CENTER | wx.TOP, -size[1])
-#
-# self.SetSizer(self.sizer)
-#
-# self.label.SetForegroundColour(wx.WHITE)
-# font = self.label.GetFont()
-# font.SetWeight(wx.FONTWEIGHT_BOLD)
-# self.label.SetFont(font)
-#
-# def SetValue(self, val):
-# self.gauge.SetValue(val)
-# self.label.SetLabel(f"{val}%")
-# self.label.Refresh()
-#
-#
-# class TreeExplorerController:
-# on_cancel_table: Callable
-# do_cancel_table: Callable
-#
-# def __init__(self, tree_ctrl_explorer: wx.lib.agw.hypertreelist.HyperTreeList):
-# self.app = wx.GetApp()
-#
-# self.tree_ctrl_explorer = tree_ctrl_explorer
-#
-# self.tree_ctrl_explorer.AddColumn("Name", width=200)
-# self.tree_ctrl_explorer.AddColumn("Usage", width=100, flag=wx.ALIGN_RIGHT)
-#
-# self.tree_ctrl_explorer.SetMainColumn(0)
-# self.tree_ctrl_explorer.AssignImageList(ImageList)
-# self.tree_ctrl_explorer._main_win.Bind(
-# wx.EVT_MOUSE_EVENTS, lambda e: None if e.LeftDown() else e.Skip()
-# )
-#
-# self.populate_tree()
-#
-# self.tree_ctrl_explorer.Bind(wx.EVT_TREE_SEL_CHANGED, self._load_items)
-# self.tree_ctrl_explorer.Bind(wx.EVT_TREE_ITEM_EXPANDING, self._load_items)
-# self.tree_ctrl_explorer.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._load_items)
-#
-# CONNECTIONS_LIST.subscribe(self.append_connection, CallbackEvent.ON_APPEND)
-#
-# # CURRENT_DATABASE.get_value().tables.subscribe(self.load_observables, CallbackEvent.ON_APPEND)
-# # CURRENT_DATABASE.get_value().views.subscribe(self.load_observables, CallbackEvent.ON_APPEND)
-# # CURRENT_DATABASE.get_value().procedures.subscribe(self.load_observables, CallbackEvent.ON_APPEND)
-# # CURRENT_DATABASE.get_value().functions.subscribe(self.load_observables, CallbackEvent.ON_APPEND)
-# # CURRENT_DATABASE.get_value().triggers.subscribe(self.load_observables, CallbackEvent.ON_APPEND)
-# # CURRENT_DATABASE.get_value().events.subscribe(self.load_observables, CallbackEvent.ON_APPEND)
-#
-# def _load_items(self, event: wx.lib.agw.hypertreelist.TreeEvent):
-# with Loader.cursor_wait():
-# item = event.GetItem()
-# if not item.IsOk():
-# event.Skip()
-# return
-#
-# if NEW_TABLE.get_value() and not self.on_cancel_table(event):
-# event.Skip()
-# return
-#
-# self.reset_current_objects()
-#
-# obj = self.tree_ctrl_explorer.GetItemPyData(item)
-# if obj is None:
-# event.Skip()
-# return
-#
-# if isinstance(obj, Connection):
-# self.select_connection(obj, event)
-# elif isinstance(obj, SQLDatabase):
-# self.select_database(obj, item, event)
-# elif isinstance(
-# obj,
-# (SQLTable, SQLView, SQLTrigger, SQLProcedure, SQLFunction, SQLEvent),
-# ):
-# self.select_sql_object(obj)
-#
-# event.Skip()
-#
-# def populate_tree(self):
-# self.tree_ctrl_explorer.DeleteAllItems()
-# self.root_item = self.tree_ctrl_explorer.AddRoot("")
-#
-# for connection in CONNECTIONS_LIST.get_value():
-# self.append_connection(connection)
-#
-# def append_connection(self, connection: Connection):
-# self.root_item = self.tree_ctrl_explorer.GetRootItem()
-#
-# connection_item = self.tree_ctrl_explorer.AppendItem(self.root_item, connection.name, image=getattr(IconList, f"ENGINE_{connection.engine.name}"), data=connection)
-# for database in connection.context.databases.get_value():
-# db_item = self.tree_ctrl_explorer.AppendItem(connection_item, database.name, image=IconList.SYSTEM_DATABASE, data=database)
-# self.tree_ctrl_explorer.SetItemText(db_item, bytes_to_human(database.total_bytes), column=1)
-# self.tree_ctrl_explorer.AppendItem(db_item, "Loading...", image=IconList.CLOCK, data=None)
-#
-# self.tree_ctrl_explorer.Expand(connection_item)
-# self.tree_ctrl_explorer.EnsureVisible(connection_item)
-#
-# self.tree_ctrl_explorer.Layout()
-#
-# def load_observables(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=getattr(IconList, f"SYSTEM_{observable_name[:-1].upper()}", IconList.NOT_FOUND),
-# data=None
-# )
-#
-# if observable is None:
-# continue
-#
-# if database != CURRENT_DATABASE.get_value() and not observable.is_loaded:
-# continue
-#
-# objs = observable.get_value()
-# if not objs:
-# continue
-#
-#
-# # wx.CallAfter(self.tree_ctrl_explorer.Expand, category_item)
-#
-# for obj in objs:
-# obj_item = self.tree_ctrl_explorer.AppendItem(
-# category_item,
-# obj.name,
-# image=getattr(IconList, f"SYSTEM_{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
-#
-# 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)
-# self.tree_ctrl_explorer.SetItemWindow(obj_item, gauge_panel, column=1)
-# else:
-# self.tree_ctrl_explorer.SetItemText(obj_item, "", column=1)
-#
-# loading_item, index_item = self.tree_ctrl_explorer.GetFirstChild(db_item)
-# if loading_item and loading_item.GetData() is None:
-# self.tree_ctrl_explorer.Delete(loading_item)
-#
-# def reset_current_objects(self):
-# 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)
-#
-# def select_connection(self, connection: Connection, event):
-# if connection == CURRENT_CONNECTION.get_value() and CURRENT_DATABASE.get_value():
-# event.Skip()
-# return
-# CURRENT_CONNECTION.set_value(connection)
-# CURRENT_DATABASE.set_value(None)
-#
-# def select_database(self, database: SQLDatabase, item, event):
-# if database != CURRENT_DATABASE.get_value():
-# connection = database.context.connection
-# if connection != CURRENT_CONNECTION.get_value():
-# CURRENT_CONNECTION.set_value(connection)
-# CURRENT_DATABASE.set_value(database)
-# self.load_observables(item, database)
-#
-# if not self.tree_ctrl_explorer.IsExpanded(item):
-# wx.CallAfter(self.tree_ctrl_explorer.Expand, item)
-#
-# def select_sql_object(self, sql_obj):
-# database = sql_obj.database
-# connection = database.context.connection
-#
-# if connection != CURRENT_CONNECTION.get_value():
-# CURRENT_CONNECTION.set_value(connection)
-#
-# if database != CURRENT_DATABASE.get_value():
-# CURRENT_DATABASE.set_value(database)
-#
-# if isinstance(sql_obj, SQLTable):
-# if not CURRENT_TABLE.get_value() or sql_obj != CURRENT_TABLE.get_value():
-# CURRENT_TABLE.set_value(sql_obj.copy())
-#
-# elif isinstance(sql_obj, SQLView):
-# if not CURRENT_VIEW.get_value() or sql_obj != CURRENT_VIEW.get_value():
-# CURRENT_VIEW.set_value(sql_obj.copy())
-#
-# elif isinstance(sql_obj, SQLTrigger):
-# if not CURRENT_TRIGGER.get_value() or sql_obj != CURRENT_TRIGGER.get_value():
-# CURRENT_TRIGGER.set_value(sql_obj.copy())
-#
-# elif isinstance(sql_obj, SQLProcedure):
-# if not CURRENT_PROCEDURE.get_value() or sql_obj != CURRENT_PROCEDURE.get_value():
-# CURRENT_PROCEDURE.set_value(sql_obj.copy())
-#
-# elif isinstance(sql_obj, SQLFunction):
-# if not CURRENT_FUNCTION.get_value() or sql_obj != CURRENT_FUNCTION.get_value():
-# CURRENT_FUNCTION.set_value(sql_obj.copy())
-#
-# 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())
-
-
-class ExplorerTreeModel(BaseDataViewTreeModel):
- def __init__(self):
- super().__init__(column_count=2)
- self._parent_map = {}
-
- def GetColumnType(self, col):
- if col == 0:
- return wx.dataview.DataViewIconText
-
- return "long"
-
- def GetChildren(self, parent, children):
- if not parent:
- for connection in CONNECTIONS_LIST.get_value():
- children.append(self.ObjectToItem(connection))
- else:
- obj = self.ItemToObject(parent)
- if isinstance(obj, Connection):
- for db in obj.context.databases.get_value():
- children.append(self.ObjectToItem(db))
- self._parent_map[self.ObjectToItem(db)] = parent
- elif isinstance(obj, SQLDatabase):
- categories = [
- ("Tables", BitmapList.DATABASE_TABLE, "tables"),
- ("Views", BitmapList.SYSTEM_VIEW, "views"),
- ("Procedures", BitmapList.SYSTEM_PROCEDURE, "procedures"),
- ("Functions", BitmapList.SYSTEM_FUNCTION, "functions"),
- ("Triggers", BitmapList.SYSTEM_TRIGGER, "triggers"),
- ("Events", BitmapList.SYSTEM_EVENT, "events"),
- ]
- for name, icon, attr in categories:
- cat = TreeCategory(name, icon, obj)
- children.append(self.ObjectToItem(cat))
- self._parent_map[self.ObjectToItem(cat)] = parent
-
- elif isinstance(obj, TreeCategory):
- observable = getattr(obj.database, obj.name.lower(), None)
- if observable and observable.is_loaded:
- objs = observable.get_value()
- for o in objs:
- children.append(self.ObjectToItem(o))
- self._parent_map[self.ObjectToItem(o)] = parent
-
- return len(children)
-
- def IsContainer(self, item):
- if not item:
- return True
-
- obj = self.ItemToObject(item)
- return isinstance(obj, (Connection, SQLDatabase, TreeCategory))
-
- def GetParent(self, item):
- return self._parent_map.get(item, wx.dataview.NullDataViewItem)
-
- def GetValue(self, item, col):
- node = self.ItemToObject(item)
-
- # if isinstance(node, Connection):
- # bitmap = node.engine.value.bitmap
- # mapper = {0: wx.dataview.DataViewIconText(node.name, bitmap), 1: ""}
- # elif isinstance(node, SQLDatabase):
- # mapper = {0: wx.dataview.DataViewIconText(node.name, BitmapList.SYSTEM_DATABASE), 1: bytes_to_human(node.total_bytes)}
- # elif isinstance(node, Category):
- # mapper = {0: wx.dataview.DataViewIconText(node.name, node.icon), 1: ""}
- # elif isinstance(node, (SQLTable, SQLView, SQLTrigger, SQLProcedure, SQLFunction, SQLEvent)):
- # icon_name = f"SYSTEM_{type(node).__name__[3:].upper()}"
- # icon = getattr(BitmapList, icon_name, BitmapList.NOT_FOUND)
- # mapper = {0: wx.dataview.DataViewIconText(node.name, icon), 1: ""}
- # else:
- mapper = {}
-
-
- if isinstance(node, Connection):
- mapper = {0: wx.dataview.DataViewIconText(node.name, node.engine.value.bitmap), 1: "", }
- elif isinstance(node, SQLDatabase):
- mapper = {0: wx.dataview.DataViewIconText(node.name, BitmapList.DATABASE), 1: bytes_to_human(node.total_bytes)}
- elif isinstance(node, TreeCategory):
- mapper = {0: wx.dataview.DataViewIconText(node.name, node.icon), 1: ""}
- elif isinstance(node, SQLTable):
- mapper = {0: wx.dataview.DataViewIconText(node.name, BitmapList.DATABASE_TABLE), 1: int((node.total_bytes / node.database.total_bytes) * 100)}
- elif isinstance(node, SQLView):
- mapper = {0: wx.dataview.DataViewIconText(node.name, BitmapList.SYSTEM_VIEW), 1: 0}
- elif isinstance(node, SQLProcedure):
- mapper = {0: wx.dataview.DataViewIconText(node.name, BitmapList.SYSTEM_PROCEDURE), 1: 0}
- elif isinstance(node, SQLFunction):
- mapper = {0: wx.dataview.DataViewIconText(node.name, BitmapList.SYSTEM_FUNCTION), 1: 0}
- elif isinstance(node, SQLTrigger):
- mapper = {0: wx.dataview.DataViewIconText(node.name, BitmapList.SYSTEM_TRIGGER), 1: 0}
- elif isinstance(node, SQLEvent):
- mapper = {0: wx.dataview.DataViewIconText(node.name, BitmapList.SYSTEM_EVENT), 1: 0}
-
- try :
- return mapper[col]
- except Exception as ex :
- print("qsdq")
-
-class TreeExplorerController:
- on_cancel_table: Callable
- do_cancel_table: Callable
-
- def __init__(self, tree_ctrl_explorer: wx.dataview.DataViewCtrl):
- self.app = wx.GetApp()
-
- self.tree_ctrl_explorer = tree_ctrl_explorer
-
- self.model = ExplorerTreeModel()
- self.tree_ctrl_explorer.AssociateModel(self.model)
-
- # Set up columns
- column0 = wx.dataview.DataViewColumn("Name", wx.dataview.DataViewIconTextRenderer(), 0, width=200, align=wx.ALIGN_LEFT)
- column1 = wx.dataview.DataViewColumn("Usage", wx.dataview.DataViewProgressRenderer(), 1, width=100, align=wx.ALIGN_LEFT)
- self.tree_ctrl_explorer.AppendColumn(column0)
- self.tree_ctrl_explorer.AppendColumn(column1)
-
- self.tree_ctrl_explorer.Bind(wx.dataview.EVT_DATAVIEW_SELECTION_CHANGED, self._on_selection_changed)
- self.tree_ctrl_explorer.Bind(wx.dataview.EVT_DATAVIEW_ITEM_EXPANDING, self._on_item_expanding)
- self.tree_ctrl_explorer.Bind(wx.dataview.EVT_DATAVIEW_ITEM_ACTIVATED, self._on_item_activated)
-
- CONNECTIONS_LIST.subscribe(self._on_connections_changed)
-
- def _on_connections_changed(self, connections):
- self.model.Cleared()
-
- def _on_item_expanding(self, event):
- item = event.GetItem()
- if not item.IsOk():
- return
-
- obj = self.model.ItemToObject(item)
- if isinstance(obj, SQLDatabase):
- for attr in ['tables', 'views', 'procedures', 'functions', 'triggers', 'events']:
- observable = getattr(obj, attr, None)
- if observable and not observable.is_loaded:
- observable.load()
- elif isinstance(obj, TreeCategory):
- observable = getattr(obj.database, obj.name.lower(), None)
- if observable and not observable.is_loaded:
- observable.load()
- event.Skip()
-
- def _on_selection_changed(self, event):
- item = event.GetItem()
- if not item.IsOk():
- return
-
- if NEW_TABLE.get_value() and not self.on_cancel_table(event):
- return
-
- self.reset_current_objects()
-
- obj = self.model.ItemToObject(item)
-
- if isinstance(obj, Connection):
- self.select_connection(obj)
- elif isinstance(obj, SQLDatabase):
- self.select_database(obj)
- elif isinstance(obj, (SQLTable, SQLView, SQLTrigger, SQLProcedure, SQLFunction, SQLEvent)):
- self.select_sql_object(obj)
-
- event.Skip()
-
- def _on_item_activated(self, event):
- item = event.GetItem()
- if not item.IsOk():
- return
-
- obj = self.model.ItemToObject(item)
-
- if isinstance(obj, (Connection, SQLDatabase, TreeCategory)):
- if self.tree_ctrl_explorer.IsExpanded(item):
- self.tree_ctrl_explorer.Collapse(item)
- else:
- self.tree_ctrl_explorer.Expand(item)
- event.Skip()
-
- def reset_current_objects(self):
- 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)
-
- def select_connection(self, connection: Connection):
- if connection == CURRENT_CONNECTION.get_value() and CURRENT_DATABASE.get_value():
- return
- CURRENT_CONNECTION.set_value(connection)
- CURRENT_DATABASE.set_value(None)
-
- def select_database(self, database: SQLDatabase):
- if database != CURRENT_DATABASE.get_value():
- connection = database.context.connection
- if connection != CURRENT_CONNECTION.get_value():
- CURRENT_CONNECTION.set_value(connection)
- CURRENT_DATABASE.set_value(database)
-
- if not self.tree_ctrl_explorer.IsExpanded(self.model.ObjectToItem(database)):
- wx.CallAfter(self.tree_ctrl_explorer.Expand, self.model.ObjectToItem(database))
-
- def select_sql_object(self, sql_obj):
- database = sql_obj.database
- connection = database.context.connection
-
- if connection != CURRENT_CONNECTION.get_value():
- CURRENT_CONNECTION.set_value(connection)
-
- if database != CURRENT_DATABASE.get_value():
- CURRENT_DATABASE.set_value(database)
-
- if isinstance(sql_obj, SQLTable):
- if not CURRENT_TABLE.get_value() or sql_obj != CURRENT_TABLE.get_value():
- CURRENT_TABLE.set_value(sql_obj.copy())
-
- elif isinstance(sql_obj, SQLView):
- if not CURRENT_VIEW.get_value() or sql_obj != CURRENT_VIEW.get_value():
- CURRENT_VIEW.set_value(sql_obj.copy())
-
- elif isinstance(sql_obj, SQLTrigger):
- if not CURRENT_TRIGGER.get_value() or sql_obj != CURRENT_TRIGGER.get_value():
- CURRENT_TRIGGER.set_value(sql_obj.copy())
-
- elif isinstance(sql_obj, SQLProcedure):
- if not CURRENT_PROCEDURE.get_value() or sql_obj != CURRENT_PROCEDURE.get_value():
- CURRENT_PROCEDURE.set_value(sql_obj.copy())
-
- elif isinstance(sql_obj, SQLFunction):
- if not CURRENT_FUNCTION.get_value() or sql_obj != CURRENT_FUNCTION.get_value():
- CURRENT_FUNCTION.set_value(sql_obj.copy())
-
- 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())
diff --git a/windows/main/__init__.py b/windows/main/__init__.py
index f326963..8d7fd9e 100755
--- a/windows/main/__init__.py
+++ b/windows/main/__init__.py
@@ -1,24 +1,35 @@
-from helpers.observables import Observable, ObservableList
+from windows.state import (
+ AUTO_APPLY,
+ CURRENT_COLUMN,
+ CURRENT_CONNECTION,
+ CURRENT_DATABASE,
+ CURRENT_EVENT,
+ CURRENT_FOREIGN_KEY,
+ CURRENT_FUNCTION,
+ CURRENT_INDEX,
+ CURRENT_PROCEDURE,
+ CURRENT_RECORDS,
+ CURRENT_SESSION,
+ CURRENT_TABLE,
+ CURRENT_TRIGGER,
+ CURRENT_VIEW,
+ SESSIONS_LIST,
+)
-from structures.session import Session
-from structures.connection import Connection
-from structures.engines.database import SQLDatabase, SQLTable, SQLColumn, SQLForeignKey, SQLIndex, SQLRecord, SQLTrigger, SQLView
-
-SESSIONS_LIST: ObservableList[Session] = ObservableList()
-# CONNECTIONS_LIST: ObservableList[Connection] = ObservableList()
-
-CURRENT_SESSION: Observable[Session] = Observable()
-CURRENT_CONNECTION: Observable[Connection] = Observable()
-CURRENT_DATABASE: Observable[SQLDatabase] = Observable()
-CURRENT_TABLE: Observable[SQLTable] = Observable()
-CURRENT_VIEW: Observable[SQLView] = Observable()
-CURRENT_TRIGGER: Observable[SQLTrigger] = Observable()
-CURRENT_FUNCTION: Observable[SQLTrigger] = Observable()
-CURRENT_PROCEDURE: Observable[SQLTrigger] = Observable()
-CURRENT_EVENT: Observable[SQLTrigger] = Observable()
-CURRENT_COLUMN: Observable[SQLColumn] = Observable()
-CURRENT_INDEX: Observable[SQLIndex] = Observable()
-CURRENT_FOREIGN_KEY: Observable[SQLForeignKey] = Observable()
-CURRENT_RECORDS: ObservableList[SQLRecord] = ObservableList()
-
-AUTO_APPLY: Observable[bool] = Observable(True)
+__all__ = [
+ "AUTO_APPLY",
+ "CURRENT_COLUMN",
+ "CURRENT_CONNECTION",
+ "CURRENT_DATABASE",
+ "CURRENT_EVENT",
+ "CURRENT_FOREIGN_KEY",
+ "CURRENT_FUNCTION",
+ "CURRENT_INDEX",
+ "CURRENT_PROCEDURE",
+ "CURRENT_RECORDS",
+ "CURRENT_SESSION",
+ "CURRENT_TABLE",
+ "CURRENT_TRIGGER",
+ "CURRENT_VIEW",
+ "SESSIONS_LIST",
+]
diff --git a/windows/main/main_frame.py b/windows/main/controller.py
similarity index 83%
rename from windows/main/main_frame.py
rename to windows/main/controller.py
index 06e6d42..78a58da 100755
--- a/windows/main/main_frame.py
+++ b/windows/main/controller.py
@@ -16,25 +16,29 @@
from helpers.logger import logger
from helpers.observables import CallbackEvent
+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 windows import MainFrameView
+from windows.views import MainFrameView
from windows.components.stc.styles import apply_stc_theme
from windows.components.stc.profiles import SQL
-from windows.components.stc.auto_complete import SQLAutoCompleteController, SQLCompletionProvider
+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.table import EditTableModel, NEW_TABLE
-from windows.main.index import TableIndexController
-from windows.main.check import TableCheckController
-from windows.main.column import TableColumnsController
-from windows.main.records import TableRecordsController
-from windows.main.database import ListDatabaseTable
-from windows.main.explorer import TreeExplorerController
-from windows.main.foreign_key import TableForeignKeyController
+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.explorer import TreeExplorerController
+from windows.main.tabs.foreign_key import TableForeignKeyController
+from windows.main.tabs.view import ViewEditorController
class MainFrameController(MainFrameView):
@@ -43,7 +47,7 @@ class MainFrameController(MainFrameView):
def __init__(self):
super().__init__(None)
- self.styled_text_ctrls_name = ["sql_query_logs", "sql_view", "sql_query_filters", "sql_create_table", "sql_query"]
+ self.styled_text_ctrls_name = ["sql_query_logs", "stc_view_select", "sql_query_filters", "sql_create_table", "sql_query_editor"]
self.edit_table_model = EditTableModel()
self.edit_table_model.bind_controls(
@@ -66,6 +70,10 @@ 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_view_editor = ViewEditorController(self)
+
self._setup_query_editors()
self._setup_subscribers()
@@ -101,6 +109,14 @@ def _setup_query_editors(self):
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(),
)
def _setup_subscribers(self):
@@ -188,56 +204,79 @@ def do_close(self, event):
wx.GetApp().ExitMainLoop()
def do_open_connection_manager(self, event):
- from windows.connections.manager import ConnectionsManager
+ from windows.dialogs.connections.view import ConnectionsManager
sm = ConnectionsManager(self)
sm.Show()
- def toggle_panel(self, current: Optional[Union[Connection, SQLDatabase, SQLTable, SQLView, SQLTrigger]] = None):
- current_connection = CURRENT_CONNECTION()
- current_database = CURRENT_DATABASE()
- current_table = CURRENT_TABLE()
- current_view = CURRENT_VIEW()
- current_trigger = CURRENT_TRIGGER()
+ def on_open_settings(self, event):
+ from windows.dialogs.settings.controller import SettingsController
+
+ controller = SettingsController(self, wx.GetApp().settings)
+ if controller.show_modal() == wx.ID_OK:
+ wx.MessageBox(_("Settings saved successfully"), _("Settings"), wx.OK | wx.ICON_INFORMATION)
- self.MainFrameNotebook.SetSelection(0)
+ def toggle_panel(self, current: Optional[Union[SQLDatabase, SQLTable, SQLView, SQLTrigger]] = None):
+ # self.MainFrameNotebook.SetSelection(0)
- if not current_database:
- self.MainFrameNotebook.GetPage(1).Hide()
+ current_session = CURRENT_SESSION.get_value()
+ current_database = CURRENT_DATABASE.get_value()
+ current_table = CURRENT_TABLE.get_value()
+ current_view = CURRENT_VIEW.get_value()
+ current_trigger = CURRENT_TRIGGER.get_value()
- if not current_table:
- self.MainFrameNotebook.GetPage(2).Hide()
+ total_pages = self.MainFrameNotebook.GetPageCount()
- if not current_view:
- self.MainFrameNotebook.GetPage(3).Hide()
+ if not current:
+ if not current_session:
+ for page in range(total_pages):
+ self.MainFrameNotebook.GetPage(page).Hide()
- if not current_trigger:
- self.MainFrameNotebook.GetPage(4).Hide()
+ if not current_database:
+ for page in range(1, total_pages):
+ self.MainFrameNotebook.GetPage(page).Hide()
- if not current_table and not current_view:
- self.MainFrameNotebook.GetPage(5).Hide()
+ if not current_table:
+ self.MainFrameNotebook.GetPage(2).Hide()
+ self.MainFrameNotebook.GetPage(5).Hide()
- if isinstance(current, Connection):
+ if not current_view:
+ self.MainFrameNotebook.GetPage(3).Hide()
+ self.MainFrameNotebook.GetPage(5).Hide()
+
+ if not current_trigger:
+ self.MainFrameNotebook.GetPage(4).Hide()
+
+ return
+
+ if isinstance(current, Session):
+ self.MainFrameNotebook.GetPage(0).Show()
self.MainFrameNotebook.SetSelection(0)
elif isinstance(current, SQLDatabase):
self.MainFrameNotebook.GetPage(1).Show()
+ self.MainFrameNotebook.GetPage(6).Show()
self.MainFrameNotebook.SetSelection(1)
elif isinstance(current, SQLTable) or isinstance(current, SQLView):
if isinstance(current, SQLTable):
self.MainFrameNotebook.GetPage(2).Show()
- self.MainFrameNotebook.SetSelection(2)
+ if self.MainFrameNotebook.GetSelection() < 2:
+ self.MainFrameNotebook.SetSelection(2)
if isinstance(current, SQLView):
self.MainFrameNotebook.GetPage(3).Show()
- self.MainFrameNotebook.SetSelection(3)
+ if self.MainFrameNotebook.GetSelection() < 3:
+ self.MainFrameNotebook.SetSelection(3)
self.MainFrameNotebook.GetPage(5).Show()
+ self.MainFrameNotebook.GetPage(6).Show()
elif isinstance(current, SQLTrigger):
self.MainFrameNotebook.GetPage(4).Show()
- self.MainFrameNotebook.SetSelection(3)
+ self.MainFrameNotebook.GetPage(6).Show()
+ if self.MainFrameNotebook.GetSelection() < 4:
+ self.MainFrameNotebook.SetSelection(3)
def on_page_chaged(self, event):
if int(event.Selection) == 5:
diff --git a/windows/main/tabs/__init__.py b/windows/main/tabs/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/windows/main/check.py b/windows/main/tabs/check.py
similarity index 98%
rename from windows/main/check.py
rename to windows/main/tabs/check.py
index e50faa1..2928d75 100755
--- a/windows/main/check.py
+++ b/windows/main/tabs/check.py
@@ -9,7 +9,7 @@
from structures.engines.database import SQLCheck, SQLTable
from windows.main import CURRENT_INDEX, CURRENT_TABLE
-from windows.main.column import NEW_TABLE
+from windows.main.tabs.column import NEW_TABLE
class TableCheckModel(BaseDataViewListModel):
diff --git a/windows/main/column.py b/windows/main/tabs/column.py
similarity index 98%
rename from windows/main/column.py
rename to windows/main/tabs/column.py
index be40b72..c4aca9b 100644
--- a/windows/main/column.py
+++ b/windows/main/tabs/column.py
@@ -12,9 +12,8 @@
from structures.engines.database import SQLColumn, SQLDatabase, SQLIndex, SQLTable
from structures.engines.indextype import SQLIndexType
-from windows import TableColumnsDataViewCtrl
-from windows.main import CURRENT_COLUMN, CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE
-from windows.main.table import NEW_TABLE
+from windows.views import TableColumnsDataViewCtrl
+from windows.state import CURRENT_COLUMN, CURRENT_SESSION, CURRENT_DATABASE, CURRENT_TABLE, NEW_TABLE
class ColumnModel(BaseDataViewListModel):
diff --git a/windows/main/database.py b/windows/main/tabs/database.py
similarity index 100%
rename from windows/main/database.py
rename to windows/main/tabs/database.py
diff --git a/windows/main/explorer.py b/windows/main/tabs/explorer.py
similarity index 96%
rename from windows/main/explorer.py
rename to windows/main/tabs/explorer.py
index a323bdb..db28fd1 100755
--- a/windows/main/explorer.py
+++ b/windows/main/tabs/explorer.py
@@ -14,8 +14,7 @@
from structures.connection import Connection
from structures.engines.database import SQLDatabase, SQLTable, SQLView, SQLTrigger, SQLProcedure, SQLFunction, SQLEvent
-from windows.main import CURRENT_DATABASE, CURRENT_TABLE, CURRENT_CONNECTION, CURRENT_SESSION, CURRENT_VIEW, CURRENT_TRIGGER, SESSIONS_LIST, CURRENT_EVENT, CURRENT_FUNCTION, CURRENT_PROCEDURE
-from windows.main.table import NEW_TABLE
+from windows.state import CURRENT_DATABASE, CURRENT_TABLE, CURRENT_CONNECTION, CURRENT_SESSION, CURRENT_VIEW, CURRENT_TRIGGER, SESSIONS_LIST, CURRENT_EVENT, CURRENT_FUNCTION, CURRENT_PROCEDURE, NEW_TABLE
@dataclasses.dataclass
@@ -192,7 +191,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():
@@ -206,6 +205,7 @@ def select_database(self, database: SQLDatabase, item, event):
session.connect()
CURRENT_DATABASE.set_value(database)
+ CURRENT_SESSION.get_value().context.set_database(database)
self.load_observables(item, database)
@@ -221,6 +221,8 @@ 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):
if not CURRENT_TABLE.get_value() or sql_obj != CURRENT_TABLE.get_value():
diff --git a/windows/main/foreign_key.py b/windows/main/tabs/foreign_key.py
similarity index 97%
rename from windows/main/foreign_key.py
rename to windows/main/tabs/foreign_key.py
index 95260ff..20c2c54 100755
--- a/windows/main/foreign_key.py
+++ b/windows/main/tabs/foreign_key.py
@@ -10,9 +10,8 @@
from structures.helpers import merge_original_current
-from windows import TableForeignKeysDataViewCtrl
-from windows.main import CURRENT_TABLE, CURRENT_FOREIGN_KEY, CURRENT_SESSION
-from windows.main.table import NEW_TABLE
+from windows.views import TableForeignKeysDataViewCtrl
+from windows.state import CURRENT_TABLE, CURRENT_FOREIGN_KEY, CURRENT_SESSION, NEW_TABLE
from structures.engines.database import SQLForeignKey, SQLTable
diff --git a/windows/main/index.py b/windows/main/tabs/index.py
similarity index 98%
rename from windows/main/index.py
rename to windows/main/tabs/index.py
index a9c10d0..00bce6e 100755
--- a/windows/main/index.py
+++ b/windows/main/tabs/index.py
@@ -6,7 +6,7 @@
from structures.helpers import merge_original_current
from windows.main import CURRENT_TABLE, CURRENT_INDEX
-from windows.main.column import NEW_TABLE
+from windows.main.tabs.column import NEW_TABLE
from structures.engines.database import SQLTable, SQLIndex
diff --git a/windows/main/tabs/query.py b/windows/main/tabs/query.py
new file mode 100644
index 0000000..bc35b70
--- /dev/null
+++ b/windows/main/tabs/query.py
@@ -0,0 +1,501 @@
+import dataclasses
+import enum
+import threading
+import time
+
+from typing import Any, Callable, Optional
+from gettext import gettext as _
+
+import wx
+import wx.dataview
+
+from helpers.logger import logger
+
+from structures.session import Session
+from structures.connection import ConnectionEngine
+
+from windows.components.dataview import QueryEditorResultsDataViewCtrl
+
+
+@dataclasses.dataclass
+class ParsedStatement:
+ text: str
+ start_pos: int
+ end_pos: int
+ statement_index: int
+
+
+@dataclasses.dataclass
+class ExecutionResult:
+ statement: ParsedStatement
+ success: bool
+ columns: Optional[list[str]] = None
+ rows: Optional[list[tuple]] = None
+ affected_rows: Optional[int] = None
+ elapsed_ms: float = 0.0
+ error: Optional[str] = None
+ warnings: list[str] = dataclasses.field(default_factory=list)
+
+
+class ExecutionMode(enum.Enum):
+ ALL = "all"
+ SELECTION = "selection"
+ CURRENT = "current"
+
+
+class SQLStatementParser:
+ def __init__(self, engine: ConnectionEngine):
+ self.engine = engine
+
+ def parse(self, sql_text: str) -> list[ParsedStatement]:
+ if not sql_text.strip():
+ return []
+
+ statements = []
+ statement_index = 0
+ current_start = 0
+ i = 0
+ length = len(sql_text)
+
+ in_single_quote = False
+ in_double_quote = False
+ in_line_comment = False
+ in_block_comment = False
+
+ while i < length:
+ char = sql_text[i]
+
+ if in_line_comment:
+ if char == '\n':
+ in_line_comment = False
+ i += 1
+ continue
+
+ if in_block_comment:
+ if i + 1 < length and sql_text[i:i+2] == '*/':
+ in_block_comment = False
+ i += 2
+ continue
+ i += 1
+ continue
+
+ if not in_single_quote and not in_double_quote:
+ if self._is_line_comment_start(sql_text, i):
+ in_line_comment = True
+ i += 2
+ continue
+
+ if self._is_block_comment_start(sql_text, i):
+ in_block_comment = True
+ i += 2
+ continue
+
+ if char == "'" and not in_double_quote:
+ if i + 1 < length and sql_text[i+1] == "'":
+ i += 2
+ continue
+ in_single_quote = not in_single_quote
+
+ elif char == '"' and not in_single_quote:
+ if i + 1 < length and sql_text[i+1] == '"':
+ i += 2
+ continue
+ in_double_quote = not in_double_quote
+
+ elif char == ';' and not in_single_quote and not in_double_quote:
+ statement_text = sql_text[current_start:i].strip()
+ if statement_text:
+ statements.append(ParsedStatement(
+ text=statement_text,
+ start_pos=current_start,
+ end_pos=i,
+ statement_index=statement_index
+ ))
+ statement_index += 1
+ current_start = i + 1
+
+ i += 1
+
+ final_statement = sql_text[current_start:].strip()
+ if final_statement:
+ statements.append(ParsedStatement(
+ text=final_statement,
+ start_pos=current_start,
+ end_pos=length,
+ statement_index=statement_index
+ ))
+
+ return statements
+
+ def _is_line_comment_start(self, text: str, pos: int) -> bool:
+ if pos + 1 >= len(text):
+ return False
+ return text[pos:pos+2] in ('--', '# ')
+
+ def _is_block_comment_start(self, text: str, pos: int) -> bool:
+ if pos + 1 >= len(text):
+ return False
+ return text[pos:pos+2] == '/*'
+
+
+class StatementSelector:
+ def __init__(self, stc_editor: wx.stc.StyledTextCtrl):
+ self.editor = stc_editor
+
+ def get_execution_scope(
+ self,
+ statements: list[ParsedStatement]
+ ) -> tuple[ExecutionMode, list[ParsedStatement]]:
+ selection_start = self.editor.GetSelectionStart()
+ selection_end = self.editor.GetSelectionEnd()
+
+ if selection_start != selection_end:
+ selected_text = self.editor.GetSelectedText().strip()
+ if selected_text:
+ return (ExecutionMode.SELECTION, [ParsedStatement(
+ text=selected_text,
+ start_pos=selection_start,
+ end_pos=selection_end,
+ statement_index=0
+ )])
+
+ caret_pos = self.editor.GetCurrentPos()
+ current_stmt = self._find_statement_at_caret(caret_pos, statements)
+
+ if current_stmt:
+ return (ExecutionMode.CURRENT, [current_stmt])
+
+ return (ExecutionMode.ALL, statements)
+
+ def _find_statement_at_caret(
+ self,
+ caret_pos: int,
+ statements: list[ParsedStatement]
+ ) -> Optional[ParsedStatement]:
+ for stmt in statements:
+ if stmt.start_pos <= caret_pos <= stmt.end_pos:
+ return stmt
+
+ # Caret is in whitespace: execute next statement
+ for stmt in statements:
+ if caret_pos < stmt.start_pos:
+ return stmt
+
+ # Caret after all statements: execute last
+ if statements:
+ return statements[-1]
+
+ return None
+
+
+class QueryExecutor:
+ def __init__(self, session: Session):
+ self.session = session
+ self._cancel_requested = False
+ self._current_thread: Optional[threading.Thread] = None
+ self._lock = threading.Lock()
+
+ def execute_statements(
+ self,
+ statements: list[ParsedStatement],
+ on_statement_complete: Callable[[ExecutionResult], None],
+ on_all_complete: Callable[[], None],
+ stop_on_error: bool = True
+ ) -> None:
+ self._cancel_requested = False
+
+ self._current_thread = threading.Thread(
+ target=self._execute_worker,
+ args=(statements, on_statement_complete, on_all_complete, stop_on_error),
+ daemon=True
+ )
+ self._current_thread.start()
+
+ def _execute_worker(
+ self,
+ statements: list[ParsedStatement],
+ on_statement_complete: Callable[[ExecutionResult], None],
+ on_all_complete: Callable[[], None],
+ stop_on_error: bool
+ ) -> None:
+ try:
+ for stmt in statements:
+ if self._cancel_requested:
+ break
+
+ result = self._execute_single(stmt)
+ # Thread-safe UI update
+ wx.CallAfter(on_statement_complete, result)
+
+ if not result.success and stop_on_error:
+ break
+
+ except Exception as ex:
+ logger.error(f"Execution worker error: {ex}", exc_info=True)
+ finally:
+ wx.CallAfter(on_all_complete)
+
+ def _execute_single(self, statement: ParsedStatement) -> ExecutionResult:
+ start_time = time.time()
+
+ try:
+ self.session.context.execute(statement.text)
+
+ elapsed_ms = (time.time() - start_time) * 1000
+
+ cursor = self.session.context.cursor
+ if cursor.description:
+ columns = [desc[0] for desc in cursor.description]
+ rows = self.session.context.fetchall()
+
+ return ExecutionResult(
+ statement=statement,
+ success=True,
+ columns=columns,
+ rows=rows,
+ affected_rows=len(rows),
+ elapsed_ms=elapsed_ms
+ )
+ else:
+ affected = cursor.rowcount if cursor.rowcount >= 0 else 0
+
+ return ExecutionResult(
+ statement=statement,
+ success=True,
+ affected_rows=affected,
+ elapsed_ms=elapsed_ms
+ )
+
+ except Exception as ex:
+ elapsed_ms = (time.time() - start_time) * 1000
+
+ return ExecutionResult(
+ statement=statement,
+ success=False,
+ error=str(ex),
+ elapsed_ms=elapsed_ms
+ )
+
+ def cancel(self) -> None:
+ self._cancel_requested = True
+
+ def is_running(self) -> bool:
+ return self._current_thread is not None and self._current_thread.is_alive()
+
+
+class QueryResultsRenderer:
+ def __init__(self, notebook: wx.Notebook, session: Session):
+ self.notebook = notebook
+ self.session = session
+ self._tab_counter = 0
+
+ def create_result_tab(self, result: ExecutionResult) -> wx.Panel:
+ self._tab_counter += 1
+
+ panel = wx.Panel(self.notebook)
+ sizer = wx.BoxSizer(wx.VERTICAL)
+
+ if result.success and result.columns:
+ grid = QueryEditorResultsDataViewCtrl(panel)
+ self._populate_grid(grid, result)
+ sizer.Add(grid, 1, wx.EXPAND | wx.ALL, 5)
+
+ tab_name = self._generate_tab_name(result)
+ elif result.success:
+ msg = wx.StaticText(panel, label=_("{} rows affected").format(result.affected_rows or 0))
+ msg.SetFont(msg.GetFont().MakeBold())
+ sizer.Add(msg, 1, wx.ALIGN_CENTER | wx.ALL, 20)
+
+ tab_name = _("Query {}").format(self._tab_counter)
+ else:
+ error_panel = self._create_error_panel(panel, result)
+ sizer.Add(error_panel, 1, wx.EXPAND | wx.ALL, 5)
+
+ tab_name = _("Query {} (Error)").format(self._tab_counter)
+
+ footer = self._create_footer(panel, result)
+ sizer.Add(footer, 0, wx.EXPAND | wx.ALL, 5)
+
+ panel.SetSizer(sizer)
+ self.notebook.AddPage(panel, tab_name, select=True)
+
+ return panel
+
+ def _generate_tab_name(self, result: ExecutionResult) -> str:
+ if result.columns and result.rows is not None:
+ return _("Query {} ({} rows ร {} cols)").format(
+ self._tab_counter,
+ len(result.rows),
+ len(result.columns)
+ )
+ return _("Query {}").format(self._tab_counter)
+
+ def _populate_grid(
+ self,
+ grid: QueryEditorResultsDataViewCtrl,
+ result: ExecutionResult
+ ) -> None:
+ if not result.columns or not result.rows:
+ return
+
+ for col_name in result.columns:
+ grid.AppendTextColumn(col_name, wx.dataview.DATAVIEW_CELL_INERT)
+
+ model = grid.GetModel()
+ if hasattr(model, 'data'):
+ model.data = list(result.rows)
+ model.Reset(len(result.rows))
+
+ def _create_footer(self, parent: wx.Panel, result: ExecutionResult) -> wx.StaticText:
+ parts = []
+
+ if result.affected_rows is not None:
+ parts.append(_("{} rows").format(result.affected_rows))
+
+ parts.append(_("{:.1f} ms").format(result.elapsed_ms))
+
+ if result.warnings:
+ parts.append(_("{} warnings").format(len(result.warnings)))
+
+ footer_text = " | ".join(parts)
+ footer = wx.StaticText(parent, label=footer_text)
+ footer.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))
+
+ return footer
+
+ def _create_error_panel(self, parent: wx.Panel, result: ExecutionResult) -> wx.Panel:
+ error_panel = wx.Panel(parent)
+ error_sizer = wx.BoxSizer(wx.VERTICAL)
+
+ error_label = wx.StaticText(error_panel, label=_("Error:"))
+ error_label.SetFont(error_label.GetFont().MakeBold())
+ error_sizer.Add(error_label, 0, wx.ALL, 5)
+
+ error_text = wx.TextCtrl(
+ error_panel,
+ value=result.error or _("Unknown error"),
+ style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_WORDWRAP
+ )
+ error_text.SetBackgroundColour(wx.Colour(255, 240, 240))
+ error_sizer.Add(error_text, 1, wx.EXPAND | wx.ALL, 5)
+
+ error_panel.SetSizer(error_sizer)
+ return error_panel
+
+ def clear_all_tabs(self) -> None:
+ while self.notebook.GetPageCount() > 0:
+ self.notebook.DeletePage(0)
+ self._tab_counter = 0
+
+
+class QueryEditorController:
+ def __init__(
+ self,
+ stc_editor: wx.stc.StyledTextCtrl,
+ results_notebook: wx.Notebook,
+ session_provider: Callable[[], Optional[Session]]
+ ):
+ self.editor = stc_editor
+ self.notebook = results_notebook
+ self.get_session = session_provider
+
+ self.parser: Optional[SQLStatementParser] = None
+ self.selector = StatementSelector(stc_editor)
+ self.executor: Optional[QueryExecutor] = None
+ self.renderer: Optional[QueryResultsRenderer] = None
+
+ self._bind_shortcuts()
+
+ def _bind_shortcuts(self) -> None:
+ self.editor.Bind(wx.EVT_KEY_DOWN, self._on_key_down)
+
+ def _on_key_down(self, event: wx.KeyEvent) -> None:
+ key_code = event.GetKeyCode()
+ ctrl_down = event.ControlDown()
+ shift_down = event.ShiftDown()
+
+ if key_code == wx.WXK_F5:
+ if shift_down:
+ self.cancel_execution(event)
+ else:
+ self.execute_all(event)
+ return
+
+ if ctrl_down and key_code == wx.WXK_RETURN:
+ self.execute_current(event)
+ return
+
+ if ctrl_down and shift_down and key_code == ord('C'):
+ self.cancel_execution(event)
+ return
+
+ event.Skip()
+
+ def execute_all(self, event: wx.Event) -> None:
+ self._execute(ExecutionMode.ALL)
+
+ def execute_current(self, event: wx.Event) -> None:
+ self._execute(ExecutionMode.CURRENT)
+
+ def cancel_execution(self, event: wx.Event) -> None:
+ if self.executor and self.executor.is_running():
+ self.executor.cancel()
+ logger.info("Query execution cancelled")
+
+ def _execute(self, mode: ExecutionMode) -> None:
+ session = self.get_session()
+ if not session or not session.is_connected:
+ wx.MessageBox(
+ _("No active database connection"),
+ _("Error"),
+ wx.OK | wx.ICON_ERROR
+ )
+ return
+
+ if not self.parser or self.parser.engine != session.engine:
+ self.parser = SQLStatementParser(session.engine)
+ self.executor = QueryExecutor(session)
+ self.renderer = QueryResultsRenderer(self.notebook, session)
+
+ sql_text = self.editor.GetText()
+ if not sql_text.strip():
+ return
+
+ statements = self.parser.parse(sql_text)
+ if not statements:
+ return
+
+ if mode == ExecutionMode.CURRENT or mode == ExecutionMode.SELECTION:
+ _, statements_to_execute = self.selector.get_execution_scope(statements)
+ else:
+ statements_to_execute = statements
+
+ if not statements_to_execute:
+ return
+
+ self.renderer.clear_all_tabs()
+
+ self.executor.execute_statements(
+ statements=statements_to_execute,
+ on_statement_complete=self._on_statement_complete,
+ on_all_complete=self._on_all_complete,
+ stop_on_error=True
+ )
+
+ def _on_statement_complete(self, result: ExecutionResult) -> None:
+ if self.renderer:
+ self.renderer.create_result_tab(result)
+
+ def _on_all_complete(self) -> None:
+ logger.info("Query execution completed")
+
+
+class QueryResultsController(QueryEditorController):
+ def __init__(self, stc_sql_query: wx.stc.StyledTextCtrl, notebook_sql_results: wx.Notebook):
+ from windows.main import CURRENT_SESSION
+
+ super().__init__(
+ stc_editor=stc_sql_query,
+ results_notebook=notebook_sql_results,
+ session_provider=lambda: CURRENT_SESSION.get_value()
+ )
diff --git a/windows/main/records.py b/windows/main/tabs/records.py
similarity index 82%
rename from windows/main/records.py
rename to windows/main/tabs/records.py
index 5d45755..4f15fd9 100644
--- a/windows/main/records.py
+++ b/windows/main/tabs/records.py
@@ -13,11 +13,9 @@
from structures.engines.database import SQLTable, SQLDatabase, SQLColumn, SQLRecord
from structures.engines.datatype import DataTypeCategory
-from windows import TableRecordsDataViewCtrl, AdvancedCellEditorDialog
+from windows.views import TableRecordsDataViewCtrl
-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.dialogs.advanced_cell_editor import AdvancedCellEditorController
from windows.main import CURRENT_TABLE, CURRENT_SESSION, CURRENT_DATABASE, AUTO_APPLY, CURRENT_RECORDS
@@ -302,60 +300,3 @@ def do_delete_record(self):
# records = sorted(self.list_ctrl_records.GetModel().records, key=sort_func)
# model = RecordsModel(self.table, records)
# self.list_ctrl_records.AssociateModel(model)
-
-
-class AdvancedCellEditorController(AdvancedCellEditorDialog):
- app = wx.GetApp()
-
- def __init__(self, parent, value: str):
- super().__init__(parent)
-
- self.syntax_choice.AppendItems(self.app.syntax_registry.labels())
- self.advanced_stc_editor.SetText(value or "")
- self.advanced_stc_editor.EmptyUndoBuffer()
-
- self.app.theme_manager.register(self.advanced_stc_editor, self._get_current_syntax_profile)
-
- self.syntax_choice.SetStringSelection(self._auto_syntax_profile().label)
-
- self.do_apply_syntax(do_format=True)
-
- def _auto_syntax_profile(self) -> SyntaxProfile:
- text = self.advanced_stc_editor.GetText()
-
- syntax_id = detect_syntax_id(text)
- return self.app.syntax_registry.get(syntax_id)
-
- def _get_current_syntax_profile(self) -> SyntaxProfile:
- label = self.syntax_choice.GetStringSelection()
- # text = self.advanced_stc_editor.GetText()
- #
- # syntax_id = detect_syntax_id(text)
- return self.app.syntax_registry.get(label)
-
- def on_syntax_changed(self, _evt):
- label = self.syntax_choice.GetStringSelection()
- self.do_apply_syntax(label)
-
- def do_apply_syntax(self, do_format: bool = True):
- label = self.syntax_choice.GetStringSelection()
- syntax_profile = self.app.syntax_registry.by_label(label)
-
- apply_stc_theme(self.advanced_stc_editor, syntax_profile)
-
- if do_format and syntax_profile.formatter:
- old = self.advanced_stc_editor.GetText()
- try:
- formatted = syntax_profile.formatter(old)
- except Exception:
- return
-
- if formatted != old:
- self._replace_text_undo_friendly(formatted)
-
- def _replace_text_undo_friendly(self, new_text: str):
- self.advanced_stc_editor.BeginUndoAction()
- try:
- self.advanced_stc_editor.SetText(new_text)
- finally:
- self.advanced_stc_editor.EndUndoAction()
diff --git a/windows/main/table.py b/windows/main/tabs/table.py
similarity index 94%
rename from windows/main/table.py
rename to windows/main/tabs/table.py
index 883ac4e..e53116f 100644
--- a/windows/main/table.py
+++ b/windows/main/tabs/table.py
@@ -1,11 +1,9 @@
from helpers.bindings import AbstractModel
from helpers.observables import Observable, debounce, ObservableList
-from windows.main import CURRENT_TABLE, CURRENT_DATABASE
-
from structures.engines.database import SQLTable
-NEW_TABLE: Observable[SQLTable] = Observable()
+from windows.state import CURRENT_TABLE, CURRENT_DATABASE, NEW_TABLE
class EditTableModel(AbstractModel):
diff --git a/windows/main/tabs/view.py b/windows/main/tabs/view.py
new file mode 100644
index 0000000..18f2ec8
--- /dev/null
+++ b/windows/main/tabs/view.py
@@ -0,0 +1,316 @@
+from typing import Optional
+
+import wx
+import wx.stc
+
+from helpers.bindings import AbstractModel
+from helpers.observables import Observable, debounce
+
+from structures.engines.database import SQLView
+
+from windows.main import CURRENT_SESSION, CURRENT_VIEW
+
+
+class EditViewModel(AbstractModel):
+ def __init__(self):
+ self.name = Observable()
+ self.schema = Observable()
+ self.definer = Observable()
+ self.sql_security = Observable()
+ self.algorithm = Observable()
+ self.constraint = Observable()
+ self.security_barrier = Observable()
+ self.force = Observable()
+ self.select_statement = Observable()
+
+ debounce(
+ self.name, self.schema, self.definer, self.sql_security,
+ self.algorithm, self.constraint, self.security_barrier,
+ self.force, self.select_statement,
+ callback=self.update_view
+ )
+
+ CURRENT_VIEW.subscribe(self._load_view)
+
+ def _load_view(self, view: Optional[SQLView]):
+ if view is None:
+ return
+
+ self.name.set_initial(view.name)
+ self.select_statement.set_initial(view.sql)
+
+ session = CURRENT_SESSION.get_value()
+ if not session:
+ return
+
+ engine = session.engine.value.name.lower()
+
+ if engine in ["mysql", "mariadb"]:
+ self._load_mysql_fields(view)
+ elif engine == "postgresql":
+ self._load_postgresql_fields(view)
+ elif engine == "oracle":
+ self._load_oracle_fields(view)
+
+ def update_view(self, *args):
+ if not any(args):
+ return
+
+ view = CURRENT_VIEW.get_value()
+ if not view:
+ return
+
+ view.name = self.name.get_value()
+ view.sql = self.select_statement.get_value()
+
+ session = CURRENT_SESSION.get_value()
+ if not session:
+ return
+
+ engine = session.engine.value.name.lower()
+
+ if engine in ["mysql", "mariadb"]:
+ self._update_mysql_fields(view)
+ elif engine == "postgresql":
+ self._update_postgresql_fields(view)
+ elif engine == "oracle":
+ self._update_oracle_fields(view)
+
+ def _load_mysql_fields(self, view: SQLView):
+ if hasattr(view, "definer"):
+ self.definer.set_initial(view.definer)
+ if hasattr(view, "sql_security"):
+ self.sql_security.set_initial(view.sql_security)
+ if hasattr(view, "algorithm"):
+ self.algorithm.set_initial(view.algorithm)
+ if hasattr(view, "constraint"):
+ self.constraint.set_initial(view.constraint)
+
+ def _load_postgresql_fields(self, view: SQLView):
+ if hasattr(view, "schema"):
+ self.schema.set_initial(view.schema)
+ if hasattr(view, "constraint"):
+ self.constraint.set_initial(view.constraint)
+ if hasattr(view, "security_barrier"):
+ self.security_barrier.set_initial(view.security_barrier)
+
+ def _load_oracle_fields(self, view: SQLView):
+ if hasattr(view, "schema"):
+ self.schema.set_initial(view.schema)
+ if hasattr(view, "constraint"):
+ self.constraint.set_initial(view.constraint)
+ if hasattr(view, "force"):
+ self.force.set_initial(view.force)
+
+ def _update_mysql_fields(self, view: SQLView):
+ if hasattr(view, "definer"):
+ view.definer = self.definer.get_value()
+ if hasattr(view, "sql_security"):
+ view.sql_security = self.sql_security.get_value()
+ if hasattr(view, "algorithm"):
+ view.algorithm = self.algorithm.get_value()
+ if hasattr(view, "constraint"):
+ view.constraint = self.constraint.get_value()
+
+ def _update_postgresql_fields(self, view: SQLView):
+ if hasattr(view, "schema"):
+ view.schema = self.schema.get_value()
+ if hasattr(view, "constraint"):
+ view.constraint = self.constraint.get_value()
+ if hasattr(view, "security_barrier"):
+ view.security_barrier = self.security_barrier.get_value()
+
+ def _update_oracle_fields(self, view: SQLView):
+ if hasattr(view, "schema"):
+ view.schema = self.schema.get_value()
+ if hasattr(view, "constraint"):
+ view.constraint = self.constraint.get_value()
+ if hasattr(view, "force"):
+ view.force = self.force.get_value()
+
+
+class ViewEditorController:
+ def __init__(self, parent):
+ self.parent = parent
+ self.model = EditViewModel()
+
+ self._bind_controls()
+ CURRENT_VIEW.subscribe(self.on_current_view_changed)
+
+ def _bind_controls(self):
+ algorithm_radios = [
+ self.parent.rad_view_algorithm_undefined,
+ self.parent.rad_view_algorithm_merge,
+ self.parent.rad_view_algorithm_temptable,
+ ]
+
+ constraint_radios = [
+ self.parent.rad_view_constraint_none,
+ self.parent.rad_view_constraint_local,
+ self.parent.rad_view_constraint_cascaded,
+ self.parent.rad_view_constraint_check_only,
+ self.parent.rad_view_constraint_read_only,
+ ]
+
+ self.model.bind_controls(
+ name=self.parent.txt_view_name,
+ schema=self.parent.cho_view_schema,
+ definer=self.parent.cmb_view_definer,
+ sql_security=self.parent.cho_view_sql_security,
+ algorithm=algorithm_radios,
+ constraint=constraint_radios,
+ security_barrier=self.parent.chk_view_security_barrier,
+ force=self.parent.chk_view_force,
+ select_statement=self.parent.stc_view_select,
+ )
+
+ def on_current_view_changed(self, view: Optional[SQLView]):
+ if view is None:
+ return
+
+ session = CURRENT_SESSION.get_value()
+ if session:
+ engine = session.engine.value.name.lower()
+ self.apply_engine_visibility(engine)
+
+ def apply_engine_visibility(self, engine: str):
+ if engine in ["mysql", "mariadb"]:
+ self._apply_mysql_visibility()
+ elif engine == "postgresql":
+ self._apply_postgresql_visibility()
+ elif engine == "oracle":
+ self._apply_oracle_visibility()
+ elif engine == "sqlite":
+ self._apply_sqlite_visibility()
+
+ self.parent.pnl_view_editor_root.Layout()
+
+ def _apply_mysql_visibility(self):
+ widgets_to_show = [
+ self.parent.cmb_view_definer,
+ self.parent.lbl_view_definer,
+ self.parent.cho_view_sql_security,
+ self.parent.lbl_view_sql_security,
+ self.parent.rad_view_algorithm_undefined,
+ self.parent.rad_view_algorithm_merge,
+ self.parent.rad_view_algorithm_temptable,
+ self.parent.rad_view_constraint_none,
+ self.parent.rad_view_constraint_local,
+ self.parent.rad_view_constraint_cascaded,
+ ]
+ widgets_to_hide = [
+ self.parent.cho_view_schema,
+ self.parent.lbl_view_schema,
+ self.parent.rad_view_constraint_check_only,
+ self.parent.rad_view_constraint_read_only,
+ self.parent.chk_view_security_barrier,
+ self.parent.chk_view_force,
+ ]
+ self._batch_show_hide(widgets_to_show, widgets_to_hide)
+ self._normalize_radio_selection_algorithm()
+ self._normalize_radio_selection_constraint()
+
+ def _apply_postgresql_visibility(self):
+ widgets_to_show = [
+ self.parent.cho_view_schema,
+ self.parent.lbl_view_schema,
+ self.parent.rad_view_constraint_none,
+ self.parent.rad_view_constraint_local,
+ self.parent.rad_view_constraint_cascaded,
+ self.parent.chk_view_security_barrier,
+ ]
+ widgets_to_hide = [
+ self.parent.cmb_view_definer,
+ self.parent.lbl_view_definer,
+ self.parent.cho_view_sql_security,
+ self.parent.lbl_view_sql_security,
+ self.parent.rad_view_algorithm_undefined,
+ self.parent.rad_view_algorithm_merge,
+ self.parent.rad_view_algorithm_temptable,
+ self.parent.rad_view_constraint_check_only,
+ self.parent.rad_view_constraint_read_only,
+ self.parent.chk_view_force,
+ ]
+ self._batch_show_hide(widgets_to_show, widgets_to_hide)
+ self._normalize_radio_selection_constraint()
+
+ def _apply_oracle_visibility(self):
+ widgets_to_show = [
+ self.parent.cho_view_schema,
+ self.parent.lbl_view_schema,
+ self.parent.rad_view_constraint_none,
+ self.parent.rad_view_constraint_check_only,
+ self.parent.rad_view_constraint_read_only,
+ self.parent.chk_view_force,
+ ]
+ widgets_to_hide = [
+ self.parent.cmb_view_definer,
+ self.parent.lbl_view_definer,
+ self.parent.cho_view_sql_security,
+ self.parent.lbl_view_sql_security,
+ self.parent.rad_view_algorithm_undefined,
+ self.parent.rad_view_algorithm_merge,
+ self.parent.rad_view_algorithm_temptable,
+ self.parent.rad_view_constraint_local,
+ self.parent.rad_view_constraint_cascaded,
+ self.parent.chk_view_security_barrier,
+ ]
+ self._batch_show_hide(widgets_to_show, widgets_to_hide)
+ self._normalize_radio_selection_constraint()
+
+ def _apply_sqlite_visibility(self):
+ widgets_to_hide = [
+ self.parent.cho_view_schema,
+ self.parent.lbl_view_schema,
+ self.parent.cmb_view_definer,
+ self.parent.lbl_view_definer,
+ self.parent.cho_view_sql_security,
+ self.parent.lbl_view_sql_security,
+ self.parent.rad_view_algorithm_undefined,
+ self.parent.rad_view_algorithm_merge,
+ self.parent.rad_view_algorithm_temptable,
+ self.parent.rad_view_constraint_none,
+ self.parent.rad_view_constraint_local,
+ self.parent.rad_view_constraint_cascaded,
+ self.parent.rad_view_constraint_check_only,
+ self.parent.rad_view_constraint_read_only,
+ self.parent.chk_view_security_barrier,
+ self.parent.chk_view_force,
+ ]
+ self._batch_show_hide([], widgets_to_hide)
+
+ def _batch_show_hide(self, show: list[wx.Window], hide: list[wx.Window]):
+ for widget in show:
+ widget.Show(True)
+ for widget in hide:
+ widget.Show(False)
+
+ def _normalize_radio_selection_algorithm(self):
+ radios = [
+ self.parent.rad_view_algorithm_undefined,
+ self.parent.rad_view_algorithm_merge,
+ self.parent.rad_view_algorithm_temptable,
+ ]
+ self._normalize_radio_group(radios)
+
+ def _normalize_radio_selection_constraint(self):
+ radios = [
+ self.parent.rad_view_constraint_none,
+ self.parent.rad_view_constraint_local,
+ self.parent.rad_view_constraint_cascaded,
+ self.parent.rad_view_constraint_check_only,
+ self.parent.rad_view_constraint_read_only,
+ ]
+ self._normalize_radio_group(radios)
+
+ def _normalize_radio_group(self, radios: list[wx.RadioButton]):
+ visible = [r for r in radios if r.IsShown()]
+
+ if not visible:
+ return
+
+ selected = next((r for r in visible if r.GetValue()), None)
+
+ if selected is None:
+ visible[0].SetValue(True)
+
diff --git a/windows/state.py b/windows/state.py
new file mode 100644
index 0000000..19a861e
--- /dev/null
+++ b/windows/state.py
@@ -0,0 +1,25 @@
+from helpers.observables import Observable, ObservableList
+
+from structures.session import Session
+from structures.connection import Connection
+from structures.engines.database import SQLColumn, SQLDatabase, SQLForeignKey, SQLIndex, SQLRecord, SQLTable, SQLTrigger, SQLView
+
+SESSIONS_LIST: ObservableList[Session] = ObservableList()
+
+CURRENT_SESSION: Observable[Session] = Observable()
+CURRENT_CONNECTION: Observable[Connection] = Observable()
+CURRENT_DATABASE: Observable[SQLDatabase] = Observable()
+CURRENT_TABLE: Observable[SQLTable] = Observable()
+CURRENT_VIEW: Observable[SQLView] = Observable()
+CURRENT_TRIGGER: Observable[SQLTrigger] = Observable()
+CURRENT_FUNCTION: Observable[SQLTrigger] = Observable()
+CURRENT_PROCEDURE: Observable[SQLTrigger] = Observable()
+CURRENT_EVENT: Observable[SQLTrigger] = Observable()
+CURRENT_COLUMN: Observable[SQLColumn] = Observable()
+CURRENT_INDEX: Observable[SQLIndex] = Observable()
+CURRENT_FOREIGN_KEY: Observable[SQLForeignKey] = Observable()
+CURRENT_RECORDS: ObservableList[SQLRecord] = ObservableList()
+
+NEW_TABLE: Observable[SQLTable] = Observable()
+
+AUTO_APPLY: Observable[bool] = Observable(True)
diff --git a/windows/views.py b/windows/views.py
new file mode 100755
index 0000000..7bb309e
--- /dev/null
+++ b/windows/views.py
@@ -0,0 +1,2739 @@
+# -*- coding: utf-8 -*-
+
+###########################################################################
+## Python code generated with wxFormBuilder (version 4.2.1-111-g5faebfea)
+## http://www.wxformbuilder.org/
+##
+## PLEASE DO *NOT* EDIT THIS FILE!
+###########################################################################
+
+from .components.dataview import TableIndexesDataViewCtrl
+from .components.dataview import TableForeignKeysDataViewCtrl
+from .components.dataview import TableCheckDataViewCtrl
+from .components.dataview import TableColumnsDataViewCtrl
+from .components.dataview import TableRecordsDataViewCtrl
+import wx
+import wx.xrc
+import wx.dataview
+import wx.stc
+import wx.lib.agw.hypertreelist
+
+import gettext
+_ = gettext.gettext
+
+###########################################################################
+## Class ConnectionsDialog
+###########################################################################
+
+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 )
+
+ self.SetSizeHints( wx.Size( -1,-1 ), wx.DefaultSize )
+
+ bSizer34 = wx.BoxSizer( wx.VERTICAL )
+
+ self.m_splitter3 = wx.SplitterWindow( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_LIVE_UPDATE )
+ self.m_splitter3.Bind( wx.EVT_IDLE, self.m_splitter3OnIdle )
+ self.m_splitter3.SetMinimumPaneSize( 250 )
+
+ self.m_panel16 = wx.Panel( self.m_splitter3, wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,-1 ), wx.TAB_TRAVERSAL )
+ bSizer35 = wx.BoxSizer( wx.VERTICAL )
+
+ self.connections_tree_ctrl = wx.dataview.DataViewCtrl( self.m_panel16, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.DV_ROW_LINES )
+ self.connection_name = self.connections_tree_ctrl.AppendIconTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_EDITABLE, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE )
+ self.connection_last_connection = self.connections_tree_ctrl.AppendTextColumn( _(u"Last connection"), 1, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE )
+ bSizer35.Add( self.connections_tree_ctrl, 1, wx.ALL|wx.EXPAND|wx.TOP, 5 )
+
+ self.search_connection = wx.SearchCtrl( self.m_panel16, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.search_connection.ShowSearchButton( True )
+ self.search_connection.ShowCancelButton( True )
+ bSizer35.Add( self.search_connection, 0, wx.BOTTOM|wx.EXPAND|wx.LEFT|wx.RIGHT, 5 )
+
+
+ self.m_panel16.SetSizer( bSizer35 )
+ self.m_panel16.Layout()
+ bSizer35.Fit( self.m_panel16 )
+ self.m_menu5 = wx.Menu()
+ self.m_menuItem4 = wx.MenuItem( self.m_menu5, wx.ID_ANY, _(u"New directory"), wx.EmptyString, wx.ITEM_NORMAL )
+ self.m_menu5.Append( self.m_menuItem4 )
+
+ self.m_menuItem5 = wx.MenuItem( self.m_menu5, wx.ID_ANY, _(u"New Session"), wx.EmptyString, wx.ITEM_NORMAL )
+ self.m_menu5.Append( self.m_menuItem5 )
+
+ self.m_menu5.AppendSeparator()
+
+ self.m_menuItem10 = wx.MenuItem( self.m_menu5, wx.ID_ANY, _(u"Import"), wx.EmptyString, wx.ITEM_NORMAL )
+ self.m_menu5.Append( self.m_menuItem10 )
+
+ self.m_panel16.Bind( wx.EVT_RIGHT_DOWN, self.m_panel16OnContextMenu )
+
+ self.m_panel17 = wx.Panel( self.m_splitter3, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer36 = wx.BoxSizer( wx.VERTICAL )
+
+ self.m_notebook4 = wx.Notebook( self.m_panel17, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.NB_FIXEDWIDTH )
+ self.panel_connection = wx.Panel( self.m_notebook4, wx.ID_ANY, wx.DefaultPosition, wx.Size( 600,-1 ), wx.BORDER_NONE|wx.TAB_TRAVERSAL )
+ self.panel_connection.SetMinSize( wx.Size( 600,-1 ) )
+
+ bSizer12 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer1211 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText211 = wx.StaticText( self.panel_connection, wx.ID_ANY, _(u"Name"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 )
+ self.m_staticText211.Wrap( -1 )
+
+ bSizer1211.Add( self.m_staticText211, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.name = wx.TextCtrl( self.panel_connection, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer1211.Add( self.name, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+
+ bSizer12.Add( bSizer1211, 0, wx.EXPAND, 5 )
+
+ bSizer13 = wx.BoxSizer( wx.HORIZONTAL )
+
+ bSizer13.SetMinSize( wx.Size( -1,0 ) )
+ self.m_staticText2 = wx.StaticText( self.panel_connection, wx.ID_ANY, _(u"Engine"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 )
+ self.m_staticText2.Wrap( -1 )
+
+ bSizer13.Add( self.m_staticText2, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ engineChoices = []
+ self.engine = wx.Choice( self.panel_connection, wx.ID_ANY, wx.DefaultPosition, wx.Size( 400,-1 ), engineChoices, 0 )
+ self.engine.SetSelection( 0 )
+ bSizer13.Add( self.engine, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+
+ bSizer12.Add( bSizer13, 0, wx.EXPAND, 5 )
+
+ self.m_staticline41 = wx.StaticLine( self.panel_connection, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL )
+ bSizer12.Add( self.m_staticline41, 0, wx.EXPAND | wx.ALL, 5 )
+
+ self.panel_credentials = wx.Panel( self.panel_connection, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer103 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer121 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText21 = wx.StaticText( self.panel_credentials, wx.ID_ANY, _(u"Host + port"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 )
+ self.m_staticText21.Wrap( -1 )
+
+ bSizer121.Add( self.m_staticText21, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.hostname = wx.TextCtrl( self.panel_credentials, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer121.Add( self.hostname, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.port = wx.SpinCtrl( self.panel_credentials, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_ARROW_KEYS, 0, 65536, 3306 )
+ bSizer121.Add( self.port, 0, wx.ALL, 5 )
+
+
+ bSizer103.Add( bSizer121, 0, wx.EXPAND, 5 )
+
+ bSizer122 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText22 = wx.StaticText( self.panel_credentials, wx.ID_ANY, _(u"Username"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 )
+ self.m_staticText22.Wrap( -1 )
+
+ bSizer122.Add( self.m_staticText22, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.username = wx.TextCtrl( self.panel_credentials, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer122.Add( self.username, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
+
+
+ bSizer103.Add( bSizer122, 0, wx.EXPAND, 5 )
+
+ bSizer1221 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText221 = wx.StaticText( self.panel_credentials, wx.ID_ANY, _(u"Password"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 )
+ self.m_staticText221.Wrap( -1 )
+
+ bSizer1221.Add( self.m_staticText221, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.password = wx.TextCtrl( self.panel_credentials, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_PASSWORD )
+ bSizer1221.Add( self.password, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
+
+
+ bSizer103.Add( bSizer1221, 0, wx.EXPAND, 5 )
+
+ bSizer116 = wx.BoxSizer( wx.HORIZONTAL )
+
+
+ bSizer116.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 )
+
+
+ bSizer103.Add( bSizer116, 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.panel_credentials.SetSizer( bSizer103 )
+ self.panel_credentials.Layout()
+ bSizer103.Fit( self.panel_credentials )
+ bSizer12.Add( self.panel_credentials, 0, wx.EXPAND | wx.ALL, 0 )
+
+ self.panel_source = wx.Panel( self.panel_connection, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ self.panel_source.Hide()
+
+ bSizer105 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer106 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText50 = wx.StaticText( self.panel_source, wx.ID_ANY, _(u"Filename"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 )
+ self.m_staticText50.Wrap( -1 )
+
+ bSizer106.Add( self.m_staticText50, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.filename = wx.FilePickerCtrl( self.panel_source, 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 )
+ bSizer106.Add( self.filename, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+
+ bSizer105.Add( bSizer106, 1, wx.EXPAND, 5 )
+
+
+ self.panel_source.SetSizer( bSizer105 )
+ self.panel_source.Layout()
+ bSizer105.Fit( self.panel_source )
+ bSizer12.Add( self.panel_source, 0, wx.EXPAND | wx.ALL, 0 )
+
+ 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 )
+ self.m_staticText22111.Wrap( -1 )
+
+ bSizer122111.Add( self.m_staticText22111, 0, wx.ALL, 5 )
+
+ self.comments = wx.TextCtrl( self.panel_connection, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size( -1,200 ), wx.TE_MULTILINE )
+ bSizer122111.Add( self.comments, 1, wx.ALL|wx.EXPAND, 5 )
+
+
+ bSizer12.Add( bSizer122111, 0, wx.EXPAND, 5 )
+
+
+ self.panel_connection.SetSizer( bSizer12 )
+ self.panel_connection.Layout()
+ 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 )
+
+ bSizer1213 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText213 = wx.StaticText( self.panel_ssh_tunnel, wx.ID_ANY, _(u"SSH executable"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 )
+ self.m_staticText213.Wrap( -1 )
+
+ bSizer1213.Add( self.m_staticText213, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.ssh_tunnel_executable = wx.TextCtrl( self.panel_ssh_tunnel, wx.ID_ANY, _(u"ssh"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer1213.Add( self.ssh_tunnel_executable, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+
+ bSizer102.Add( bSizer1213, 0, wx.EXPAND, 5 )
+
+ bSizer12131 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText2131 = wx.StaticText( self.panel_ssh_tunnel, wx.ID_ANY, _(u"SSH host + port"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 )
+ self.m_staticText2131.Wrap( -1 )
+
+ bSizer12131.Add( self.m_staticText2131, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.ssh_tunnel_hostname = wx.TextCtrl( self.panel_ssh_tunnel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer12131.Add( self.ssh_tunnel_hostname, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.ssh_tunnel_port = wx.SpinCtrl( self.panel_ssh_tunnel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_ARROW_KEYS, 0, 65536, 22 )
+ bSizer12131.Add( self.ssh_tunnel_port, 0, wx.ALL, 5 )
+
+
+ bSizer102.Add( bSizer12131, 0, wx.EXPAND, 5 )
+
+ bSizer12132 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText2132 = wx.StaticText( self.panel_ssh_tunnel, wx.ID_ANY, _(u"SSH username"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 )
+ self.m_staticText2132.Wrap( -1 )
+
+ bSizer12132.Add( self.m_staticText2132, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.ssh_tunnel_username = wx.TextCtrl( self.panel_ssh_tunnel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer12132.Add( self.ssh_tunnel_username, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+
+ bSizer102.Add( bSizer12132, 0, wx.EXPAND, 5 )
+
+ bSizer121321 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText21321 = wx.StaticText( self.panel_ssh_tunnel, wx.ID_ANY, _(u"SSH password"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 )
+ self.m_staticText21321.Wrap( -1 )
+
+ bSizer121321.Add( self.m_staticText21321, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.ssh_tunnel_password = wx.TextCtrl( self.panel_ssh_tunnel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_PASSWORD )
+ bSizer121321.Add( self.ssh_tunnel_password, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+
+ bSizer102.Add( bSizer121321, 0, wx.EXPAND, 5 )
+
+ bSizer1213211 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText213211 = wx.StaticText( self.panel_ssh_tunnel, wx.ID_ANY, _(u"Local port"), wx.DefaultPosition, wx.Size( 150,-1 ), 0 )
+ self.m_staticText213211.Wrap( -1 )
+
+ bSizer1213211.Add( self.m_staticText213211, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.ssh_tunnel_local_port = wx.SpinCtrl( self.panel_ssh_tunnel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_ARROW_KEYS, 0, 65536, 0 )
+ self.ssh_tunnel_local_port.SetToolTip( _(u"if the value is set to 0, the first available port will be used") )
+
+ bSizer1213211.Add( self.ssh_tunnel_local_port, 1, wx.ALL, 5 )
+
+
+ bSizer102.Add( bSizer1213211, 0, wx.EXPAND, 5 )
+
+
+ self.panel_ssh_tunnel.SetSizer( bSizer102 )
+ self.panel_ssh_tunnel.Layout()
+ bSizer102.Fit( self.panel_ssh_tunnel )
+ self.m_notebook4.AddPage( self.panel_ssh_tunnel, _(u"SSH Tunnel"), False )
+ self.panel_statistics = wx.Panel( self.m_notebook4, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer361 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer37 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText15 = wx.StaticText( self.panel_statistics, wx.ID_ANY, _(u"Created at"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_staticText15.Wrap( -1 )
+
+ self.m_staticText15.SetMinSize( wx.Size( 200,-1 ) )
+
+ bSizer37.Add( self.m_staticText15, 0, wx.ALL, 5 )
+
+ self.created_at = wx.StaticText( self.panel_statistics, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.created_at.Wrap( -1 )
+
+ bSizer37.Add( self.created_at, 0, wx.ALL, 5 )
+
+
+ bSizer361.Add( bSizer37, 0, wx.EXPAND, 5 )
+
+ bSizer371 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText151 = wx.StaticText( self.panel_statistics, wx.ID_ANY, _(u"Last connection"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_staticText151.Wrap( -1 )
+
+ self.m_staticText151.SetMinSize( wx.Size( 200,-1 ) )
+
+ bSizer371.Add( self.m_staticText151, 0, wx.ALL, 5 )
+
+ self.last_connection_at = wx.StaticText( self.panel_statistics, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.last_connection_at.Wrap( -1 )
+
+ bSizer371.Add( self.last_connection_at, 0, wx.ALL, 5 )
+
+
+ bSizer361.Add( bSizer371, 0, wx.EXPAND, 5 )
+
+ bSizer3711 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText1511 = wx.StaticText( self.panel_statistics, wx.ID_ANY, _(u"Successful connections"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_staticText1511.Wrap( -1 )
+
+ self.m_staticText1511.SetMinSize( wx.Size( 200,-1 ) )
+
+ bSizer3711.Add( self.m_staticText1511, 0, wx.ALL, 5 )
+
+ self.successful_connections = wx.StaticText( self.panel_statistics, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.successful_connections.Wrap( -1 )
+
+ bSizer3711.Add( self.successful_connections, 0, wx.ALL, 5 )
+
+
+ bSizer361.Add( bSizer3711, 0, wx.EXPAND, 5 )
+
+ bSizer37111 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText15111 = wx.StaticText( self.panel_statistics, wx.ID_ANY, _(u"Unsuccessful connections"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_staticText15111.Wrap( -1 )
+
+ self.m_staticText15111.SetMinSize( wx.Size( 200,-1 ) )
+
+ bSizer37111.Add( self.m_staticText15111, 0, wx.ALL, 5 )
+
+ self.unsuccessful_connections = wx.StaticText( self.panel_statistics, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.unsuccessful_connections.Wrap( -1 )
+
+ bSizer37111.Add( self.unsuccessful_connections, 0, wx.ALL, 5 )
+
+
+ bSizer361.Add( bSizer37111, 0, wx.EXPAND, 5 )
+
+
+ self.panel_statistics.SetSizer( bSizer361 )
+ self.panel_statistics.Layout()
+ bSizer361.Fit( self.panel_statistics )
+ self.m_notebook4.AddPage( self.panel_statistics, _(u"Statistics"), False )
+
+ bSizer36.Add( self.m_notebook4, 1, wx.ALL|wx.EXPAND, 5 )
+
+
+ self.m_panel17.SetSizer( bSizer36 )
+ self.m_panel17.Layout()
+ bSizer36.Fit( self.m_panel17 )
+ self.m_splitter3.SplitVertically( self.m_panel16, self.m_panel17, 250 )
+ bSizer34.Add( self.m_splitter3, 1, wx.EXPAND, 5 )
+
+ self.m_staticline4 = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL )
+ bSizer34.Add( self.m_staticline4, 0, wx.EXPAND | wx.ALL, 5 )
+
+ bSizer28 = wx.BoxSizer( wx.HORIZONTAL )
+
+ bSizer301 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.btn_create = wx.Button( self, wx.ID_ANY, _(u"Create"), wx.DefaultPosition, wx.DefaultSize, 0 )
+
+ self.btn_create.SetBitmap( wx.Bitmap( u"icons/16x16/add.png", wx.BITMAP_TYPE_ANY ) )
+ bSizer301.Add( self.btn_create, 0, wx.ALL|wx.BOTTOM, 5 )
+
+ self.btn_create_directory = wx.Button( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT|wx.BU_NOTEXT )
+
+ self.btn_create_directory.SetBitmap( wx.Bitmap( u"icons/16x16/folder.png", wx.BITMAP_TYPE_ANY ) )
+ bSizer301.Add( self.btn_create_directory, 0, wx.ALL, 5 )
+
+ self.btn_delete = wx.Button( self, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, 0 )
+
+ self.btn_delete.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) )
+ self.btn_delete.Enable( False )
+
+ bSizer301.Add( self.btn_delete, 0, wx.ALL, 5 )
+
+
+ bSizer28.Add( bSizer301, 1, wx.EXPAND, 5 )
+
+ bSizer110 = wx.BoxSizer( wx.HORIZONTAL )
+
+
+ bSizer28.Add( bSizer110, 1, wx.EXPAND, 5 )
+
+ bSizer29 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.btn_cancel = wx.Button( self, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.btn_cancel.Hide()
+
+ bSizer29.Add( self.btn_cancel, 0, wx.ALL, 5 )
+
+ self.btn_save = wx.Button( self, wx.ID_ANY, _(u"Save"), wx.DefaultPosition, wx.DefaultSize, 0 )
+
+ self.btn_save.SetBitmap( wx.Bitmap( u"icons/16x16/disk.png", wx.BITMAP_TYPE_ANY ) )
+ self.btn_save.Enable( False )
+
+ bSizer29.Add( self.btn_save, 0, wx.ALL, 5 )
+
+ self.btn_test = wx.Button( self, wx.ID_ANY, _(u"Test"), wx.DefaultPosition, wx.DefaultSize, 0 )
+
+ self.btn_test.SetBitmap( wx.Bitmap( u"icons/16x16/world_go.png", wx.BITMAP_TYPE_ANY ) )
+ self.btn_test.Enable( False )
+
+ bSizer29.Add( self.btn_test, 0, wx.ALL, 5 )
+
+ self.btn_open = wx.Button( self, wx.ID_ANY, _(u"Connect"), wx.DefaultPosition, wx.DefaultSize, 0 )
+
+ self.btn_open.SetBitmap( wx.Bitmap( u"icons/16x16/server_go.png", wx.BITMAP_TYPE_ANY ) )
+ self.btn_open.Enable( False )
+
+ bSizer29.Add( self.btn_open, 0, wx.ALL, 5 )
+
+
+ bSizer28.Add( bSizer29, 0, wx.EXPAND, 5 )
+
+
+ bSizer34.Add( bSizer28, 0, wx.EXPAND, 0 )
+
+
+ self.SetSizer( bSizer34 )
+ self.Layout()
+
+ self.Centre( wx.BOTH )
+
+ # Connect Events
+ self.Bind( wx.EVT_CLOSE, self.on_close )
+ self.Bind( wx.EVT_MENU, self.on_new_directory, id = self.m_menuItem4.GetId() )
+ self.Bind( wx.EVT_MENU, self.on_new_session, id = self.m_menuItem5.GetId() )
+ self.Bind( wx.EVT_MENU, self.on_import, id = self.m_menuItem10.GetId() )
+ self.engine.Bind( wx.EVT_CHOICE, self.on_choice_engine )
+ self.btn_create.Bind( wx.EVT_BUTTON, self.on_create_session )
+ self.btn_create_directory.Bind( wx.EVT_BUTTON, self.on_create_directory )
+ self.btn_delete.Bind( wx.EVT_BUTTON, self.on_delete )
+ self.btn_save.Bind( wx.EVT_BUTTON, self.on_save )
+ self.btn_open.Bind( wx.EVT_BUTTON, self.on_connect )
+
+ def __del__( self ):
+ pass
+
+
+ # Virtual event handlers, override them in your derived class
+ def on_close( self, event ):
+ event.Skip()
+
+ def on_new_directory( self, event ):
+ event.Skip()
+
+ def on_new_session( self, event ):
+ event.Skip()
+
+ def on_import( self, event ):
+ event.Skip()
+
+ def on_choice_engine( self, event ):
+ event.Skip()
+
+ def on_create_session( self, event ):
+ event.Skip()
+
+ def on_create_directory( self, event ):
+ event.Skip()
+
+ def on_delete( self, event ):
+ event.Skip()
+
+ def on_save( self, event ):
+ event.Skip()
+
+ def on_connect( self, event ):
+ event.Skip()
+
+ def m_splitter3OnIdle( self, event ):
+ self.m_splitter3.SetSashPosition( 250 )
+ self.m_splitter3.Unbind( wx.EVT_IDLE )
+
+ def m_panel16OnContextMenu( self, event ):
+ self.m_panel16.PopupMenu( self.m_menu5, event.GetPosition() )
+
+
+###########################################################################
+## Class SettingsDialog
+###########################################################################
+
+class SettingsDialog ( wx.Dialog ):
+
+ def __init__( self, parent ):
+ wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"Settings"), pos = wx.DefaultPosition, size = wx.Size( 800,600 ), style = wx.DEFAULT_DIALOG_STYLE )
+
+ self.SetSizeHints( wx.Size( 800,600 ), wx.DefaultSize )
+
+ bSizer63 = wx.BoxSizer( wx.VERTICAL )
+
+ self.m_notebook4 = wx.Notebook( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.locales = wx.Panel( self.m_notebook4, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer65 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer64 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText27 = wx.StaticText( self.locales, wx.ID_ANY, _(u"Language"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_staticText27.Wrap( -1 )
+
+ bSizer64.Add( self.m_staticText27, 0, wx.ALL, 5 )
+
+ m_choice5Choices = [ _(u"English"), _(u"Italian"), _(u"French") ]
+ self.m_choice5 = wx.Choice( self.locales, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_choice5Choices, 0|wx.BORDER_NONE )
+ self.m_choice5.SetSelection( 0 )
+ bSizer64.Add( self.m_choice5, 1, wx.ALL, 5 )
+
+
+ bSizer65.Add( bSizer64, 1, wx.EXPAND, 5 )
+
+
+ self.locales.SetSizer( bSizer65 )
+ self.locales.Layout()
+ bSizer65.Fit( self.locales )
+ self.m_notebook4.AddPage( self.locales, _(u"Locale"), False )
+
+ bSizer63.Add( self.m_notebook4, 1, wx.EXPAND | wx.ALL, 5 )
+
+
+ self.SetSizer( bSizer63 )
+ self.Layout()
+
+ self.Centre( wx.BOTH )
+
+ def __del__( self ):
+ pass
+
+
+###########################################################################
+## Class AdvancedCellEditorDialog
+###########################################################################
+
+class AdvancedCellEditorDialog ( 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 )
+
+ self.SetSizeHints( wx.Size( 640,480 ), wx.DefaultSize )
+
+ bSizer111 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer112 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer113 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText51 = wx.StaticText( self, wx.ID_ANY, _(u"Syntax"), wx.DefaultPosition, wx.Size( -1,-1 ), 0 )
+ self.m_staticText51.Wrap( -1 )
+
+ bSizer113.Add( self.m_staticText51, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ syntax_choiceChoices = []
+ self.syntax_choice = wx.Choice( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, syntax_choiceChoices, 0 )
+ self.syntax_choice.SetSelection( 0 )
+ bSizer113.Add( self.syntax_choice, 0, wx.ALL, 5 )
+
+
+ bSizer112.Add( bSizer113, 1, wx.EXPAND, 5 )
+
+
+ bSizer111.Add( bSizer112, 0, wx.EXPAND, 5 )
+
+ self.advanced_stc_editor = wx.stc.StyledTextCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0)
+ self.advanced_stc_editor.SetUseTabs ( False )
+ self.advanced_stc_editor.SetTabWidth ( 4 )
+ self.advanced_stc_editor.SetIndent ( 4 )
+ self.advanced_stc_editor.SetTabIndents( True )
+ self.advanced_stc_editor.SetBackSpaceUnIndents( True )
+ self.advanced_stc_editor.SetViewEOL( False )
+ self.advanced_stc_editor.SetViewWhiteSpace( False )
+ self.advanced_stc_editor.SetMarginWidth( 2, 0 )
+ self.advanced_stc_editor.SetIndentationGuides( True )
+ self.advanced_stc_editor.SetReadOnly( False )
+ self.advanced_stc_editor.SetMarginWidth( 1, 0 )
+ self.advanced_stc_editor.SetMarginType( 0, wx.stc.STC_MARGIN_NUMBER )
+ self.advanced_stc_editor.SetMarginWidth( 0, self.advanced_stc_editor.TextWidth( wx.stc.STC_STYLE_LINENUMBER, "_99999" ) )
+ self.advanced_stc_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS )
+ self.advanced_stc_editor.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK)
+ self.advanced_stc_editor.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE)
+ self.advanced_stc_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS )
+ self.advanced_stc_editor.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK )
+ self.advanced_stc_editor.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE )
+ self.advanced_stc_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY )
+ self.advanced_stc_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS )
+ self.advanced_stc_editor.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK )
+ self.advanced_stc_editor.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE )
+ self.advanced_stc_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUS )
+ self.advanced_stc_editor.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.BLACK)
+ self.advanced_stc_editor.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.WHITE)
+ self.advanced_stc_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY )
+ self.advanced_stc_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY )
+ self.advanced_stc_editor.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) )
+ self.advanced_stc_editor.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) )
+ bSizer111.Add( self.advanced_stc_editor, 1, wx.EXPAND | wx.ALL, 5 )
+
+ bSizer114 = wx.BoxSizer( wx.HORIZONTAL )
+
+
+ bSizer114.Add( ( 0, 0), 1, wx.EXPAND, 5 )
+
+ self.m_button49 = wx.Button( self, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer114.Add( self.m_button49, 0, wx.ALL, 5 )
+
+ self.m_button48 = wx.Button( self, wx.ID_ANY, _(u"Ok"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer114.Add( self.m_button48, 0, wx.ALL, 5 )
+
+
+ bSizer111.Add( bSizer114, 0, wx.EXPAND, 5 )
+
+
+ self.SetSizer( bSizer111 )
+ self.Layout()
+
+ self.Centre( wx.BOTH )
+
+ # Connect Events
+ self.syntax_choice.Bind( wx.EVT_CHOICE, self.on_syntax_changed )
+
+ def __del__( self ):
+ pass
+
+
+ # Virtual event handlers, override them in your derived class
+ def on_syntax_changed( self, event ):
+ event.Skip()
+
+
+###########################################################################
+## Class MainFrameView
+###########################################################################
+
+class MainFrameView ( wx.Frame ):
+
+ def __init__( self, parent ):
+ wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = _(u"PeterSQL"), pos = wx.DefaultPosition, size = wx.Size( 1280,1024 ), style = wx.DEFAULT_FRAME_STYLE|wx.MAXIMIZE_BOX|wx.TAB_TRAVERSAL )
+
+ self.SetSizeHints( wx.Size( 800,600 ), wx.DefaultSize )
+
+ self.m_menubar2 = wx.MenuBar( 0 )
+ self.m_menu2 = wx.Menu()
+ self.m_menubar2.Append( self.m_menu2, _(u"File") )
+
+ self.m_menu4 = wx.Menu()
+ self.m_menuItem15 = wx.MenuItem( self.m_menu4, wx.ID_ANY, _(u"About"), wx.EmptyString, wx.ITEM_NORMAL )
+ self.m_menu4.Append( self.m_menuItem15 )
+
+ self.m_menubar2.Append( self.m_menu4, _(u"Help") )
+
+ self.SetMenuBar( self.m_menubar2 )
+
+ 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.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()
+
+ 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.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.Realize()
+
+ bSizer19 = wx.BoxSizer( wx.VERTICAL )
+
+ self.m_panel13 = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer21 = wx.BoxSizer( wx.VERTICAL )
+
+ self.m_splitter51 = wx.SplitterWindow( self.m_panel13, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_3D|wx.SP_LIVE_UPDATE )
+ self.m_splitter51.SetSashGravity( 1 )
+ self.m_splitter51.Bind( wx.EVT_IDLE, self.m_splitter51OnIdle )
+
+ self.m_panel22 = wx.Panel( self.m_splitter51, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer72 = wx.BoxSizer( wx.VERTICAL )
+
+ self.m_splitter4 = wx.SplitterWindow( self.m_panel22, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_LIVE_UPDATE )
+ 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 )
+ bSizer24 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.tree_ctrl_explorer = wx.lib.agw.hypertreelist.HyperTreeList(
+ self.m_panel14, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize,
+ agwStyle=wx.TR_DEFAULT_STYLE|wx.TR_SINGLE|wx.TR_FULL_ROW_HIGHLIGHT|wx.TR_HIDE_ROOT|wx.TR_LINES_AT_ROOT
+ )
+ bSizer24.Add( self.tree_ctrl_explorer, 1, wx.ALL|wx.EXPAND, 5 )
+
+
+ self.m_panel14.SetSizer( bSizer24 )
+ self.m_panel14.Layout()
+ bSizer24.Fit( self.m_panel14 )
+ self.m_menu5 = wx.Menu()
+ self.m_menuItem4 = wx.MenuItem( self.m_menu5, wx.ID_ANY, _(u"MyMenuItem"), wx.EmptyString, wx.ITEM_NORMAL )
+ self.m_menu5.Append( self.m_menuItem4 )
+
+ self.m_menu1 = wx.Menu()
+ self.m_menuItem5 = wx.MenuItem( self.m_menu1, wx.ID_ANY, _(u"MyMenuItem"), wx.EmptyString, wx.ITEM_NORMAL )
+ self.m_menu1.Append( self.m_menuItem5 )
+
+ self.m_menu5.AppendSubMenu( self.m_menu1, _(u"MyMenu") )
+
+ self.m_panel14.Bind( wx.EVT_RIGHT_DOWN, self.m_panel14OnContextMenu )
+
+ self.m_panel15 = wx.Panel( self.m_splitter4, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer25 = wx.BoxSizer( wx.VERTICAL )
+
+ self.MainFrameNotebook = wx.Notebook( self.m_panel15, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.NB_FIXEDWIDTH )
+ MainFrameNotebookImageSize = wx.Size( 16,16 )
+ MainFrameNotebookIndex = 0
+ MainFrameNotebookImages = wx.ImageList( MainFrameNotebookImageSize.GetWidth(), MainFrameNotebookImageSize.GetHeight() )
+ self.MainFrameNotebook.AssignImageList( MainFrameNotebookImages )
+ self.panel_system = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer272 = wx.BoxSizer( wx.VERTICAL )
+
+ self.m_staticText291 = wx.StaticText( self.panel_system, wx.ID_ANY, _(u"MyLabel"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_staticText291.Wrap( -1 )
+
+ bSizer272.Add( self.m_staticText291, 0, wx.ALL, 5 )
+
+ self.system_databases = wx.dataview.DataViewListCtrl( self.panel_system, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_dataViewListColumn1 = self.system_databases.AppendTextColumn( _(u"Databases"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE )
+ self.m_dataViewListColumn2 = self.system_databases.AppendTextColumn( _(u"Size"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE )
+ self.m_dataViewListColumn3 = self.system_databases.AppendTextColumn( _(u"Elements"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE )
+ self.m_dataViewListColumn4 = self.system_databases.AppendTextColumn( _(u"Modified at"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE )
+ self.m_dataViewListColumn5 = self.system_databases.AppendTextColumn( _(u"Tables"), wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE )
+ bSizer272.Add( self.system_databases, 1, wx.ALL|wx.EXPAND, 5 )
+
+
+ self.panel_system.SetSizer( bSizer272 )
+ self.panel_system.Layout()
+ bSizer272.Fit( self.panel_system )
+ self.MainFrameNotebook.AddPage( self.panel_system, _(u"System"), False )
+ MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/server.png", wx.BITMAP_TYPE_ANY )
+ if ( MainFrameNotebookBitmap.IsOk() ):
+ MainFrameNotebookImages.Add( MainFrameNotebookBitmap )
+ self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex )
+ MainFrameNotebookIndex += 1
+
+ self.panel_database = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer27 = wx.BoxSizer( wx.VERTICAL )
+
+ self.m_notebook6 = wx.Notebook( self.panel_database, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_panel30 = wx.Panel( self.m_notebook6, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer80 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer531 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText391 = wx.StaticText( self.m_panel30, 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_panel30, 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_panel30, 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_table = wx.Button( self.m_panel30, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE )
+
+ self.btn_delete_table.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) )
+ self.btn_delete_table.Enable( False )
+
+ bSizer531.Add( self.btn_delete_table, 0, wx.ALL|wx.EXPAND, 2 )
+
+
+ bSizer531.Add( ( 0, 0), 1, wx.EXPAND, 5 )
+
+
+ bSizer80.Add( bSizer531, 0, wx.EXPAND, 5 )
+
+ self.list_ctrl_database_tables = wx.dataview.DataViewCtrl( self.m_panel30, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_dataViewColumn12 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Name"), 0, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE )
+ self.m_dataViewColumn13 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Rows"), 1, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_RIGHT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE )
+ self.m_dataViewColumn14 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Size"), 2, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_RIGHT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE )
+ self.m_dataViewColumn15 = self.list_ctrl_database_tables.AppendDateColumn( _(u"Created at"), 3, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE )
+ self.m_dataViewColumn16 = self.list_ctrl_database_tables.AppendDateColumn( _(u"Updated at"), 4, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE )
+ self.m_dataViewColumn17 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Engine"), 5, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE )
+ self.m_dataViewColumn19 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Collation"), 6, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE )
+ self.m_dataViewColumn18 = self.list_ctrl_database_tables.AppendTextColumn( _(u"Comments"), 7, wx.dataview.DATAVIEW_CELL_INERT, -1, wx.ALIGN_LEFT, wx.dataview.DATAVIEW_COL_RESIZABLE|wx.dataview.DATAVIEW_COL_SORTABLE )
+ bSizer80.Add( self.list_ctrl_database_tables, 1, wx.ALL|wx.EXPAND, 5 )
+
+
+ self.m_panel30.SetSizer( bSizer80 )
+ self.m_panel30.Layout()
+ bSizer80.Fit( self.m_panel30 )
+ self.m_notebook6.AddPage( self.m_panel30, _(u"Tables"), False )
+ self.m_panel31 = wx.Panel( self.m_notebook6, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer82 = wx.BoxSizer( wx.VERTICAL )
+
+
+ self.m_panel31.SetSizer( bSizer82 )
+ self.m_panel31.Layout()
+ bSizer82.Fit( self.m_panel31 )
+ self.m_notebook6.AddPage( self.m_panel31, _(u"Diagram"), False )
+
+ bSizer27.Add( self.m_notebook6, 1, wx.EXPAND | wx.ALL, 5 )
+
+
+ self.panel_database.SetSizer( bSizer27 )
+ self.panel_database.Layout()
+ bSizer27.Fit( self.panel_database )
+ 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 )
+ MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/database.png", wx.BITMAP_TYPE_ANY )
+ if ( MainFrameNotebookBitmap.IsOk() ):
+ MainFrameNotebookImages.Add( MainFrameNotebookBitmap )
+ self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex )
+ MainFrameNotebookIndex += 1
+
+ self.panel_table = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ 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.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 )
+
+ table_collationChoices = []
+ self.table_collation = wx.Choice( self.PanelTableOptions, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, table_collationChoices, 0 )
+ self.table_collation.SetSelection( 0 )
+ bSizer2721.Add( self.table_collation, 1, wx.ALL, 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 )
+
+ bSizer791 = wx.BoxSizer( wx.VERTICAL )
+
+ self.btn_delete_index = wx.Button( self.PanelTableIndex, wx.ID_ANY, _(u"Remove"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE )
+
+ self.btn_delete_index.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) )
+ self.btn_delete_index.Enable( False )
+
+ bSizer791.Add( self.btn_delete_index, 0, wx.ALL|wx.EXPAND, 5 )
+
+ self.btn_clear_index = wx.Button( self.PanelTableIndex, wx.ID_ANY, _(u"Clear"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE )
+
+ self.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 )
+
+ 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 )
+
+
+ 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
+
+ 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 )
+
+ self.btn_delete_foreign_key = wx.Button( self.PanelTableFK, wx.ID_ANY, _(u"Remove"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE )
+
+ self.btn_delete_foreign_key.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) )
+ self.btn_delete_foreign_key.Enable( False )
+
+ bSizer79.Add( self.btn_delete_foreign_key, 0, wx.ALL|wx.EXPAND, 5 )
+
+ self.btn_clear_foreign_key = wx.Button( self.PanelTableFK, wx.ID_ANY, _(u"Clear"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE )
+
+ self.btn_clear_foreign_key.SetBitmap( wx.Bitmap( u"icons/16x16/cross.png", wx.BITMAP_TYPE_ANY ) )
+ bSizer79.Add( self.btn_clear_foreign_key, 0, wx.ALL|wx.EXPAND, 5 )
+
+
+ bSizer78.Add( bSizer79, 0, wx.ALIGN_CENTER, 5 )
+
+ 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 )
+
+
+ self.PanelTableFK.SetSizer( bSizer77 )
+ self.PanelTableFK.Layout()
+ bSizer77.Fit( self.PanelTableFK )
+ self.m_notebook3.AddPage( self.PanelTableFK, _(u"Foreign Keys"), False )
+ m_notebook3Bitmap = wx.Bitmap( u"icons/16x16/table_relationship.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.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 )
+
+ self.btn_delete_check = wx.Button( self.PanelTableCheck, wx.ID_ANY, _(u"Remove"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE )
+
+ self.btn_delete_check.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) )
+ self.btn_delete_check.Enable( False )
+
+ bSizer792.Add( self.btn_delete_check, 0, wx.ALL|wx.EXPAND, 5 )
+
+ self.btn_clear_check = wx.Button( self.PanelTableCheck, wx.ID_ANY, _(u"Clear"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE )
+
+ self.btn_clear_check.SetBitmap( wx.Bitmap( u"icons/16x16/cross.png", wx.BITMAP_TYPE_ANY ) )
+ bSizer792.Add( self.btn_clear_check, 0, wx.ALL|wx.EXPAND, 5 )
+
+
+ bSizer781.Add( bSizer792, 0, wx.ALIGN_CENTER, 5 )
+
+ 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 )
+
+
+ self.PanelTableCheck.SetSizer( bSizer771 )
+ self.PanelTableCheck.Layout()
+ bSizer771.Fit( self.PanelTableCheck )
+ self.m_notebook3.AddPage( self.PanelTableCheck, _(u"Checks"), False )
+ m_notebook3Bitmap = wx.Bitmap( u"icons/16x16/tick.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.PanelTableCreate = wx.Panel( self.m_notebook3, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer109 = wx.BoxSizer( wx.VERTICAL )
+
+ self.sql_create_table = wx.stc.StyledTextCtrl( self.PanelTableCreate, wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,200 ), 0)
+ self.sql_create_table.SetUseTabs ( True )
+ self.sql_create_table.SetTabWidth ( 4 )
+ self.sql_create_table.SetIndent ( 4 )
+ self.sql_create_table.SetTabIndents( True )
+ self.sql_create_table.SetBackSpaceUnIndents( True )
+ self.sql_create_table.SetViewEOL( False )
+ self.sql_create_table.SetViewWhiteSpace( False )
+ self.sql_create_table.SetMarginWidth( 2, 0 )
+ self.sql_create_table.SetIndentationGuides( True )
+ self.sql_create_table.SetReadOnly( False )
+ self.sql_create_table.SetMarginWidth( 1, 0 )
+ self.sql_create_table.SetMarginType( 0, wx.stc.STC_MARGIN_NUMBER )
+ self.sql_create_table.SetMarginWidth( 0, self.sql_create_table.TextWidth( wx.stc.STC_STYLE_LINENUMBER, "_99999" ) )
+ self.sql_create_table.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS )
+ self.sql_create_table.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK)
+ self.sql_create_table.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE)
+ self.sql_create_table.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS )
+ self.sql_create_table.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK )
+ self.sql_create_table.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE )
+ self.sql_create_table.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY )
+ self.sql_create_table.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS )
+ self.sql_create_table.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK )
+ self.sql_create_table.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE )
+ self.sql_create_table.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUS )
+ self.sql_create_table.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.BLACK)
+ self.sql_create_table.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.WHITE)
+ self.sql_create_table.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY )
+ self.sql_create_table.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY )
+ self.sql_create_table.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) )
+ self.sql_create_table.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) )
+ bSizer109.Add( self.sql_create_table, 1, wx.EXPAND | wx.ALL, 5 )
+
+
+ self.PanelTableCreate.SetSizer( bSizer109 )
+ self.PanelTableCreate.Layout()
+ bSizer109.Fit( self.PanelTableCreate )
+ self.m_notebook3.AddPage( self.PanelTableCreate, _(u"Create"), False )
+ m_notebook3Bitmap = wx.Bitmap( u"icons/16x16/code-folding.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_delete_column = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE )
+
+ self.btn_delete_column.SetBitmap( wx.Bitmap( u"icons/16x16/delete.png", wx.BITMAP_TYPE_ANY ) )
+ self.btn_delete_column.Enable( False )
+
+ bSizer53.Add( self.btn_delete_column, 0, wx.LEFT|wx.RIGHT, 2 )
+
+ self.btn_move_up_column = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Up"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE )
+
+ self.btn_move_up_column.SetBitmap( wx.Bitmap( u"icons/16x16/arrow_up.png", wx.BITMAP_TYPE_ANY ) )
+ self.btn_move_up_column.Enable( False )
+
+ bSizer53.Add( self.btn_move_up_column, 0, wx.LEFT|wx.RIGHT, 2 )
+
+ self.btn_move_down_column = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Down"), wx.DefaultPosition, wx.DefaultSize, wx.BORDER_NONE )
+
+ self.btn_move_down_column.SetBitmap( wx.Bitmap( u"icons/16x16/arrow_down.png", wx.BITMAP_TYPE_ANY ) )
+ self.btn_move_down_column.Enable( False )
+
+ bSizer53.Add( self.btn_move_down_column, 0, wx.LEFT|wx.RIGHT, 2 )
+
+
+ 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_delete_table = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer52.Add( self.btn_delete_table, 0, wx.ALL, 5 )
+
+ self.btn_cancel_table = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.btn_cancel_table.Enable( False )
+
+ bSizer52.Add( self.btn_cancel_table, 0, wx.ALL, 5 )
+
+ self.btn_apply_table = wx.Button( self.panel_table_columns, wx.ID_ANY, _(u"Apply"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.btn_apply_table.Enable( False )
+
+ bSizer52.Add( self.btn_apply_table, 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.panel_table.SetSizer( bSizer251 )
+ self.panel_table.Layout()
+ bSizer251.Fit( self.panel_table )
+ 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 )
+ self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex )
+ MainFrameNotebookIndex += 1
+
+ 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.pnl_view_editor_root = wx.Panel( self.m_notebook7, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer85 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer87 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.m_staticText40 = wx.StaticText( self.pnl_view_editor_root, wx.ID_ANY, _(u"Name"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_staticText40.Wrap( -1 )
+
+ self.m_staticText40.SetMinSize( wx.Size( 150,-1 ) )
+
+ bSizer87.Add( self.m_staticText40, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ self.txt_view_name = wx.TextCtrl( self.pnl_view_editor_root, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer87.Add( self.txt_view_name, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+
+ bSizer85.Add( bSizer87, 0, wx.ALL|wx.EXPAND, 5 )
+
+ bSizer89 = wx.BoxSizer( wx.HORIZONTAL )
+
+ bSizer116 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer87211 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.lbl_view_schema = wx.StaticText( self.pnl_view_editor_root, wx.ID_ANY, _(u"Schema"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.lbl_view_schema.Wrap( -1 )
+
+ self.lbl_view_schema.SetMinSize( wx.Size( 150,-1 ) )
+
+ bSizer87211.Add( self.lbl_view_schema, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ cho_view_schemaChoices = []
+ self.cho_view_schema = wx.Choice( self.pnl_view_editor_root, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, cho_view_schemaChoices, 0 )
+ self.cho_view_schema.SetSelection( 0 )
+ bSizer87211.Add( self.cho_view_schema, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+
+ bSizer116.Add( bSizer87211, 0, wx.ALL|wx.EXPAND, 5 )
+
+ bSizer872 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.lbl_view_definer = wx.StaticText( self.pnl_view_editor_root, 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 ) )
+
+ bSizer872.Add( self.lbl_view_definer, 0, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+ cmb_view_definerChoices = []
+ self.cmb_view_definer = wx.ComboBox( self.pnl_view_editor_root, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, cmb_view_definerChoices, 0 )
+ bSizer872.Add( self.cmb_view_definer, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+
+ bSizer116.Add( bSizer872, 0, wx.ALL|wx.EXPAND, 5 )
+
+
+ bSizer89.Add( bSizer116, 1, wx.EXPAND, 5 )
+
+ bSizer8711 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer8721 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.lbl_view_sql_security = wx.StaticText( self.pnl_view_editor_root, 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 ) )
+
+ bSizer8721.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_view_editor_root, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, cho_view_sql_securityChoices, 0 )
+ self.cho_view_sql_security.SetSelection( 0 )
+ bSizer8721.Add( self.cho_view_sql_security, 1, wx.ALIGN_CENTER|wx.ALL, 5 )
+
+
+ bSizer8711.Add( bSizer8721, 0, wx.ALL|wx.EXPAND, 5 )
+
+ sbSizer1 = wx.StaticBoxSizer( wx.VERTICAL, self.pnl_view_editor_root, _(u"Algorithm") )
+
+ self.rad_view_algorithm_undefined = wx.RadioButton( sbSizer1.GetStaticBox(), wx.ID_ANY, _(u"UNDEFINED"), wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP )
+ sbSizer1.Add( self.rad_view_algorithm_undefined, 0, wx.ALL, 5 )
+
+ self.rad_view_algorithm_merge = wx.RadioButton( sbSizer1.GetStaticBox(), wx.ID_ANY, _(u"MERGE"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer1.Add( self.rad_view_algorithm_merge, 0, wx.ALL, 5 )
+
+ self.rad_view_algorithm_temptable = wx.RadioButton( sbSizer1.GetStaticBox(), wx.ID_ANY, _(u"TEMPTABLE"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer1.Add( self.rad_view_algorithm_temptable, 0, wx.ALL, 5 )
+
+
+ bSizer8711.Add( sbSizer1, 0, wx.EXPAND, 5 )
+
+ sbSizer11 = wx.StaticBoxSizer( wx.VERTICAL, self.pnl_view_editor_root, _(u"View constraint") )
+
+ self.rad_view_constraint_none = wx.RadioButton( sbSizer11.GetStaticBox(), wx.ID_ANY, _(u"None"), wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP )
+ sbSizer11.Add( self.rad_view_constraint_none, 0, wx.ALL, 5 )
+
+ self.rad_view_constraint_local = wx.RadioButton( sbSizer11.GetStaticBox(), wx.ID_ANY, _(u"LOCAL"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer11.Add( self.rad_view_constraint_local, 0, wx.ALL, 5 )
+
+ self.rad_view_constraint_cascaded = wx.RadioButton( sbSizer11.GetStaticBox(), wx.ID_ANY, _(u"CASCADE"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer11.Add( self.rad_view_constraint_cascaded, 0, wx.ALL, 5 )
+
+ self.rad_view_constraint_check_only = wx.RadioButton( sbSizer11.GetStaticBox(), wx.ID_ANY, _(u"CHECK ONLY"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer11.Add( self.rad_view_constraint_check_only, 0, wx.ALL, 5 )
+
+ self.rad_view_constraint_read_only = wx.RadioButton( sbSizer11.GetStaticBox(), wx.ID_ANY, _(u"READ ONLY"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ sbSizer11.Add( self.rad_view_constraint_read_only, 0, wx.ALL, 5 )
+
+
+ bSizer8711.Add( sbSizer11, 0, wx.EXPAND, 5 )
+
+ self.chk_view_security_barrier = wx.CheckBox( self.pnl_view_editor_root, wx.ID_ANY, _(u"Security barrier"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer8711.Add( self.chk_view_security_barrier, 0, wx.ALL, 5 )
+
+ self.chk_view_force = wx.CheckBox( self.pnl_view_editor_root, wx.ID_ANY, _(u"Force"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer8711.Add( self.chk_view_force, 0, wx.ALL, 5 )
+
+
+ bSizer89.Add( bSizer8711, 1, wx.EXPAND, 5 )
+
+
+ bSizer85.Add( bSizer89, 0, wx.EXPAND, 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, 1, wx.EXPAND | wx.ALL, 5 )
+
+ self.stc_view_select = wx.stc.StyledTextCtrl( self.panel_views, wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,200 ), 0)
+ self.stc_view_select.SetUseTabs ( True )
+ self.stc_view_select.SetTabWidth ( 4 )
+ self.stc_view_select.SetIndent ( 4 )
+ self.stc_view_select.SetTabIndents( True )
+ self.stc_view_select.SetBackSpaceUnIndents( True )
+ self.stc_view_select.SetViewEOL( False )
+ self.stc_view_select.SetViewWhiteSpace( False )
+ self.stc_view_select.SetMarginWidth( 2, 0 )
+ self.stc_view_select.SetIndentationGuides( True )
+ self.stc_view_select.SetReadOnly( False )
+ self.stc_view_select.SetMarginWidth( 1, 0 )
+ self.stc_view_select.SetMarginType( 0, wx.stc.STC_MARGIN_NUMBER )
+ self.stc_view_select.SetMarginWidth( 0, self.stc_view_select.TextWidth( wx.stc.STC_STYLE_LINENUMBER, "_99999" ) )
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS )
+ self.stc_view_select.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK)
+ self.stc_view_select.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE)
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS )
+ self.stc_view_select.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK )
+ self.stc_view_select.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE )
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY )
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS )
+ self.stc_view_select.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK )
+ self.stc_view_select.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE )
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUS )
+ self.stc_view_select.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.BLACK)
+ self.stc_view_select.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.WHITE)
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY )
+ self.stc_view_select.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY )
+ self.stc_view_select.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) )
+ self.stc_view_select.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) )
+ bSizer84.Add( self.stc_view_select, 1, wx.EXPAND | wx.ALL, 5 )
+
+ bSizer91 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.btn_delete_view = wx.Button( self.panel_views, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer91.Add( self.btn_delete_view, 0, wx.ALL, 5 )
+
+ self.btn_cancel_view = wx.Button( self.panel_views, wx.ID_ANY, _(u"Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.btn_cancel_view.Enable( False )
+
+ bSizer91.Add( self.btn_cancel_view, 0, wx.ALL, 5 )
+
+ self.btn_save_view = wx.Button( self.panel_views, wx.ID_ANY, _(u"Save"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.btn_save_view.Enable( False )
+
+ bSizer91.Add( self.btn_save_view, 0, wx.ALL, 5 )
+
+
+ bSizer84.Add( bSizer91, 0, wx.EXPAND, 5 )
+
+
+ self.panel_views.SetSizer( bSizer84 )
+ self.panel_views.Layout()
+ bSizer84.Fit( self.panel_views )
+ self.MainFrameNotebook.AddPage( self.panel_views, _(u"Views"), False )
+ MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/view.png", wx.BITMAP_TYPE_ANY )
+ if ( MainFrameNotebookBitmap.IsOk() ):
+ MainFrameNotebookImages.Add( MainFrameNotebookBitmap )
+ self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex )
+ MainFrameNotebookIndex += 1
+
+ self.panel_triggers = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ self.MainFrameNotebook.AddPage( self.panel_triggers, _(u"Triggers"), False )
+ MainFrameNotebookBitmap = wx.Bitmap( u"icons/16x16/cog.png", wx.BITMAP_TYPE_ANY )
+ if ( MainFrameNotebookBitmap.IsOk() ):
+ MainFrameNotebookImages.Add( MainFrameNotebookBitmap )
+ self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex )
+ MainFrameNotebookIndex += 1
+
+ self.panel_records = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer61 = wx.BoxSizer( wx.VERTICAL )
+
+ bSizer94 = wx.BoxSizer( wx.HORIZONTAL )
+
+ self.name_database_table = wx.StaticText( self.panel_records, wx.ID_ANY, _(u"Table `%(database_name)s`.`%(table_name)s`: %(total_rows) rows total"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.name_database_table.Wrap( -1 )
+
+ bSizer94.Add( self.name_database_table, 0, wx.ALL, 5 )
+
+
+ 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 )
+
+
+ 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 )
+ self.m_collapsiblePane1.Collapse( False )
+
+ bSizer831 = wx.BoxSizer( wx.VERTICAL )
+
+ self.sql_query_filters = wx.stc.StyledTextCtrl( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,100 ), 0)
+ self.sql_query_filters.SetUseTabs ( True )
+ self.sql_query_filters.SetTabWidth ( 4 )
+ self.sql_query_filters.SetIndent ( 4 )
+ self.sql_query_filters.SetTabIndents( True )
+ self.sql_query_filters.SetBackSpaceUnIndents( True )
+ self.sql_query_filters.SetViewEOL( False )
+ self.sql_query_filters.SetViewWhiteSpace( False )
+ self.sql_query_filters.SetMarginWidth( 2, 0 )
+ self.sql_query_filters.SetIndentationGuides( True )
+ self.sql_query_filters.SetReadOnly( False )
+ self.sql_query_filters.SetMarginWidth( 1, 0 )
+ self.sql_query_filters.SetMarginWidth ( 0, 0 )
+ self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS )
+ self.sql_query_filters.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK)
+ self.sql_query_filters.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE)
+ self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS )
+ self.sql_query_filters.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK )
+ self.sql_query_filters.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE )
+ self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY )
+ self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS )
+ self.sql_query_filters.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK )
+ self.sql_query_filters.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE )
+ self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUS )
+ self.sql_query_filters.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.BLACK)
+ self.sql_query_filters.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.WHITE)
+ self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY )
+ self.sql_query_filters.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY )
+ self.sql_query_filters.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) )
+ 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 )
+
+ 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.SetHelpText( _(u"CTRL+ENTER") )
+
+ bSizer831.Add( self.m_button41, 0, wx.ALL, 5 )
+
+
+ self.m_collapsiblePane1.GetPane().SetSizer( bSizer831 )
+ self.m_collapsiblePane1.GetPane().Layout()
+ bSizer831.Fit( self.m_collapsiblePane1.GetPane() )
+ bSizer61.Add( self.m_collapsiblePane1, 0, wx.ALL|wx.EXPAND, 5 )
+
+ self.list_ctrl_table_records = TableRecordsDataViewCtrl( self.panel_records, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.DV_MULTIPLE )
+ self.list_ctrl_table_records.SetFont( wx.Font( 10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) )
+
+ bSizer61.Add( self.list_ctrl_table_records, 1, wx.ALL|wx.EXPAND, 5 )
+
+
+ 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.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.panel_records.Bind( wx.EVT_RIGHT_DOWN, self.panel_recordsOnContextMenu )
+
+ 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 )
+ self.MainFrameNotebook.SetPageImage( MainFrameNotebookIndex, MainFrameNotebookIndex )
+ MainFrameNotebookIndex += 1
+
+ self.panel_query = wx.Panel( self.MainFrameNotebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ self.panel_query.Enable( False )
+
+ bSizer26 = wx.BoxSizer( wx.VERTICAL )
+
+ self.m_textCtrl10 = wx.TextCtrl( self.panel_query, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE|wx.TE_RICH|wx.TE_RICH2 )
+ bSizer26.Add( self.m_textCtrl10, 1, wx.ALL|wx.EXPAND, 5 )
+
+ self.m_button12 = wx.Button( self.panel_query, wx.ID_ANY, _(u"New"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer26.Add( self.m_button12, 0, wx.ALIGN_RIGHT|wx.ALL, 5 )
+
+ self.sql_query_editor = wx.stc.StyledTextCtrl( self.panel_query, 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 )
+ self.sql_query_editor.SetTabIndents( True )
+ self.sql_query_editor.SetBackSpaceUnIndents( True )
+ self.sql_query_editor.SetViewEOL( False )
+ self.sql_query_editor.SetViewWhiteSpace( False )
+ self.sql_query_editor.SetMarginWidth( 2, 0 )
+ self.sql_query_editor.SetIndentationGuides( True )
+ self.sql_query_editor.SetReadOnly( False )
+ self.sql_query_editor.SetMarginType ( 1, wx.stc.STC_MARGIN_SYMBOL )
+ self.sql_query_editor.SetMarginMask ( 1, wx.stc.STC_MASK_FOLDERS )
+ self.sql_query_editor.SetMarginWidth ( 1, 16)
+ self.sql_query_editor.SetMarginSensitive( 1, True )
+ self.sql_query_editor.SetProperty ( "fold", "1" )
+ self.sql_query_editor.SetFoldFlags ( wx.stc.STC_FOLDFLAG_LINEBEFORE_CONTRACTED | wx.stc.STC_FOLDFLAG_LINEAFTER_CONTRACTED )
+ self.sql_query_editor.SetMarginType( 0, wx.stc.STC_MARGIN_NUMBER )
+ self.sql_query_editor.SetMarginWidth( 0, self.sql_query_editor.TextWidth( wx.stc.STC_STYLE_LINENUMBER, "_99999" ) )
+ self.sql_query_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS )
+ self.sql_query_editor.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK)
+ self.sql_query_editor.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE)
+ self.sql_query_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS )
+ self.sql_query_editor.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK )
+ self.sql_query_editor.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE )
+ self.sql_query_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY )
+ self.sql_query_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS )
+ self.sql_query_editor.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK )
+ self.sql_query_editor.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE )
+ self.sql_query_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUS )
+ self.sql_query_editor.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.BLACK)
+ self.sql_query_editor.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.WHITE)
+ self.sql_query_editor.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY )
+ 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 ) )
+ bSizer26.Add( self.sql_query_editor, 1, wx.EXPAND | wx.ALL, 5 )
+
+ self.notebook_sql_results = wx.Notebook( self.panel_query, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 )
+
+ bSizer26.Add( self.notebook_sql_results, 1, wx.EXPAND | wx.ALL, 5 )
+
+
+ self.panel_query.SetSizer( bSizer26 )
+ self.panel_query.Layout()
+ bSizer26.Fit( self.panel_query )
+ 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 )
+
+
+ self.m_panel15.SetSizer( bSizer25 )
+ self.m_panel15.Layout()
+ bSizer25.Fit( self.m_panel15 )
+ 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.m_panel15.Bind( wx.EVT_RIGHT_DOWN, self.m_panel15OnContextMenu )
+
+ self.m_splitter4.SplitVertically( self.m_panel14, self.m_panel15, 320 )
+ bSizer72.Add( self.m_splitter4, 1, wx.EXPAND, 5 )
+
+
+ self.m_panel22.SetSizer( bSizer72 )
+ self.m_panel22.Layout()
+ bSizer72.Fit( self.m_panel22 )
+ self.LogSQLPanel = wx.Panel( self.m_splitter51, wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,-1 ), wx.TAB_TRAVERSAL )
+ sizer_log_sql = wx.BoxSizer( wx.VERTICAL )
+
+ self.sql_query_logs = wx.stc.StyledTextCtrl( self.LogSQLPanel, wx.ID_ANY, wx.DefaultPosition, wx.Size( -1,200 ), 0)
+ self.sql_query_logs.SetUseTabs ( True )
+ self.sql_query_logs.SetTabWidth ( 4 )
+ self.sql_query_logs.SetIndent ( 4 )
+ self.sql_query_logs.SetTabIndents( True )
+ self.sql_query_logs.SetBackSpaceUnIndents( True )
+ self.sql_query_logs.SetViewEOL( False )
+ self.sql_query_logs.SetViewWhiteSpace( False )
+ self.sql_query_logs.SetMarginWidth( 2, 0 )
+ self.sql_query_logs.SetIndentationGuides( True )
+ self.sql_query_logs.SetReadOnly( False )
+ self.sql_query_logs.SetMarginWidth( 1, 0 )
+ self.sql_query_logs.SetMarginType( 0, wx.stc.STC_MARGIN_NUMBER )
+ self.sql_query_logs.SetMarginWidth( 0, self.sql_query_logs.TextWidth( wx.stc.STC_STYLE_LINENUMBER, "_99999" ) )
+ self.sql_query_logs.MarkerDefine( wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS )
+ self.sql_query_logs.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDER, wx.BLACK)
+ self.sql_query_logs.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDER, wx.WHITE)
+ self.sql_query_logs.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS )
+ self.sql_query_logs.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.BLACK )
+ self.sql_query_logs.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPEN, wx.WHITE )
+ self.sql_query_logs.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY )
+ self.sql_query_logs.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUS )
+ self.sql_query_logs.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEREND, wx.BLACK )
+ self.sql_query_logs.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEREND, wx.WHITE )
+ self.sql_query_logs.MarkerDefine( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUS )
+ self.sql_query_logs.MarkerSetBackground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.BLACK)
+ self.sql_query_logs.MarkerSetForeground( wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.WHITE)
+ self.sql_query_logs.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY )
+ self.sql_query_logs.MarkerDefine( wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY )
+ self.sql_query_logs.SetSelBackground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT ) )
+ self.sql_query_logs.SetSelForeground( True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT ) )
+ sizer_log_sql.Add( self.sql_query_logs, 1, wx.EXPAND | wx.ALL, 5 )
+
+
+ self.LogSQLPanel.SetSizer( sizer_log_sql )
+ self.LogSQLPanel.Layout()
+ sizer_log_sql.Fit( self.LogSQLPanel )
+ self.m_splitter51.SplitHorizontally( self.m_panel22, self.LogSQLPanel, 432 )
+ bSizer21.Add( self.m_splitter51, 1, wx.EXPAND, 5 )
+
+
+ self.m_panel13.SetSizer( bSizer21 )
+ self.m_panel13.Layout()
+ bSizer21.Fit( self.m_panel13 )
+ bSizer19.Add( self.m_panel13, 1, wx.EXPAND | wx.ALL, 0 )
+
+
+ self.SetSizer( bSizer19 )
+ self.Layout()
+ self.status_bar = self.CreateStatusBar( 4, wx.STB_SIZEGRIP, wx.ID_ANY )
+
+ self.Centre( wx.BOTH )
+
+ # Connect Events
+ self.Bind( wx.EVT_CLOSE, self.do_close )
+ 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.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_table.Bind( wx.EVT_BUTTON, self.on_delete_table )
+ 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.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_delete_table.Bind( wx.EVT_BUTTON, self.on_delete_table )
+ self.btn_cancel_table.Bind( wx.EVT_BUTTON, self.on_cancel_table )
+ self.btn_apply_table.Bind( wx.EVT_BUTTON, self.do_apply_table )
+ self.btn_insert_record.Bind( wx.EVT_BUTTON, self.on_insert_record )
+ self.btn_duplicate_record.Bind( wx.EVT_BUTTON, self.on_duplicate_record )
+ self.btn_delete_record.Bind( wx.EVT_BUTTON, self.on_delete_record )
+ self.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 )
+
+ def __del__( self ):
+ pass
+
+
+ # Virtual event handlers, override them in your derived class
+ def do_close( self, event ):
+ event.Skip()
+
+ def on_menu_about( self, event ):
+ event.Skip()
+
+ def do_open_connection_manager( self, event ):
+ event.Skip()
+
+ def do_disconnect( self, event ):
+ event.Skip()
+
+ def on_page_chaged( 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()
+
+ def on_delete_index( self, event ):
+ event.Skip()
+
+ def on_clear_index( self, event ):
+ event.Skip()
+
+ def on_insert_foreign_key( self, event ):
+ event.Skip()
+
+ def on_delete_foreign_key( self, event ):
+ event.Skip()
+
+ def on_clear_foreign_key( self, event ):
+ event.Skip()
+
+
+
+
+ def on_insert_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_cancel_table( self, event ):
+ event.Skip()
+
+ def do_apply_table( self, event ):
+ event.Skip()
+
+ 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_auto_apply( self, event ):
+ event.Skip()
+
+ def on_next_records( self, event ):
+ event.Skip()
+
+ def on_collapsible_pane_changed( self, event ):
+ event.Skip()
+
+ def on_apply_filters( self, event ):
+ event.Skip()
+
+ def m_splitter51OnIdle( self, event ):
+ self.m_splitter51.SetSashPosition( 432 )
+ self.m_splitter51.Unbind( wx.EVT_IDLE )
+
+ def m_splitter4OnIdle( self, event ):
+ self.m_splitter4.SetSashPosition( 320 )
+ self.m_splitter4.Unbind( wx.EVT_IDLE )
+
+ def m_panel14OnContextMenu( self, event ):
+ self.m_panel14.PopupMenu( self.m_menu5, event.GetPosition() )
+
+ def panel_databaseOnContextMenu( self, event ):
+ self.panel_database.PopupMenu( self.m_menu15, event.GetPosition() )
+
+ 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() )
+
+ def panel_recordsOnContextMenu( self, event ):
+ self.panel_records.PopupMenu( self.m_menu10, event.GetPosition() )
+
+ def m_panel15OnContextMenu( self, event ):
+ self.m_panel15.PopupMenu( self.m_menu3, event.GetPosition() )
+
+
+###########################################################################
+## 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 )
+
+ bSizer90 = wx.BoxSizer( wx.VERTICAL )
+
+ self.m_textCtrl221 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
+ bSizer90.Add( self.m_textCtrl221, 1, wx.ALL|wx.EXPAND, 5 )
+
+ bSizer93 = wx.BoxSizer( wx.VERTICAL )
+
+ self.tree_ctrl_explorer____ = wx.dataview.TreeListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.TL_DEFAULT_STYLE )
+ self.tree_ctrl_explorer____.AppendColumn( _(u"Column5"), wx.COL_WIDTH_DEFAULT, wx.ALIGN_LEFT, wx.COL_RESIZABLE )
+
+ bSizer93.Add( self.tree_ctrl_explorer____, 1, wx.EXPAND | wx.ALL, 5 )
+
+ bSizer129 = wx.BoxSizer( wx.VERTICAL )
+
+ self.m_radioBtn11 = wx.RadioButton( self, wx.ID_ANY, _(u"UNDEFINED"), wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP )
+ bSizer129.Add( self.m_radioBtn11, 1, wx.ALL|wx.EXPAND, 5 )
+
+ self.m_radioBtn21 = wx.RadioButton( self, wx.ID_ANY, _(u"MERGE"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ 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 )
+
+ bSizer86 = wx.BoxSizer( wx.HORIZONTAL )
+
+
+ bSizer129.Add( bSizer86, 0, 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 )
+ bSizer129.Add( self.rad_view_constraint, 0, wx.ALL|wx.EXPAND, 5 )
+
+
+ bSizer93.Add( bSizer129, 1, wx.EXPAND, 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 )
+
+ bSizer92 = wx.BoxSizer( wx.VERTICAL )
+
+
+ self.m_collapsiblePane2.GetPane().SetSizer( bSizer92 )
+ self.m_collapsiblePane2.GetPane().Layout()
+ bSizer92.Fit( self.m_collapsiblePane2.GetPane() )
+ 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 )
+ 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.m_panel35 = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
+ bSizer96 = wx.BoxSizer( wx.VERTICAL )
+
+
+ self.m_panel35.SetSizer( bSizer96 )
+ self.m_panel35.Layout()
+ bSizer96.Fit( self.m_panel35 )
+ bSizer90.Add( self.m_panel35, 1, wx.EXPAND | wx.ALL, 5 )
+
+ 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 )
+
+
+ self.SetSizer( bSizer90 )
+ self.Layout()
+ self.m_menu11 = wx.Menu()
+ self.Bind( wx.EVT_RIGHT_DOWN, self.TrashOnContextMenu )
+
+
+ # Connect Events
+ 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 show_tree_ctrl_menu( self, event ):
+ event.Skip()
+
+ def tree_ctrl_sessionsOnContextMenu( self, event ):
+ self.tree_ctrl_sessions.PopupMenu( self.m_menu12, 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
+
+
+###########################################################################
+## 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() )
+
+
From 50e70b90c25cb5a00c6ef79aef53fd03b60d2741 Mon Sep 17 00:00:00 2001
From: gtripoli
Date: Thu, 26 Feb 2026 20:32:25 +0100
Subject: [PATCH 02/72] ui: update wxFormBuilder project with view editor
enhancements
- Change output file from windows/__init__ to windows/views
- Rename Settings dialog to SettingsDialog
- Expand MainFrameView and adjust default size from 1024x762 to 1280x1024
- Update splitter window sash position from -150 to 432
- Expand Views notebook page and its child components
- Rename view editor panel from m_panel34 to pnl_view_editor_root
- Add view name input field with label and text control
- Add schema selection with
---
PeterSQL.fbp | 5262 +++++++++++++++++++++++++++++++++++++-------------
1 file changed, 3894 insertions(+), 1368 deletions(-)
diff --git a/PeterSQL.fbp b/PeterSQL.fbp
index ef4f6c3..6efdabb 100755
--- a/PeterSQL.fbp
+++ b/PeterSQL.fbp
@@ -13,7 +13,7 @@
0
/home/gtripoli/Projects/PeterSQL/windows
UTF-8
- windows/__init__
+ windows/views
1000
1
1
@@ -3971,7 +3971,7 @@
wxID_ANY
800,600
- Settings
+ SettingsDialog
800,600
wxDEFAULT_DIALOG_STYLE
@@ -4673,7 +4673,7 @@
-
+
0
@@ -4692,7 +4692,7 @@
800,600
MainFrameView
- 1024,762
+ 1280,1024
wxDEFAULT_FRAME_STYLE|wxMAXIMIZE_BOX
PeterSQL
@@ -4871,16 +4871,16 @@
-
+
bSizer19
wxVERTICAL
none
-
+
0
wxEXPAND | wxALL
1
-
+
1
1
1
@@ -4932,16 +4932,16 @@
wxTAB_TRAVERSAL
-
+
bSizer21
wxVERTICAL
none
-
+
5
wxEXPAND
1
-
+
1
1
1
@@ -4987,7 +4987,7 @@
Resizable
1
- -150
+ 432
-1
1
@@ -4999,8 +4999,8 @@
-
-
+
+
1
1
1
@@ -5052,16 +5052,16 @@
wxTAB_TRAVERSAL
-
+
bSizer72
wxVERTICAL
none
-
+
5
wxEXPAND
1
-
+
1
1
1
@@ -5119,8 +5119,8 @@
-
-
+
+
1
1
1
@@ -5289,8 +5289,8 @@
-
-
+
+
1
1
1
@@ -5342,16 +5342,16 @@
wxTAB_TRAVERSAL
-
+
bSizer25
wxVERTICAL
none
-
+
5
wxALL|wxEXPAND
1
-
+
1
1
1
@@ -9226,11 +9226,11 @@
-
+
Load From File; icons/16x16/view.png
Views
0
-
+
1
1
1
@@ -9282,16 +9282,16 @@
wxTAB_TRAVERSAL
-
+
bSizer84
wxVERTICAL
none
-
+
5
wxEXPAND | wxALL
1
-
+
1
1
1
@@ -9345,11 +9345,11 @@
-
+
Options
0
-
+
1
1
1
@@ -9385,7 +9385,7 @@
0
1
- m_panel34
+ pnl_view_editor_root
1
@@ -9401,36 +9401,174 @@
wxTAB_TRAVERSAL
-
+
bSizer85
wxVERTICAL
none
-
+
5
- wxEXPAND
- 1
+ wxALL|wxEXPAND
+ 0
- bSizer86
- wxVERTICAL
+ bSizer87
+ wxHORIZONTAL
none
5
- wxEXPAND
+ wxALIGN_CENTER|wxALL
0
-
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ Name
+ 0
+
+ 0
+
+
+ 0
+ 150,-1
+ 1
+ m_staticText40
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+
+
+ -1
+
+
+
+ 5
+ wxALIGN_CENTER|wxALL
+ 1
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+
+ 0
+
+ 0
+
+ 0
+
+ 1
+ txt_view_name
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+
+
+
+
+
+
+
+
+
+ 5
+ wxEXPAND
+ 0
+
+
+ bSizer89
+ wxHORIZONTAL
+ none
+
+ 5
+ wxEXPAND
+ 1
+
- bSizer89
- wxHORIZONTAL
+ bSizer116
+ wxVERTICAL
none
-
+
5
- wxEXPAND
- 1
-
+ wxALL|wxEXPAND
+ 0
+
- bSizer87
+ bSizer87211
wxHORIZONTAL
none
@@ -9466,16 +9604,16 @@
0
0
wxID_ANY
- Name
+ Schema
0
0
0
-
+ 150,-1
1
- m_staticText40
+ lbl_view_schema
1
@@ -9497,9 +9635,9 @@
5
- wxALL|wxEXPAND
+ wxALIGN_CENTER|wxALL
1
-
+
1
1
1
@@ -9513,6 +9651,7 @@
1
0
+
1
1
@@ -9531,12 +9670,11 @@
0
- 0
0
1
- m_textCtrl22
+ cho_view_schema
1
@@ -9544,6 +9682,7 @@
1
Resizable
+ 0
1
@@ -9554,7 +9693,6 @@
wxFILTER_NONE
wxDefaultValidator
-
@@ -9562,13 +9700,13 @@
-
+
5
- wxEXPAND
- 1
-
+ wxALL|wxEXPAND
+ 0
+
- bSizer871
+ bSizer872
wxHORIZONTAL
none
@@ -9604,16 +9742,16 @@
0
0
wxID_ANY
- Temporary
+ Definer
0
0
0
-
+ 150,-1
1
- m_staticText401
+ lbl_view_definer
1
@@ -9636,8 +9774,8 @@
5
wxALIGN_CENTER|wxALL
- 0
-
+ 1
+
1
1
1
@@ -9651,7 +9789,7 @@
1
0
- 0
+
1
1
@@ -9667,7 +9805,6 @@
0
0
wxID_ANY
-
0
@@ -9675,7 +9812,7 @@
0
1
- m_checkBox5
+ cmb_view_definer
1
@@ -9683,6 +9820,7 @@
1
Resizable
+ -1
1
@@ -9693,6 +9831,7 @@
wxFILTER_NONE
wxDefaultValidator
+
@@ -9702,99 +9841,926 @@
-
-
-
-
-
-
-
-
- 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_view
- 1
-
-
- protected
- 1
-
- 0
- Resizable
- 1
- -1,200
- ; ; forward_declare
- 1
- 4
- 0
-
- 1
- 0
- 0
-
-
-
-
-
-
- 5
- wxEXPAND
- 0
-
-
- bSizer91
- wxHORIZONTAL
- none
-
- 5
- wxALL
- 0
-
- 1
- 1
- 1
- 1
+
+ 5
+ wxEXPAND
+ 1
+
+
+ bSizer8711
+ wxVERTICAL
+ none
+
+ 5
+ wxALL|wxEXPAND
+ 0
+
+
+ bSizer8721
+ wxHORIZONTAL
+ none
+
+ 5
+ wxALIGN_CENTER|wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ SQL security
+ 0
+
+ 0
+
+
+ 0
+ 150,-1
+ 1
+ lbl_view_sql_security
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+
+
+ -1
+
+
+
+ 5
+ wxALIGN_CENTER|wxALL
+ 1
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ "DEFINER" "INVOKER"
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+
+ 0
+
+
+ 0
+
+ 1
+ cho_view_sql_security
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 0
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+
+
+
+
+
+
+
+
+ 5
+ wxEXPAND
+ 0
+
+ Algorithm
+
+ sbSizer1
+ wxVERTICAL
+ 1
+ none
+
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ UNDEFINED
+
+ 0
+
+
+ 0
+
+ 1
+ rad_view_algorithm_undefined
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+ wxRB_GROUP
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
+
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ MERGE
+
+ 0
+
+
+ 0
+
+ 1
+ rad_view_algorithm_merge
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
+
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ TEMPTABLE
+
+ 0
+
+
+ 0
+
+ 1
+ rad_view_algorithm_temptable
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
+
+
+
+
+ 5
+ wxEXPAND
+ 0
+
+ View constraint
+
+ sbSizer11
+ 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
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ Security barrier
+
+ 0
+
+
+ 0
+
+ 1
+ chk_view_security_barrier
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+
+
+
+
+
+
+ 5
+ 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 | 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
+ stc_view_select
+ 1
+
+
+ protected
+ 1
+
+ 0
+ Resizable
+ 1
+ -1,200
+ ; ; forward_declare
+ 1
+ 4
+ 0
+
+ 1
+ 0
+ 0
+
+
+
+
+
+
+ 5
+ wxEXPAND
+ 0
+
+
+ bSizer91
+ wxHORIZONTAL
+ none
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
0
0
@@ -11076,11 +12042,11 @@
-
+
Load From File; icons/16x16/arrow_right.png
Query
0
-
+
1
1
1
@@ -11116,7 +12082,7 @@
0
1
- QueryPanel
+ panel_query
1
@@ -11132,7 +12098,7 @@
wxTAB_TRAVERSAL
-
+
bSizer26
wxVERTICAL
@@ -11276,14 +12242,142 @@
+
+ 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
+ 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
+
+
+ ; ; forward_declare
+ 0
+
+
+
+
+
+
-
+
Query #2
0
-
+
1
1
1
@@ -11335,7 +12429,7 @@
wxTAB_TRAVERSAL
-
+
bSizer263
wxVERTICAL
@@ -11605,8 +12699,1313 @@
-
-
+
+
+ 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
+ LogSQLPanel
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+ -1,-1
+ ; ; forward_declare
+ 0
+
+
+
+ wxTAB_TRAVERSAL
+
+
+ sizer_log_sql
+ wxVERTICAL
+ none
+
+ 5
+ wxEXPAND | wxALL
+ 1
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+ 1
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+ 0
+
+ 0
+ 0
+ wxID_ANY
+ 1
+ 1
+
+ 0
+ -1,-1
+
+ 0
+
+ 1
+ sql_query_logs
+ 1
+
+
+ protected
+ 1
+
+ 0
+ Resizable
+ 1
+ -1,200
+ ; ; forward_declare
+ 1
+ 4
+ 0
+
+ 1
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ 0
+ 1
+
+ 4
+
+ 0
+ wxID_ANY
+
+
+ status_bar
+ protected
+
+
+ wxSTB_SIZEGRIP
+
+
+
+
+
+
+
+
+ 0
+ wxAUI_MGR_DEFAULT
+
+
+ 1
+ 0
+ 1
+ impl_virtual
+
+
+ 0
+ wxID_ANY
+
+
+ Trash
+
+ 500,300
+ ; ; forward_declare
+
+ 0
+
+
+ wxTAB_TRAVERSAL
+
+
+ bSizer90
+ wxVERTICAL
+ none
+
+ 5
+ wxALL|wxEXPAND
+ 1
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+
+ 0
+
+ 0
+
+ 0
+
+ 1
+ m_textCtrl221
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+
+
+
+
+
+
+
+ 5
+ wxEXPAND
+ 1
+
+
+ bSizer93
+ wxVERTICAL
+ none
+
+ 5
+ wxEXPAND | wxALL
+ 1
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+
+ 0
+
+
+ 0
+
+ 1
+ tree_ctrl_explorer____
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+ wxTL_DEFAULT_STYLE
+ ; ; forward_declare
+ 0
+
+
+
+
+
+ wxALIGN_LEFT
+ wxCOL_RESIZABLE
+ Column5
+ wxCOL_WIDTH_DEFAULT
+
+
+
+
+ 5
+ wxEXPAND
+ 1
+
+
+ bSizer129
+ wxVERTICAL
+ none
+
+ 5
+ wxALL|wxEXPAND
+ 1
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ UNDEFINED
+
+ 0
+
+
+ 0
+
+ 1
+ m_radioBtn11
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+ wxRB_GROUP
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
+
+
+ 5
+ wxALL|wxEXPAND
+ 1
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ MERGE
+
+ 0
+
+
+ 0
+
+ 1
+ m_radioBtn21
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
+
+
+ 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
+ wxEXPAND
+ 0
+
+
+ bSizer86
+ wxHORIZONTAL
+ none
+
+
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ 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
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+ bSizer92
+ 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
+
+ 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
+
+
+
+
+ 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
@@ -11617,6 +14016,7 @@
0
+
1
0
@@ -11637,12 +14037,12 @@
wxID_ANY
0
- -1,-1
+
0
1
- LogSQLPanel
+ m_notebook8
1
@@ -11651,85 +14051,215 @@
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
-
-
-
-
+
+
+
+
+
+
+
+ 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
+
+
+
@@ -11737,142 +14267,11 @@
-
-
-
-
-
-
- 1
- 0
- 1
-
- 4
-
- 0
- wxID_ANY
-
-
- status_bar
- protected
-
-
- wxSTB_SIZEGRIP
-
-
-
-
-
-
-
-
- 0
- wxAUI_MGR_DEFAULT
-
-
- 1
- 0
- 1
- impl_virtual
-
-
- 0
- wxID_ANY
-
-
- Trash
-
- 500,300
- ; ; forward_declare
-
- 0
-
-
- wxTAB_TRAVERSAL
-
-
- bSizer90
- wxVERTICAL
- none
-
- 5
- wxALL|wxEXPAND
- 1
-
- 1
- 1
- 1
- 1
- 0
-
- 0
- 0
-
-
-
- 1
- 0
- 1
-
- 1
- 0
- Dock
- 0
- Left
- 0
- 1
-
- 1
-
- 0
- 0
- wxID_ANY
-
- 0
-
- 0
-
- 0
-
- 1
- m_textCtrl221
- 1
-
-
- protected
- 1
-
- Resizable
- 1
-
-
- ; ; forward_declare
- 0
-
-
- wxFILTER_NONE
- wxDefaultValidator
-
-
-
-
-
-
-
-
- 5
- wxEXPAND
- 1
-
-
- bSizer93
- wxVERTICAL
- none
5
- wxEXPAND | wxALL
- 1
-
+ wxALIGN_CENTER|wxALL
+ 0
+
1
1
1
@@ -11901,14 +14300,16 @@
0
0
wxID_ANY
+ Port
+ 0
0
0
-
+ -1,-1
1
- tree_ctrl_explorer____
+ m_staticText2211
1
@@ -11917,20 +14318,15 @@
Resizable
1
-
- wxTL_DEFAULT_STYLE
- ; ; forward_declare
+ 150,-1
+
+
0
-
- wxALIGN_LEFT
- wxCOL_RESIZABLE
- Column5
- wxCOL_WIDTH_DEFAULT
-
+ -1
@@ -11939,7 +14335,7 @@
5
wxEXPAND | wxALL
1
-
+
1
1
1
@@ -11954,7 +14350,6 @@
1
0
1
- 0
1
0
@@ -11969,7 +14364,6 @@
0
0
wxID_ANY
- collapsible
0
@@ -11977,7 +14371,7 @@
0
1
- m_collapsiblePane2
+ m_panel35
1
@@ -11987,20 +14381,15 @@
Resizable
1
- wxCP_DEFAULT_STYLE
; ; forward_declare
0
-
- wxFILTER_NONE
- wxDefaultValidator
-
-
+ wxTAB_TRAVERSAL
- bSizer92
+ bSizer96
wxVERTICAL
none
@@ -12008,9 +14397,9 @@
5
- wxALL|wxEXPAND
- 1
-
+ wxALIGN_CENTER|wxALL
+ 0
+
1
1
1
@@ -12042,11 +14431,12 @@
0
+ 0
0
1
- tree_ctrl_sessions
+ ssh_tunnel_port
1
@@ -12056,26 +14446,25 @@
Resizable
1
- wxTR_DEFAULT_STYLE|wxTR_FULL_ROW_HIGHLIGHT|wxTR_HAS_BUTTONS|wxTR_HIDE_ROOT|wxTR_TWIST_BUTTONS
+
; ; forward_declare
0
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+
- show_tree_ctrl_menu
-
5
- wxEXPAND | wxALL
+ wxALIGN_CENTER|wxALL
1
-
+
1
1
1
@@ -12107,11 +14496,12 @@
0
+ 0
0
1
- m_treeListCtrl3
+ ssh_tunnel_local_port
1
@@ -12121,7 +14511,71 @@
Resizable
1
- wxTL_DEFAULT_STYLE
+
+
+ 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
@@ -12161,7 +14615,7 @@
1
0
- 0
+ 1
wxID_ANY
0
@@ -12170,7 +14624,7 @@
0
1
- tree_ctrl_sessions1
+ tree_ctrl_sessions_bkp3
1
@@ -12180,7 +14634,7 @@
Resizable
1
- wxTL_DEFAULT_STYLE
+ wxTL_DEFAULT_STYLE|wxTL_SINGLE
; ; forward_declare
0
@@ -12190,13 +14644,13 @@
wxALIGN_LEFT
wxCOL_RESIZABLE
- Column3
+ Name
wxCOL_WIDTH_DEFAULT
wxALIGN_LEFT
wxCOL_RESIZABLE
- Column4
+ Usage
wxCOL_WIDTH_DEFAULT
@@ -12205,7 +14659,59 @@
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
@@ -12234,15 +14740,16 @@
0
0
wxID_ANY
+ %(total_rows)s
+ 0
0
- 0
0
1
- table_collationdd
+ rows_database_table
1
@@ -12253,24 +14760,20 @@
1
-
+ ; ; forward_declare
0
-
- wxFILTER_NONE
- wxDefaultValidator
-
-
+ -1
5
- wxALL|wxEXPAND
- 1
-
+ wxALL
+ 0
+
1
1
1
@@ -12299,381 +14802,281 @@
0
0
wxID_ANY
+ rows total
+ 0
0
- 0
0
1
- m_textCtrl21
+ m_staticText44
1
protected
- 1
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+
+
+ -1
+
+
+
+ 5
+ wxALL
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+
+ 0
+ wxID_ANY
+
+
+ ____list_ctrl_database_tables
+ protected
- Resizable
- 1
- wxTE_MULTILINE
+
; ; forward_declare
- 0
-
- wxFILTER_NONE
- wxDefaultValidator
-
-
+
+ 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
+
- 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
-
-
-
-
-
-
-
-
+ 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
-
- 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
-
-
-
-
-
-
-
-
-
+
+ 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
- wxEXPAND | wxALL
- 1
-
+ wxALL|wxEXPAND
+ 0
+
1
1
1
@@ -12709,35 +15112,84 @@
0
1
- m_panel35
+ m_gauge1
1
protected
1
+ 100
Resizable
1
+
; ; forward_declare
0
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
- wxTAB_TRAVERSAL
-
-
- bSizer96
- wxVERTICAL
- none
-
+
5
- wxALIGN_CENTER|wxALL
+ 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
@@ -12769,12 +15221,11 @@
0
- 0
0
1
- ssh_tunnel_port
+ m_vlistBox1
1
@@ -12788,21 +15239,16 @@
; ; forward_declare
0
-
- wxFILTER_NONE
- wxDefaultValidator
-
-
-
+
5
- wxALIGN_CENTER|wxALL
- 1
-
+ wxALL
+ 0
+
1
1
1
@@ -12816,6 +15262,7 @@
1
0
+
1
1
@@ -12834,12 +15281,11 @@
0
- 0
0
1
- ssh_tunnel_local_port
+ m_listBox1
1
@@ -12850,14 +15296,13 @@
1
-
+ ; ; forward_declare
0
wxFILTER_NONE
wxDefaultValidator
-
@@ -12866,10 +15311,10 @@
5
wxEXPAND
- 0
+ 1
- bSizer12211
+ bSizer871
wxHORIZONTAL
none
@@ -12905,16 +15350,16 @@
0
0
wxID_ANY
- Port
+ Temporary
0
0
0
- -1,-1
+
1
- m_staticText2211
+ m_staticText401
1
@@ -12923,9 +15368,9 @@
Resizable
1
- 150,-1
+
-
+ ; ; forward_declare
0
@@ -12934,72 +15379,78 @@
-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
- 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
@@ -13014,6 +15465,7 @@
1
0
1
+ 0
1
0
@@ -13026,8 +15478,9 @@
1
0
- 1
+ 0
wxID_ANY
+ Engine options
0
@@ -13035,7 +15488,7 @@
0
1
- tree_ctrl_sessions_bkp3
+ m_collapsiblePane3
1
@@ -13045,84 +15498,204 @@
Resizable
1
- wxTL_DEFAULT_STYLE|wxTL_SINGLE
+ wxCP_DEFAULT_STYLE
; ; forward_declare
0
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
-
- wxALIGN_LEFT
- wxCOL_RESIZABLE
- Name
- wxCOL_WIDTH_DEFAULT
-
-
- wxALIGN_LEFT
- wxCOL_RESIZABLE
- Usage
- wxCOL_WIDTH_DEFAULT
+
+
+ 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
- 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
@@ -13151,16 +15724,15 @@
0
0
wxID_ANY
- %(total_rows)s
- 0
0
+ 0
0
1
- rows_database_table
+ m_textCtrl2211
1
@@ -13174,17 +15746,21 @@
; ; forward_declare
0
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+
- -1
-
+
5
- wxALL
- 0
-
+ wxALL|wxEXPAND
+ 1
+
1
1
1
@@ -13213,16 +15789,15 @@
0
0
wxID_ANY
- rows total
- 0
0
+ 0
0
1
- m_staticText44
+ m_textCtrl2212
1
@@ -13236,430 +15811,380 @@
; ; forward_declare
0
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+
- -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
+
+
+ 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
+
+
+
+
+
+
-
- wxALIGN_LEFT
-
- wxDATAVIEW_COL_RESIZABLE
- Comments
- wxDATAVIEW_CELL_INERT
- m_dataViewListColumn12
- protected
- Text
- -1
+
+ 5
+ wxEXPAND
+ 0
+
+
+ bSizer12211
+ wxHORIZONTAL
+ none
+
-
- 5
- wxALL|wxEXPAND
- 0
-
- 1
- 1
- 1
- 1
- 0
-
- 0
- 0
-
-
-
- 1
- 0
- 1
-
- 1
- 0
- Dock
- 0
- Left
- 0
- 1
-
- 1
-
- 0
- 0
- wxID_ANY
-
- 0
-
-
- 0
-
- 1
- 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
@@ -13673,7 +16198,6 @@
1
0
-
1
1
@@ -13689,6 +16213,7 @@
0
0
wxID_ANY
+ RadioBtn
0
@@ -13696,7 +16221,7 @@
0
1
- m_listBox1
+ m_radioBtn10
1
@@ -13714,6 +16239,7 @@
wxFILTER_NONE
wxDefaultValidator
+ 0
From 8f676e89d5d09df9ca30b3c19a661f19d1c6bab2 Mon Sep 17 00:00:00 2001
From: gtripoli
Date: Fri, 27 Feb 2026 17:12:04 +0100
Subject: [PATCH 03/72] ui: collapse wxFormBuilder tree nodes in view editor
dialog
- Change all expanded properties from "true" to "false" in SettingsDialog
- Revert splitter sash position from 432 to -150 in Views panel
- Change Views notebook proportion from 1 to 0
- Add new pnl_view_editor_root panel with view name and schema inputs
- Collapse all tree nodes in view editor components
---
PeterSQL.fbp | 2553 +++++++++++++----------
assets/view_options_matrix.md | 119 ++
helpers/sql.py | 11 +
structures/engines/mariadb/context.py | 8 +
structures/engines/mysql/context.py | 8 +
windows/__init__.py | 2702 +------------------------
windows/main/tabs/view.py | 198 +-
windows/views.py | 169 +-
8 files changed, 1866 insertions(+), 3902 deletions(-)
create mode 100644 assets/view_options_matrix.md
create mode 100644 helpers/sql.py
diff --git a/PeterSQL.fbp b/PeterSQL.fbp
index 6efdabb..e962ffa 100755
--- a/PeterSQL.fbp
+++ b/PeterSQL.fbp
@@ -4251,7 +4251,7 @@
-
+
0
wxAUI_MGR_DEFAULT
@@ -4279,34 +4279,34 @@
-
+
bSizer111
wxVERTICAL
none
-
+
5
wxEXPAND
0
-
+
bSizer112
wxVERTICAL
none
-
+
5
wxEXPAND
1
-
+
bSizer113
wxHORIZONTAL
none
-
+
5
wxALIGN_CENTER|wxALL
0
-
+
1
1
1
@@ -4364,11 +4364,11 @@
-1
-
+
5
wxALL
0
-
+
1
1
1
@@ -4434,11 +4434,11 @@
-
+
5
wxEXPAND | wxALL
1
-
+
1
1
1
@@ -4502,30 +4502,30 @@
-
+
5
wxEXPAND
0
-
+
bSizer114
wxHORIZONTAL
none
-
+
5
wxEXPAND
1
-
+
0
protected
0
-
+
5
wxALL
0
-
+
1
1
1
@@ -4595,11 +4595,11 @@
-
+
5
wxALL
0
-
+
1
1
1
@@ -4987,7 +4987,7 @@
Resizable
1
- 432
+ -150
-1
1
@@ -5000,7 +5000,7 @@
-
+
1
1
1
@@ -5052,16 +5052,16 @@
wxTAB_TRAVERSAL
-
+
bSizer72
wxVERTICAL
none
-
+
5
wxEXPAND
1
-
+
1
1
1
@@ -5119,8 +5119,8 @@
-
-
+
+
1
1
1
@@ -5289,8 +5289,8 @@
-
-
+
+
1
1
1
@@ -5342,16 +5342,16 @@
wxTAB_TRAVERSAL
-
+
bSizer25
wxVERTICAL
none
-
+
5
wxALL|wxEXPAND
1
-
+
1
1
1
@@ -9226,11 +9226,11 @@
-
+
Load From File; icons/16x16/view.png
Views
0
-
+
1
1
1
@@ -9282,16 +9282,16 @@
wxTAB_TRAVERSAL
-
+
bSizer84
wxVERTICAL
none
-
+
5
wxEXPAND | wxALL
- 1
-
+ 0
+
1
1
1
@@ -9345,11 +9345,11 @@
-
+
Options
0
-
+
1
1
1
@@ -9401,12 +9401,12 @@
wxTAB_TRAVERSAL
-
+
bSizer85
wxVERTICAL
none
-
+
5
wxALL|wxEXPAND
0
@@ -9544,1003 +9544,1268 @@
-
+
5
wxEXPAND
0
-
+
bSizer89
wxHORIZONTAL
none
-
+
5
wxEXPAND
1
-
+
bSizer116
wxVERTICAL
none
-
- 5
- wxALL|wxEXPAND
- 0
-
-
- bSizer87211
- 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
+ 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
- bSizer872
- 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
+ 1
+ pnl_row_definer
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+ ; ; forward_declare
+ 0
+
+
+
+ wxTAB_TRAVERSAL
+
+
+ szr_view_definer
+ wxHORIZONTAL
+ none
+
+ 5
+ wxALIGN_CENTER|wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ Definer
+ 0
+
+ 0
+
+
+ 0
+ 150,-1
+ 1
+ lbl_view_definer
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+
+
+ -1
+
-
-
- 5
- wxALIGN_CENTER|wxALL
- 1
-
- 1
- 1
- 1
- 1
- 0
-
- 0
- 0
-
-
-
- 1
- 0
-
- 1
-
- 1
- 0
- Dock
- 0
- Left
- 0
- 1
-
- 1
-
- 0
- 0
- wxID_ANY
-
- 0
-
-
- 0
-
- 1
- cmb_view_definer
- 1
-
-
- protected
- 1
-
- Resizable
- -1
- 1
-
-
- ; ; forward_declare
- 0
-
-
- wxFILTER_NONE
- wxDefaultValidator
-
-
-
-
-
+
+ 5
+ wxALIGN_CENTER|wxALL
+ 1
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+
+ 0
+
+
+ 0
+
+ 1
+ cmb_view_definer
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ -1
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+
+
+
+
+
-
-
-
- 5
- wxEXPAND
- 1
-
-
- bSizer8711
- wxVERTICAL
- none
-
+
5
- wxALL|wxEXPAND
+ wxEXPAND | wxALL
0
-
-
- bSizer8721
- wxHORIZONTAL
- none
-
- 5
- wxALIGN_CENTER|wxALL
- 0
-
- 1
- 1
- 1
- 1
- 0
-
- 0
- 0
-
-
-
- 1
- 0
- 1
-
- 1
- 0
- Dock
- 0
- Left
- 0
- 1
-
- 1
-
- 0
- 0
- wxID_ANY
- SQL security
- 0
-
- 0
-
-
- 0
- 150,-1
- 1
- lbl_view_sql_security
- 1
-
-
- protected
- 1
-
- Resizable
- 1
-
-
- ; ; forward_declare
- 0
-
-
-
-
- -1
-
-
-
- 5
- wxALIGN_CENTER|wxALL
- 1
-
- 1
- 1
- 1
- 1
- 0
-
- 0
- 0
-
-
-
- 1
- 0
- "DEFINER" "INVOKER"
- 1
-
- 1
- 0
- Dock
- 0
- Left
- 0
- 1
-
- 1
-
- 0
- 0
- wxID_ANY
-
- 0
-
-
- 0
-
- 1
- cho_view_sql_security
- 1
-
-
- protected
- 1
-
- Resizable
- 0
- 1
-
-
- ; ; forward_declare
- 0
-
-
- wxFILTER_NONE
- wxDefaultValidator
-
-
-
-
-
-
-
-
-
- 5
- wxEXPAND
- 0
-
- Algorithm
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+
+ 0
+
+
+ 0
- sbSizer1
- 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_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
- 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
+ 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
- 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
+ wxEXPAND
+ 1
+
+
+ bSizer8711
+ wxVERTICAL
+ none
+
+ 5
+ wxEXPAND | wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+
+ 0
+
+
+ 0
+
+ 1
+ pnl_row_sql_security
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+ ; ; forward_declare
+ 0
+
+
+
+ wxTAB_TRAVERSAL
+
+
+ szr_view_sql_security
+ wxHORIZONTAL
+ none
+
+ 5
+ wxALIGN_CENTER|wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ SQL security
+ 0
+
+ 0
+
+
+ 0
+ 150,-1
+ 1
+ lbl_view_sql_security
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+
+
+ -1
+
+
+
+ 5
+ wxALIGN_CENTER|wxALL
+ 1
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ "DEFINER" "INVOKER"
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+
+ 0
+
+
+ 0
+
+ 1
+ cho_view_sql_security
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 0
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+
+
+
+
+
+
+
+
+
+ 5
+ wxALL|wxEXPAND
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+
+ 0
+
+
+ 0
+
+ 1
+ pnl_row_algorithm
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+ ; ; forward_declare
+ 0
+
+
+
+ wxTAB_TRAVERSAL
+
+ Algorithm
+
+ szr_view_algorithm
+ wxVERTICAL
+ 1
+ none
+
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ UNDEFINED
+
+ 0
+
+
+ 0
+
+ 1
+ rad_view_algorithm_undefined
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+ wxRB_GROUP
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
+
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ MERGE
+
+ 0
+
+
+ 0
+
+ 1
+ rad_view_algorithm_merge
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
+
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ TEMPTABLE
+
+ 0
+
+
+ 0
+
+ 1
+ rad_view_algorithm_temptable
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
+
+
+
+
+
+ 5
+ wxALL|wxEXPAND
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+
+ 0
+
+
+ 0
+
+ 1
+ pnl_row_constraint
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+ ; ; forward_declare
+ 0
+
+
+
+ wxTAB_TRAVERSAL
+
+ View constraint
+
+ szr_view_constraint
+ wxVERTICAL
+ 1
+ none
+
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ None
+
+ 0
+
+
+ 0
+
+ 1
+ rad_view_constraint_none
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+ wxRB_GROUP
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
+
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ LOCAL
+
+ 0
+
+
+ 0
+
+ 1
+ rad_view_constraint_local
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
+
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ CASCADE
+
+ 0
+
+
+ 0
+
+ 1
+ rad_view_constraint_cascaded
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
+
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ CHECK ONLY
+
+ 0
+
+
+ 0
+
+ 1
+ rad_view_constraint_check_only
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
+
+
+ 5
+ wxALL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+ 0
+
+ 0
+ 0
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 0
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ READ ONLY
+
+ 0
+
+
+ 0
+
+ 1
+ rad_view_constraint_read_only
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+ 0
+
+
+
+
-
+
5
wxEXPAND
0
-
- View constraint
-
- sbSizer11
- 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
- 0
-
+
1
1
1
@@ -10554,7 +10819,6 @@
1
0
- 0
1
1
@@ -10570,7 +10834,6 @@
0
0
wxID_ANY
- Security barrier
0
@@ -10578,7 +10841,7 @@
0
1
- chk_view_security_barrier
+ pnl_row_security_barrier
1
@@ -10588,24 +10851,90 @@
Resizable
1
-
; ; forward_declare
0
-
- wxFILTER_NONE
- wxDefaultValidator
-
-
+ 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
- wxALL
+ wxEXPAND
0
-
+
1
1
1
@@ -10619,7 +10948,6 @@
1
0
- 0
1
1
@@ -10635,7 +10963,6 @@
0
0
wxID_ANY
- Force
0
@@ -10643,7 +10970,7 @@
0
1
- chk_view_force
+ pnl_row_force
1
@@ -10653,17 +10980,83 @@
Resizable
1
-
; ; forward_declare
0
-
- wxFILTER_NONE
- wxDefaultValidator
-
-
+ 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
+
+
+
+
+
+
+
@@ -10717,7 +11110,7 @@
-1,-1
0
-
+ -1,200
1
stc_view_select
1
@@ -10729,7 +11122,7 @@
0
Resizable
1
- -1,200
+ -1,-1
; ; forward_declare
1
4
@@ -12042,11 +12435,11 @@
-
+
Load From File; icons/16x16/arrow_right.png
Query
0
-
+
1
1
1
@@ -12098,7 +12491,7 @@
wxTAB_TRAVERSAL
-
+
bSizer26
wxVERTICAL
@@ -12242,11 +12635,11 @@
-
+
5
wxEXPAND | wxALL
1
-
+
1
1
1
@@ -12310,11 +12703,11 @@
-
+
5
wxEXPAND | wxALL
1
-
+
1
1
1
@@ -12373,11 +12766,11 @@
-
+
Query #2
0
-
+
1
1
1
@@ -12429,7 +12822,7 @@
wxTAB_TRAVERSAL
-
+
bSizer263
wxVERTICAL
@@ -12699,8 +13092,8 @@
-
-
+
+
1
1
1
@@ -12736,7 +13129,7 @@
0
1
- LogSQLPanel
+ panel_sql_log
1
@@ -13321,17 +13714,6 @@
-
- 5
- wxEXPAND
- 0
-
-
- bSizer86
- wxHORIZONTAL
- none
-
-
5
wxALL
@@ -16245,6 +16627,75 @@
+
+ 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
+
+
+
+