diff --git a/CHANGELOG.md b/CHANGELOG.md index 709b00d..a695162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Added + +- `get_foreign_keys` and `get_all_foreign_keys_batch` now return `ref_schema`, + the schema of the referenced table. A foreign key into another schema + (`billing.carry_forwards.organization_id → core.organizations.id`) no longer + makes the Related Records panel query `billing.organizations`. Byte-for-byte + parity with the built-in driver (companion to the tabularis host change). + ## [1.0.0-rc.14] - 2026-11-10 ### Added diff --git a/src/handlers/metadata.rs b/src/handlers/metadata.rs index 3f6eaed..8b3441e 100644 --- a/src/handlers/metadata.rs +++ b/src/handlers/metadata.rs @@ -427,6 +427,9 @@ fn row_to_foreign_key(r: &tokio_postgres::Row) -> Value { let column_name: String = r.try_get("column_name").unwrap_or_default(); let ref_table: String = r.try_get("foreign_table_name").unwrap_or_default(); let ref_column: String = r.try_get("foreign_column_name").unwrap_or_default(); + // The referenced table can live in another schema than the source table; + // without this the host qualifies `ref_table` with the source schema. + let ref_schema: Option = r.try_get("foreign_schema_name").ok().flatten(); let on_update: Option = r.try_get("update_rule").ok().flatten(); let on_delete: Option = r.try_get("delete_rule").ok().flatten(); // WITH ORDINALITY yields the position within conkey/confkey — the true @@ -449,6 +452,12 @@ fn row_to_foreign_key(r: &tokio_postgres::Row) -> Value { "on_update": on_update, }); + if let Some(schema) = ref_schema { + fk.as_object_mut() + .unwrap() + .insert("ref_schema".to_string(), json!(schema)); + } + if let Some(seq) = seq_in_fk { fk.as_object_mut() .unwrap() diff --git a/tests/live_db.rs b/tests/live_db.rs index a050507..bd95207 100644 --- a/tests/live_db.rs +++ b/tests/live_db.rs @@ -2640,6 +2640,47 @@ fn get_materialized_view_definition_returns_ddl_not_not_implemented() { ); } +/// A foreign key into another schema reports that schema as `ref_schema` +/// from both `get_foreign_keys` and `get_all_foreign_keys_batch`, so the +/// host doesn't qualify `ref_table` with the source schema. +#[test] +fn foreign_keys_report_the_referenced_schema_for_cross_schema_references() { + let mut plugin = Plugin::spawn(); + let params = conn_params(); + + for query in [ + "DROP SCHEMA IF EXISTS live_db_fk_ref CASCADE", + "DROP SCHEMA IF EXISTS live_db_fk_src CASCADE", + "CREATE SCHEMA live_db_fk_ref", + "CREATE SCHEMA live_db_fk_src", + "CREATE TABLE live_db_fk_ref.parent (id INTEGER PRIMARY KEY)", + "CREATE TABLE live_db_fk_src.child \ + (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES live_db_fk_ref.parent(id))", + ] { + plugin.call_ok("execute_query", json!({"params": params, "query": query})); + } + + let fks = plugin.call_ok( + "get_foreign_keys", + json!({"params": params, "table": "child", "schema": "live_db_fk_src"}), + ); + assert_eq!(fks[0]["ref_table"], json!("parent")); + assert_eq!(fks[0]["ref_schema"], json!("live_db_fk_ref")); + + let batch = plugin.call_ok( + "get_all_foreign_keys_batch", + json!({"params": params, "schema": "live_db_fk_src"}), + ); + assert_eq!(batch["child"][0]["ref_schema"], json!("live_db_fk_ref")); + + for query in [ + "DROP SCHEMA IF EXISTS live_db_fk_src CASCADE", + "DROP SCHEMA IF EXISTS live_db_fk_ref CASCADE", + ] { + plugin.call_ok("execute_query", json!({"params": params, "query": query})); + } +} + /// A nonexistent materialized view surfaces a clean error, not a silent /// success or a crash — the `($1::text)::regclass` cast raises a PostgreSQL /// error that maps to a -32603 response. Mirrors the existing