@@ -30,6 +30,14 @@ class ReferringForeignKey:
3030 is_validated : bool
3131 referring_table : str
3232 schema : str
33+ is_owned_by_user : bool
34+
35+
36+ @dataclasses .dataclass
37+ class ReferredForeignKey :
38+ name : str
39+ schema : str
40+ privileges : list [str ]
3341
3442
3543@dataclasses .dataclass
@@ -239,7 +247,16 @@ def get_referring_fks(self, *, table: str) -> list[ReferringForeignKey]:
239247 pg_get_constraintdef(pg_constraint.oid) AS definition,
240248 pg_constraint.convalidated AS is_validated,
241249 referring_pg_class.relname AS referring_table,
242- pg_namespace.nspname AS schema
250+ pg_namespace.nspname AS schema,
251+ (
252+ SELECT
253+ tableowner = current_user
254+ FROM
255+ pg_tables
256+ WHERE
257+ schemaname = {schema}
258+ AND tablename = referring_pg_class.relname
259+ ) AS is_owned_by_user
243260 FROM
244261 pg_catalog.pg_constraint
245262 INNER JOIN
@@ -260,7 +277,10 @@ def get_referring_fks(self, *, table: str) -> list[ReferringForeignKey]:
260277 definition;
261278 """ )
262279 )
263- .format (table = psycopg .sql .Literal (table ))
280+ .format (
281+ table = psycopg .sql .Literal (table ),
282+ schema = psycopg .sql .Literal (self .schema ),
283+ )
264284 .as_string (self .conn )
265285 )
266286 results = self .cur .fetchall ()
@@ -272,8 +292,74 @@ def get_referring_fks(self, *, table: str) -> list[ReferringForeignKey]:
272292 is_validated = is_validated ,
273293 referring_table = referring_table ,
274294 schema = schema ,
295+ is_owned_by_user = is_owned_by_user ,
275296 )
276- for name , definition , is_validated , referring_table , schema in results
297+ for name , definition , is_validated , referring_table , schema , is_owned_by_user in results
298+ ]
299+
300+ def get_referred_fks (self , * , table : str , schema : str ) -> list [ReferredForeignKey ]:
301+ self .cur .execute (
302+ psycopg .sql .SQL (
303+ dedent ("""
304+ SELECT
305+ referred_pg_class.relname AS referred_table,
306+ referred_pg_namespace.nspname AS referred_schema,
307+ array_agg(
308+ table_privileges.privilege_type
309+ ORDER BY table_privileges.privilege_type
310+ ) AS privileges
311+ FROM
312+ pg_catalog.pg_constraint
313+ INNER JOIN
314+ pg_catalog.pg_class AS referring_pg_class
315+ ON (pg_constraint.conrelid = referring_pg_class.oid)
316+ INNER JOIN
317+ pg_catalog.pg_class AS referred_pg_class
318+ ON (pg_constraint.confrelid = referred_pg_class.oid)
319+ INNER JOIN
320+ pg_catalog.pg_namespace AS referring_pg_namespace
321+ ON (referring_pg_namespace.oid = referring_pg_class.relnamespace)
322+ INNER JOIN
323+ pg_catalog.pg_namespace AS referred_pg_namespace
324+ ON (referred_pg_namespace.oid = referred_pg_class.relnamespace)
325+ LEFT OUTER JOIN
326+ information_schema.table_privileges
327+ ON (
328+ table_privileges.table_name = referred_pg_class.relname
329+ AND table_privileges.table_schema = referred_pg_namespace.nspname
330+ )
331+ WHERE
332+ pg_constraint.conrelid = referring_pg_class.oid
333+ AND referring_pg_class.relname = {table}
334+ AND referring_pg_namespace.nspname = {schema}
335+ AND pg_constraint.contype = 'f'
336+ GROUP BY
337+ referred_pg_namespace.nspname,
338+ referred_pg_class.relname
339+ ORDER BY
340+ referred_pg_namespace.nspname,
341+ referred_pg_class.relname;
342+ """ )
343+ )
344+ .format (
345+ table = psycopg .sql .Literal (table ),
346+ schema = psycopg .sql .Literal (schema ),
347+ )
348+ .as_string (self .conn )
349+ )
350+ results = self .cur .fetchall ()
351+ assert results is not None
352+ return [
353+ ReferredForeignKey (
354+ name = name ,
355+ schema = schema ,
356+ privileges = (
357+ privileges .strip ("{}" ).split ("," )
358+ if "NULL" not in privileges
359+ else []
360+ ),
361+ )
362+ for name , schema , privileges in results
277363 ]
278364
279365 def table_is_empty (self , * , table : str ) -> int :
@@ -529,3 +615,48 @@ def get_backfill_batch(self, *, table: str) -> BackfillBatch | None:
529615 if not (result := self .cur .fetchone ()):
530616 return None
531617 return BackfillBatch (id = result [0 ], start = result [1 ], end = result [2 ])
618+
619+ def get_user (self ) -> str :
620+ self .cur .execute (psycopg .sql .SQL ("SELECT current_user;" ).as_string (self .conn ))
621+ result = self .cur .fetchone ()
622+ assert result is not None
623+ return str (result [0 ])
624+
625+ def has_create_and_usage_privilege_on_schema (self ) -> bool :
626+ self .cur .execute (
627+ psycopg .sql .SQL (
628+ dedent ("""
629+ SELECT
630+ has_schema_privilege({schema}, 'CREATE'),
631+ has_schema_privilege({schema}, 'USAGE');
632+ """ )
633+ )
634+ .format (schema = psycopg .sql .Literal (self .schema ))
635+ .as_string (self .conn )
636+ )
637+ result = self .cur .fetchone ()
638+ assert result is not None
639+ return bool (result [0 ] and result [1 ])
640+
641+ def is_table_owner (self , * , table : str , schema : str ) -> bool :
642+ self .cur .execute (
643+ psycopg .sql .SQL (
644+ dedent ("""
645+ SELECT
646+ tableowner = current_user
647+ FROM
648+ pg_tables
649+ WHERE
650+ schemaname = {schema}
651+ AND tablename = {table}
652+ """ )
653+ )
654+ .format (
655+ schema = psycopg .sql .Literal (schema ),
656+ table = psycopg .sql .Literal (table ),
657+ )
658+ .as_string (self .conn )
659+ )
660+ result = self .cur .fetchone ()
661+ assert result is not None
662+ return bool (result [0 ])
0 commit comments