sqlide 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. sqlide-0.1.0/.github/workflows/ci.yml +37 -0
  2. sqlide-0.1.0/.github/workflows/release.yml +17 -0
  3. sqlide-0.1.0/.gitignore +7 -0
  4. sqlide-0.1.0/.python-version +1 -0
  5. sqlide-0.1.0/LICENSE +21 -0
  6. sqlide-0.1.0/PKG-INFO +151 -0
  7. sqlide-0.1.0/README.md +123 -0
  8. sqlide-0.1.0/packaging/homebrew/README.md +11 -0
  9. sqlide-0.1.0/packaging/homebrew/sqlide.rb +36 -0
  10. sqlide-0.1.0/pyproject.toml +69 -0
  11. sqlide-0.1.0/src/sqlide/__init__.py +3 -0
  12. sqlide-0.1.0/src/sqlide/__main__.py +5 -0
  13. sqlide-0.1.0/src/sqlide/app.py +46 -0
  14. sqlide-0.1.0/src/sqlide/cli.py +64 -0
  15. sqlide-0.1.0/src/sqlide/clipboard.py +65 -0
  16. sqlide-0.1.0/src/sqlide/config/__init__.py +1 -0
  17. sqlide-0.1.0/src/sqlide/config/_toml.py +41 -0
  18. sqlide-0.1.0/src/sqlide/config/connections.py +76 -0
  19. sqlide-0.1.0/src/sqlide/config/keymap.py +25 -0
  20. sqlide-0.1.0/src/sqlide/config/paths.py +48 -0
  21. sqlide-0.1.0/src/sqlide/config/secrets.py +82 -0
  22. sqlide-0.1.0/src/sqlide/config/settings.py +32 -0
  23. sqlide-0.1.0/src/sqlide/consoles.py +100 -0
  24. sqlide-0.1.0/src/sqlide/db/__init__.py +1 -0
  25. sqlide-0.1.0/src/sqlide/db/completion.py +127 -0
  26. sqlide-0.1.0/src/sqlide/db/factory.py +37 -0
  27. sqlide-0.1.0/src/sqlide/db/metadata.py +174 -0
  28. sqlide-0.1.0/src/sqlide/db/result.py +52 -0
  29. sqlide-0.1.0/src/sqlide/db/session.py +339 -0
  30. sqlide-0.1.0/src/sqlide/db/types.py +84 -0
  31. sqlide-0.1.0/src/sqlide/doctor.py +60 -0
  32. sqlide-0.1.0/src/sqlide/drivers/__init__.py +1 -0
  33. sqlide-0.1.0/src/sqlide/drivers/catalog.toml +103 -0
  34. sqlide-0.1.0/src/sqlide/drivers/cli.py +74 -0
  35. sqlide-0.1.0/src/sqlide/drivers/custom.py +69 -0
  36. sqlide-0.1.0/src/sqlide/drivers/loader.py +75 -0
  37. sqlide-0.1.0/src/sqlide/drivers/maven.py +108 -0
  38. sqlide-0.1.0/src/sqlide/drivers/registry.py +117 -0
  39. sqlide-0.1.0/src/sqlide/export/__init__.py +12 -0
  40. sqlide-0.1.0/src/sqlide/export/base.py +86 -0
  41. sqlide-0.1.0/src/sqlide/export/csv_.py +40 -0
  42. sqlide-0.1.0/src/sqlide/export/html.py +44 -0
  43. sqlide-0.1.0/src/sqlide/export/json_.py +47 -0
  44. sqlide-0.1.0/src/sqlide/export/markdown.py +27 -0
  45. sqlide-0.1.0/src/sqlide/export/service.py +34 -0
  46. sqlide-0.1.0/src/sqlide/export/sql_insert.py +37 -0
  47. sqlide-0.1.0/src/sqlide/export/xlsx.py +109 -0
  48. sqlide-0.1.0/src/sqlide/grid/__init__.py +1 -0
  49. sqlide-0.1.0/src/sqlide/grid/copyfmt.py +97 -0
  50. sqlide-0.1.0/src/sqlide/grid/formatting.py +43 -0
  51. sqlide-0.1.0/src/sqlide/grid/model.py +109 -0
  52. sqlide-0.1.0/src/sqlide/history/__init__.py +5 -0
  53. sqlide-0.1.0/src/sqlide/history/store.py +117 -0
  54. sqlide-0.1.0/src/sqlide/jvm/__init__.py +1 -0
  55. sqlide-0.1.0/src/sqlide/jvm/locate.py +98 -0
  56. sqlide-0.1.0/src/sqlide/jvm/runtime.py +44 -0
  57. sqlide-0.1.0/src/sqlide/sql/__init__.py +1 -0
  58. sqlide-0.1.0/src/sqlide/sql/context.py +195 -0
  59. sqlide-0.1.0/src/sqlide/sql/dialects.py +93 -0
  60. sqlide-0.1.0/src/sqlide/sql/format.py +50 -0
  61. sqlide-0.1.0/src/sqlide/sql/keywords.py +143 -0
  62. sqlide-0.1.0/src/sqlide/sql/lexer.py +148 -0
  63. sqlide-0.1.0/src/sqlide/sql/snippets.py +44 -0
  64. sqlide-0.1.0/src/sqlide/sql/splitter.py +325 -0
  65. sqlide-0.1.0/src/sqlide/ui/__init__.py +1 -0
  66. sqlide-0.1.0/src/sqlide/ui/app.tcss +67 -0
  67. sqlide-0.1.0/src/sqlide/ui/commands.py +59 -0
  68. sqlide-0.1.0/src/sqlide/ui/keymap.py +32 -0
  69. sqlide-0.1.0/src/sqlide/ui/screens/__init__.py +0 -0
  70. sqlide-0.1.0/src/sqlide/ui/screens/connection_editor.py +113 -0
  71. sqlide-0.1.0/src/sqlide/ui/screens/dialogs.py +110 -0
  72. sqlide-0.1.0/src/sqlide/ui/screens/driver_manager.py +195 -0
  73. sqlide-0.1.0/src/sqlide/ui/screens/export_dialog.py +158 -0
  74. sqlide-0.1.0/src/sqlide/ui/screens/grid_dialogs.py +92 -0
  75. sqlide-0.1.0/src/sqlide/ui/screens/history.py +130 -0
  76. sqlide-0.1.0/src/sqlide/ui/screens/main.py +383 -0
  77. sqlide-0.1.0/src/sqlide/ui/screens/settings.py +77 -0
  78. sqlide-0.1.0/src/sqlide/ui/widgets/__init__.py +0 -0
  79. sqlide-0.1.0/src/sqlide/ui/widgets/completion_popup.py +81 -0
  80. sqlide-0.1.0/src/sqlide/ui/widgets/connections_list.py +54 -0
  81. sqlide-0.1.0/src/sqlide/ui/widgets/console_export.py +124 -0
  82. sqlide-0.1.0/src/sqlide/ui/widgets/console_tab.py +335 -0
  83. sqlide-0.1.0/src/sqlide/ui/widgets/console_tabs.py +117 -0
  84. sqlide-0.1.0/src/sqlide/ui/widgets/result_grid.py +435 -0
  85. sqlide-0.1.0/src/sqlide/ui/widgets/result_panel.py +73 -0
  86. sqlide-0.1.0/src/sqlide/ui/widgets/result_view.py +50 -0
  87. sqlide-0.1.0/src/sqlide/ui/widgets/schema_tree.py +145 -0
  88. sqlide-0.1.0/src/sqlide/ui/widgets/sql_editor.py +310 -0
  89. sqlide-0.1.0/src/sqlide/ui/widgets/status_bar.py +32 -0
  90. sqlide-0.1.0/src/sqlide/workspace.py +97 -0
  91. sqlide-0.1.0/tests/__init__.py +0 -0
  92. sqlide-0.1.0/tests/conftest.py +35 -0
  93. sqlide-0.1.0/tests/db/test_completion.py +68 -0
  94. sqlide-0.1.0/tests/db/test_embedded_dbs.py +104 -0
  95. sqlide-0.1.0/tests/db/test_h2_driver.py +14 -0
  96. sqlide-0.1.0/tests/db/test_metadata.py +28 -0
  97. sqlide-0.1.0/tests/db/test_session.py +94 -0
  98. sqlide-0.1.0/tests/db/test_stream_export.py +63 -0
  99. sqlide-0.1.0/tests/docker/conftest.py +107 -0
  100. sqlide-0.1.0/tests/docker/multi.py +91 -0
  101. sqlide-0.1.0/tests/docker/test_databases.py +166 -0
  102. sqlide-0.1.0/tests/docker/test_postgres.py +57 -0
  103. sqlide-0.1.0/tests/docker/test_sql_battery.py +172 -0
  104. sqlide-0.1.0/tests/ui/__init__.py +0 -0
  105. sqlide-0.1.0/tests/ui/conftest.py +20 -0
  106. sqlide-0.1.0/tests/ui/helpers.py +37 -0
  107. sqlide-0.1.0/tests/ui/test_app_e2e.py +170 -0
  108. sqlide-0.1.0/tests/ui/test_completion.py +98 -0
  109. sqlide-0.1.0/tests/ui/test_export_ui.py +156 -0
  110. sqlide-0.1.0/tests/ui/test_grid.py +328 -0
  111. sqlide-0.1.0/tests/ui/test_history.py +95 -0
  112. sqlide-0.1.0/tests/ui/test_polish.py +94 -0
  113. sqlide-0.1.0/tests/ui/test_schema.py +76 -0
  114. sqlide-0.1.0/tests/ui/test_tabs.py +356 -0
  115. sqlide-0.1.0/tests/unit/config/test_config.py +125 -0
  116. sqlide-0.1.0/tests/unit/drivers/test_custom.py +53 -0
  117. sqlide-0.1.0/tests/unit/drivers/test_maven.py +78 -0
  118. sqlide-0.1.0/tests/unit/drivers/test_registry_loader.py +81 -0
  119. sqlide-0.1.0/tests/unit/export/test_exporters.py +173 -0
  120. sqlide-0.1.0/tests/unit/grid/test_copyfmt.py +57 -0
  121. sqlide-0.1.0/tests/unit/grid/test_model.py +100 -0
  122. sqlide-0.1.0/tests/unit/jvm/test_locate.py +45 -0
  123. sqlide-0.1.0/tests/unit/sql/test_lexer.py +57 -0
  124. sqlide-0.1.0/tests/unit/sql/test_splitter.py +269 -0
  125. sqlide-0.1.0/tests/unit/test_cli.py +21 -0
  126. sqlide-0.1.0/tests/unit/test_clipboard.py +68 -0
  127. sqlide-0.1.0/tests/unit/test_consoles.py +65 -0
  128. sqlide-0.1.0/tests/unit/test_context.py +81 -0
  129. sqlide-0.1.0/tests/unit/test_format.py +39 -0
  130. sqlide-0.1.0/tests/unit/test_history.py +53 -0
  131. sqlide-0.1.0/tests/unit/test_keymap.py +23 -0
  132. sqlide-0.1.0/tests/unit/test_private_db.py +21 -0
  133. sqlide-0.1.0/tests/unit/test_snippets.py +54 -0
  134. sqlide-0.1.0/tests/unit/test_version.py +9 -0
  135. sqlide-0.1.0/uv.lock +1605 -0
