Skip to content

Commit edc52a6

Browse files
committed
Add postgate_helpers schema with tenant utility functions
- Add list_tables() and describe_table() helper functions - Allow postgate_helpers.* qualified names in SQL parser - Add migration 002_helper_functions.sql - Bump version to 0.1.3
1 parent c2b4d78 commit edc52a6

5 files changed

Lines changed: 160 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "postgate"
3-
version = "0.1.2"
3+
version = "0.1.3"
44
edition = "2024"
55
default-run = "postgate"
66
description = "Secure HTTP proxy for PostgreSQL with SQL validation and multi-tenant support"

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,31 @@ SELECT * FROM public.users
270270

271271
-- ❌ Blocked: System table access
272272
SELECT * FROM pg_tables
273+
274+
-- ✅ Allowed: postgate_helpers functions
275+
SELECT * FROM postgate_helpers.list_tables()
276+
```
277+
278+
## Helper Functions
279+
280+
The `postgate_helpers` schema provides utility functions accessible to all tenants:
281+
282+
### postgate_helpers.list_tables()
283+
284+
List all tables in the current tenant's schema with row counts.
285+
286+
```sql
287+
SELECT * FROM postgate_helpers.list_tables();
288+
-- Returns: { table_name: "users", row_count: 42 }, ...
289+
```
290+
291+
### postgate_helpers.describe_table(name)
292+
293+
Describe columns of a table in the current tenant's schema.
294+
295+
```sql
296+
SELECT * FROM postgate_helpers.describe_table('users');
297+
-- Returns: { column_name, data_type, is_nullable, column_default, is_primary_key }
273298
```
274299

275300
## Multi-Tenant Isolation
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
-- ============================================================================
2+
-- POSTGATE HELPER FUNCTIONS
3+
-- ============================================================================
4+
--
5+
-- Utility functions accessible to all tenants via qualified names:
6+
-- SELECT * FROM postgate_helpers.list_tables();
7+
-- SELECT * FROM postgate_helpers.describe_table('users');
8+
--
9+
10+
-- ============================================================================
11+
-- SCHEMA
12+
-- ============================================================================
13+
14+
CREATE SCHEMA IF NOT EXISTS postgate_helpers;
15+
16+
COMMENT ON SCHEMA postgate_helpers IS 'Utility functions for tenants (accessible via PostGate)';
17+
18+
-- ============================================================================
19+
-- FUNCTION: list_tables()
20+
-- ============================================================================
21+
-- Lists all tables in the current tenant's schema with row counts.
22+
--
23+
-- Example:
24+
-- SELECT * FROM postgate_helpers.list_tables();
25+
26+
CREATE OR REPLACE FUNCTION postgate_helpers.list_tables()
27+
RETURNS TABLE(table_name text, row_count bigint)
28+
LANGUAGE plpgsql
29+
SECURITY DEFINER
30+
AS $$
31+
DECLARE
32+
v_schema text;
33+
tbl record;
34+
cnt bigint;
35+
BEGIN
36+
v_schema := current_schema();
37+
38+
-- Prevent access to system schemas
39+
IF v_schema IN ('public', 'postgate_helpers') THEN
40+
RAISE EXCEPTION 'Cannot list tables in system schemas';
41+
END IF;
42+
43+
FOR tbl IN
44+
SELECT tablename
45+
FROM pg_tables
46+
WHERE schemaname = v_schema
47+
ORDER BY tablename
48+
LOOP
49+
EXECUTE format('SELECT count(*) FROM %I.%I', v_schema, tbl.tablename) INTO cnt;
50+
table_name := tbl.tablename;
51+
row_count := cnt;
52+
RETURN NEXT;
53+
END LOOP;
54+
END;
55+
$$;
56+
57+
COMMENT ON FUNCTION postgate_helpers.list_tables() IS 'List all tables in the current tenant schema with row counts';
58+
59+
-- ============================================================================
60+
-- FUNCTION: describe_table(table_name)
61+
-- ============================================================================
62+
-- Describes columns of a table in the current tenant's schema.
63+
--
64+
-- Example:
65+
-- SELECT * FROM postgate_helpers.describe_table('users');
66+
67+
CREATE OR REPLACE FUNCTION postgate_helpers.describe_table(p_table_name text)
68+
RETURNS TABLE(
69+
column_name text,
70+
data_type text,
71+
is_nullable boolean,
72+
column_default text,
73+
is_primary_key boolean
74+
)
75+
LANGUAGE plpgsql
76+
SECURITY DEFINER
77+
AS $$
78+
DECLARE
79+
v_schema text;
80+
BEGIN
81+
v_schema := current_schema();
82+
83+
-- Prevent access to system schemas
84+
IF v_schema IN ('public', 'postgate_helpers') THEN
85+
RAISE EXCEPTION 'Cannot describe tables in system schemas';
86+
END IF;
87+
88+
RETURN QUERY
89+
SELECT
90+
c.column_name::text,
91+
c.data_type::text,
92+
(c.is_nullable = 'YES')::boolean,
93+
c.column_default::text,
94+
COALESCE(
95+
(SELECT true
96+
FROM information_schema.table_constraints tc
97+
JOIN information_schema.key_column_usage kcu
98+
ON tc.constraint_name = kcu.constraint_name
99+
AND tc.table_schema = kcu.table_schema
100+
WHERE tc.constraint_type = 'PRIMARY KEY'
101+
AND tc.table_schema = v_schema
102+
AND tc.table_name = p_table_name
103+
AND kcu.column_name = c.column_name
104+
LIMIT 1),
105+
false
106+
)::boolean
107+
FROM information_schema.columns c
108+
WHERE c.table_schema = v_schema
109+
AND c.table_name = p_table_name
110+
ORDER BY c.ordinal_position;
111+
END;
112+
$$;
113+
114+
COMMENT ON FUNCTION postgate_helpers.describe_table(text) IS 'Describe columns of a table in the current tenant schema';
115+
116+
-- ============================================================================
117+
-- PERMISSIONS
118+
-- ============================================================================
119+
120+
GRANT USAGE ON SCHEMA postgate_helpers TO PUBLIC;
121+
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA postgate_helpers TO PUBLIC;

src/parser.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,12 @@ fn validate_table_refs(table_refs: &[TableRef]) -> Result<HashSet<String>, Parse
157157

158158
for table_ref in table_refs {
159159
// Block qualified names (schema.table)
160+
// Exception: postgate_helpers contains utility functions (list_tables, describe_table)
160161
if let Some(schema) = &table_ref.schema {
161-
let full_name = format!("{}.{}", schema, table_ref.name);
162-
return Err(ParseError::QualifiedTableName(full_name));
162+
if schema != "postgate_helpers" {
163+
let full_name = format!("{}.{}", schema, table_ref.name);
164+
return Err(ParseError::QualifiedTableName(full_name));
165+
}
163166
}
164167

165168
let name_lower = table_ref.name.to_lowercase();
@@ -267,4 +270,11 @@ mod tests {
267270
let result = parse_and_validate("SELECT * FROM information_schema.tables", &ops);
268271
assert!(matches!(result, Err(ParseError::QualifiedTableName(_))));
269272
}
273+
274+
#[test]
275+
fn test_postgate_helpers_allowed() {
276+
let ops = all_operations();
277+
let result = parse_and_validate("SELECT * FROM postgate_helpers.list_tables()", &ops);
278+
assert!(result.is_ok());
279+
}
270280
}

0 commit comments

Comments
 (0)