v0.6.0: release workflow, constraint-aware cell editing, pending-cell styling (#9)
* feat: demo DB seed/regenerate, backup/restore/sync refinements, SSH/SSL polish - Demo SQLite DB: feature-rich seed (12 objects, 500-row audit log) + regenerate action in Settings - Backup/restore: headless-testable core logic, psql -f for plain dumps, sync passes --clean --if-exists - SSH/SSL runtime refinements and connection testing improvements - Data grid: DataTypeIcon component, FK popover, table tree polish - Docs: AGENTS.md/README updates, new screenshots, MIT LICENSE * chore: optimize README screenshots (4.7 MB → 916 KB via pngquant, quality 70-90) * v0.6.0: release workflow, constraint-aware cell editing, pending-cell styling - Bump version to 0.6.0 across package.json, Cargo.toml, tauri.conf.json - Add .github/workflows/release.yml: tag-triggered CI builds macOS (aarch64 + x64), Windows, and Linux installers into a draft GitHub Release - README: installer download table (unsigned note, per-platform files), "how releases are made" section - CellEditor: constraint-aware commit — empty input on nullable columns becomes NULL, NOT NULL text-like types fall back to empty string, all other types blocked with an inline error bubble; replace "Set NULL" checkbox with a NULL row in the FK dropdown / empty enum option - VirtualDataGrid: pending-edit dot → animated pending outline (ring) on staged cells; matching test updates - docs-coverage test: align with rewritten README comparison table
This commit is contained in:
@@ -430,20 +430,31 @@ mod tests {
|
||||
#[test]
|
||||
fn pg_indexes_query_is_parameterized_and_joins() {
|
||||
let sql = pg_indexes_query("public");
|
||||
assert!(sql.contains("$1"), "schema must be parameterized; got: {}", sql);
|
||||
assert!(
|
||||
sql.contains("$1"),
|
||||
"schema must be parameterized; got: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
sql.contains("pg_indexes") || sql.contains("pg_index"),
|
||||
"should query pg_index; got: {}",
|
||||
sql
|
||||
);
|
||||
assert!(sql.contains("pg_get_indexdef"), "should include index definition");
|
||||
assert!(
|
||||
sql.contains("pg_get_indexdef"),
|
||||
"should include index definition"
|
||||
);
|
||||
assert!(sql.contains("indisunique"), "should include uniqueness");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pg_constraints_query_filters_check_unique_exclusion() {
|
||||
let sql = pg_constraints_query("public");
|
||||
assert!(sql.contains("$1"), "schema must be parameterized; got: {}", sql);
|
||||
assert!(
|
||||
sql.contains("$1"),
|
||||
"schema must be parameterized; got: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
sql.contains("pg_constraint"),
|
||||
"should query pg_constraint; got: {}",
|
||||
@@ -453,7 +464,10 @@ mod tests {
|
||||
assert!(sql.contains("'c'"), "should filter CHECK ('c')");
|
||||
assert!(sql.contains("'u'"), "should filter UNIQUE ('u')");
|
||||
assert!(sql.contains("'x'"), "should filter EXCLUSION ('x')");
|
||||
assert!(sql.contains("pg_get_constraintdef"), "should include definition");
|
||||
assert!(
|
||||
sql.contains("pg_get_constraintdef"),
|
||||
"should include definition"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -610,4 +624,4 @@ mod tests {
|
||||
let sql = pg_extensions_query();
|
||||
assert!(sql.contains("pg_extension"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod pool;
|
||||
pub mod introspection;
|
||||
pub mod pool;
|
||||
pub mod tls;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use pool::{ConnectionPoolManager, DbConfig, DbHandle};
|
||||
pub use pool::{ConnectionPoolManager, DbConfig, DbHandle};
|
||||
|
||||
+41
-12
@@ -250,22 +250,48 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn db_config_ssh_config_is_none_when_no_host() {
|
||||
let cfg = DbConfig { db_type: "PostgreSQL".into(), host: "h".into(), port: Some(5432),
|
||||
username: None, password: None, database: None, ssl_mode: None, ssl_ca_path: None,
|
||||
ssl_cert_path: None, ssl_key_path: None, ssh_host: None, ssh_port: None, ssh_user: None,
|
||||
ssh_auth_method: None, ssh_password: None, ssh_private_key_path: None, ssh_passphrase: None,
|
||||
let cfg = DbConfig {
|
||||
db_type: "PostgreSQL".into(),
|
||||
host: "h".into(),
|
||||
port: Some(5432),
|
||||
username: None,
|
||||
password: None,
|
||||
database: None,
|
||||
ssl_mode: None,
|
||||
ssl_ca_path: None,
|
||||
ssl_cert_path: None,
|
||||
ssl_key_path: None,
|
||||
ssh_host: None,
|
||||
ssh_port: None,
|
||||
ssh_user: None,
|
||||
ssh_auth_method: None,
|
||||
ssh_password: None,
|
||||
ssh_private_key_path: None,
|
||||
ssh_passphrase: None,
|
||||
};
|
||||
assert!(cfg.ssh_config().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn db_config_ssh_config_builds_from_flat_fields() {
|
||||
let cfg = DbConfig { db_type: "PostgreSQL".into(), host: "db".into(), port: Some(5432),
|
||||
username: None, password: None, database: None, ssl_mode: None, ssl_ca_path: None,
|
||||
ssl_cert_path: None, ssl_key_path: None,
|
||||
ssh_host: Some("jump".into()), ssh_port: Some(2222), ssh_user: Some("u".into()),
|
||||
ssh_auth_method: Some("password".into()), ssh_password: Some("pw".into()),
|
||||
ssh_private_key_path: None, ssh_passphrase: None,
|
||||
let cfg = DbConfig {
|
||||
db_type: "PostgreSQL".into(),
|
||||
host: "db".into(),
|
||||
port: Some(5432),
|
||||
username: None,
|
||||
password: None,
|
||||
database: None,
|
||||
ssl_mode: None,
|
||||
ssl_ca_path: None,
|
||||
ssl_cert_path: None,
|
||||
ssl_key_path: None,
|
||||
ssh_host: Some("jump".into()),
|
||||
ssh_port: Some(2222),
|
||||
ssh_user: Some("u".into()),
|
||||
ssh_auth_method: Some("password".into()),
|
||||
ssh_password: Some("pw".into()),
|
||||
ssh_private_key_path: None,
|
||||
ssh_passphrase: None,
|
||||
};
|
||||
let s = cfg.ssh_config().expect("ssh config present");
|
||||
assert_eq!(s.host, "jump");
|
||||
@@ -339,7 +365,10 @@ mod tests {
|
||||
manager.register("d", DbHandle::Sqlite(conn_d));
|
||||
|
||||
assert_eq!(manager.pools().len(), 3);
|
||||
assert!(manager.contains("a"), "'a' was recently accessed, should survive");
|
||||
assert!(
|
||||
manager.contains("a"),
|
||||
"'a' was recently accessed, should survive"
|
||||
);
|
||||
assert!(!manager.contains("b"), "'b' is LRU and should be evicted");
|
||||
assert!(manager.contains("c"));
|
||||
assert!(manager.contains("d"));
|
||||
@@ -385,4 +414,4 @@ mod tests {
|
||||
manager.set_max_pools(1);
|
||||
assert_eq!(evicted.lock().unwrap().as_slice(), ["a".to_string()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-24
@@ -113,14 +113,13 @@ fn load_client_identity(
|
||||
);
|
||||
}
|
||||
let cb = std::fs::read(cert_path).map_err(|e| format!("read cert: {e}"))?;
|
||||
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut std::io::BufReader::new(
|
||||
cb.as_slice(),
|
||||
))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("parse cert: {e}"))?
|
||||
.into_iter()
|
||||
.map(|c| c.into_owned())
|
||||
.collect();
|
||||
let certs: Vec<CertificateDer<'static>> =
|
||||
rustls_pemfile::certs(&mut std::io::BufReader::new(cb.as_slice()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("parse cert: {e}"))?
|
||||
.into_iter()
|
||||
.map(|c| c.into_owned())
|
||||
.collect();
|
||||
if certs.is_empty() {
|
||||
return Err("no client certificates parsed".into());
|
||||
}
|
||||
@@ -179,29 +178,37 @@ mod tests {
|
||||
#[test]
|
||||
fn tls_decision_maps_modes() {
|
||||
assert!(matches!(tls_decision(None), TlsDecision::Disable));
|
||||
assert!(matches!(tls_decision(Some("disable")), TlsDecision::Disable));
|
||||
assert!(matches!(tls_decision(Some("require")), TlsDecision::Require));
|
||||
assert!(matches!(tls_decision(Some("verify-ca")), TlsDecision::Verify));
|
||||
assert!(matches!(tls_decision(Some("verify-full")), TlsDecision::Verify));
|
||||
assert!(matches!(
|
||||
tls_decision(Some("disable")),
|
||||
TlsDecision::Disable
|
||||
));
|
||||
assert!(matches!(
|
||||
tls_decision(Some("require")),
|
||||
TlsDecision::Require
|
||||
));
|
||||
assert!(matches!(
|
||||
tls_decision(Some("verify-ca")),
|
||||
TlsDecision::Verify
|
||||
));
|
||||
assert!(matches!(
|
||||
tls_decision(Some("verify-full")),
|
||||
TlsDecision::Verify
|
||||
));
|
||||
assert!(matches!(tls_decision(Some("bogus")), TlsDecision::Disable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tls_disable_returns_none() {
|
||||
assert!(
|
||||
build_tls_config(TlsDecision::Disable, None, None, None)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert!(build_tls_config(TlsDecision::Disable, None, None, None)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tls_require_returns_some_without_files() {
|
||||
assert!(
|
||||
build_tls_config(TlsDecision::Require, None, None, None)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
assert!(build_tls_config(TlsDecision::Require, None, None, None)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -214,8 +221,13 @@ mod tests {
|
||||
#[test]
|
||||
fn build_tls_client_cert_missing_key_errors() {
|
||||
// cert set without key
|
||||
let err = build_tls_config(TlsDecision::Require, None, Some("/nonexistent/cert.pem"), None)
|
||||
.unwrap_err();
|
||||
let err = build_tls_config(
|
||||
TlsDecision::Require,
|
||||
None,
|
||||
Some("/nonexistent/cert.pem"),
|
||||
None,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.to_lowercase().contains("cert") || err.to_lowercase().contains("key"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user