feat(usable): integrate current dbtool implementation snapshot
Some checks failed
release-smoke / macos-13 / x86_64-apple-darwin (push) Has been cancelled
release-smoke / ubuntu-latest / x86_64-unknown-linux-gnu (push) Has been cancelled
release-smoke / windows-latest / x86_64-pc-windows-msvc (push) Has been cancelled

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Paperclip CTO
2026-04-02 08:26:18 +00:00
parent a28dab4cd9
commit d5f69462b0
73 changed files with 5895 additions and 322 deletions

View File

@@ -1,7 +1,11 @@
use db_config::ConnectionProfile;
use db_config::{
ConfigError, ConnectionProfile, ConnectionProfileCatalog, ConnectionProfileStore,
SessionSecretStore,
};
use db_core::{
ColumnSummary, CoreError, ExportFormat, ExportRequest, InspectRequest, InspectResult,
QueryRequest, QueryResult, QuerySource, SchemaSummary, TableSummary,
QueryRequest, QueryResult, QuerySource, SchemaAvailability as CoreSchemaAvailability,
SchemaSummary, TableSummary,
};
use db_drivers::{DriverError, DriverRegistry};
use serde::Serialize;
@@ -14,6 +18,10 @@ pub enum AppOperation {
Inspect,
Query,
Export,
LoadProfiles,
SaveProfile,
DeleteProfile,
ActivateProfile,
}
impl AppOperation {
@@ -23,6 +31,10 @@ impl AppOperation {
Self::Inspect => "inspect",
Self::Query => "query",
Self::Export => "export",
Self::LoadProfiles => "load_profiles",
Self::SaveProfile => "save_profile",
Self::DeleteProfile => "delete_profile",
Self::ActivateProfile => "activate_profile",
}
}
@@ -32,6 +44,10 @@ impl AppOperation {
"inspect" => Self::Inspect,
"query" => Self::Query,
"export" => Self::Export,
"load_profiles" => Self::LoadProfiles,
"save_profile" => Self::SaveProfile,
"delete_profile" => Self::DeleteProfile,
"activate_profile" => Self::ActivateProfile,
other => panic!("unsupported app operation: {other}"),
}
}
@@ -115,6 +131,41 @@ impl ConnectResponse {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SecretSource {
None,
Session,
EnvVar,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct StoredProfileSummary {
pub target: ConnectionSummary,
pub is_active: bool,
pub secret_source: SecretSource,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ProfileCatalogResponse {
pub active_profile_name: Option<String>,
pub profiles: Vec<StoredProfileSummary>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SessionSecretInput {
Preserve,
Set(String),
Clear,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SaveProfileRequest {
pub profile: ConnectionProfile,
pub set_active: bool,
pub session_secret: SessionSecretInput,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct InspectScope {
pub schema: Option<String>,
@@ -164,14 +215,37 @@ impl InspectResponse {
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SchemaAvailability {
Ready,
Restricted,
}
impl From<CoreSchemaAvailability> for SchemaAvailability {
fn from(value: CoreSchemaAvailability) -> Self {
match value {
CoreSchemaAvailability::Ready => Self::Ready,
CoreSchemaAvailability::Restricted => Self::Restricted,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct SchemaItem {
pub name: String,
pub availability: SchemaAvailability,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
impl From<SchemaSummary> for SchemaItem {
fn from(value: SchemaSummary) -> Self {
Self { name: value.name }
Self {
name: value.name,
availability: SchemaAvailability::from(value.availability),
note: value.note,
}
}
}
@@ -349,6 +423,7 @@ pub enum AppErrorKind {
Inspect,
Query,
Export,
Config,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
@@ -397,6 +472,120 @@ pub fn connect(profile: &ConnectionProfile) -> Result<ConnectResponse, AppError>
})
}
pub fn load_profile_catalog(
store: &ConnectionProfileStore,
secrets: &SessionSecretStore,
) -> Result<ProfileCatalogResponse, AppError> {
let catalog = load_catalog(store, "load_profiles")?;
Ok(profile_catalog_response(&catalog, secrets))
}
pub fn save_profile(
store: &ConnectionProfileStore,
secrets: &mut SessionSecretStore,
request: SaveProfileRequest,
) -> Result<ProfileCatalogResponse, AppError> {
validate_profile("save_profile", &request.profile)?;
let inline_secret = request.profile.target.password.clone();
let mut catalog = load_catalog(store, "save_profile")?;
catalog
.upsert_profile(request.profile.clone())
.map_err(|error| map_config_error("save_profile", Some(&request.profile), error))?;
if request.set_active {
catalog
.set_active_profile(Some(&request.profile.name))
.map_err(|error| map_config_error("save_profile", Some(&request.profile), error))?;
}
apply_session_secret_change(
secrets,
&request.profile,
request.session_secret,
inline_secret,
)
.map_err(|error| map_config_error("save_profile", Some(&request.profile), error))?;
save_catalog(store, "save_profile", Some(&request.profile), &catalog)?;
Ok(profile_catalog_response(&catalog, secrets))
}
pub fn delete_profile(
store: &ConnectionProfileStore,
secrets: &mut SessionSecretStore,
profile_name: &str,
) -> Result<ProfileCatalogResponse, AppError> {
let mut catalog = load_catalog(store, "delete_profile")?;
if catalog.remove_profile(profile_name).is_none() {
return Err(map_config_error(
"delete_profile",
None,
ConfigError::ProfileNotFound(profile_name.to_string()),
));
}
secrets.remove(profile_name);
save_catalog(store, "delete_profile", None, &catalog)?;
Ok(profile_catalog_response(&catalog, secrets))
}
pub fn activate_profile(
store: &ConnectionProfileStore,
secrets: &SessionSecretStore,
profile_name: &str,
) -> Result<ProfileCatalogResponse, AppError> {
let mut catalog = load_catalog(store, "activate_profile")?;
catalog
.set_active_profile(Some(profile_name))
.map_err(|error| map_config_error("activate_profile", None, error))?;
save_catalog(
store,
"activate_profile",
catalog.profile(profile_name),
&catalog,
)?;
Ok(profile_catalog_response(&catalog, secrets))
}
pub fn resolve_profile(
store: &ConnectionProfileStore,
secrets: &SessionSecretStore,
profile_name: &str,
) -> Result<ConnectionProfile, AppError> {
let catalog = load_catalog(store, "load_profiles")?;
let profile = catalog.profile(profile_name).ok_or_else(|| {
map_config_error(
"load_profiles",
None,
ConfigError::ProfileNotFound(profile_name.to_string()),
)
})?;
Ok(secrets.materialize_profile(profile))
}
pub fn resolve_active_profile(
store: &ConnectionProfileStore,
secrets: &SessionSecretStore,
) -> Result<Option<ConnectionProfile>, AppError> {
let catalog = load_catalog(store, "load_profiles")?;
Ok(catalog
.active_profile()
.map(|profile| secrets.materialize_profile(profile)))
}
pub fn connect_saved_profile(
store: &ConnectionProfileStore,
secrets: &SessionSecretStore,
profile_name: &str,
) -> Result<ConnectResponse, AppError> {
let profile = resolve_profile(store, secrets, profile_name)?;
connect(&profile)
}
pub fn inspect(
profile: &ConnectionProfile,
request: &InspectRequest,
@@ -570,6 +759,93 @@ fn map_driver_error(
}
}
fn load_catalog(
store: &ConnectionProfileStore,
operation: &'static str,
) -> Result<ConnectionProfileCatalog, AppError> {
store
.load()
.map_err(|error| map_config_error(operation, None, error))
}
fn save_catalog(
store: &ConnectionProfileStore,
operation: &'static str,
profile: Option<&ConnectionProfile>,
catalog: &ConnectionProfileCatalog,
) -> Result<(), AppError> {
store
.save(catalog)
.map_err(|error| map_config_error(operation, profile, error))
}
fn map_config_error(
operation: &'static str,
profile: Option<&ConnectionProfile>,
error: ConfigError,
) -> AppError {
AppError {
operation,
kind: AppErrorKind::Config,
message: error.to_string(),
target: profile.map(ConnectionSummary::from),
}
}
fn profile_catalog_response(
catalog: &ConnectionProfileCatalog,
secrets: &SessionSecretStore,
) -> ProfileCatalogResponse {
ProfileCatalogResponse {
active_profile_name: catalog.active_profile.clone(),
profiles: catalog
.profiles
.iter()
.map(|profile| StoredProfileSummary {
target: ConnectionSummary::from(profile),
is_active: catalog.active_profile.as_deref() == Some(profile.name.as_str()),
secret_source: profile_secret_source(profile, secrets),
})
.collect(),
}
}
fn profile_secret_source(
profile: &ConnectionProfile,
secrets: &SessionSecretStore,
) -> SecretSource {
if secrets.contains(&profile.name) {
SecretSource::Session
} else if profile.password_env_var.is_some() {
SecretSource::EnvVar
} else {
SecretSource::None
}
}
fn apply_session_secret_change(
secrets: &mut SessionSecretStore,
profile: &ConnectionProfile,
session_secret: SessionSecretInput,
inline_secret: Option<String>,
) -> Result<(), ConfigError> {
match session_secret {
SessionSecretInput::Preserve => {
if let Some(secret) = inline_secret {
secrets.set(profile.name.clone(), secret)?;
}
}
SessionSecretInput::Set(secret) => {
secrets.set(profile.name.clone(), secret)?;
}
SessionSecretInput::Clear => {
secrets.remove(&profile.name);
}
}
Ok(())
}
fn export_format_name(format: ExportFormat) -> &'static str {
match format {
ExportFormat::Csv => "csv",
@@ -701,6 +977,21 @@ mod tests {
);
}
#[test]
fn schema_item_preserves_restricted_availability_and_note() {
let item = SchemaItem::from(SchemaSummary::restricted(
"restricted_probe",
"schema access is restricted: restricted_probe",
));
assert_eq!(item.name, "restricted_probe");
assert_eq!(item.availability, SchemaAvailability::Restricted);
assert_eq!(
item.note.as_deref(),
Some("schema access is restricted: restricted_probe")
);
}
#[test]
fn inspect_response_uses_empty_state_for_empty_payload() {
let response = InspectResponse {
@@ -796,6 +1087,110 @@ mod tests {
assert!(json.get("data").is_none());
}
#[test]
fn save_profile_persists_redacted_catalog_and_keeps_session_secret_only_in_memory() {
let path = temp_store_path("dbtool-app-profile-save");
let store = ConnectionProfileStore::new(&path);
let mut secrets = SessionSecretStore::default();
let profile = ConnectionProfile::new(
"reporting-postgres",
ConnectionTarget::new(
DatabaseKind::Postgres,
ConnectionTransport::Tcp {
host: String::from("127.0.0.1"),
port: 55432,
},
Some(String::from("dbtool_demo")),
Some(String::from("dbtool")),
)
.expect("target should be valid"),
Some(String::from("DBTOOL_PASSWORD")),
)
.expect("profile should be valid")
.with_password(Some(String::from("super-secret")));
let response = save_profile(
&store,
&mut secrets,
SaveProfileRequest {
profile,
set_active: true,
session_secret: SessionSecretInput::Preserve,
},
)
.expect("profile should save");
let raw = fs::read_to_string(&path).expect("store file should exist");
let resolved =
resolve_active_profile(&store, &secrets).expect("active profile should resolve");
assert_eq!(
response.active_profile_name.as_deref(),
Some("reporting-postgres")
);
assert_eq!(response.profiles.len(), 1);
assert_eq!(response.profiles[0].secret_source, SecretSource::Session);
assert!(!raw.contains("super-secret"));
assert!(!raw.contains("\"password\":"));
assert_eq!(
resolved
.expect("active profile should exist")
.target
.password
.as_deref(),
Some("super-secret")
);
fs::remove_file(path).expect("temporary store should be removed");
}
#[test]
fn connect_saved_profile_uses_persisted_sqlite_target() {
let db_path = temp_sqlite_db_path("dbtool-app-connect-saved");
let connection =
SqliteConnection::open(&db_path).expect("temporary sqlite database should open");
connection
.execute_batch(include_str!("../../../examples/fixtures/sqlite/init.sql"))
.expect("sqlite fixture should seed");
drop(connection);
let store_path = temp_store_path("dbtool-app-profile-connect");
let store = ConnectionProfileStore::new(&store_path);
let mut catalog = ConnectionProfileCatalog::empty();
catalog
.upsert_profile(
ConnectionProfile::new(
"sqlite-local",
ConnectionTarget::new(
DatabaseKind::Sqlite,
ConnectionTransport::File {
path: db_path.clone(),
},
None,
None,
)
.expect("target should be valid"),
None,
)
.expect("profile should be valid"),
)
.expect("profile should be inserted");
catalog
.set_active_profile(Some("sqlite-local"))
.expect("active profile should be set");
store.save(&catalog).expect("catalog should save");
let response =
connect_saved_profile(&store, &SessionSecretStore::default(), "sqlite-local")
.expect("saved sqlite profile should connect");
assert_eq!(response.target.profile_name, "sqlite-local");
assert_eq!(response.target.driver, "sqlite");
assert_eq!(response.status, "connected");
fs::remove_file(store_path).expect("temporary store should be removed");
fs::remove_file(db_path).expect("temporary sqlite database should be removed");
}
fn connection_summary() -> ConnectionSummary {
ConnectionSummary {
profile_name: String::from("qa-demo"),
@@ -812,4 +1207,12 @@ mod tests {
.as_nanos();
env::temp_dir().join(format!("{prefix}-{unique}.sqlite"))
}
fn temp_store_path(prefix: &str) -> std::path::PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
env::temp_dir().join(format!("{prefix}-{unique}.json"))
}
}