Skip to content

Commit f8737bf

Browse files
committed
Merge remote-tracking branch 'origin/main' into pr-162
# Conflicts: # pyproject.toml
2 parents eb7c03e + b3caa5d commit f8737bf

67 files changed

Lines changed: 3846 additions & 121 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
---
2626

2727
### Connect
28-
Supports all major databases: SQL Server, PostgreSQL, MySQL, SQLite, MariaDB, FirebirdSQL, Oracle, DuckDB, CockroachDB, ClickHouse, Snowflake, Supabase, CloudFlare D1, Turso, Athena, BigQuery, RedShift, IBM Db2, SAP HANA, Teradata, Trino, Presto and Apache Flight SQL.
28+
Supports all major databases: SQL Server, PostgreSQL, MySQL, SQLite, MariaDB, FirebirdSQL, Oracle, DuckDB, CockroachDB, ClickHouse, Snowflake, Supabase, CloudFlare D1, Turso, Athena, BigQuery, Spanner, RedShift, IBM Db2, SAP HANA, Teradata, Trino, Presto and Apache Flight SQL.
2929

3030
![Database Providers](docs/demos/demo-providers.gif)
3131

@@ -233,13 +233,15 @@ Autocomplete triggers automatically in INSERT mode. Use `Tab` to accept.
233233

234234
## Configuration
235235

236-
Connections and settings are stored in `~/.sqlit/`.
236+
Connections and settings are stored in `$XDG_CONFIG_HOME/sqlit/` (default: `~/.config/sqlit/`). Override the location by setting `SQLIT_CONFIG_DIR`.
237+
238+
If an older install left files in `~/.sqlit/`, they are moved to the new location automatically on first run.
237239

238240
## FAQ
239241

240242
### How are sensitive credentials stored?
241243

242-
Connection details are stored in `~/.sqlit/connections.json`, but passwords are stored in your OS keyring when available (macOS Keychain, Windows Credential Locker, Linux Secret Service).
244+
Connection details are stored in `connections.json` inside the config directory, but passwords are stored in your OS keyring when available (macOS Keychain, Windows Credential Locker, Linux Secret Service).
243245

244246
### How does sqlit compare to Harlequin, Lazysql, etc.?
245247

@@ -281,6 +283,8 @@ Most of the time you can just run `sqlit` and connect. If a Python driver is mis
281283
| Snowflake | `snowflake-connector-python` | `pipx inject sqlit-tui snowflake-connector-python` | `python -m pip install snowflake-connector-python` |
282284
| Firebird | `firebirdsql` | `pipx inject sqlit-tui firebirdsql` | `python -m pip install firebirdsql` |
283285
| Athena | `pyathena` | `pipx inject sqlit-tui pyathena` | `python -m pip install pyathena` |
286+
| BigQuery | `google-cloud-bigquery` | `pipx inject sqlit-tui google-cloud-bigquery` | `python -m pip install google-cloud-bigquery` |
287+
| Spanner | `google-cloud-spanner` | `pipx inject sqlit-tui google-cloud-spanner` | `python -m pip install google-cloud-spanner` |
284288
| Apache Arrow Flight SQL | `adbc-driver-flightsql` | `pipx inject sqlit-tui adbc-driver-flightsql` | `python -m pip install adbc-driver-flightsql` |
285289

286290
### SSH Tunnel Support

flake.nix

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,53 +28,85 @@
2828
shortRev = if self ? shortRev then self.shortRev else "dirty";
2929
version = if tag != "" then tag else "0.0.0+${shortRev}";
3030

