Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions sqlit/domains/connections/app/save_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,13 @@ def save_connection(
config.name = ensure_unique_name(existing_names, config.name)
connections.append(config)

persist_connections = connections
if getattr(connection_store, "is_persistent", True):
try:
persist_connections = connection_store.load_all()
except Exception:
persist_connections = connections
else:
persist_connections = [c for c in persist_connections if c.name != config.name]
persist_connections.append(config)

warning = None
warning_severity = "warning"
if not getattr(connection_store, "is_persistent", True):
warning = "Connections are not persisted in this session"

try:
connection_store.save_all(persist_connections)
connection_store.save_one(config)
return SaveConnectionResult(
config=config,
saved=True,
Expand Down
75 changes: 64 additions & 11 deletions sqlit/domains/connections/store/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ def _config_to_dict_without_passwords(self, config: ConnectionConfig) -> dict:
"""
return config.to_dict(include_passwords=False)

def _write_index(self, connections: list[ConnectionConfig]) -> None:
"""Write the JSON index file (without passwords) for all connections.

The index is a single file shared by every connection, so it is always
rewritten in full. This write never touches the OS keyring.
"""
payload = [self._config_to_dict_without_passwords(c) for c in connections]
self._write_json(self._wrap_connections_payload(payload))

def save_all(self, connections: list[ConnectionConfig]) -> None:
"""Save all connections.

Expand All @@ -203,8 +212,54 @@ def save_all(self, connections: list[ConnectionConfig]) -> None:
for config in persist_connections:
errors.extend(self._save_credentials(config))

payload = [self._config_to_dict_without_passwords(c) for c in persist_connections]
self._write_json(self._wrap_connections_payload(payload))
self._write_index(persist_connections)
if errors:
raise CredentialsPersistError(errors)

def save_one(
self,
connection: ConnectionConfig,
previous_name: str | None = None,
) -> None:
"""Persist a single connection without rewriting other credentials.

Only the given connection's keyring entries are written. When
``previous_name`` differs from the connection's current name (a
rename), the stale entries under the old name are removed. The JSON
index file is rewritten in full because all connections share one
file, but that write never touches the OS keyring for other
connections.

Args:
connection: The connection to persist.
previous_name: The connection's prior name when renaming.
"""
from sqlit.domains.connections.app.persist_utils import build_persist_connections

renamed = bool(previous_name and previous_name != connection.name)

existing = self.load_all(load_credentials=False)
filtered = [
c
for c in existing
if c.name != connection.name and not (renamed and c.name == previous_name)
]
filtered.append(connection)
self._write_index(filtered)

errors: list[CredentialsStoreError] = []
if renamed:
for deleter in (
self.credentials_service.delete_password,
self.credentials_service.delete_ssh_password,
):
try:
deleter(previous_name) # type: ignore[arg-type]
except CredentialsStoreError as exc:
errors.append(exc)

target = build_persist_connections([connection], self.credentials_service)[0]
errors.extend(self._save_credentials(target))
if errors:
raise CredentialsPersistError(errors)

Expand All @@ -231,11 +286,10 @@ def add(self, connection: ConnectionConfig) -> None:
Raises:
ValueError: If a connection with the same name already exists.
"""
connections = self.load_all()
connections = self.load_all(load_credentials=False)
if any(c.name == connection.name for c in connections):
raise ValueError(f"Connection '{connection.name}' already exists")
connections.append(connection)
self.save_all(connections)
self.save_one(connection)

def update(self, connection: ConnectionConfig) -> None:
"""Update an existing connection.
Expand All @@ -246,11 +300,10 @@ def update(self, connection: ConnectionConfig) -> None:
Raises:
ValueError: If connection doesn't exist.
"""
connections = self.load_all()
for i, c in enumerate(connections):
connections = self.load_all(load_credentials=False)
for c in connections:
if c.name == connection.name:
connections[i] = connection
self.save_all(connections)
self.save_one(connection)
return
raise ValueError(f"Connection '{connection.name}' not found")

Expand All @@ -265,13 +318,13 @@ def delete(self, name: str) -> bool:
Returns:
True if deleted, False if not found.
"""
connections = self.load_all()
connections = self.load_all(load_credentials=False)
original_count = len(connections)
connections = [c for c in connections if c.name != name]
if len(connections) < original_count:
# Delete credentials from keyring
self.credentials_service.delete_all_for_connection(name)
self.save_all(connections)
self._write_index(connections)
return True
return False

Expand Down
10 changes: 10 additions & 0 deletions sqlit/domains/connections/store/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ def load_all(self, load_credentials: bool = True) -> list[ConnectionConfig]:
def save_all(self, connections: list[ConnectionConfig]) -> None:
self._connections = copy.deepcopy(connections)

def save_one(
self,
connection: ConnectionConfig,
previous_name: str | None = None,
) -> None:
if previous_name and previous_name != connection.name:
self._connections = [c for c in self._connections if c.name != previous_name]
self._connections = [c for c in self._connections if c.name != connection.name]
self._connections.append(copy.deepcopy(connection))

def set_credentials_service(self, service: CredentialsService) -> None:
"""No-op for in-memory store."""
return None
Expand Down
17 changes: 1 addition & 16 deletions sqlit/domains/connections/ui/mixins/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,22 +541,7 @@ def do_save(with_config: ConnectionConfig, orig_name: str | None = None) -> None
if not self.services.connection_store.is_persistent:
self.notify("Connections are not persisted in this session")
try:
persist_connections = self.connections
if self.services.connection_store.is_persistent:
try:
persist_connections = self.services.connection_store.load_all()
except Exception:
persist_connections = self.connections
else:
if orig_name:
persist_connections = [
c for c in persist_connections if c.name != orig_name
]
persist_connections = [
c for c in persist_connections if c.name != with_config.name
]
persist_connections.append(with_config)
self.services.connection_store.save_all(persist_connections)
self.services.connection_store.save_one(with_config, previous_name=orig_name)
except CredentialsPersistError as exc:
credentials_error = exc
self._refresh_connection_tree()
Expand Down
8 changes: 8 additions & 0 deletions sqlit/shared/core/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ def save_all(self, connections: list[ConnectionConfig]) -> None:
"""Save connections."""
...

def save_one(
self,
connection: ConnectionConfig,
previous_name: str | None = None,
) -> None:
"""Persist a single connection without rewriting other credentials."""
...

def set_credentials_service(self, service: CredentialsService) -> None:
"""Attach a credentials service for loading stored secrets."""
...
Expand Down
Loading