Skip to content

Commit cde2df8

Browse files
committed
Refactoring - Add mock layer for connections and queries
1 parent 103d8a6 commit cde2df8

27 files changed

+1306
-516
lines changed

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# sqlit
22

3-
**The lazygit of SQL databases.** Connect to Postgres, MySQL, SQL Server, SQLite, Turso, and more from your terminal in seconds.
3+
**The lazygit of SQL databases.** Connect to Postgres, MySQL, SQL Server, SQLite, Supabase, Turso, and more from your terminal in seconds.
44

55
A lightweight TUI for people who just want to run some queries fast.
66

@@ -18,7 +18,7 @@ A lightweight TUI for people who just want to run some queries fast.
1818

1919
- **Connection manager UI** - Save connections, switch between databases without CLI args
2020
- **Just run `sqlit`** - No CLI config needed, pick a connection and go
21-
- **Multi-database out of the box** - SQL Server, PostgreSQL, MySQL, SQLite, MariaDB, Oracle, DuckDB, CockroachDB, Turso - no adapters to install
21+
- **Multi-database out of the box** - SQL Server, PostgreSQL, MySQL, SQLite, MariaDB, Oracle, DuckDB, CockroachDB, Supabase, Turso - no adapters to install
2222
- **SSH tunnels built-in** - Connect to remote databases securely with password or key auth
2323
- **Vim-style editing** - Modal editing for terminal purists
2424
- **Query history** - Automatically saves queries per connection, searchable and sortable
@@ -40,7 +40,7 @@ The problem got severely worse when I switched to Linux and had to rely on VS CO
4040

4141
I tried to use some existing TUI's for SQL, but they were not intuitive for me and I missed the immediate ease of use that other TUI's such as Lazygit provides.
4242

43-
sqlit is a lightweight database TUI that is easy to use and beautiful to look at, just connect and query. It's for you that just wants to run queries toward your database without launching applications that eats your ram and takes time to load up. Sqlit supports SQL Server, PostgreSQL, MySQL, SQLite, MariaDB, Oracle, DuckDB, CockroachDB, and Turso, and is designed to make it easy and enjoyable to access your data, not painful.
43+
sqlit is a lightweight database TUI that is easy to use and beautiful to look at, just connect and query. It's for you that just wants to run queries toward your database without launching applications that eats your ram and takes time to load up. Sqlit supports SQL Server, PostgreSQL, MySQL, SQLite, MariaDB, Oracle, DuckDB, CockroachDB, Supabase, and Turso, and is designed to make it easy and enjoyable to access your data, not painful.
4444

4545

4646
## Installation
@@ -59,6 +59,14 @@ sqlit
5959

6060
The keybindings are shown at the bottom of the screen.
6161

62+
### Try it without a database
63+
64+
Want to explore the UI without connecting to a real database? Run with mock data:
65+
66+
```bash
67+
sqlit --mock=sqlite-demo
68+
```
69+
6270
### CLI
6371

