Skip to content

Commit 49be99e

Browse files
authored
Merge pull request #415 from supabase/feat/foreign-tables
feat: foreign tables
2 parents e09f9e1 + 3b368c5 commit 49be99e

File tree

10 files changed

+178
-0
lines changed

10 files changed

+178
-0
lines changed

src/lib/PostgresMeta.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as Parser from './Parser'
33
import PostgresMetaColumns from './PostgresMetaColumns'
44
import PostgresMetaConfig from './PostgresMetaConfig'
55
import PostgresMetaExtensions from './PostgresMetaExtensions'
6+
import PostgresMetaForeignTables from './PostgresMetaForeignTables'
67
import PostgresMetaFunctions from './PostgresMetaFunctions'
78
import PostgresMetaPolicies from './PostgresMetaPolicies'
89
import PostgresMetaPublications from './PostgresMetaPublications'
@@ -15,12 +16,14 @@ import PostgresMetaVersion from './PostgresMetaVersion'
1516
import PostgresMetaViews from './PostgresMetaViews'
1617
import { init } from './db'
1718
import { PostgresMetaResult } from './types'
19+
1820
export default class PostgresMeta {
1921
query: (sql: string) => Promise<PostgresMetaResult<any>>
2022
end: () => Promise<void>
2123
columns: PostgresMetaColumns
2224
config: PostgresMetaConfig
2325
extensions: PostgresMetaExtensions
26+
foreignTables: PostgresMetaForeignTables
2427
functions: PostgresMetaFunctions
2528
policies: PostgresMetaPolicies
2629
publications: PostgresMetaPublications
@@ -43,6 +46,7 @@ export default class PostgresMeta {
4346
this.columns = new PostgresMetaColumns(this.query)
4447
this.config = new PostgresMetaConfig(this.query)
4548
this.extensions = new PostgresMetaExtensions(this.query)
49+
this.foreignTables = new PostgresMetaForeignTables(this.query)
4650
this.functions = new PostgresMetaFunctions(this.query)
4751
this.policies = new PostgresMetaPolicies(this.query)
4852
this.publications = new PostgresMetaPublications(this.query)

src/lib/PostgresMetaForeignTables.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { coalesceRowsToArray } from './helpers'
2+
import { columnsSql, foreignTablesSql } from './sql'
3+
import { PostgresMetaResult, PostgresForeignTable } from './types'
4+
5+
export default class PostgresMetaForeignTables {
6+
query: (sql: string) => Promise<PostgresMetaResult<any>>
7+
8+
constructor(query: (sql: string) => Promise<PostgresMetaResult<any>>) {
9+
this.query = query
10+
}
11+
12+
async list({
13+
limit,
14+
offset,
15+
}: {
16+
limit?: number
17+
offset?: number
18+
} = {}): Promise<PostgresMetaResult<PostgresForeignTable[]>> {
19+
let sql = enrichedForeignTablesSql
20+
if (limit) {
21+
sql = `${sql} LIMIT ${limit}`
22+
}
23+
if (offset) {
24+
sql = `${sql} OFFSET ${offset}`
25+
}
26+
return await this.query(sql)
27+
}
28+
}
29+
30+
const enrichedForeignTablesSql = `
31+
WITH foreign_tables AS (${foreignTablesSql}),
32+
columns AS (${columnsSql})
33+
SELECT
34+
*,
35+
${coalesceRowsToArray('columns', 'columns.table_id = foreign_tables.id')}
36+
FROM foreign_tables`

src/lib/sql/foreign_tables.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
SELECT
2+
c.oid :: int8 AS id,
3+
n.nspname AS schema,
4+
c.relname AS name,
5+
obj_description(c.oid) AS comment
6+
FROM
7+
pg_class c
8+
JOIN pg_namespace n ON n.oid = c.relnamespace
9+
WHERE
10+
c.relkind = 'f'

src/lib/sql/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { resolve } from 'path'
44
export const columnsSql = readFileSync(resolve(__dirname, 'columns.sql'), 'utf-8')
55
export const configSql = readFileSync(resolve(__dirname, 'config.sql'), 'utf-8')
66
export const extensionsSql = readFileSync(resolve(__dirname, 'extensions.sql'), 'utf-8')
7+
export const foreignTablesSql = readFileSync(resolve(__dirname, 'foreign_tables.sql'), 'utf-8')
78
export const functionsSql = readFileSync(resolve(__dirname, 'functions.sql'), 'utf-8')
89
export const policiesSql = readFileSync(resolve(__dirname, 'policies.sql'), 'utf-8')
910
export const primaryKeysSql = readFileSync(resolve(__dirname, 'primary_keys.sql'), 'utf-8')

src/lib/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ export const postgresExtensionSchema = Type.Object({
7575
})
7676
export type PostgresExtension = Static<typeof postgresExtensionSchema>
7777

78+
export const postgresForeignTableSchema = Type.Object({
79+
id: Type.Integer(),
80+
schema: Type.String(),
81+
name: Type.String(),
82+
comment: Type.Union([Type.String(), Type.Null()]),
83+
columns: Type.Array(postgresColumnSchema),
84+
})
85+
export type PostgresForeignTable = Static<typeof postgresForeignTableSchema>
86+
7887
const postgresFunctionSchema = Type.Object({
7988
id: Type.Integer(),
8089
schema: Type.String(),

src/server/routes/foreign-tables.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { FastifyInstance } from 'fastify'
2+
import { PostgresMeta } from '../../lib'
3+
import { DEFAULT_POOL_CONFIG } from '../constants'
4+
import { extractRequestForLogging } from '../utils'
5+
6+
export default async (fastify: FastifyInstance) => {
7+
fastify.get<{
8+
Headers: { pg: string }
9+
Querystring: {
10+
limit?: number
11+
offset?: number
12+
}
13+
}>('/', async (request, reply) => {
14+
const connectionString = request.headers.pg
15+
const limit = request.query.limit
16+
const offset = request.query.offset
17+
18+
const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString })
19+
const { data, error } = await pgMeta.foreignTables.list({ limit, offset })
20+
await pgMeta.end()
21+
if (error) {
22+
request.log.error({ error, request: extractRequestForLogging(request) })
23+
reply.code(500)
24+
return { error: error.message }
25+
}
26+
27+
return data
28+
})
29+
}

src/server/routes/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default async (fastify: FastifyInstance) => {
2828
fastify.register(require('./columns'), { prefix: '/columns' })
2929
fastify.register(require('./config'), { prefix: '/config' })
3030
fastify.register(require('./extensions'), { prefix: '/extensions' })
31+
fastify.register(require('./foreign-tables'), { prefix: '/foreign-tables' })
3132
fastify.register(require('./functions'), { prefix: '/functions' })
3233
fastify.register(require('./policies'), { prefix: '/policies' })
3334
fastify.register(require('./publications'), { prefix: '/publications' })

test/db/00-init.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,12 @@ create function public.blurb(public.todos) returns text as
5656
$$
5757
select substring($1.details, 1, 3);
5858
$$ language sql stable;
59+
60+
create extension postgres_fdw;
61+
create server foreign_server foreign data wrapper postgres_fdw options (host 'localhost', port '5432', dbname 'postgres');
62+
create user mapping for postgres server foreign_server options (user 'postgres', password 'postgres');
63+
create foreign table foreign_table (
64+
id int8,
65+
name text,
66+
status user_status
67+
) server foreign_server options (schema_name 'public', table_name 'users');

test/lib/foreign-tables.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { pgMeta } from './utils'
2+
3+
test('list', async () => {
4+
const res = await pgMeta.foreignTables.list()
5+
expect(res.data?.find(({ name }) => name === 'foreign_table')).toMatchInlineSnapshot(
6+
{ id: expect.any(Number) },
7+
`
8+
Object {
9+
"columns": Array [
10+
Object {
11+
"comment": null,
12+
"data_type": "bigint",
13+
"default_value": null,
14+
"enums": Array [],
15+
"format": "int8",
16+
"id": "16434.1",
17+
"identity_generation": null,
18+
"is_generated": false,
19+
"is_identity": false,
20+
"is_nullable": true,
21+
"is_unique": false,
22+
"is_updatable": true,
23+
"name": "id",
24+
"ordinal_position": 1,
25+
"schema": "public",
26+
"table": "foreign_table",
27+
"table_id": 16434,
28+
},
29+
Object {
30+
"comment": null,
31+
"data_type": "text",
32+
"default_value": null,
33+
"enums": Array [],
34+
"format": "text",
35+
"id": "16434.2",
36+
"identity_generation": null,
37+
"is_generated": false,
38+
"is_identity": false,
39+
"is_nullable": true,
40+
"is_unique": false,
41+
"is_updatable": true,
42+
"name": "name",
43+
"ordinal_position": 2,
44+
"schema": "public",
45+
"table": "foreign_table",
46+
"table_id": 16434,
47+
},
48+
Object {
49+
"comment": null,
50+
"data_type": "USER-DEFINED",
51+
"default_value": null,
52+
"enums": Array [
53+
"ACTIVE",
54+
"INACTIVE",
55+
],
56+
"format": "user_status",
57+
"id": "16434.3",
58+
"identity_generation": null,
59+
"is_generated": false,
60+
"is_identity": false,
61+
"is_nullable": true,
62+
"is_unique": false,
63+
"is_updatable": true,
64+
"name": "status",
65+
"ordinal_position": 3,
66+
"schema": "public",
67+
"table": "foreign_table",
68+
"table_id": 16434,
69+
},
70+
],
71+
"comment": null,
72+
"id": Any<Number>,
73+
"name": "foreign_table",
74+
"schema": "public",
75+
}
76+
`
77+
)
78+
})

test/lib/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ import './policies'
1414
import './publications'
1515
import './triggers'
1616
import './views'
17+
import './foreign-tables'

0 commit comments

Comments
 (0)