Skip to content

Commit cca7b67

Browse files
tianzhouclaude
andauthored
fix(postgres): include foreign tables in getTables so search_objects can discover them (#419)
Postgres reports foreign tables (postgres_fdw, file_fdw, ...) in information_schema.tables with table_type = 'FOREIGN', so the 'BASE TABLE' filter hid them from search_objects even though every per-table detail query (columns, comment, row count, tableExists) already handled them. Widen the filter to include 'FOREIGN'. Also let getTableIndexes see partitioned tables (relkind 'p'), which carry their own index entries since PG11 but were filtered out. Adds integration coverage using a handler-less FDW so the foreign table can be enumerated and described without a remote server. Closes #418 Claude-Session: https://claude.ai/code/session_01Yc1YYa3jHCEW7FDxC66R9Z Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4ac6e89 commit cca7b67

3 files changed

Lines changed: 43 additions & 3 deletions

File tree

src/connectors/__tests__/postgres.integration.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,21 @@ class PostgreSQLIntegrationTest extends IntegrationTestBase<PostgreSQLTestContai
173173
`, {});
174174
await connector.executeSQL(`COMMENT ON VIEW active_users IS 'Users aged 25 or older'`, {});
175175

176+
// Create a foreign table (for foreign table discovery tests, #418). A
177+
// handler-less FDW is enough: the catalog entries exist and can be
178+
// enumerated/described, the table just can't be scanned.
179+
await connector.executeSQL('CREATE FOREIGN DATA WRAPPER dummy_fdw', {});
180+
await connector.executeSQL('CREATE SERVER IF NOT EXISTS dummy_server FOREIGN DATA WRAPPER dummy_fdw', {});
181+
await connector.executeSQL(`
182+
CREATE FOREIGN TABLE IF NOT EXISTS remote_users (
183+
id INTEGER,
184+
name VARCHAR(100),
185+
email VARCHAR(100),
186+
age INTEGER
187+
) SERVER dummy_server
188+
`, {});
189+
await connector.executeSQL(`COMMENT ON FOREIGN TABLE remote_users IS 'Users on a remote server'`, {});
190+
176191
// Create test stored procedures using SQL language to avoid dollar quoting
177192
await connector.executeSQL(`
178193
CREATE OR REPLACE FUNCTION get_user_count()
@@ -346,6 +361,28 @@ describe('PostgreSQL Connector Integration Tests', () => {
346361
expect(comment).toBe('Users aged 25 or older');
347362
});
348363

364+
it('should list foreign tables as tables, not views', async () => {
365+
const tables = await postgresTest.connector.getTables();
366+
expect(tables).toContain('remote_users');
367+
368+
const views = await postgresTest.connector.getViews();
369+
expect(views).not.toContain('remote_users');
370+
371+
expect(await postgresTest.connector.tableExists('remote_users')).toBe(true);
372+
});
373+
374+
it('should describe foreign tables like regular tables', async () => {
375+
const columns = await postgresTest.connector.getTableSchema('remote_users');
376+
expect(columns.map((c) => c.column_name)).toEqual(['id', 'name', 'email', 'age']);
377+
378+
const comment = await postgresTest.connector.getTableComment!('remote_users');
379+
expect(comment).toBe('Users on a remote server');
380+
381+
// Foreign tables cannot have indexes; this must return empty rather than throw.
382+
const indexes = await postgresTest.connector.getTableIndexes('remote_users');
383+
expect(indexes).toEqual([]);
384+
});
385+
349386
it('should report connection pool state and buffer cache hit ratio via getHealthCheck', async () => {
350387
const health = await postgresTest.connector.getHealthCheck!();
351388

src/connectors/__tests__/shared/integration-test-base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export abstract class IntegrationTestBase<TContainer extends TestContainer> {
104104

105105
it('should list views without overlapping tables', async () => {
106106
// Verifies getViews() is wired and its query executes against the real
107-
// database. getTables() (BASE TABLE only) and getViews() must be disjoint.
107+
// database. getTables() (tables, never views) and getViews() must be disjoint.
108108
const tables = await this.connector.getTables();
109109
const views = await this.connector.getViews();
110110
expect(Array.isArray(views)).toBe(true);

src/connectors/postgres/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,15 @@ export class PostgresConnector implements Connector {
259259
// Use the configured default schema (from search_path config, defaults to 'public')
260260
const schemaToUse = schema || this.defaultSchema;
261261

262+
// 'FOREIGN' covers foreign tables (postgres_fdw, file_fdw, ...). They are
263+
// queryable like base tables and information_schema.columns already
264+
// describes them, so they must be discoverable here too (#418).
262265
const result = await client.query(
263266
`
264267
SELECT table_name
265268
FROM information_schema.tables
266269
WHERE table_schema = $1
267-
AND table_type = 'BASE TABLE'
270+
AND table_type IN ('BASE TABLE', 'FOREIGN')
268271
ORDER BY table_name
269272
`,
270273
[schemaToUse]
@@ -358,7 +361,7 @@ export class PostgresConnector implements Connector {
358361
AND i.oid = ix.indexrelid
359362
AND a.attrelid = t.oid
360363
AND a.attnum = ANY(ix.indkey)
361-
AND t.relkind = 'r'
364+
AND t.relkind IN ('r','p')
362365
AND t.relname = $1
363366
AND ns.oid = t.relnamespace
364367
AND ns.nspname = $2

0 commit comments

Comments
 (0)