@@ -231,10 +231,17 @@ pub fn generate_crud_from_parsed(
231231 }
232232
233233 // --- insert (skip for views) ---
234- if !is_view && methods. insert && !non_pk_fields. is_empty ( ) {
234+ if !is_view && methods. insert && ( !non_pk_fields. is_empty ( ) || !pk_fields . is_empty ( ) ) {
235235 let insert_params_ident = format_ident ! ( "Insert{}Params" , entity. struct_name) ;
236236
237- let insert_fields: Vec < TokenStream > = non_pk_fields
237+ // When all columns are PKs (e.g. junction tables), use pk_fields for insert
238+ let insert_source_fields: Vec < & ParsedField > = if non_pk_fields. is_empty ( ) {
239+ pk_fields. clone ( )
240+ } else {
241+ non_pk_fields. clone ( )
242+ } ;
243+
244+ let insert_fields: Vec < TokenStream > = insert_source_fields
238245 . iter ( )
239246 . map ( |f| {
240247 let name = format_ident ! ( "{}" , f. rust_name) ;
@@ -243,11 +250,11 @@ pub fn generate_crud_from_parsed(
243250 } )
244251 . collect ( ) ;
245252
246- let col_names: Vec < & str > = non_pk_fields . iter ( ) . map ( |f| f. column_name . as_str ( ) ) . collect ( ) ;
253+ let col_names: Vec < & str > = insert_source_fields . iter ( ) . map ( |f| f. column_name . as_str ( ) ) . collect ( ) ;
247254 let col_list = col_names. join ( ", " ) ;
248255 // Use casted placeholders for macro mode, plain for runtime
249- let placeholders = build_placeholders ( non_pk_fields . len ( ) , db_kind, 1 ) ;
250- let placeholders_cast = build_placeholders_with_cast ( & non_pk_fields , db_kind, 1 , true ) ;
256+ let placeholders = build_placeholders ( insert_source_fields . len ( ) , db_kind, 1 ) ;
257+ let placeholders_cast = build_placeholders_with_cast ( & insert_source_fields , db_kind, 1 , true ) ;
251258
252259 let build_insert_sql = |ph : & str | match db_kind {
253260 DatabaseKind :: Postgres | DatabaseKind :: Sqlite => {
@@ -266,7 +273,7 @@ pub fn generate_crud_from_parsed(
266273 let sql = build_insert_sql ( & placeholders) ;
267274 let sql_macro = build_insert_sql ( & placeholders_cast) ;
268275
269- let binds: Vec < TokenStream > = non_pk_fields
276+ let binds: Vec < TokenStream > = insert_source_fields
270277 . iter ( )
271278 . map ( |f| {
272279 let name = format_ident ! ( "{}" , f. rust_name) ;
@@ -283,7 +290,7 @@ pub fn generate_crud_from_parsed(
283290 db_kind,
284291 & table_name,
285292 & pk_fields,
286- & non_pk_fields ,
293+ & insert_source_fields ,
287294 use_macro,
288295 ) ;
289296 method_tokens. push ( insert_method) ;
@@ -296,8 +303,8 @@ pub fn generate_crud_from_parsed(
296303 } ) ;
297304 }
298305
299- // --- update (skip for views) ---
300- if !is_view && methods. update && !pk_fields. is_empty ( ) {
306+ // --- update (skip for views, skip when all columns are PKs — nothing to SET ) ---
307+ if !is_view && methods. update && !pk_fields. is_empty ( ) && !non_pk_fields . is_empty ( ) {
301308 let update_params_ident = format_ident ! ( "Update{}Params" , entity. struct_name) ;
302309
303310 let update_fields: Vec < TokenStream > = entity
@@ -1568,4 +1575,51 @@ mod tests {
15681575 let code = parse_and_format ( & tokens) ;
15691576 assert ! ( code. contains( "as_slice()" ) , "Expected as_slice() in generated code:\n {}" , code) ;
15701577 }
1578+
1579+ // --- composite PK only (junction table) ---
1580+
1581+ fn junction_entity ( ) -> ParsedEntity {
1582+ ParsedEntity {
1583+ struct_name : "AnalysisRecord" . to_string ( ) ,
1584+ table_name : "analysis.analysis__record" . to_string ( ) ,
1585+ schema_name : None ,
1586+ is_view : false ,
1587+ fields : vec ! [
1588+ make_field( "record_id" , "record_id" , "uuid::Uuid" , false , true ) ,
1589+ make_field( "analysis_id" , "analysis_id" , "uuid::Uuid" , false , true ) ,
1590+ ] ,
1591+ imports : vec ! [ ] ,
1592+ }
1593+ }
1594+
1595+ #[ test]
1596+ fn test_composite_pk_only_insert_generated ( ) {
1597+ let code = gen ( & junction_entity ( ) , DatabaseKind :: Postgres ) ;
1598+ assert ! ( code. contains( "pub struct InsertAnalysisRecordParams" ) , "Expected InsertAnalysisRecordParams struct:\n {}" , code) ;
1599+ assert ! ( code. contains( "pub record_id" ) , "Expected record_id field in insert params:\n {}" , code) ;
1600+ assert ! ( code. contains( "pub analysis_id" ) , "Expected analysis_id field in insert params:\n {}" , code) ;
1601+ assert ! ( code. contains( "INSERT INTO analysis.analysis__record (record_id, analysis_id) VALUES ($1, $2) RETURNING *" ) , "Expected valid INSERT SQL:\n {}" , code) ;
1602+ assert ! ( code. contains( "pub async fn insert" ) , "Expected insert method:\n {}" , code) ;
1603+ }
1604+
1605+ #[ test]
1606+ fn test_composite_pk_only_no_update ( ) {
1607+ let code = gen ( & junction_entity ( ) , DatabaseKind :: Postgres ) ;
1608+ assert ! ( !code. contains( "UpdateAnalysisRecordParams" ) , "Expected no UpdateAnalysisRecordParams struct:\n {}" , code) ;
1609+ assert ! ( !code. contains( "pub async fn update" ) , "Expected no update method:\n {}" , code) ;
1610+ }
1611+
1612+ #[ test]
1613+ fn test_composite_pk_only_delete_generated ( ) {
1614+ let code = gen ( & junction_entity ( ) , DatabaseKind :: Postgres ) ;
1615+ assert ! ( code. contains( "pub async fn delete" ) , "Expected delete method:\n {}" , code) ;
1616+ assert ! ( code. contains( "DELETE FROM analysis.analysis__record WHERE record_id = $1 AND analysis_id = $2" ) , "Expected valid DELETE SQL:\n {}" , code) ;
1617+ }
1618+
1619+ #[ test]
1620+ fn test_composite_pk_only_get_generated ( ) {
1621+ let code = gen ( & junction_entity ( ) , DatabaseKind :: Postgres ) ;
1622+ assert ! ( code. contains( "pub async fn get" ) , "Expected get method:\n {}" , code) ;
1623+ assert ! ( code. contains( "WHERE record_id = $1 AND analysis_id = $2" ) , "Expected WHERE clause with both PK columns:\n {}" , code) ;
1624+ }
15711625}
0 commit comments