Skip to content

Commit 888b8a9

Browse files
committed
Add tenant helper functions and improve migrate command
- Add migration 20_tenant_helper_functions.sql with list_tables() and describe_table() - Show pending migrations before applying in `ow migrate run`
1 parent f95e1db commit 888b8a9

4 files changed

Lines changed: 164 additions & 3 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 = "openworkers-cli"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
edition = "2024"
55
license = "MIT"
66

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

src/commands/migrate.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,30 @@ async fn connect(database_url: &str) -> Result<PgPool, MigrateError> {
8383
}
8484

8585
async fn cmd_run(pool: &PgPool) -> Result<(), MigrateError> {
86-
println!("Running migrations...\n");
86+
// Get already applied migrations
87+
let applied: Vec<i64> = sqlx::query_scalar("SELECT version FROM _sqlx_migrations")
88+
.fetch_all(pool)
89+
.await
90+
.unwrap_or_default();
91+
92+
// Find pending migrations
93+
let pending: Vec<_> = MIGRATOR
94+
.iter()
95+
.filter(|m| !applied.contains(&m.version))
96+
.collect();
97+
98+
if pending.is_empty() {
99+
println!("{}", "No pending migrations.".green());
100+
return Ok(());
101+
}
102+
103+
println!("Running {} migration(s)...\n", pending.len());
104+
105+
for migration in &pending {
106+
println!(" {} {}", "Applying".blue(), migration.description);
107+
}
108+
109+
println!();
87110

88111
MIGRATOR.run(pool).await?;
89112

0 commit comments

Comments
 (0)