Skip to content

Commit 0ac11ca

Browse files
committed
fix: support primary and foreign keys
1 parent be9485f commit 0ac11ca

File tree

4 files changed

+67
-13
lines changed

4 files changed

+67
-13
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/sqlx_gen/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sqlx-gen"
3-
version = "0.4.7"
3+
version = "0.4.8"
44
edition = "2021"
55
description = "Generate Rust structs from database schema introspection"
66
license = "MIT"

crates/sqlx_gen/src/codegen/crud_gen.rs

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

crates/sqlx_gen_macros/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sqlx-gen-macros"
3-
version = "0.4.7"
3+
version = "0.4.8"
44
edition = "2021"
55
description = "No-op attribute macros for sqlx-gen generated code"
66
license = "MIT"

0 commit comments

Comments
 (0)