31-
sqlit = pyPkgs.buildPythonApplication {
32-
pname = "sqlit";
33-
inherit version;
34-
pyproject = true;
31+
# Extras mirror project.optional-dependencies in pyproject.toml,
32+
# limited to drivers packaged in nixpkgs. Others (mssql-python,
33+
# oracledb, mariadb, ibm_db, hdbcli, teradatasql, trino,
34+
# presto-python-client, redshift-connector, clickhouse-connect,
35+
# libsql, firebirdsql, pyathena, adbc-driver-flightsql) aren't
36+
# here; install with `pipx inject` or a custom derivation.
37+
nixpkgsExtras = {
38+
ssh = [ pyPkgs.sshtunnel pyPkgs.paramiko ];
39+
postgres = [ pyPkgs.psycopg2 ];
40+
cockroachdb = [ pyPkgs.psycopg2 ];
41+
mysql = [ pyPkgs.pymysql ];
42+
duckdb = [ pyPkgs.duckdb ];
43+
bigquery = [ pyPkgs.google-cloud-bigquery ];
44+
snowflake = [ pyPkgs.snowflake-connector-python ];
45+
d1 = [ pyPkgs.requests ];
46+
};
3547

36-
src = self;
48+
resolveExtras = names:
49+
lib.concatLists (map (n:
50+
nixpkgsExtras.${n} or (throw
51+
"Unknown sqlit extra '${n}'. Available: ${
52+
lib.concatStringsSep ", " (builtins.attrNames nixpkgsExtras)
53+
}")
54+
) names);
3755

38-
build-system = [
39-
pyPkgs.hatchling
40-
pyPkgs."hatch-vcs"
41-
pyPkgs."setuptools-scm"
42-
];
56+
# Build sqlit, optionally with a set of driver extras from nixpkgs.
57+
# Example: makeSqlit { extras = [ "postgres" "ssh" ]; }
58+
makeSqlit = { extras ? [] }:
59+
pyPkgs.buildPythonApplication {
60+
pname = "sqlit";
61+
inherit version;
62+
pyproject = true;
4363

44-
nativeBuildInputs = [
45-
pyPkgs.pythonRelaxDepsHook
46-
];
64+
src = self;
4765

48-
pythonRelaxDeps = [
49-
"textual-fastdatatable"
50-
];
66+
build-system = [
67+
pyPkgs.hatchling
68+
pyPkgs."hatch-vcs"
69+
pyPkgs."setuptools-scm"
70+
];
5171

52-
SETUPTOOLS_SCM_PRETEND_VERSION = version;
72+
nativeBuildInputs = [
73+
pyPkgs.pythonRelaxDepsHook
74+
];
5375

54-
dependencies = [
55-
pyPkgs.docker
56-
pyPkgs.keyring
57-
pyPkgs.pyperclip
58-
pyPkgs.sqlparse
59-
pyPkgs.textual
60-
pyPkgs."textual-fastdatatable"
61-
];
76+
pythonRelaxDeps = [
77+
"textual-fastdatatable"
78+
];
6279

63-
pythonImportsCheck = [ "sqlit" ];
80+
SETUPTOOLS_SCM_PRETEND_VERSION = version;
6481

65-
meta = with lib; {
66-
description = "A terminal UI for SQL databases";
67-
homepage = "https://github.com/Maxteabag/sqlit";
68-
license = licenses.mit;
69-
mainProgram = "sqlit";
82+
dependencies = [
83+
pyPkgs.docker
84+
pyPkgs.keyring
85+
pyPkgs.pyperclip
86+
pyPkgs.sqlparse
87+
pyPkgs.textual
88+
pyPkgs."textual-fastdatatable"
89+
] ++ (resolveExtras extras);
90+
91+
pythonImportsCheck = [ "sqlit" ];
92+
93+
meta = with lib; {
94+
description = "A terminal UI for SQL databases";
95+
homepage = "https://github.com/Maxteabag/sqlit";
96+
license = licenses.mit;
97+
mainProgram = "sqlit";
98+
};
7099
};
71-
};
100+
101+
sqlit = makeSqlit { extras = builtins.attrNames nixpkgsExtras; };
72102
in {
73103
packages = {
74104
inherit sqlit;
75105
default = sqlit;
76106
};
77107

108+
lib = { inherit makeSqlit; };
109+
78110
apps.default = {
79111
type = "app";
80112
program = "${sqlit}/bin/sqlit";

pyproject.toml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ all = [
4848
"trino>=0.329.0",
4949
"presto-python-client>=0.8.4",
5050
"google-cloud-bigquery",
51+
"google-cloud-spanner>=3.0.0",
5152
"duckdb>=1.1.0", # min avoids known CVEs
5253
"clickhouse-connect>=0.7.0",
5354
"requests>=2.32.4", # min avoids known CVEs
@@ -75,6 +76,7 @@ teradata = ["teradatasql>=20.0.0"]
7576
trino = ["trino>=0.329.0"]
7677
presto = ["presto-python-client>=0.8.4"]
7778
bigquery = ["google-cloud-bigquery"]
79+
spanner = ["google-cloud-spanner>=3.0.0"]
7880
redshift = ["redshift-connector"]
7981
duckdb = ["duckdb>=1.1.0"] # min avoids known CVEs
8082
clickhouse = ["clickhouse-connect>=0.7.0"]
@@ -193,6 +195,7 @@ markers = [
193195
"firebird: Firebird database tests",
194196
"clickhouse: ClickHouse database tests",
195197
"flight: Apache Arrow Flight SQL database tests",
198+
"spanner: Google Cloud Spanner database tests",
196199
"asyncio: async tests",
197200
"integration: integration tests (may require external services)",
198201
]
@@ -251,7 +254,18 @@ module = [
251254
"impala",
252255
"impala.dbapi",
253256
"osquery",
254-
"surrealdb"
257+
"surrealdb",
258+
"google.cloud",
259+
"google.cloud.bigquery",
260+
"google.cloud.bigquery.dbapi",
261+
"google.cloud.spanner",
262+
"google.cloud.spanner_dbapi",
263+
"google.api_core",
264+
"google.api_core.client_options",
265+
"google.auth",
266+
"google.auth.credentials",
267+
"google.oauth2",
268+
"google.oauth2.service_account",
255269
]
256270
ignore_missing_imports = true
257271

sqlit/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ def main() -> int:
372372
parser.add_argument(
373373
"--settings",
374374
metavar="PATH",
375-
help="Path to settings JSON file (overrides ~/.sqlit/settings.json)",
375+
help="Path to settings JSON file (overrides the one in the sqlit config directory)",
376376
)
377377
parser.add_argument(
378378
"--theme",

sqlit/core/binding_contexts.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ def get_binding_contexts(ctx: InputContext) -> set[str]:
2121
contexts.add("query")
2222
if ctx.vim_mode == VimMode.INSERT:
2323
contexts.add("query_insert")
24+
elif ctx.vim_mode == VimMode.VISUAL:
25+
contexts.add("query_visual")
26+
elif ctx.vim_mode == VimMode.VISUAL_LINE:
27+
contexts.add("query_visual_line")
2428
else:
2529
contexts.add("query_normal")
2630
if ctx.autocomplete_visible:

sqlit/core/keymap.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"question_mark": "?",
1212
"slash": "/",
1313
"asterisk": "*",
14+
"circumflex_accent": "^",
1415
"dollar_sign": "$",
1516
"percent_sign": "%",
1617
"space": "<space>",
@@ -357,15 +358,65 @@ def _build_action_keys(self) -> list[ActionKeyDef]:
357358
ActionKeyDef("b", "cursor_word_back", "query_normal"),
358359
ActionKeyDef("B", "cursor_WORD_back", "query_normal"),
359360
ActionKeyDef("0", "cursor_line_start", "query_normal"),
361+
ActionKeyDef("circumflex_accent", "cursor_first_non_blank", "query_normal"),
360362
ActionKeyDef("dollar_sign", "cursor_line_end", "query_normal"),
361363
ActionKeyDef("G", "cursor_last_line", "query_normal"),
362364
ActionKeyDef("percent_sign", "cursor_matching_bracket", "query_normal"),
363365
ActionKeyDef("f", "cursor_find_char", "query_normal"),
364366
ActionKeyDef("F", "cursor_find_char_back", "query_normal"),
365367
ActionKeyDef("t", "cursor_till_char", "query_normal"),
366368
ActionKeyDef("T", "cursor_till_char_back", "query_normal"),
369+
ActionKeyDef("v", "enter_visual_mode", "query_normal"),
370+
ActionKeyDef("V", "enter_visual_line_mode", "query_normal"),
371+
ActionKeyDef("x", "delete_char", "query_normal"),
367372
ActionKeyDef("a", "append_insert_mode", "query_normal"),
368373
ActionKeyDef("A", "append_line_end", "query_normal"),
374+
# Query (visual mode - charwise)
375+
ActionKeyDef("escape", "exit_visual_mode", "query_visual"),
376+
ActionKeyDef("v", "exit_visual_mode", "query_visual", primary=False),
377+
ActionKeyDef("V", "switch_to_visual_line_mode", "query_visual"),
378+
ActionKeyDef("y", "visual_yank", "query_visual"),
379+
ActionKeyDef("d", "visual_delete", "query_visual"),
380+
ActionKeyDef("x", "visual_delete", "query_visual", primary=False),
381+
ActionKeyDef("c", "visual_change", "query_visual"),
382+
ActionKeyDef("enter", "visual_execute", "query_visual"),
383+
ActionKeyDef("h", "cursor_left", "query_visual"),
384+
ActionKeyDef("j", "cursor_down", "query_visual"),
385+
ActionKeyDef("k", "cursor_up", "query_visual"),
386+
ActionKeyDef("l", "cursor_right", "query_visual"),
387+
ActionKeyDef("w", "cursor_word_forward", "query_visual"),
388+
ActionKeyDef("W", "cursor_WORD_forward", "query_visual"),
389+
ActionKeyDef("b", "cursor_word_back", "query_visual"),
390+
ActionKeyDef("B", "cursor_WORD_back", "query_visual"),
391+
ActionKeyDef("0", "cursor_line_start", "query_visual"),
392+
ActionKeyDef("circumflex_accent", "cursor_first_non_blank", "query_visual"),
393+
ActionKeyDef("dollar_sign", "cursor_line_end", "query_visual"),
394+
ActionKeyDef("G", "cursor_last_line", "query_visual"),
395+
ActionKeyDef("g", "g_leader_key", "query_visual"),
396+
ActionKeyDef("percent_sign", "cursor_matching_bracket", "query_visual"),
397+
ActionKeyDef("f", "cursor_find_char", "query_visual"),
398+
ActionKeyDef("F", "cursor_find_char_back", "query_visual"),
399+
ActionKeyDef("t", "cursor_till_char", "query_visual"),
400+
ActionKeyDef("T", "cursor_till_char_back", "query_visual"),
401+
ActionKeyDef("down", "cursor_down", "query_visual", primary=False),
402+
ActionKeyDef("up", "cursor_up", "query_visual", primary=False),
403+
ActionKeyDef("left", "cursor_left", "query_visual", primary=False),
404+
ActionKeyDef("right", "cursor_right", "query_visual", primary=False),
405+
# Query (visual line mode)
406+
ActionKeyDef("escape", "exit_visual_line_mode", "query_visual_line"),
407+
ActionKeyDef("V", "exit_visual_line_mode", "query_visual_line", primary=False),
408+
ActionKeyDef("v", "switch_to_visual_mode", "query_visual_line"),
409+
ActionKeyDef("y", "visual_line_yank", "query_visual_line"),
410+
ActionKeyDef("d", "visual_line_delete", "query_visual_line"),
411+
ActionKeyDef("x", "visual_line_delete", "query_visual_line", primary=False),
412+
ActionKeyDef("c", "visual_line_change", "query_visual_line"),
413+
ActionKeyDef("j", "cursor_down", "query_visual_line"),
414+
ActionKeyDef("k", "cursor_up", "query_visual_line"),
415+
ActionKeyDef("G", "cursor_last_line", "query_visual_line"),
416+
ActionKeyDef("g", "g_leader_key", "query_visual_line"),
417+
ActionKeyDef("down", "cursor_down", "query_visual_line", primary=False),
418+
ActionKeyDef("up", "cursor_up", "query_visual_line", primary=False),
419+
ActionKeyDef("enter", "visual_line_execute", "query_visual_line"),
369420
# Query (insert mode)
370421
ActionKeyDef("escape", "exit_insert_mode", "query_insert"),
371422
ActionKeyDef("ctrl+enter", "execute_query_insert", "query_insert"),

sqlit/core/vim.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ class VimMode(Enum):
1010

1111
NORMAL = "NORMAL"
1212
INSERT = "INSERT"
13+
VISUAL = "VISUAL"
14+
VISUAL_LINE = "VISUAL LINE"

sqlit/domains/connections/cli/commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def _maybe_prompt_plaintext_credentials(services: AppServices) -> bool:
6363
if not sys.stdin.isatty():
6464
return False
6565

66-
answer = input("Keyring isn't available. Save passwords as plaintext in ~/.sqlit/? [y/N]: ").strip().lower()
66+
answer = input("Keyring isn't available. Save passwords as plaintext in the sqlit config directory? [y/N]: ").strip().lower()
6767
allow = answer in {"y", "yes"}
6868
settings[ALLOW_PLAINTEXT_CREDENTIALS_SETTING] = allow
6969
services.settings_store.save_all(settings)

sqlit/domains/connections/discovery/cloud/aws/cache.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
from __future__ import annotations
44

55
import json
6-
import os
76
import time
87
from dataclasses import dataclass, field
9-
from pathlib import Path
108
from typing import TYPE_CHECKING
119

10+
from sqlit.shared.core.store import CONFIG_DIR
11+
1212
if TYPE_CHECKING:
1313
from .provider import RegionResources
1414

1515
# Cache configuration
1616
AWS_CACHE_TTL_SECONDS = 300 # 5 minutes
17-
AWS_CACHE_FILE = Path(os.path.expanduser("~/.config/sqlit/aws_cache.json"))
17+
AWS_CACHE_FILE = CONFIG_DIR / "aws_cache.json"
1818

1919

2020
@dataclass

sqlit/domains/connections/discovery/cloud/azure/cache.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
from __future__ import annotations
44

55
import json
6-
import os
76
import time
87
from dataclasses import dataclass
9-
from pathlib import Path
8+
9+
from sqlit.shared.core.store import CONFIG_DIR
1010

1111
from .models import AzureSqlServer, AzureSubscription
1212

1313
# Cache configuration
1414
AZURE_CACHE_TTL_SECONDS = 300 # 5 minutes
15-
AZURE_CACHE_FILE = Path(os.path.expanduser("~/.config/sqlit/azure_cache.json"))
15+
AZURE_CACHE_FILE = CONFIG_DIR / "azure_cache.json"
1616

1717

1818
@dataclass

0 commit comments

Comments
 (0)