Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
d14b0a8
feature: forward BLOB methods through RpcDriver
aesslinger Jul 29, 2026
ab14827
feature: forward materialized view methods through RpcDriver
aesslinger Jul 29, 2026
e9e4df7
feature: resolve map_inferred_type from plugin manifest type_mappings
aesslinger Jul 30, 2026
7d3cb91
Merge branch 'plugins/forward-blob-methods' into phase-0-baseline-tests
aesslinger Jul 30, 2026
fb3ac5a
Merge branch 'plugins/forward-materialized-views' into phase-0-baseli…
aesslinger Jul 30, 2026
499daa8
Merge branch 'plugins/manifest-type-mappings' into phase-0-baseline-t…
aesslinger Jul 30, 2026
b7142e7
test: add Phase 0 PostgreSQL integration test infrastructure
aesslinger Jul 30, 2026
81650aa
test: add views, materialized views, routines, triggers, crud, multi-…
aesslinger Jul 30, 2026
67ba130
fix: correct API signatures in integration tests
aesslinger Jul 30, 2026
d35e4e1
test: complete Phase 0 integration test suite (72 tests)
aesslinger Jul 30, 2026
33eca80
fix: resolve all 72 integration test failures
aesslinger Jul 30, 2026
42e1c54
fix: tighten test assertions to strict TDD (no lenient passing)
aesslinger Jul 30, 2026
826653c
test: add golden file capture and 17 golden snapshot tests
aesslinger Jul 30, 2026
9ecd64a
test: un-ignore existing PostgreSQL integration tests
aesslinger Jul 30, 2026
05387d6
docs: add planning docs for postgres plugin migration
aesslinger Jul 30, 2026
ce2d25f
fix: address code review findings for CI reliability
aesslinger Jul 30, 2026
1c241b1
feature: forward BLOB methods through RpcDriver
aesslinger Jul 29, 2026
47be550
feature: forward materialized view methods through RpcDriver
aesslinger Jul 29, 2026
a95a781
feature: resolve map_inferred_type from plugin manifest type_mappings
aesslinger Jul 30, 2026
e619750
Merge branch 'plugins/rpc-extensions' into postgres-plugin-migration
aesslinger Jul 30, 2026
1e600bd
Merge remote-tracking branch 'upstream/main' into postgres-plugin-mig…
aesslinger Aug 4, 2026
bbd6cda
fix: retry transient pool errors in flaky integration tests
aesslinger Aug 4, 2026
09b3b1c
test: add parity test harness for dual-driver comparison
aesslinger Aug 4, 2026
bc91e50
fix: limit CI test parallelism to prevent pool exhaustion
aesslinger Aug 4, 2026
702936a
fix: run integration tests sequentially to eliminate pool flakiness
aesslinger Aug 4, 2026
9b0143a
fix: correct field name in parity_get_foreign_keys test
aesslinger Aug 4, 2026
1bc984f
test: add missing golden capture tests and CI artifact upload
aesslinger Aug 4, 2026
9c60f72
fix: handle known MV definition error in golden capture test
aesslinger Aug 4, 2026
3b3434b
test: commit generated golden files from CI
aesslinger Aug 4, 2026
c59b2a4
docs: mark Phase 0 complete, document golden file scope decisions
aesslinger Aug 4, 2026
1f52847
feature: scaffold postgres-plugin crate (Phase 1 Sprint 1)
aesslinger Aug 4, 2026
1561446
feature: implement client.rs with deadpool-postgres + TLS
aesslinger Aug 4, 2026
bad4393
feature: wire parity harness to spawn plugin driver
aesslinger Aug 4, 2026
e0de9c4
ci: build postgres-plugin and enable dual-driver parity testing
aesslinger Aug 4, 2026
655e090
fix: resolve compilation issues from Sprint 1 review
aesslinger Aug 4, 2026
432ee02
ci: separate parity tests from baseline (continue-on-error)
aesslinger Aug 4, 2026
4fd75f9
feature: implement get_databases, get_schemas, get_tables (Sprint 2)
aesslinger Aug 4, 2026
f4622c1
feature: implement get_columns, get_indexes, get_foreign_keys (Sprint 3)
aesslinger Aug 4, 2026
226cf2d
fix: match builtin driver's character_maximum_length extraction
aesslinger Aug 4, 2026
c399c84
feature: implement views, materialized views, routines, triggers (Spr…
aesslinger Aug 4, 2026
992d3c0
feature: implement execute_query, execute_query_batch, explain_query …
aesslinger Aug 4, 2026
604e448
fix: resolve compilation errors in Sprint 5
aesslinger Aug 4, 2026
b367d62
test: add 26 RED parity tests for full TDD coverage (Sprint 5)
aesslinger Aug 4, 2026
c4dc8dd
test: complete 80-test parity suite — full CP-4 TDD specification
aesslinger Aug 4, 2026
6961822
docs: update planning docs to reflect 80-test parity architecture
aesslinger Aug 4, 2026
9d44672
docs: note repo-extraction timing decision and open question
aesslinger Aug 5, 2026
d717639
feature: implement insert_record, update_record, delete_record (Sprin…
aesslinger Aug 5, 2026
40b5603
fix: correct 7 test-authoring bugs in the 80-test parity suite
aesslinger Aug 5, 2026
47bd5b2
fix: add missing BLOB wire-format decoding to binding cascade (regres…
aesslinger Aug 5, 2026
9e87cf7
fix: strip user LIMIT/OFFSET before appending pagination clause
aesslinger Aug 5, 2026
8dcaae9
fix: implement missing type extractors and truncated field (regression)
aesslinger Aug 5, 2026
74a0232
ci: run plugin unit tests (binding_tests, pagination_tests)
aesslinger Aug 5, 2026
3cfe752
fix: implement Debug for BoundValue so unit tests compile
aesslinger Aug 5, 2026
7b277c9
fix: cache connection pools by identity instead of rebuilding per call
aesslinger Aug 5, 2026
3be48ee
fix: exclude execution_time_ms from execute_batch parity comparison
aesslinger Aug 5, 2026
1f5bb58
fix: rewrite destructive-mutation parity tests to run per-target
aesslinger Aug 5, 2026
efaaaec
fix: add missing enum-CAST binding to CRUD (regression)
aesslinger Aug 5, 2026
b0eb4fe
feature: implement DDL generation methods (Sprint 7)
aesslinger Aug 5, 2026
23a5c95
feature: implement view and materialized view lifecycle (Sprint 8, ch…
aesslinger Aug 5, 2026
d95e007
feature: implement routine and trigger metadata/mutation (Sprint 8, c…
aesslinger Aug 5, 2026
ad765f3
feature: implement BLOB save/fetch (Sprint 8, chunk 3/3) — 82/82 parity
aesslinger Aug 5, 2026
2078949
Merge remote-tracking branch 'upstream/main' into postgres-plugin-mig…
aesslinger Aug 12, 2026
4149839
chore: remove in-tree postgres-plugin, plugin repo is now source of t…
aesslinger Aug 12, 2026
77fcb63
fix: SSL mode dropdown branches on capability, not driver id (#614)
aesslinger Aug 12, 2026
29fc233
fix: migrate already-persisted stale ssl_mode values (#614)
aesslinger Aug 12, 2026
72ed31b
fix: MCP schema default keys off dialect, not driver id (#614)
aesslinger Aug 12, 2026
c9a38fb
fix: identifier quoting keys off dialect, not driver id (#614)
aesslinger Aug 12, 2026
0b46ece
fix: trivial capability-driven quoting call-site swaps (#614)
aesslinger Aug 12, 2026
a82eff2
fix: thread driver capabilities through sidebar components (#614)
aesslinger Aug 12, 2026
da0fd9f
fix: thread driver capabilities through remaining call chains (#614)
aesslinger Aug 12, 2026
839eb59
fix: make sql_dialect a true Option so absence isn't silently postgres
aesslinger Aug 12, 2026
7f2e443
fix: connection-form host/port grid keys off dialect, not driver id (…
aesslinger Aug 12, 2026
c48a490
test: add parity coverage for enum values through execute_query
aesslinger Aug 12, 2026
98018dc
chore: reframe PR as release-ready, fix golden-file CI tautology
aesslinger Aug 13, 2026
7f1f755
Merge remote-tracking branch 'upstream/main' into postgres-plugin-mig…
aesslinger Aug 13, 2026
a969968
Merge remote-tracking branch 'upstream/main' into postgres-plugin-mig…
aesslinger Aug 13, 2026
a268872
chore: remove completed migration planning docs
aesslinger Aug 14, 2026
a2a6cfe
fix: parity harness must fail loud on a bad POSTGRES_PLUGIN_BIN
aesslinger Aug 14, 2026
0f85952
fix: assert_golden must fail on a missing golden file
aesslinger Aug 14, 2026
d626253
fix: 4 useCallback deps missing activeCapabilities in Editor.tsx
aesslinger Aug 14, 2026
3ddc463
fix: strip trailing blank line at EOF in mcp/tests.rs
aesslinger Aug 14, 2026
d8f386b
style: wrap long dialects.insert call to satisfy rustfmt
aesslinger Aug 14, 2026
5b8a77e
style: cargo fmt + remove unused imports in the new postgres_integrat…
aesslinger Aug 14, 2026
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
87 changes: 87 additions & 0 deletions .github/workflows/pg-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Integration Tests (PostgreSQL)

concurrency:
group: pg-integration-${{ github.ref }}
cancel-in-progress: true

on:
push:
branches: [main]
paths:
- 'src-tauri/**'
- '.github/workflows/pg-integration.yml'
- 'tests/fixtures/**'
pull_request:
branches: [main]
paths:
- 'src-tauri/**'
- '.github/workflows/pg-integration.yml'
- 'tests/fixtures/**'
workflow_dispatch:
inputs:
regenerate_golden:
description: 'Regenerate golden files and upload as an artifact for review (does not commit)'
type: boolean
default: false

jobs:
test-postgres:
runs-on: ubuntu-24.04

services:
postgres:
image: postgres:16
ports:
- 54320:5432
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6

- name: Seed PostgreSQL databases
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends postgresql-client
bash tests/fixtures/seed_postgres.sh

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry and build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-pg-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-pg-

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libsoup-3.0-dev build-essential libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev

- name: Run PostgreSQL integration tests
working-directory: src-tauri
env:
RUST_TEST_THREADS: "1"
REGENERATE_GOLDEN: ${{ (github.event_name == 'workflow_dispatch' && inputs.regenerate_golden) && '1' || '' }}
run: cargo test --test postgres_integration -- --include-ignored

- name: Upload golden files
if: github.event_name == 'workflow_dispatch' && inputs.regenerate_golden
uses: actions/upload-artifact@v4
with:
name: golden-files
path: src-tauri/tests/postgres_integration/golden/
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

270 changes: 268 additions & 2 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1424,8 +1424,9 @@ pub async fn duplicate_connection<R: Runtime>(
pub async fn get_connections<R: Runtime>(
app: AppHandle<R>,
) -> Result<Vec<SavedConnection>, String> {
// Run migration if needed
// Run migrations if needed
migrate_ssh_connections(&app).await.ok();
migrate_postgres_ssl_mode_spelling(&app).await.ok();

let path = get_config_path(&app)?;
// Use persistence function that handles both old and new formats
Expand Down Expand Up @@ -1559,6 +1560,120 @@ async fn migrate_ssh_connections<R: Runtime>(app: &AppHandle<R>) -> Result<(), S
Ok(())
}

// ==================== PostgreSQL Plugin SSL Mode Migration ====================

/// The SSL mode dropdown used to branch on `driver === "postgres"` literally
/// (issue #614), so a postgres-dialect driver with a different id (e.g. the
/// standalone PostgreSQL plugin, id `"postgresql"`) offered MySQL-style
/// underscored `ssl_mode` values instead of Postgres-style hyphenated ones.
/// The plugin's own TLS check only recognizes the hyphenated spelling, so a
/// connection saved with the wrong-family value connects in cleartext with no
/// error. Fixing the dropdown only stops *new* saves from getting the wrong
/// value — this rewrites values already persisted before the fix shipped.
///
/// Maps a stale MySQL-style `ssl_mode` value to its Postgres-style
/// equivalent. Returns `None` for a value that isn't one of the stale
/// MySQL-style spellings (including values already correct, or driver-
/// specific values like ClickHouse's `"disable"`/`"require"`, which happen to
/// already be spelled correctly in both families and need no rewrite).
fn stale_postgres_ssl_mode_replacement(value: &str) -> Option<&'static str> {
match value {
"disabled" => Some("disable"),
"preferred" => Some("prefer"),
"required" => Some("require"),
"verify_ca" => Some("verify-ca"),
"verify_identity" => Some("verify-full"),
_ => None,
}
}

/// Rewrites `conn.params.ssl_mode` in place if `conn`'s driver resolves (via
/// `dialects`) to the postgres SQL dialect and its stored value is a stale
/// MySQL-style spelling. Builtin `"postgres"` connections are excluded —
/// their own dropdown was always correct, so nothing there needs migrating.
/// A driver whose manifest doesn't declare `sql_dialect` (`None`, or absent
/// from the map because it never resolved) is treated as NOT postgres —
/// deliberately not defaulted, unlike the splitter's own historical
/// postgres-default, because this migration must distinguish "explicitly
/// postgres" from "unspecified" to avoid rewriting a driver's SSL value
/// based on a guess.
/// Pure and synchronous so it can be exercised directly in tests without a
/// live driver registry. Returns whether a rewrite happened.
fn migrate_connection_ssl_mode_in_place(
conn: &mut SavedConnection,
dialects: &HashMap<String, Option<crate::drivers::driver_trait::SqlDialect>>,
) -> bool {
if conn.params.driver == "postgres" {
return false;
}
let is_postgres_dialect = dialects
.get(&conn.params.driver)
.copied()
.flatten()
.is_some_and(|d| d == crate::drivers::driver_trait::SqlDialect::Postgres);
if !is_postgres_dialect {
return false;
}
let Some(stale) = conn.params.ssl_mode.as_deref() else {
return false;
};
let Some(replacement) = stale_postgres_ssl_mode_replacement(stale) else {
return false;
};
conn.params.ssl_mode = Some(replacement.to_string());
true
}

/// Migrates already-persisted `ssl_mode` values on postgres-dialect
/// connections (driver id other than the builtin `"postgres"`) from the
/// stale MySQL-style spelling to the Postgres-style spelling the plugin
/// actually understands. Idempotent — a no-op once every affected
/// connection has been rewritten.
async fn migrate_postgres_ssl_mode_spelling<R: Runtime>(app: &AppHandle<R>) -> Result<(), String> {
let conn_path = get_config_path(app)?;
if !conn_path.exists() {
return Ok(()); // Nothing to migrate
}

let mut conn_file = persistence::load_connections_file(&conn_path)?;

// Resolve each distinct non-builtin driver id's dialect once, not once
// per connection — the registry lookup is async and connections commonly
// share a driver.
let mut dialects: HashMap<String, Option<crate::drivers::driver_trait::SqlDialect>> =
HashMap::new();
for conn in &conn_file.connections {
let driver_id = &conn.params.driver;
if driver_id == "postgres" || dialects.contains_key(driver_id) {
continue; // builtin driver's own dropdown was always correct
}
if let Some(driver) = crate::drivers::registry::get_driver(driver_id).await {
dialects.insert(
driver_id.clone(),
driver.manifest().capabilities.sql_dialect,
);
}
}

let mut migrated_count = 0usize;
for conn in conn_file.connections.iter_mut() {
if migrate_connection_ssl_mode_in_place(conn, &dialects) {
migrated_count += 1;
}
}

if migrated_count == 0 {
return Ok(()); // No migration needed
}

eprintln!(
"[Migration] Rewriting stale ssl_mode spelling on {} postgres-dialect connection(s)",
migrated_count
);
save_connections_and_invalidate(app, &conn_path, &conn_file)?;
Ok(())
}

#[tauri::command]
pub async fn get_ssh_connections<R: Runtime>(
app: AppHandle<R>,
Expand Down Expand Up @@ -2458,6 +2573,156 @@ mod tests {
}
}

#[test]
fn stale_postgres_ssl_mode_replacement_maps_every_mysql_style_value() {
assert_eq!(stale_postgres_ssl_mode_replacement("disabled"), Some("disable"));
assert_eq!(stale_postgres_ssl_mode_replacement("preferred"), Some("prefer"));
assert_eq!(stale_postgres_ssl_mode_replacement("required"), Some("require"));
assert_eq!(stale_postgres_ssl_mode_replacement("verify_ca"), Some("verify-ca"));
assert_eq!(
stale_postgres_ssl_mode_replacement("verify_identity"),
Some("verify-full"),
);
}

#[test]
fn stale_postgres_ssl_mode_replacement_leaves_already_correct_values_alone() {
for already_correct in
["disable", "allow", "prefer", "require", "verify-ca", "verify-full"]
{
assert_eq!(
stale_postgres_ssl_mode_replacement(already_correct),
None,
"{already_correct} should not be rewritten",
);
}
}

#[test]
fn stale_postgres_ssl_mode_replacement_ignores_unrecognized_values() {
assert_eq!(stale_postgres_ssl_mode_replacement(""), None);
assert_eq!(stale_postgres_ssl_mode_replacement("not-a-real-mode"), None);
}

fn saved_connection(driver: &str, ssl_mode: Option<&str>) -> SavedConnection {
SavedConnection {
id: "conn-1".to_string(),
name: "test".to_string(),
params: ConnectionParams {
driver: driver.to_string(),
ssl_mode: ssl_mode.map(str::to_string),
..base_params()
},
group_id: None,
sort_order: None,
detect_json_in_text_columns: None,
appearance: None,
tag_ids: None,
environment: None,
}
}

#[test]
fn migrate_connection_ssl_mode_rewrites_a_plugin_postgres_connection() {
let mut dialects = HashMap::new();
dialects.insert(
"postgresql".to_string(),
Some(crate::drivers::driver_trait::SqlDialect::Postgres),
);
let mut conn = saved_connection("postgresql", Some("required"));

let rewrote = migrate_connection_ssl_mode_in_place(&mut conn, &dialects);

assert!(rewrote);
assert_eq!(conn.params.ssl_mode.as_deref(), Some("require"));
}

#[test]
fn migrate_connection_ssl_mode_leaves_a_mysql_connection_alone() {
// "required" is the CORRECT spelling for mysql — this is the case
// that makes the migration driver-aware rather than a blanket
// string-remap. A dialect map that (incorrectly) resolved mysql to
// Postgres would also demonstrate the bug this guards against, so
// this test exercises the real decision, not just the dialect map.
let mut dialects = HashMap::new();
dialects.insert(
"mysql".to_string(),
Some(crate::drivers::driver_trait::SqlDialect::Mysql),
);
let mut conn = saved_connection("mysql", Some("required"));

let rewrote = migrate_connection_ssl_mode_in_place(&mut conn, &dialects);

assert!(!rewrote);
assert_eq!(conn.params.ssl_mode.as_deref(), Some("required"));
}

#[test]
fn migrate_connection_ssl_mode_leaves_the_builtin_postgres_driver_alone() {
// The builtin driver's own dropdown was always correct — even if it
// somehow ended up with a stale value, this migration is scoped to
// plugin-driven connections only (driver id != "postgres").
let mut dialects = HashMap::new();
dialects.insert(
"postgres".to_string(),
Some(crate::drivers::driver_trait::SqlDialect::Postgres),
);
let mut conn = saved_connection("postgres", Some("required"));

let rewrote = migrate_connection_ssl_mode_in_place(&mut conn, &dialects);

assert!(!rewrote);
assert_eq!(conn.params.ssl_mode.as_deref(), Some("required"));
}

#[test]
fn migrate_connection_ssl_mode_leaves_an_unresolved_driver_alone() {
// No entry in `dialects` (e.g. the driver failed to resolve from the
// registry) must not be treated as postgres-dialect by default.
let dialects = HashMap::new();
let mut conn = saved_connection("postgresql", Some("required"));

let rewrote = migrate_connection_ssl_mode_in_place(&mut conn, &dialects);

assert!(!rewrote);
assert_eq!(conn.params.ssl_mode.as_deref(), Some("required"));
}

#[test]
fn migrate_connection_ssl_mode_leaves_a_resolved_driver_with_no_declared_dialect_alone() {
// A driver that resolves from the registry but whose manifest omits
// `sql_dialect` entirely (e.g. the Oracle plugin, which sets
// supports_ssl but declares no dialect) must be treated as NOT
// postgres-dialect — `None`, not defaulted to `Some(Postgres)`.
// Getting this wrong would rewrite that driver's legitimately-spelled
// SSL value based on a guess, exactly the bug this test guards
// against.
let mut dialects = HashMap::new();
dialects.insert("oracle".to_string(), None);
let mut conn = saved_connection("oracle", Some("required"));

let rewrote = migrate_connection_ssl_mode_in_place(&mut conn, &dialects);

assert!(!rewrote);
assert_eq!(conn.params.ssl_mode.as_deref(), Some("required"));
}

#[test]
fn migrate_connection_ssl_mode_is_idempotent() {
let mut dialects = HashMap::new();
dialects.insert(
"postgresql".to_string(),
Some(crate::drivers::driver_trait::SqlDialect::Postgres),
);
let mut conn = saved_connection("postgresql", Some("required"));

assert!(migrate_connection_ssl_mode_in_place(&mut conn, &dialects));
assert_eq!(conn.params.ssl_mode.as_deref(), Some("require"));
// Second pass: the value is already correct, nothing to rewrite.
assert!(!migrate_connection_ssl_mode_in_place(&mut conn, &dialects));
assert_eq!(conn.params.ssl_mode.as_deref(), Some("require"));
}

#[test]
fn persisted_params_never_contain_the_connection_uri() {
let sentinel = "mongodb+srv://fixture-user:fixture-password@cluster.example.invalid/app";
Expand Down Expand Up @@ -5466,8 +5731,9 @@ pub async fn get_connection_groups<R: Runtime>(
pub async fn get_connections_with_groups<R: Runtime>(
app: AppHandle<R>,
) -> Result<ConnectionsFile, String> {
// Run migration if needed
// Run migrations if needed
migrate_ssh_connections(&app).await.ok();
migrate_postgres_ssl_mode_spelling(&app).await.ok();

let path = get_config_path(&app)?;
persistence::load_connections_file(&path)
Expand Down
Loading
Loading