@@ -20,6 +20,9 @@ pub async fn introspect(
2020 if !views. is_empty ( ) {
2121 let nullability_info = fetch_view_column_nullability ( pool, schemas) . await ?;
2222 resolve_view_nullability ( & mut views, & nullability_info) ;
23+
24+ let pk_info = fetch_view_column_primary_keys ( pool, schemas) . await ?;
25+ resolve_view_primary_keys ( & mut views, & pk_info) ;
2326 }
2427
2528 let enums = fetch_enums ( pool, schemas) . await ?;
@@ -230,6 +233,93 @@ fn resolve_view_nullability(
230233 }
231234}
232235
236+ struct ViewColumnPrimaryKey {
237+ view_schema : String ,
238+ view_name : String ,
239+ source_column_name : String ,
240+ source_is_pk : bool ,
241+ }
242+
243+ async fn fetch_view_column_primary_keys (
244+ pool : & PgPool ,
245+ schemas : & [ String ] ,
246+ ) -> Result < Vec < ViewColumnPrimaryKey > > {
247+ let rows = sqlx:: query_as :: < _ , ( String , String , String , bool ) > (
248+ r#"
249+ SELECT DISTINCT
250+ v_ns.nspname AS view_schema,
251+ v.relname AS view_name,
252+ src_attr.attname AS source_column_name,
253+ COALESCE(
254+ EXISTS (
255+ SELECT 1
256+ FROM pg_constraint con
257+ WHERE con.conrelid = src_attr.attrelid
258+ AND con.contype = 'p'
259+ AND src_attr.attnum = ANY(con.conkey)
260+ ),
261+ false
262+ ) AS source_is_pk
263+ FROM pg_class v
264+ JOIN pg_namespace v_ns ON v_ns.oid = v.relnamespace
265+ JOIN pg_rewrite rw ON rw.ev_class = v.oid
266+ JOIN pg_depend d ON d.objid = rw.oid
267+ AND d.classid = 'pg_rewrite'::regclass
268+ AND d.refobjsubid > 0
269+ AND d.deptype = 'n'
270+ JOIN pg_attribute src_attr ON src_attr.attrelid = d.refobjid
271+ AND src_attr.attnum = d.refobjsubid
272+ AND NOT src_attr.attisdropped
273+ WHERE v_ns.nspname = ANY($1)
274+ AND v.relkind = 'v'
275+ "# ,
276+ )
277+ . bind ( schemas)
278+ . fetch_all ( pool)
279+ . await ?;
280+
281+ Ok ( rows
282+ . into_iter ( )
283+ . map (
284+ |( view_schema, view_name, source_column_name, source_is_pk) | ViewColumnPrimaryKey {
285+ view_schema,
286+ view_name,
287+ source_column_name,
288+ source_is_pk,
289+ } ,
290+ )
291+ . collect ( ) )
292+ }
293+
294+ fn resolve_view_primary_keys (
295+ views : & mut [ TableInfo ] ,
296+ pk_info : & [ ViewColumnPrimaryKey ] ,
297+ ) {
298+ // Build lookup: (view_schema, view_name, column_name) -> Vec<is_pk>
299+ let mut lookup: HashMap < ( & str , & str , & str ) , Vec < bool > > = HashMap :: new ( ) ;
300+ for info in pk_info {
301+ lookup
302+ . entry ( ( & info. view_schema , & info. view_name , & info. source_column_name ) )
303+ . or_default ( )
304+ . push ( info. source_is_pk ) ;
305+ }
306+
307+ for view in views. iter_mut ( ) {
308+ for col in view. columns . iter_mut ( ) {
309+ if let Some ( pk_flags) = lookup. get ( & (
310+ view. schema_name . as_str ( ) ,
311+ view. name . as_str ( ) ,
312+ col. name . as_str ( ) ,
313+ ) ) {
314+ // Only mark as PK if ALL source columns are PKs
315+ if !pk_flags. is_empty ( ) && pk_flags. iter ( ) . all ( |& pk| pk) {
316+ col. is_primary_key = true ;
317+ }
318+ }
319+ }
320+ }
321+ }
322+
233323async fn fetch_enums ( pool : & PgPool , schemas : & [ String ] ) -> Result < Vec < EnumInfo > > {
234324 let rows = sqlx:: query_as :: < _ , ( String , String , String ) > (
235325 r#"
@@ -447,4 +537,73 @@ mod tests {
447537 assert ! ( !views[ 0 ] . columns[ 0 ] . is_nullable) ;
448538 assert ! ( views[ 1 ] . columns[ 0 ] . is_nullable) ;
449539 }
540+
541+ // --- resolve_view_primary_keys tests ---
542+
543+ fn make_pk_info (
544+ view_schema : & str ,
545+ view_name : & str ,
546+ source_column : & str ,
547+ is_pk : bool ,
548+ ) -> ViewColumnPrimaryKey {
549+ ViewColumnPrimaryKey {
550+ view_schema : view_schema. to_string ( ) ,
551+ view_name : view_name. to_string ( ) ,
552+ source_column_name : source_column. to_string ( ) ,
553+ source_is_pk : is_pk,
554+ }
555+ }
556+
557+ #[ test]
558+ fn test_resolve_pk_column ( ) {
559+ let mut views = vec ! [ make_view( "public" , "my_view" , vec![ "id" , "name" ] ) ] ;
560+ let info = vec ! [
561+ make_pk_info( "public" , "my_view" , "id" , true ) ,
562+ make_pk_info( "public" , "my_view" , "name" , false ) ,
563+ ] ;
564+ resolve_view_primary_keys ( & mut views, & info) ;
565+ assert ! ( views[ 0 ] . columns[ 0 ] . is_primary_key) ;
566+ assert ! ( !views[ 0 ] . columns[ 1 ] . is_primary_key) ;
567+ }
568+
569+ #[ test]
570+ fn test_resolve_pk_mixed_sources ( ) {
571+ let mut views = vec ! [ make_view( "public" , "my_view" , vec![ "id" ] ) ] ;
572+ let info = vec ! [
573+ make_pk_info( "public" , "my_view" , "id" , true ) ,
574+ make_pk_info( "public" , "my_view" , "id" , false ) ,
575+ ] ;
576+ resolve_view_primary_keys ( & mut views, & info) ;
577+ assert ! ( !views[ 0 ] . columns[ 0 ] . is_primary_key) ;
578+ }
579+
580+ #[ test]
581+ fn test_resolve_pk_no_match ( ) {
582+ let mut views = vec ! [ make_view( "public" , "my_view" , vec![ "computed_col" ] ) ] ;
583+ let info = vec ! [ make_pk_info( "public" , "my_view" , "id" , true ) ] ;
584+ resolve_view_primary_keys ( & mut views, & info) ;
585+ assert ! ( !views[ 0 ] . columns[ 0 ] . is_primary_key) ;
586+ }
587+
588+ #[ test]
589+ fn test_resolve_pk_empty_info ( ) {
590+ let mut views = vec ! [ make_view( "public" , "my_view" , vec![ "id" ] ) ] ;
591+ resolve_view_primary_keys ( & mut views, & [ ] ) ;
592+ assert ! ( !views[ 0 ] . columns[ 0 ] . is_primary_key) ;
593+ }
594+
595+ #[ test]
596+ fn test_resolve_pk_cross_schema ( ) {
597+ let mut views = vec ! [
598+ make_view( "public" , "v1" , vec![ "id" ] ) ,
599+ make_view( "auth" , "v2" , vec![ "id" ] ) ,
600+ ] ;
601+ let info = vec ! [
602+ make_pk_info( "public" , "v1" , "id" , true ) ,
603+ make_pk_info( "auth" , "v2" , "id" , false ) ,
604+ ] ;
605+ resolve_view_primary_keys ( & mut views, & info) ;
606+ assert ! ( views[ 0 ] . columns[ 0 ] . is_primary_key) ;
607+ assert ! ( !views[ 1 ] . columns[ 0 ] . is_primary_key) ;
608+ }
450609}
0 commit comments