6472
```bash
@@ -161,6 +169,7 @@ Each database provider requires specific Python packages. sqlit will prompt you
161169
| Oracle | `oracledb` | `pip install oracledb` |
162170
| DuckDB | `duckdb` | `pip install duckdb` |
163171
| CockroachDB | `psycopg2-binary` | `pip install psycopg2-binary` |
172+
| Supabase | `psycopg2-binary` | `pip install psycopg2-binary` |
164173
| Turso | `libsql-client` | `pip install libsql-client` |
165174

166175
**Note:** SQL Server also requires the ODBC driver. On first connection attempt, sqlit will detect if it's missing and help you install it.

demo-sqlite.gif

289 KB
Loading

demo.db

20 KB
Binary file not shown.

sqlit/app.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@
2323
save_settings,
2424
)
2525
from .db import DatabaseAdapter
26+
from .mocks import MockProfile
2627
from .state_machine import (
2728
get_leader_bindings,
28-
get_leader_binding_actions,
29-
check_leader_action_guard,
3029
UIStateMachine,
3130
)
3231
from .ui.mixins import (
@@ -215,8 +214,9 @@ class SSMSTUI(
215214
Binding("ctrl+c", "cancel_operation", "Cancel", show=False),
216215
]
217216

218-
def __init__(self):
217+
def __init__(self, mock_profile: MockProfile | None = None):
219218
super().__init__()
219+
self._mock_profile = mock_profile
220220
self.connections: list[ConnectionConfig] = []
221221
self.current_connection: Any | None = None
222222
self.current_config: ConnectionConfig | None = None
@@ -264,6 +264,30 @@ def __init__(self):
264264
self._columns_loading: set[str] = set()
265265
self._state_machine = UIStateMachine()
266266

267+
if mock_profile:
268+
self._session_factory = self._create_mock_session_factory(mock_profile)
269+
270+
def _create_mock_session_factory(self, profile: MockProfile):
271+
"""Create a session factory that uses mock adapters."""
272+
from .services import ConnectionSession
273+
274+
def mock_adapter_factory(db_type: str):
275+
"""Return mock adapter for the given db type."""
276+
return profile.get_adapter(db_type)
277+
278+
def mock_tunnel_factory(config):
279+
"""Return no tunnel for mock connections."""
280+
return None, config.server, int(config.port or "0")
281+
282+
def factory(config):
283+
return ConnectionSession.create(
284+
config,
285+
adapter_factory=mock_adapter_factory,
286+
tunnel_factory=mock_tunnel_factory,
287+
)
288+
289+
return factory
290+
267291
@property
268292
def object_tree(self) -> Tree:
269293
return self.query_one("#object-tree", Tree)
@@ -314,16 +338,11 @@ def pop_screen(self):
314338
return result
315339

316340
def check_action(self, action: str, parameters: tuple) -> bool | None:
317-
"""Check if an action is allowed in the current state."""
318-
if action in get_leader_binding_actions():
319-
if not getattr(self, "_leader_pending", False):
320-
return False
321-
if hasattr(self, "_leader_timer") and self._leader_timer is not None:
322-
self._leader_timer.stop()
323-
self._leader_timer = None
324-
self._leader_pending = False
325-
return check_leader_action_guard(self, action)
341+
"""Check if an action is allowed in the current state.
326342
343+
This method is pure - it only checks, never mutates state.
344+
State transitions happen in the action methods themselves.
345+
"""
327346
return self._state_machine.check_action(self, action)
328347

329348
def compose(self) -> ComposeResult:
@@ -363,7 +382,7 @@ def compose(self) -> ComposeResult:
363382

364383
def on_mount(self) -> None:
365384
"""Initialize the app."""
366-
if not PYODBC_AVAILABLE:
385+
if not PYODBC_AVAILABLE and not self._mock_profile:
367386
self.notify(
368387
"pyodbc not installed. Run: pip install pyodbc",
369388
severity="warning",
@@ -382,7 +401,11 @@ def on_mount(self) -> None:
382401
settings = load_settings()
383402
self._expanded_paths = set(settings.get("expanded_nodes", []))
384403

385-
self.connections = load_connections()
404+
if self._mock_profile:
405+
self.connections = self._mock_profile.connections.copy()
406+
else:
407+
self.connections = load_connections()
408+
386409
self.refresh_tree()
387410
self._update_footer_bindings()
388411

@@ -392,8 +415,8 @@ def on_mount(self) -> None:
392415
self.object_tree.cursor_line = 0
393416
self._update_section_labels()
394417

395-
# Check for ODBC drivers
396-
self._check_drivers()
418+
if not self._mock_profile:
419+
self._check_drivers()
397420

398421
def _check_drivers(self) -> None:
399422
"""Check if ODBC drivers are installed and show setup if needed."""

sqlit/cli.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ def main() -> int:
1515
prog="sqlit",
1616
description="A terminal UI for SQL Server, PostgreSQL, MySQL, and SQLite databases",
1717
)
18+
19+
# Global options for TUI mode
20+
parser.add_argument(
21+
"--mock",
22+
metavar="PROFILE",
23+
help="Run with mock data (profiles: sqlite-demo, empty, multi-db)",
24+
)
25+
1826
subparsers = parser.add_subparsers(dest="command", help="Available commands")
1927

2028
# Connection commands
@@ -118,7 +126,17 @@ def main() -> int:
118126
if args.command is None:
119127
from .app import SSMSTUI
120128

121-
app = SSMSTUI()
129+
mock_profile = None
130+
if args.mock:
131+
from .mocks import get_mock_profile, list_mock_profiles
132+
133+
mock_profile = get_mock_profile(args.mock)
134+
if mock_profile is None:
135+
print(f"Unknown mock profile: {args.mock}")
136+
print(f"Available profiles: {', '.join(list_mock_profiles())}")
137+
return 1
138+
139+
app = SSMSTUI(mock_profile=mock_profile)
122140
app.run()
123141
return 0
124142

sqlit/commands.py

Lines changed: 26 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
load_connections,
1717
save_connections,
1818
)
19+
from .db.schema import get_default_port, has_advanced_auth, is_file_based
1920
from .services import ConnectionSession, QueryResult, QueryService
2021

2122
if TYPE_CHECKING:
@@ -33,19 +34,18 @@ def cmd_connection_list(args) -> int:
3334
print("-" * 95)
3435
for conn in connections:
3536
db_type_label = DATABASE_TYPE_LABELS.get(conn.get_db_type(), conn.db_type)
36-
# File-based databases (SQLite, DuckDB)
37-
if conn.db_type in ("sqlite", "duckdb"):
37+
if is_file_based(conn.db_type):
3838
conn_info = conn.file_path[:38] + ".." if len(conn.file_path) > 40 else conn.file_path
3939
auth_label = "N/A"
40-
# Server-based databases with simple auth
41-
elif conn.db_type in ("postgresql", "mysql", "mariadb", "oracle", "cockroachdb"):
40+
elif has_advanced_auth(conn.db_type):
4241
conn_info = f"{conn.server}@{conn.database}" if conn.database else conn.server
4342
conn_info = conn_info[:38] + ".." if len(conn_info) > 40 else conn_info
44-
auth_label = f"User: {conn.username}" if conn.username else "N/A"
45-
else: # mssql (SQL Server)
43+
auth_label = AUTH_TYPE_LABELS.get(conn.get_auth_type(), conn.auth_type)
44+
else:
45+
# Server-based databases with simple auth
4646
conn_info = f"{conn.server}@{conn.database}" if conn.database else conn.server
4747
conn_info = conn_info[:38] + ".." if len(conn_info) > 40 else conn_info
48-
auth_label = AUTH_TYPE_LABELS.get(conn.get_auth_type(), conn.auth_type)
48+
auth_label = f"User: {conn.username}" if conn.username else "N/A"
4949
print(
5050
f"{conn.name:<20} {db_type_label:<10} {conn_info:<40} {auth_label:<25}"
5151
)
@@ -69,8 +69,7 @@ def cmd_connection_create(args) -> int:
6969
print(f"Error: Invalid database type '{db_type}'. Valid types: {valid_types}")
7070
return 1
7171

72-
# File-based databases (SQLite, DuckDB)
73-
if db_type in ("sqlite", "duckdb"):
72+
if is_file_based(db_type):
7473
file_path = getattr(args, "file_path", None)
7574
if not file_path:
7675
print(f"Error: --file-path is required for {db_type.upper()} connections.")
@@ -81,28 +80,30 @@ def cmd_connection_create(args) -> int:
8180
db_type=db_type,
8281
file_path=file_path,
8382
)
84-
# Server-based databases with simple auth (PostgreSQL, MySQL, MariaDB, Oracle, CockroachDB)
85-
elif db_type in ("postgresql", "mysql", "mariadb", "oracle", "cockroachdb"):
83+
elif has_advanced_auth(db_type):
84+
# SQL Server connection (has Windows/Azure AD auth)
8685
if not args.server:
87-
db_label = DATABASE_TYPE_LABELS.get(DatabaseType(db_type), db_type.upper())
88-
print(f"Error: --server is required for {db_label} connections.")
86+
print("Error: --server is required for SQL Server connections.")
87+
return 1
88+
89+
auth_type_str = getattr(args, "auth_type", "sql") or "sql"
90+
try:
91+
auth_type = AuthType(auth_type_str)
92+
except ValueError:
93+
valid_types = ", ".join(t.value for t in AuthType)
94+
print(f"Error: Invalid auth type '{auth_type_str}'. Valid types: {valid_types}")
8995
return 1
9096

91-
default_ports = {
92-
"postgresql": "5432",
93-
"mysql": "3306",
94-
"mariadb": "3306",
95-
"oracle": "1521",
96-
"cockroachdb": "26257",
97-
}
9897
config = ConnectionConfig(
9998
name=args.name,
10099
db_type=db_type,
101100
server=args.server,
102-
port=args.port or default_ports.get(db_type, "1433"),
101+
port=args.port or get_default_port(db_type),
103102
database=args.database or "",
104103
username=args.username or "",
105104
password=args.password or "",
105+
auth_type=auth_type.value,
106+
trusted_connection=(auth_type == AuthType.WINDOWS),
106107
ssh_enabled=getattr(args, "ssh_enabled", False) or False,
107108
ssh_host=getattr(args, "ssh_host", "") or "",
108109
ssh_port=getattr(args, "ssh_port", "22") or "22",
@@ -112,29 +113,20 @@ def cmd_connection_create(args) -> int:
112113
ssh_password=getattr(args, "ssh_password", "") or "",
113114
)
114115
else:
115-
# SQL Server connection (mssql)
116+
# Server-based databases with simple auth
116117
if not args.server:
117-
print("Error: --server is required for SQL Server connections.")
118-
return 1
119-
120-
auth_type_str = getattr(args, "auth_type", "sql") or "sql"
121-
try:
122-
auth_type = AuthType(auth_type_str)
123-
except ValueError:
124-
valid_types = ", ".join(t.value for t in AuthType)
125-
print(f"Error: Invalid auth type '{auth_type_str}'. Valid types: {valid_types}")
118+
db_label = DATABASE_TYPE_LABELS.get(DatabaseType(db_type), db_type.upper())
119+
print(f"Error: --server is required for {db_label} connections.")
126120
return 1
127121

128122
config = ConnectionConfig(
129123
name=args.name,
130124
db_type=db_type,
131125
server=args.server,
132-
port=args.port or "1433",
126+
port=args.port or get_default_port(db_type),
133127
database=args.database or "",
134128
username=args.username or "",
135129
password=args.password or "",
136-
auth_type=auth_type.value,
137-
trusted_connection=(auth_type == AuthType.WINDOWS),
138130
ssh_enabled=getattr(args, "ssh_enabled", False) or False,
139131
ssh_host=getattr(args, "ssh_host", "") or "",
140132
ssh_port=getattr(args, "ssh_port", "22") or "22",

sqlit/config.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
from .stores.settings import load_settings, save_settings
2929

3030

31+
# Import schema capabilities - use function to avoid circular imports
32+
def _is_file_based(db_type: str) -> bool:
33+
from .db.schema import is_file_based
34+
35+
return is_file_based(db_type)
36+
37+
3138
class DatabaseType(Enum):
3239
"""Supported database types."""
3340

@@ -184,7 +191,7 @@ def get_connection_string(self) -> str:
184191

185192
def get_display_info(self) -> str:
186193
"""Get a display string for the connection."""
187-
if self.db_type in ("sqlite", "duckdb"):
194+
if _is_file_based(self.db_type):
188195
return self.file_path or self.name
189196

190197
if self.db_type == "supabase":

sqlit/db/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@
2020
SelectOption,
2121
get_all_schemas,
2222
get_connection_schema,
23+
get_default_port,
24+
get_display_name,
2325
get_supported_db_types,
26+
has_advanced_auth,
27+
is_file_based,
28+
supports_ssh,
2429
)
2530
from .tunnel import create_ssh_tunnel
2631

@@ -46,7 +51,12 @@
4651
"SelectOption",
4752
"get_all_schemas",
4853
"get_connection_schema",
54+
"get_default_port",
55+
"get_display_name",
4956
"get_supported_db_types",
57+
"has_advanced_auth",
58+
"is_file_based",
59+
"supports_ssh",
5060
# Tunnel
5161
"create_ssh_tunnel",
5262
]

0 commit comments

Comments
 (0)