mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-06 20:13:01 +09:00
Plan 75 §G5 + §G6. §G5 — app/services/db_exec.py. engine × host dispatch (mysql/postgres crossed with native/docker) was written three times. Only managed_db_user_service delegated fully to DatabaseService's executors; the other two grew private copies that had already drifted — db_process_service's docker-postgres branch built its own psql argv and never set ON_ERROR_STOP=1, so psql exited 0 for a statement the server had rejected and the caller was told it worked. db_exec owns the dispatch and nothing else; the four executors stay in DatabaseService. The three _exec_sql seams remain (tests stub them) and now delegate. An unrecognised engine is reported, never quietly routed to mysql, where it would surface as a syntax error blamed on the caller. docker_mysql_execute/docker_pg_execute gain machine_readable, which is the single flag db_config_tuner_service was keeping a whole private copy of the dispatcher to add. The MYSQL_PWD env block, pasted six times in database_service, becomes _mysql_env(): it carries a real rule — the password goes in the environment, never on the argv — and a rule in six copies only holds in the copies someone remembered. §G6 — adopting doors Round 1 built: - config_store's write is now atomic (tmp + os.replace). The §F4 consolidation deliberately kept the copies' truncating write and noted atomicity would land here once; this is that once. A write that dies mid-dump leaves a truncated file, and every load path treats a corrupt config as "use the default" — a config silently reset. - uptime_service._save_history was `except Exception: pass` over that truncating write, so a full disk destroyed the history and reported nothing while the panel kept rendering a figure from a file that had stopped updating. It now returns and logs the failure. - unit_is_active(): `systemctl is-active` was re-inlined in four services and every copy collapsed "could not ask" into "not running". The shared probe is tri-state; None is not False. - slug: preview_service and saved_view_service re-implemented slugify's regex, and saved_view re-implemented unique_slug's collision loop too. unique_slug gains start= so saved_view keeps generating the handles it always has. Checked rather than assumed: the plan lists file_integrity_service as silently losing data on a full disk. It does not — it already used tmp + os.replace and does not swallow the error. It is the pattern followed here, not a victim; only uptime_service had the silent loss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
95 lines
3.9 KiB
Python
95 lines
3.9 KiB
Python
"""One SQL-exec dispatcher (plan 75 §G5).
|
||
|
||
`engine × host` dispatch — mysql/postgres crossed with native/docker — was
|
||
written three times: `db_config_tuner_service`, `db_process_service`, and
|
||
`managed_db_user_service`. Only the last delegated fully to `DatabaseService`'s
|
||
four executors; the other two grew private copies that had already drifted (one
|
||
never learned about `ON_ERROR_STOP=1`, so psql reported success for statements
|
||
the server rejected).
|
||
|
||
This module is the dispatch, and nothing else. The four executors stay in
|
||
`DatabaseService` — this does not become a second place where SQL is run, only
|
||
the single place where "which executor" is decided.
|
||
|
||
Every executor returns the same shape, and so does this::
|
||
|
||
{'success': bool, 'output': str, 'error': str|None}
|
||
|
||
A target is a plain dict so callers can build one from a `ManagedDatabase` row,
|
||
a container name, or a tuner probe without a common base class:
|
||
|
||
{'engine': 'mysql'|'postgresql', 'container': str|None,
|
||
'user': str|None, 'password': str|None, 'database': str|None}
|
||
|
||
`container` is what decides native-vs-docker. `None`/absent means native.
|
||
"""
|
||
|
||
from typing import Optional
|
||
|
||
from app.services.database_service import DatabaseService
|
||
|
||
# The names the rest of the codebase spells these engines with. Kept here so an
|
||
# unknown engine is *reported*, never silently routed to mysql (§A).
|
||
MYSQL_ALIASES = ('mysql', 'mariadb')
|
||
POSTGRES_ALIASES = ('postgresql', 'postgres', 'pgsql')
|
||
|
||
|
||
def target(engine: str, *, container: Optional[str] = None,
|
||
user: Optional[str] = None, password: Optional[str] = None,
|
||
database: Optional[str] = None) -> dict:
|
||
"""Build a target dict. Keyword-only so a call site reads at a glance."""
|
||
return {'engine': engine, 'container': container, 'user': user,
|
||
'password': password, 'database': database}
|
||
|
||
|
||
def target_from_managed(managed, *, password: Optional[str] = None) -> dict:
|
||
"""A target for a ``ManagedDatabase`` row.
|
||
|
||
The admin secret is passed in rather than decrypted here: decryption needs
|
||
app context, and this module deliberately does not reach for it.
|
||
"""
|
||
return target(
|
||
managed.engine,
|
||
container=managed.container_ref if managed.host_kind == 'docker' else None,
|
||
user=managed.admin_username or None,
|
||
password=password,
|
||
)
|
||
|
||
|
||
def exec_sql(tgt: dict, sql: str, *, machine_readable: bool = False,
|
||
timeout: int = 30) -> dict:
|
||
"""Run *sql* against *tgt*'s engine, native or containerised.
|
||
|
||
``machine_readable`` asks the client for parse-friendly output (no headers,
|
||
tab-separated). Pass it when the caller splits the output; leave it off
|
||
when the output is shown to a person.
|
||
"""
|
||
engine = (tgt.get('engine') or '').strip().lower()
|
||
container = tgt.get('container')
|
||
user = tgt.get('user')
|
||
password = tgt.get('password')
|
||
database = tgt.get('database')
|
||
|
||
if engine in MYSQL_ALIASES:
|
||
if container:
|
||
return DatabaseService.docker_mysql_execute(
|
||
container, sql, database=database, user=user or 'root',
|
||
password=password, machine_readable=machine_readable,
|
||
timeout=timeout)
|
||
return DatabaseService.mysql_execute(
|
||
sql, database=database, root_password=password)
|
||
|
||
if engine in POSTGRES_ALIASES:
|
||
if container:
|
||
return DatabaseService.docker_pg_execute(
|
||
container, sql, database=database or 'postgres',
|
||
user=user or 'postgres', password=password,
|
||
timeout=timeout, machine_readable=machine_readable)
|
||
return DatabaseService.pg_execute(
|
||
sql, database=database or 'postgres', user=user or 'postgres')
|
||
|
||
# Not a default. An unrecognised engine that quietly ran as mysql would
|
||
# produce a syntax error blamed on the caller's SQL.
|
||
return {'success': False, 'output': '',
|
||
'error': f"unsupported engine: {tgt.get('engine')!r}"}
|