@@ -0,0 +1,37 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, macos-latest]
14
+ runs-on: ${{ matrix.os }}
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-java@v4
18
+ with:
19
+ distribution: temurin
20
+ java-version: "21"
21
+ - uses: astral-sh/setup-uv@v5
22
+ with:
23
+ enable-cache: true
24
+ - run: uv sync --locked
25
+ - run: uv run ruff check
26
+ - run: uv run ruff format --check
27
+ - run: uv run pyright
28
+ - name: Tests
29
+ shell: bash
30
+ run: uv run pytest -q --tb=short -p no:cacheprovider 2>&1 | tee pytest.log; exit "${PIPESTATUS[0]}"
31
+ - name: Show failures as annotations
32
+ if: failure()
33
+ shell: bash
34
+ run: |
35
+ grep -E "^(FAILED|ERROR)|Error|assert " pytest.log | head -30 | while IFS= read -r line; do
36
+ echo "::error::${line:0:400}"
37
+ done
@@ -0,0 +1,17 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ pypi:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write # PyPI trusted publishing, no token stored
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v5
16
+ - run: uv build
17
+ - run: uv publish
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ tests/.cache/
@@ -0,0 +1 @@
1
+ 3.12
sqlide-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 XFaIT
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
sqlide-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.5
2
+ Name: sqlide
3
+ Version: 0.1.0
4
+ Summary: Terminal SQL IDE (DataGrip-like) over JDBC
5
+ Project-URL: Homepage, https://github.com/XFaIT/sqlide
6
+ Project-URL: Repository, https://github.com/XFaIT/sqlide
7
+ Project-URL: Issues, https://github.com/XFaIT/sqlide/issues
8
+ Author: XFaIT
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: database,datagrip,jdbc,sql,terminal,tui
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Database :: Front-Ends
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: jpype1>=1.5
22
+ Requires-Dist: openpyxl>=3.1
23
+ Requires-Dist: platformdirs>=4
24
+ Requires-Dist: sqlglot>=25
25
+ Requires-Dist: textual[syntax]>=0.80
26
+ Requires-Dist: tomli-w>=1.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # sqlide
30
+
31
+ A DataGrip-style SQL IDE that runs in your terminal. Connect to any database over JDBC,
32
+ write SQL in an editor that outlines the statement about to run, and browse results in
33
+ sortable, copyable tables that export to CSV, XLSX and more.
34
+
35
+ Works on Linux, macOS and WSL.
36
+
37
+ ## Features
38
+
39
+ - **Any JDBC database.** PostgreSQL and ClickHouse work out of the box; MySQL, MariaDB,
40
+ Oracle, SQL Server, H2, SQLite and DuckDB are one keypress away. Add your own driver from
41
+ jar files or Maven coordinates, as in DataGrip. Drivers are downloaded from Maven Central
42
+ (checksum verified) and each one gets its own class loader.
43
+ - **Framed statement.** The statement under the cursor is outlined in the gutter; `F5`
44
+ runs it, a selection runs the selection, `Shift+F5` runs the whole file. Dialect-aware
45
+ splitting: `$$` quotes, nested comments, Oracle `/`, SQL Server `GO`, procedural blocks.
46
+ - **Result grid.** Sort by several columns, filter, select a rectangle, view long values,
47
+ load more rows on demand. Copy as TSV, CSV, Markdown, JSON or `INSERT` statements.
48
+ - **Export.** CSV, TSV, JSON, JSON Lines, Markdown, SQL inserts, HTML, XLSX. Export the loaded
49
+ rows or re-run the query and stream the full result to disk, with progress and cancel.
50
+ - **Schema tree and autocomplete.** Lazy metadata, `SELECT *` on Enter, alias-aware column
51
+ completion, keywords and functions per dialect.
52
+ - **Consoles and files.** Tabs with their own connection; consoles autosave and come back
53
+ after a restart; open and save `.sql` files (`sqlide a.sql b.sql`).
54
+ - **History, formatting, transactions.** Searchable query history, SQL formatting,
55
+ Auto/Manual commit with Commit/Rollback.
56
+ - **Rebindable keys, themes, command palette** (`Ctrl+P`). No AI features.
57
+
58
+ ## Install
59
+
60
+ Requirements: Python 3.12+ and a Java 11+ runtime (`brew install openjdk`,
61
+ `apt install openjdk-21-jre`, ...). Check with `sqlide doctor`.
62
+
63
+ ```sh
64
+ uv tool install sqlide # or: pipx install sqlide
65
+ sqlide
66
+ ```
67
+
68
+ Homebrew formula: see [packaging/homebrew](packaging/homebrew/README.md).
69
+
70
+ ## Quick start
71
+
72
+ ```sh
73
+ sqlide driver install postgres # or just connect: sqlide offers to download it
74
+ sqlide # Ctrl+N: new connection, Enter on it: connect
75
+ ```
76
+
77
+ Passwords are never written to disk. sqlide asks when you connect and keeps the password
78
+ in memory until you quit. A connection can instead read it from an environment variable
79
+ (`password_ref = "${env:PGPASSWORD}"`) or from a command such as a keychain CLI
80
+ (`password_cmd = "security find-generic-password -s mydb -w"`).
81
+
82
+ ## Keys
83
+
84
+ `sqlide keys` prints every rebindable action with its default key. Override in
85
+ `~/.config/sqlide/keymap.toml`:
86
+
87
+ ```toml
88
+ [keys]
89
+ "editor.run" = "f4"
90
+ ```
91
+
92
+ | Action | Keys |
93
+ |---|---|
94
+ | Run statement / selection | `F5`, `Ctrl+J`, `Ctrl+Enter`\* |
95
+ | Run all | `Shift+F5` |
96
+ | Cancel query | `Ctrl+F2` |
97
+ | Autocomplete | `Ctrl+Space` (opens by itself after a dot) |
98
+ | Format / comment | `F7` or `Ctrl+Alt+L` / `Ctrl+/` or `Alt+/` |
99
+ | New connection / console / close tab | `Ctrl+N` / `Ctrl+T` / `Alt+W` |
100
+ | Open / save file | `Ctrl+O` / `Ctrl+S` |
101
+ | History | `Alt+E` |
102
+ | Switch tab | `Alt+←` `Alt+→` |
103
+ | Focus connections / schema / editor / results | `Alt+C` / `Alt+D` / `Alt+Q` / `Alt+R` (or `F6` / `Shift+F6` to cycle) |
104
+ | Tx: toggle / commit / rollback | `F8` / `F9` / `F10` |
105
+ | Grid: sort / copy / copy as / filter / export | `s` / `Ctrl+C` / `y` / `/` / `e` |
106
+ | Command palette | `Ctrl+P` |
107
+ | Quit | `Ctrl+Q` |
108
+
109
+ \* Most terminals send `Ctrl+Enter` as plain `Enter`; it only works where the terminal
110
+ speaks the kitty keyboard protocol. `Alt+Enter` is not used on purpose: it toggles
111
+ full screen in Windows Terminal.
112
+
113
+ ## Files
114
+
115
+ | What | Where (Linux and WSL; macOS uses the platform equivalents) |
116
+ |---|---|
117
+ | Settings, connections, custom drivers, keymap | `~/.config/sqlide/` |
118
+ | Downloaded drivers, history, consoles | `~/.local/share/sqlide/` |
119
+
120
+ Override with `SQLIDE_CONFIG_DIR` and `SQLIDE_DATA_DIR`.
121
+
122
+ ## Notes
123
+
124
+ - Schema reads (tree, autocomplete) use a second connection, so they work while a query runs.
125
+ For in-memory databases (H2 `mem:`, SQLite `:memory:`, DuckDB) a second connection would see
126
+ a different database, so the query connection is shared and reads wait for a running query.
127
+ - Autocomplete reads cached metadata. DDL you run in the editor refreshes it; changes made by
128
+ other clients need `F5` in the schema tree. In manual-commit mode new objects appear after
129
+ Commit.
130
+ - MySQL `DELIMITER x` lines are understood (they are not sent to the server). On SQL Server,
131
+ `CREATE PROCEDURE/FUNCTION/TRIGGER` without `BEGIN..END` runs up to the next `GO`, as T-SQL does.
132
+ - `Alt+1`..`Alt+4` only work in terminals with the kitty keyboard protocol; most terminals
133
+ deliver them as Mac characters, so use the letter keys or `F6`.
134
+ - Tested on Linux and WSL against PostgreSQL, MySQL, MariaDB, ClickHouse, SQL Server, Oracle and
135
+ H2. macOS is covered by CI. On Windows, run it inside WSL (native Windows is not supported).
136
+
137
+ ## Development
138
+
139
+ ```sh
140
+ uv sync
141
+ uv run pytest # unit, H2 integration, Textual pilot tests
142
+ uv run pytest -m docker # also PostgreSQL in Docker
143
+ uv run ruff check && uv run pyright
144
+ ```
145
+
146
+ The code is split into blocks that depend only downwards: `ui → db, sql, grid, export,
147
+ history, config → jvm, drivers`. The core never imports the UI.
148
+
149
+ ## License
150
+
151
+ MIT
sqlide-0.1.0/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # sqlide
2
+
3
+ A DataGrip-style SQL IDE that runs in your terminal. Connect to any database over JDBC,
4
+ write SQL in an editor that outlines the statement about to run, and browse results in
5
+ sortable, copyable tables that export to CSV, XLSX and more.
6
+
7
+ Works on Linux, macOS and WSL.
8
+
9
+ ## Features
10
+
11
+ - **Any JDBC database.** PostgreSQL and ClickHouse work out of the box; MySQL, MariaDB,
12
+ Oracle, SQL Server, H2, SQLite and DuckDB are one keypress away. Add your own driver from
13
+ jar files or Maven coordinates, as in DataGrip. Drivers are downloaded from Maven Central
14
+ (checksum verified) and each one gets its own class loader.
15
+ - **Framed statement.** The statement under the cursor is outlined in the gutter; `F5`
16
+ runs it, a selection runs the selection, `Shift+F5` runs the whole file. Dialect-aware
17
+ splitting: `$$` quotes, nested comments, Oracle `/`, SQL Server `GO`, procedural blocks.
18
+ - **Result grid.** Sort by several columns, filter, select a rectangle, view long values,
19
+ load more rows on demand. Copy as TSV, CSV, Markdown, JSON or `INSERT` statements.
20
+ - **Export.** CSV, TSV, JSON, JSON Lines, Markdown, SQL inserts, HTML, XLSX. Export the loaded
21
+ rows or re-run the query and stream the full result to disk, with progress and cancel.
22
+ - **Schema tree and autocomplete.** Lazy metadata, `SELECT *` on Enter, alias-aware column
23
+ completion, keywords and functions per dialect.
24
+ - **Consoles and files.** Tabs with their own connection; consoles autosave and come back
25
+ after a restart; open and save `.sql` files (`sqlide a.sql b.sql`).
26
+ - **History, formatting, transactions.** Searchable query history, SQL formatting,
27
+ Auto/Manual commit with Commit/Rollback.
28
+ - **Rebindable keys, themes, command palette** (`Ctrl+P`). No AI features.
29
+
30
+ ## Install
31
+
32
+ Requirements: Python 3.12+ and a Java 11+ runtime (`brew install openjdk`,
33
+ `apt install openjdk-21-jre`, ...). Check with `sqlide doctor`.
34
+
35
+ ```sh
36
+ uv tool install sqlide # or: pipx install sqlide
37
+ sqlide
38
+ ```
39
+
40
+ Homebrew formula: see [packaging/homebrew](packaging/homebrew/README.md).
41
+
42
+ ## Quick start
43
+
44
+ ```sh
45
+ sqlide driver install postgres # or just connect: sqlide offers to download it
46
+ sqlide # Ctrl+N: new connection, Enter on it: connect
47
+ ```
48
+
49
+ Passwords are never written to disk. sqlide asks when you connect and keeps the password
50
+ in memory until you quit. A connection can instead read it from an environment variable
51
+ (`password_ref = "${env:PGPASSWORD}"`) or from a command such as a keychain CLI
52
+ (`password_cmd = "security find-generic-password -s mydb -w"`).
53
+
54
+ ## Keys
55
+
56
+ `sqlide keys` prints every rebindable action with its default key. Override in
57
+ `~/.config/sqlide/keymap.toml`:
58
+
59
+ ```toml
60
+ [keys]
61
+ "editor.run" = "f4"
62
+ ```
63
+
64
+ | Action | Keys |
65
+ |---|---|
66
+ | Run statement / selection | `F5`, `Ctrl+J`, `Ctrl+Enter`\* |
67
+ | Run all | `Shift+F5` |
68
+ | Cancel query | `Ctrl+F2` |
69
+ | Autocomplete | `Ctrl+Space` (opens by itself after a dot) |
70
+ | Format / comment | `F7` or `Ctrl+Alt+L` / `Ctrl+/` or `Alt+/` |
71
+ | New connection / console / close tab | `Ctrl+N` / `Ctrl+T` / `Alt+W` |
72
+ | Open / save file | `Ctrl+O` / `Ctrl+S` |
73
+ | History | `Alt+E` |
74
+ | Switch tab | `Alt+←` `Alt+→` |
75
+ | Focus connections / schema / editor / results | `Alt+C` / `Alt+D` / `Alt+Q` / `Alt+R` (or `F6` / `Shift+F6` to cycle) |
76
+ | Tx: toggle / commit / rollback | `F8` / `F9` / `F10` |
77
+ | Grid: sort / copy / copy as / filter / export | `s` / `Ctrl+C` / `y` / `/` / `e` |
78
+ | Command palette | `Ctrl+P` |
79
+ | Quit | `Ctrl+Q` |
80
+
81
+ \* Most terminals send `Ctrl+Enter` as plain `Enter`; it only works where the terminal
82
+ speaks the kitty keyboard protocol. `Alt+Enter` is not used on purpose: it toggles
83
+ full screen in Windows Terminal.
84
+
85
+ ## Files
86
+
87
+ | What | Where (Linux and WSL; macOS uses the platform equivalents) |
88
+ |---|---|
89
+ | Settings, connections, custom drivers, keymap | `~/.config/sqlide/` |
90
+ | Downloaded drivers, history, consoles | `~/.local/share/sqlide/` |
91
+
92
+ Override with `SQLIDE_CONFIG_DIR` and `SQLIDE_DATA_DIR`.
93
+
94
+ ## Notes
95
+
96
+ - Schema reads (tree, autocomplete) use a second connection, so they work while a query runs.
97
+ For in-memory databases (H2 `mem:`, SQLite `:memory:`, DuckDB) a second connection would see
98
+ a different database, so the query connection is shared and reads wait for a running query.
99
+ - Autocomplete reads cached metadata. DDL you run in the editor refreshes it; changes made by
100
+ other clients need `F5` in the schema tree. In manual-commit mode new objects appear after
101
+ Commit.
102
+ - MySQL `DELIMITER x` lines are understood (they are not sent to the server). On SQL Server,
103
+ `CREATE PROCEDURE/FUNCTION/TRIGGER` without `BEGIN..END` runs up to the next `GO`, as T-SQL does.
104
+ - `Alt+1`..`Alt+4` only work in terminals with the kitty keyboard protocol; most terminals
105
+ deliver them as Mac characters, so use the letter keys or `F6`.
106
+ - Tested on Linux and WSL against PostgreSQL, MySQL, MariaDB, ClickHouse, SQL Server, Oracle and
107
+ H2. macOS is covered by CI. On Windows, run it inside WSL (native Windows is not supported).
108
+
109
+ ## Development
110
+
111
+ ```sh
112
+ uv sync
113
+ uv run pytest # unit, H2 integration, Textual pilot tests
114
+ uv run pytest -m docker # also PostgreSQL in Docker
115
+ uv run ruff check && uv run pyright
116
+ ```
117
+
118
+ The code is split into blocks that depend only downwards: `ui → db, sql, grid, export,
119
+ history, config → jvm, drivers`. The core never imports the UI.
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,11 @@
1
+ # Homebrew
2
+
3
+ `sqlide.rb` is a template for a tap (`brew tap XFaIT/sqlide`).
4
+
5
+ 1. Release to PyPI (push a `v*` tag; `.github/workflows/release.yml` publishes).
6
+ 2. Create the repository `homebrew-sqlide` and put the formula in `Formula/sqlide.rb`.
7
+ 3. Set `url` and `sha256` from the PyPI sdist, then run
8
+ `brew update-python-resources sqlide` to add the dependency resources.
9
+ 4. `brew install --build-from-source XFaIT/sqlide/sqlide && brew test sqlide`.
10
+
11
+ Users then run `brew install XFaIT/sqlide/sqlide`.
@@ -0,0 +1,36 @@
1
+ # Homebrew formula template. Copy to your tap (homebrew-sqlide/Formula/sqlide.rb) after the
2
+ # first PyPI release, then fill url/sha256 and the resource blocks:
3
+ #
4
+ # brew update-python-resources sqlide
5
+ #
6
+ class Sqlide < Formula
7
+ include Language::Python::Virtualenv
8
+
9
+ desc "Terminal SQL IDE (DataGrip-like) over JDBC"
10
+ homepage "https://github.com/XFaIT/sqlide"
11
+ url "https://files.pythonhosted.org/packages/source/s/sqlide/sqlide-0.1.0.tar.gz"
12
+ sha256 "REPLACE_WITH_SDIST_SHA256"
13
+ license "MIT"
14
+
15
+ depends_on "openjdk"
16
+ depends_on "python@3.12"
17
+
18
+ # resource blocks (textual, jpype1, openpyxl, httpx, sqlglot, ...) are generated by
19
+ # `brew update-python-resources`
20
+
21
+ def install
22
+ virtualenv_install_with_resources
23
+ # keg-only openjdk is not on PATH: point sqlide at it
24
+ java_home = if OS.mac?
25
+ Formula["openjdk"].opt_libexec/"openjdk.jdk/Contents/Home"
26
+ else
27
+ Formula["openjdk"].opt_prefix
28
+ end
29
+ rm bin/"sqlide"
30
+ (bin/"sqlide").write_env_script libexec/"bin/sqlide", JAVA_HOME: java_home
31
+ end
32
+
33
+ test do
34
+ assert_match "sqlide", shell_output("#{bin}/sqlide --version")
35
+ end
36
+ end
@@ -0,0 +1,69 @@
1
+ [project]
2
+ name = "sqlide"
3
+ version = "0.1.0"
4
+ description = "Terminal SQL IDE (DataGrip-like) over JDBC"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ authors = [{ name = "XFaIT" }]
8
+ keywords = ["sql", "jdbc", "tui", "terminal", "database", "datagrip"]
9
+ classifiers = [
10
+ "Environment :: Console",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: POSIX :: Linux",
14
+ "Operating System :: MacOS",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Topic :: Database :: Front-Ends",
17
+ ]
18
+ requires-python = ">=3.12"
19
+ dependencies = [
20
+ "textual[syntax]>=0.80",
21
+ "jpype1>=1.5",
22
+ "openpyxl>=3.1",
23
+ "platformdirs>=4",
24
+ "tomli-w>=1.0",
25
+ "httpx>=0.27",
26
+ "sqlglot>=25",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/XFaIT/sqlide"
31
+ Repository = "https://github.com/XFaIT/sqlide"
32
+ Issues = "https://github.com/XFaIT/sqlide/issues"
33
+
34
+ [project.scripts]
35
+ sqlide = "sqlide.cli:main"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "pytest>=8",
40
+ "pytest-asyncio>=0.23",
41
+ "pytest-textual-snapshot>=1",
42
+ "ruff>=0.6",
43
+ "pyright>=1.1.380",
44
+ "textual-dev>=1.8.0",
45
+ ]
46
+
47
+ [build-system]
48
+ requires = ["hatchling"]
49
+ build-backend = "hatchling.build"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/sqlide"]
53
+
54
+ [tool.ruff]
55
+ line-length = 100
56
+ src = ["src", "tests"]
57
+
58
+ [tool.ruff.lint]
59
+ select = ["E", "F", "I", "UP", "B", "SIM"]
60
+
61
+ [tool.pytest.ini_options]
62
+ testpaths = ["tests"]
63
+ addopts = "-m 'not docker'"
64
+ asyncio_mode = "auto"
65
+ markers = ["docker: needs Docker (postgres, clickhouse)"]
66
+
67
+ [tool.pyright]
68
+ include = ["src"]
69
+ pythonVersion = "3.12"
@@ -0,0 +1,3 @@
1
+ """sqlide: terminal SQL IDE over JDBC."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from sqlide.cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,46 @@
1
+ """Textual application: only wires the workspace to the main screen."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from textual.app import App
8
+ from textual.binding import Binding
9
+
10
+ from sqlide.config.keymap import load_keymap
11
+ from sqlide.config.settings import save_settings
12
+ from sqlide.ui.commands import commands
13
+ from sqlide.ui.screens.main import MainScreen
14
+ from sqlide.workspace import Workspace
15
+
16
+
17
+ class SqlideApp(App):
18
+ CSS_PATH = str(Path(__file__).parent / "ui" / "app.tcss")
19
+ BINDINGS = [Binding("ctrl+q", "quit", "Quit", priority=True, id="app.quit")]
20
+
21
+ def __init__(self, workspace: Workspace | None = None, files: list[Path] | None = None) -> None:
22
+ super().__init__()
23
+ self.workspace = workspace or Workspace()
24
+ self._files = files or []
25
+
26
+ def on_mount(self) -> None:
27
+ self.set_keymap(load_keymap()) # user overrides from keymap.toml
28
+ if self.workspace.settings.theme in self.available_themes:
29
+ self.theme = self.workspace.settings.theme
30
+ self.theme_changed_signal.subscribe(self, self._remember_theme)
31
+ self.main = MainScreen(self.workspace, self._files)
32
+ self.push_screen(self.main)
33
+
34
+ def _remember_theme(self, theme) -> None:
35
+ settings = self.workspace.settings
36
+ if theme.name != settings.theme:
37
+ settings.theme = theme.name
38
+ save_settings(settings)
39
+
40
+ def get_system_commands(self, screen):
41
+ yield from super().get_system_commands(screen)
42
+ yield from commands(self)
43
+
44
+ async def action_quit(self) -> None:
45
+ await self.main.tabs.shutdown() # save consoles, cancel queries, close connections
46
+ self.exit()
@@ -0,0 +1,64 @@
1
+ """Command line entry point. Thin: dispatch only, no logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from sqlide import __version__
10
+ from sqlide.drivers import cli as driver_cli
11
+
12
+
13
+ def build_parser() -> argparse.ArgumentParser:
14
+ p = argparse.ArgumentParser(prog="sqlide", description="Terminal SQL IDE over JDBC")
15
+ p.add_argument("--version", action="version", version=f"sqlide {__version__}")
16
+ sub = p.add_subparsers(dest="command")
17
+ d = sub.add_parser("doctor", help="check environment (JVM, drivers, config dirs)")
18
+ d.set_defaults(handler=lambda _: _doctor())
19
+ k = sub.add_parser("keys", help="list rebindable keys (override in keymap.toml)")
20
+ k.set_defaults(handler=lambda _: _keys())
21
+ driver_cli.register(sub)
22
+ return p
23
+
24
+
25
+ def _doctor() -> int:
26
+ from sqlide import doctor
27
+
28
+ return doctor.run()
29
+
30
+
31
+ def _keys() -> int:
32
+ from sqlide.config import paths
33
+ from sqlide.ui.keymap import catalogue
34
+
35
+ print(f'Override in {paths.keymap_file()} ([keys] id = "key,key")\n')
36
+ infos = catalogue()
37
+ width = max(len(i.id) for i in infos)
38
+ for i in infos:
39
+ print(f"{i.id:<{width}} {i.keys:<40} {i.description}")
40
+ return 0
41
+
42
+
43
+ COMMANDS = {"doctor", "driver", "keys"}
44
+
45
+
46
+ def main(argv: list[str] | None = None) -> int:
47
+ argv = sys.argv[1:] if argv is None else argv
48
+ if argv and argv[0] not in COMMANDS and not argv[0].startswith("-"):
49
+ return _run_tui([Path(a) for a in argv]) # `sqlide query.sql other.sql`
50
+ args = build_parser().parse_args(argv)
51
+ if getattr(args, "handler", None):
52
+ return args.handler(args)
53
+ return _run_tui([])
54
+
55
+
56
+ def _run_tui(files: list[Path]) -> int:
57
+ from sqlide.app import SqlideApp
58
+
59
+ SqlideApp(files=files).run()
60
+ return 0
61
+
62
+
63
+ if __name__ == "__main__":
64
+ sys.exit(main())
@@ -0,0 +1,65 @@
1
+ """System clipboard across macOS, Linux (X11/Wayland), WSL; OSC52 as the last resort."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ from collections.abc import Callable
9
+ from pathlib import Path
10
+
11
+ Backend = tuple[str, list[str], Callable[[str], bytes]]
12
+
13
+
14
+ def _utf8(s: str) -> bytes:
15
+ return s.encode("utf-8")
16
+
17
+
18
+ def _utf16(s: str) -> bytes: # clip.exe reads UTF-16LE when a BOM is present
19
+ return b"\xff\xfe" + s.encode("utf-16-le")
20
+
21
+
22
+ def is_wsl() -> bool:
23
+ try:
24
+ return "microsoft" in Path("/proc/version").read_text().lower()
25
+ except OSError:
26
+ return False
27
+
28
+
29
+ def backends(platform: str | None = None, wsl: bool | None = None) -> list[Backend]:
30
+ """Installed native clipboard tools in preference order."""
31
+ import sys
32
+
33
+ platform = platform or sys.platform
34
+ wsl = is_wsl() if wsl is None else wsl
35
+ candidates: list[Backend] = []
36
+ if platform == "darwin":
37
+ candidates.append(("pbcopy", ["pbcopy"], _utf8))
38
+ elif platform == "win32":
39
+ candidates.append(("clip", ["clip"], _utf16))
40
+ else:
41
+ if os.environ.get("WAYLAND_DISPLAY"):
42
+ candidates.append(("wl-copy", ["wl-copy"], _utf8))
43
+ if os.environ.get("DISPLAY"):
44
+ candidates.append(("xclip", ["xclip", "-selection", "clipboard", "-i"], _utf8))
45
+ candidates.append(("xsel", ["xsel", "--clipboard", "--input"], _utf8))
46
+ if wsl:
47
+ candidates.append(("clip.exe", ["clip.exe"], _utf16))
48
+ return [c for c in candidates if shutil.which(c[1][0])]
49
+
50
+
51
+ def copy_native(text: str) -> str | None:
52
+ """Copy via the first working tool. Returns its name, or None if none worked."""
53
+ for name, cmd, encode in backends():
54
+ try:
55
+ subprocess.run(cmd, input=encode(text), check=True, timeout=5, capture_output=True)
56
+ return name
57
+ except (OSError, subprocess.SubprocessError):
58
+ continue
59
+ return None
60
+
61
+
62
+ def available() -> str:
63
+ """Human-readable description for `sqlide doctor`."""
64
+ found = backends()
65
+ return found[0][0] if found else "none (falls back to OSC52 terminal escape)"
@@ -0,0 +1 @@
1
+ """Configuration block: paths, settings, saved connections, password resolution."""