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:
2026-08-04 17:20:14 +08:00
committed by GitHub
parent a9dbf60ed5
commit 002d8345ae
66 changed files with 3591 additions and 1602 deletions
+19 -5
View File
@@ -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"));
}
}
}
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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"));
}