11//! Get all table definitions.
22pub use pgdog_stats:: Column as StatsColumn ;
3+ pub use pgdog_stats:: ForeignKey ;
4+ pub use pgdog_stats:: ForeignKeyAction ;
35use serde:: { Deserialize , Serialize } ;
46
57use super :: Error ;
@@ -10,6 +12,34 @@ use std::{
1012} ;
1113
1214static COLUMNS : & str = include_str ! ( "columns.sql" ) ;
15+ static FOREIGN_KEYS : & str = include_str ! ( "foreign_keys.sql" ) ;
16+
17+ /// Represents a row from the foreign_keys.sql query.
18+ struct ForeignKeyRow {
19+ source_schema : String ,
20+ source_table : String ,
21+ source_column : String ,
22+ ref_schema : String ,
23+ ref_table : String ,
24+ ref_column : String ,
25+ on_delete : ForeignKeyAction ,
26+ on_update : ForeignKeyAction ,
27+ }
28+
29+ impl From < DataRow > for ForeignKeyRow {
30+ fn from ( value : DataRow ) -> Self {
31+ Self {
32+ source_schema : value. get_text ( 0 ) . unwrap_or_default ( ) ,
33+ source_table : value. get_text ( 1 ) . unwrap_or_default ( ) ,
34+ source_column : value. get_text ( 2 ) . unwrap_or_default ( ) ,
35+ ref_schema : value. get_text ( 3 ) . unwrap_or_default ( ) ,
36+ ref_table : value. get_text ( 4 ) . unwrap_or_default ( ) ,
37+ ref_column : value. get_text ( 5 ) . unwrap_or_default ( ) ,
38+ on_delete : ForeignKeyAction :: from_pg_string ( & value. get_text ( 6 ) . unwrap_or_default ( ) ) ,
39+ on_update : ForeignKeyAction :: from_pg_string ( & value. get_text ( 7 ) . unwrap_or_default ( ) ) ,
40+ }
41+ }
42+ }
1343
1444#[ derive( Debug , Clone , Default , Serialize , Deserialize ) ]
1545pub struct Column {
@@ -43,7 +73,7 @@ impl DerefMut for Column {
4373}
4474
4575impl Column {
46- /// Load all columns from server.
76+ /// Load all columns from server, including foreign key information .
4777 pub async fn load (
4878 server : & mut Server ,
4979 ) -> Result < HashMap < ( String , String ) , Vec < Column > > , Error > {
@@ -57,6 +87,26 @@ impl Column {
5787 entry. push ( row) ;
5888 }
5989
90+ // Load foreign key constraints and merge into columns
91+ let fk_rows: Vec < ForeignKeyRow > = server. fetch_all ( FOREIGN_KEYS ) . await ?;
92+ for fk_row in fk_rows {
93+ let key = ( fk_row. source_schema . clone ( ) , fk_row. source_table . clone ( ) ) ;
94+ if let Some ( columns) = result. get_mut ( & key) {
95+ if let Some ( column) = columns
96+ . iter_mut ( )
97+ . find ( |c| c. column_name == fk_row. source_column )
98+ {
99+ column. foreign_keys . push ( ForeignKey {
100+ schema : fk_row. ref_schema ,
101+ table : fk_row. ref_table ,
102+ column : fk_row. ref_column ,
103+ on_delete : fk_row. on_delete ,
104+ on_update : fk_row. on_update ,
105+ } ) ;
106+ }
107+ }
108+ }
109+
60110 Ok ( result)
61111 }
62112}
@@ -75,6 +125,7 @@ impl From<DataRow> for Column {
75125 data_type : value. get_text ( 6 ) . unwrap_or_default ( ) ,
76126 ordinal_position : value. get :: < i32 > ( 7 , Format :: Text ) . unwrap_or ( 0 ) ,
77127 is_primary_key : value. get_text ( 8 ) . unwrap_or_default ( ) == "true" ,
128+ foreign_keys : Vec :: new ( ) ,
78129 } ,
79130 }
80131 }
@@ -93,4 +144,81 @@ mod test {
93144 let columns = Column :: load ( & mut conn) . await . unwrap ( ) ;
94145 println ! ( "{:#?}" , columns) ;
95146 }
147+
148+ #[ tokio:: test]
149+ async fn test_load_foreign_keys ( ) {
150+ use crate :: backend:: schema:: columns:: ForeignKeyAction ;
151+
152+ let pool = pool ( ) ;
153+ let mut conn = pool. get ( & Request :: default ( ) ) . await . unwrap ( ) ;
154+
155+ // Create test tables with FK relationships using different action types
156+ conn. execute ( "DROP TABLE IF EXISTS fk_test_orders CASCADE" )
157+ . await
158+ . unwrap ( ) ;
159+ conn. execute ( "DROP TABLE IF EXISTS fk_test_customers CASCADE" )
160+ . await
161+ . unwrap ( ) ;
162+
163+ conn. execute (
164+ "CREATE TABLE fk_test_customers (
165+ id SERIAL PRIMARY KEY,
166+ name TEXT NOT NULL
167+ )" ,
168+ )
169+ . await
170+ . unwrap ( ) ;
171+
172+ conn. execute (
173+ "CREATE TABLE fk_test_orders (
174+ id SERIAL PRIMARY KEY,
175+ customer_id INTEGER NOT NULL REFERENCES fk_test_customers(id)
176+ ON DELETE CASCADE ON UPDATE RESTRICT,
177+ amount NUMERIC
178+ )" ,
179+ )
180+ . await
181+ . unwrap ( ) ;
182+
183+ let columns = Column :: load ( & mut conn) . await . unwrap ( ) ;
184+
185+ // Find the customer_id column in fk_test_orders (uses "pgdog" schema in test db)
186+ let orders_columns = columns
187+ . get ( & ( "pgdog" . to_string ( ) , "fk_test_orders" . to_string ( ) ) )
188+ . expect ( "fk_test_orders table should exist" ) ;
189+
190+ let customer_id_col = orders_columns
191+ . iter ( )
192+ . find ( |c| c. column_name == "customer_id" )
193+ . expect ( "customer_id column should exist" ) ;
194+
195+ // Verify FK info is captured
196+ assert ! (
197+ !customer_id_col. foreign_keys. is_empty( ) ,
198+ "customer_id should have foreign key info"
199+ ) ;
200+
201+ let fk = & customer_id_col. foreign_keys [ 0 ] ;
202+ assert_eq ! ( fk. schema, "pgdog" ) ;
203+ assert_eq ! ( fk. table, "fk_test_customers" ) ;
204+ assert_eq ! ( fk. column, "id" ) ;
205+ assert_eq ! ( fk. on_delete, ForeignKeyAction :: Cascade ) ;
206+ assert_eq ! ( fk. on_update, ForeignKeyAction :: Restrict ) ;
207+
208+ // Verify the id column (PK) does NOT have FK info
209+ let id_col = orders_columns
210+ . iter ( )
211+ . find ( |c| c. column_name == "id" )
212+ . expect ( "id column should exist" ) ;
213+ assert ! ( id_col. is_primary_key) ;
214+ assert ! ( id_col. foreign_keys. is_empty( ) ) ;
215+
216+ // Clean up
217+ conn. execute ( "DROP TABLE IF EXISTS fk_test_orders CASCADE" )
218+ . await
219+ . unwrap ( ) ;
220+ conn. execute ( "DROP TABLE IF EXISTS fk_test_customers CASCADE" )
221+ . await
222+ . unwrap ( ) ;
223+ }
96224}
0 commit comments