Skip to content

Commit 11c5454

Browse files
authored
Merge pull request #43 from supabase-community/fix/remove-pg-meta-dep
fix: remove pg-meta dep by embedding required logic
2 parents d0d104d + 09e738a commit 11c5454

File tree

12 files changed

+450
-2095
lines changed

12 files changed

+450
-2095
lines changed

package-lock.json

Lines changed: 34 additions & 2057 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/mcp-server-supabase/package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,11 @@
2727
}
2828
},
2929
"dependencies": {
30-
"@gregnr/postgres-meta": "^0.82.0-dev.2",
3130
"@modelcontextprotocol/sdk": "^1.4.1",
3231
"@supabase/mcp-utils": "0.1.2",
3332
"common-tags": "^1.8.2",
3433
"openapi-fetch": "^0.13.4",
35-
"postgres": "^3.4.5",
36-
"zod": "^3.24.1",
37-
"zod-to-json-schema": "^3.24.1"
34+
"zod": "^3.24.1"
3835
},
3936
"devDependencies": {
4037
"@electric-sql/pglite": "^0.2.17",
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
-- Adapted from information_schema.columns
2+
3+
SELECT
4+
c.oid :: int8 AS table_id,
5+
nc.nspname AS schema,
6+
c.relname AS table,
7+
(c.oid || '.' || a.attnum) AS id,
8+
a.attnum AS ordinal_position,
9+
a.attname AS name,
10+
CASE
11+
WHEN a.atthasdef THEN pg_get_expr(ad.adbin, ad.adrelid)
12+
ELSE NULL
13+
END AS default_value,
14+
CASE
15+
WHEN t.typtype = 'd' THEN CASE
16+
WHEN bt.typelem <> 0 :: oid
17+
AND bt.typlen = -1 THEN 'ARRAY'
18+
WHEN nbt.nspname = 'pg_catalog' THEN format_type(t.typbasetype, NULL)
19+
ELSE 'USER-DEFINED'
20+
END
21+
ELSE CASE
22+
WHEN t.typelem <> 0 :: oid
23+
AND t.typlen = -1 THEN 'ARRAY'
24+
WHEN nt.nspname = 'pg_catalog' THEN format_type(a.atttypid, NULL)
25+
ELSE 'USER-DEFINED'
26+
END
27+
END AS data_type,
28+
COALESCE(bt.typname, t.typname) AS format,
29+
a.attidentity IN ('a', 'd') AS is_identity,
30+
CASE
31+
a.attidentity
32+
WHEN 'a' THEN 'ALWAYS'
33+
WHEN 'd' THEN 'BY DEFAULT'
34+
ELSE NULL
35+
END AS identity_generation,
36+
a.attgenerated IN ('s') AS is_generated,
37+
NOT (
38+
a.attnotnull
39+
OR t.typtype = 'd' AND t.typnotnull
40+
) AS is_nullable,
41+
(
42+
c.relkind IN ('r', 'p')
43+
OR c.relkind IN ('v', 'f') AND pg_column_is_updatable(c.oid, a.attnum, FALSE)
44+
) AS is_updatable,
45+
uniques.table_id IS NOT NULL AS is_unique,
46+
check_constraints.definition AS "check",
47+
array_to_json(
48+
array(
49+
SELECT
50+
enumlabel
51+
FROM
52+
pg_catalog.pg_enum enums
53+
WHERE
54+
enums.enumtypid = coalesce(bt.oid, t.oid)
55+
OR enums.enumtypid = coalesce(bt.typelem, t.typelem)
56+
ORDER BY
57+
enums.enumsortorder
58+
)
59+
) AS enums,
60+
col_description(c.oid, a.attnum) AS comment
61+
FROM
62+
pg_attribute a
63+
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid
64+
AND a.attnum = ad.adnum
65+
JOIN (
66+
pg_class c
67+
JOIN pg_namespace nc ON c.relnamespace = nc.oid
68+
) ON a.attrelid = c.oid
69+
JOIN (
70+
pg_type t
71+
JOIN pg_namespace nt ON t.typnamespace = nt.oid
72+
) ON a.atttypid = t.oid
73+
LEFT JOIN (
74+
pg_type bt
75+
JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid
76+
) ON t.typtype = 'd'
77+
AND t.typbasetype = bt.oid
78+
LEFT JOIN (
79+
SELECT DISTINCT ON (table_id, ordinal_position)
80+
conrelid AS table_id,
81+
conkey[1] AS ordinal_position
82+
FROM pg_catalog.pg_constraint
83+
WHERE contype = 'u' AND cardinality(conkey) = 1
84+
) AS uniques ON uniques.table_id = c.oid AND uniques.ordinal_position = a.attnum
85+
LEFT JOIN (
86+
-- We only select the first column check
87+
SELECT DISTINCT ON (table_id, ordinal_position)
88+
conrelid AS table_id,
89+
conkey[1] AS ordinal_position,
90+
substring(
91+
pg_get_constraintdef(pg_constraint.oid, true),
92+
8,
93+
length(pg_get_constraintdef(pg_constraint.oid, true)) - 8
94+
) AS "definition"
95+
FROM pg_constraint
96+
WHERE contype = 'c' AND cardinality(conkey) = 1
97+
ORDER BY table_id, ordinal_position, oid asc
98+
) AS check_constraints ON check_constraints.table_id = c.oid AND check_constraints.ordinal_position = a.attnum
99+
WHERE
100+
NOT pg_is_other_temp_schema(nc.oid)
101+
AND a.attnum > 0
102+
AND NOT a.attisdropped
103+
AND (c.relkind IN ('r', 'v', 'm', 'f', 'p'))
104+
AND (
105+
pg_has_role(c.relowner, 'USAGE')
106+
OR has_column_privilege(
107+
c.oid,
108+
a.attnum,
109+
'SELECT, INSERT, UPDATE, REFERENCES'
110+
)
111+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
SELECT
2+
e.name,
3+
n.nspname AS schema,
4+
e.default_version,
5+
x.extversion AS installed_version,
6+
e.comment
7+
FROM
8+
pg_available_extensions() e(name, default_version, comment)
9+
LEFT JOIN pg_extension x ON e.name = x.extname
10+
LEFT JOIN pg_namespace n ON x.extnamespace = n.oid
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { stripIndent } from 'common-tags';
2+
import columnsSql from './columns.sql';
3+
import extensionsSql from './extensions.sql';
4+
import tablesSql from './tables.sql';
5+
6+
/**
7+
* Generates the SQL query to list tables in the database.
8+
*/
9+
export function listTablesSql(schemas: string[] = []) {
10+
let sql = stripIndent`
11+
with
12+
tables as (${tablesSql}),
13+
columns as (${columnsSql})
14+
select
15+
*,
16+
${coalesceRowsToArray('columns', 'columns.table_id = tables.id')}
17+
from tables
18+
`;
19+
20+
if (schemas.length > 0) {
21+
sql += ` where schema in (${schemas.map((s) => `'${s}'`).join(',')})`;
22+
}
23+
24+
return sql;
25+
}
26+
27+
/**
28+
* Generates the SQL query to list all extensions in the database.
29+
*/
30+
export function listExtensionsSql() {
31+
return extensionsSql;
32+
}
33+
34+
/**
35+
* Generates a SQL segment that coalesces rows into an array of JSON objects.
36+
*/
37+
export const coalesceRowsToArray = (source: string, filter: string) => {
38+
return stripIndent`
39+
COALESCE(
40+
(
41+
SELECT
42+
array_agg(row_to_json(${source})) FILTER (WHERE ${filter})
43+
FROM
44+
${source}
45+
),
46+
'{}'
47+
) AS ${source}
48+
`;
49+
};
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
SELECT
2+
c.oid :: int8 AS id,
3+
nc.nspname AS schema,
4+
c.relname AS name,
5+
c.relrowsecurity AS rls_enabled,
6+
c.relforcerowsecurity AS rls_forced,
7+
CASE
8+
WHEN c.relreplident = 'd' THEN 'DEFAULT'
9+
WHEN c.relreplident = 'i' THEN 'INDEX'
10+
WHEN c.relreplident = 'f' THEN 'FULL'
11+
ELSE 'NOTHING'
12+
END AS replica_identity,
13+
pg_total_relation_size(format('%I.%I', nc.nspname, c.relname)) :: int8 AS bytes,
14+
pg_size_pretty(
15+
pg_total_relation_size(format('%I.%I', nc.nspname, c.relname))
16+
) AS size,
17+
pg_stat_get_live_tuples(c.oid) AS live_rows_estimate,
18+
pg_stat_get_dead_tuples(c.oid) AS dead_rows_estimate,
19+
obj_description(c.oid) AS comment,
20+
coalesce(pk.primary_keys, '[]') as primary_keys,
21+
coalesce(
22+
jsonb_agg(relationships) filter (where relationships is not null),
23+
'[]'
24+
) as relationships
25+
FROM
26+
pg_namespace nc
27+
JOIN pg_class c ON nc.oid = c.relnamespace
28+
left join (
29+
select
30+
table_id,
31+
jsonb_agg(_pk.*) as primary_keys
32+
from (
33+
select
34+
n.nspname as schema,
35+
c.relname as table_name,
36+
a.attname as name,
37+
c.oid :: int8 as table_id
38+
from
39+
pg_index i,
40+
pg_class c,
41+
pg_attribute a,
42+
pg_namespace n
43+
where
44+
i.indrelid = c.oid
45+
and c.relnamespace = n.oid
46+
and a.attrelid = c.oid
47+
and a.attnum = any (i.indkey)
48+
and i.indisprimary
49+
) as _pk
50+
group by table_id
51+
) as pk
52+
on pk.table_id = c.oid
53+
left join (
54+
select
55+
c.oid :: int8 as id,
56+
c.conname as constraint_name,
57+
nsa.nspname as source_schema,
58+
csa.relname as source_table_name,
59+
sa.attname as source_column_name,
60+
nta.nspname as target_table_schema,
61+
cta.relname as target_table_name,
62+
ta.attname as target_column_name
63+
from
64+
pg_constraint c
65+
join (
66+
pg_attribute sa
67+
join pg_class csa on sa.attrelid = csa.oid
68+
join pg_namespace nsa on csa.relnamespace = nsa.oid
69+
) on sa.attrelid = c.conrelid and sa.attnum = any (c.conkey)
70+
join (
71+
pg_attribute ta
72+
join pg_class cta on ta.attrelid = cta.oid
73+
join pg_namespace nta on cta.relnamespace = nta.oid
74+
) on ta.attrelid = c.confrelid and ta.attnum = any (c.confkey)
75+
where
76+
c.contype = 'f'
77+
) as relationships
78+
on (relationships.source_schema = nc.nspname and relationships.source_table_name = c.relname)
79+
or (relationships.target_table_schema = nc.nspname and relationships.target_table_name = c.relname)
80+
WHERE
81+
c.relkind IN ('r', 'p')
82+
AND NOT pg_is_other_temp_schema(nc.oid)
83+
AND (
84+
pg_has_role(c.relowner, 'USAGE')
85+
OR has_table_privilege(
86+
c.oid,
87+
'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'
88+
)
89+
OR has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES')
90+
)
91+
group by
92+
c.oid,
93+
c.relname,
94+
c.relrowsecurity,
95+
c.relforcerowsecurity,
96+
c.relreplident,
97+
nc.nspname,
98+
pk.primary_keys
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { z } from 'zod';
2+
3+
export const postgresPrimaryKeySchema = z.object({
4+
schema: z.string(),
5+
table_name: z.string(),
6+
name: z.string(),
7+
table_id: z.number().int(),
8+
});
9+
10+
export const postgresRelationshipSchema = z.object({
11+
id: z.number().int(),
12+
constraint_name: z.string(),
13+
source_schema: z.string(),
14+
source_table_name: z.string(),
15+
source_column_name: z.string(),
16+
target_table_schema: z.string(),
17+
target_table_name: z.string(),
18+
target_column_name: z.string(),
19+
});
20+
21+
export const postgresColumnSchema = z.object({
22+
table_id: z.number().int(),
23+
schema: z.string(),
24+
table: z.string(),
25+
id: z.string().regex(/^(\d+)\.(\d+)$/),
26+
ordinal_position: z.number().int(),
27+
name: z.string(),
28+
default_value: z.any(),
29+
data_type: z.string(),
30+
format: z.string(),
31+
is_identity: z.boolean(),
32+
identity_generation: z.union([
33+
z.literal('ALWAYS'),
34+
z.literal('BY DEFAULT'),
35+
z.null(),
36+
]),
37+
is_generated: z.boolean(),
38+
is_nullable: z.boolean(),
39+
is_updatable: z.boolean(),
40+
is_unique: z.boolean(),
41+
enums: z.array(z.string()),
42+
check: z.union([z.string(), z.null()]),
43+
comment: z.union([z.string(), z.null()]),
44+
});
45+
46+
export const postgresTableSchema = z.object({
47+
id: z.number().int(),
48+
schema: z.string(),
49+
name: z.string(),
50+
rls_enabled: z.boolean(),
51+
rls_forced: z.boolean(),
52+
replica_identity: z.union([
53+
z.literal('DEFAULT'),
54+
z.literal('INDEX'),
55+
z.literal('FULL'),
56+
z.literal('NOTHING'),
57+
]),
58+
bytes: z.number().int(),
59+
size: z.string(),
60+
live_rows_estimate: z.number().int(),
61+
dead_rows_estimate: z.number().int(),
62+
comment: z.string().nullable(),
63+
columns: z.array(postgresColumnSchema).optional(),
64+
primary_keys: z.array(postgresPrimaryKeySchema),
65+
relationships: z.array(postgresRelationshipSchema),
66+
});
67+
68+
export const postgresExtensionSchema = z.object({
69+
name: z.string(),
70+
schema: z.union([z.string(), z.null()]),
71+
default_version: z.string(),
72+
installed_version: z.union([z.string(), z.null()]),
73+
comment: z.union([z.string(), z.null()]),
74+
});
75+
76+
export type PostgresPrimaryKey = z.infer<typeof postgresPrimaryKeySchema>;
77+
export type PostgresRelationship = z.infer<typeof postgresRelationshipSchema>;
78+
export type PostgresColumn = z.infer<typeof postgresColumnSchema>;
79+
export type PostgresTable = z.infer<typeof postgresTableSchema>;
80+
export type PostgresExtension = z.infer<typeof postgresExtensionSchema>;

0 commit comments

Comments
 (0)