Skip to content

Commit 8fc84df

Browse files
committed
fix: add primarykey introspection for views
1 parent ca79a48 commit 8fc84df

File tree

6 files changed

+350
-4
lines changed

6 files changed

+350
-4
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.5"
3+
version = "0.4.6"
44
edition = "2021"
55
description = "Generate Rust structs from database schema introspection"
66
license = "MIT"

crates/sqlx_gen/src/introspect/mysql.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub async fn introspect(
2020
if !views.is_empty() {
2121
let sources = fetch_view_column_sources(pool, schemas).await?;
2222
resolve_view_nullability(&mut views, &sources, &tables);
23+
resolve_view_primary_keys(&mut views, &sources, &tables);
2324
}
2425

2526
let enums = extract_enums(&tables);
@@ -248,6 +249,51 @@ fn resolve_view_nullability(
248249
}
249250
}
250251

252+
fn resolve_view_primary_keys(
253+
views: &mut [TableInfo],
254+
sources: &[ViewColumnSource],
255+
tables: &[TableInfo],
256+
) {
257+
// Build table column lookup: (schema, table, column) -> is_primary_key
258+
let mut table_lookup: HashMap<(&str, &str, &str), bool> = HashMap::new();
259+
for table in tables {
260+
for col in &table.columns {
261+
table_lookup.insert(
262+
(&table.schema_name, &table.name, &col.name),
263+
col.is_primary_key,
264+
);
265+
}
266+
}
267+
268+
// Build view column source lookup: (view_schema, view_name, column_name) -> Vec<is_pk>
269+
let mut view_lookup: HashMap<(&str, &str, &str), Vec<bool>> = HashMap::new();
270+
for src in sources {
271+
if let Some(&is_pk) =
272+
table_lookup.get(&(src.table_schema.as_str(), src.table_name.as_str(), src.column_name.as_str()))
273+
{
274+
view_lookup
275+
.entry((&src.view_schema, &src.view_name, &src.column_name))
276+
.or_default()
277+
.push(is_pk);
278+
}
279+
}
280+
281+
for view in views.iter_mut() {
282+
for col in view.columns.iter_mut() {
283+
if let Some(pk_flags) = view_lookup.get(&(
284+
view.schema_name.as_str(),
285+
view.name.as_str(),
286+
col.name.as_str(),
287+
)) {
288+
// Only mark as PK if ALL sources are PKs
289+
if !pk_flags.is_empty() && pk_flags.iter().all(|&pk| pk) {
290+
col.is_primary_key = true;
291+
}
292+
}
293+
}
294+
}
295+
}
296+
251297
/// Extract inline ENUMs from column types.
252298
/// MySQL ENUM('a','b','c') in COLUMN_TYPE gets extracted to an EnumInfo
253299
/// keyed by table_name + column_name.
@@ -565,4 +611,61 @@ mod tests {
565611
resolve_view_nullability(&mut views, &[], &tables);
566612
assert!(views[0].columns[0].is_nullable);
567613
}
614+
615+
// ========== resolve_view_primary_keys ==========
616+
617+
fn make_table_with_pk(
618+
schema: &str,
619+
name: &str,
620+
columns: Vec<(&str, bool)>,
621+
) -> TableInfo {
622+
TableInfo {
623+
schema_name: schema.to_string(),
624+
name: name.to_string(),
625+
columns: columns
626+
.into_iter()
627+
.enumerate()
628+
.map(|(i, (col, is_pk))| ColumnInfo {
629+
name: col.to_string(),
630+
data_type: "varchar".to_string(),
631+
udt_name: "varchar(255)".to_string(),
632+
is_nullable: false,
633+
is_primary_key: is_pk,
634+
ordinal_position: i as i32,
635+
schema_name: schema.to_string(),
636+
column_default: None,
637+
})
638+
.collect(),
639+
}
640+
}
641+
642+
#[test]
643+
fn test_resolve_pk_column() {
644+
let tables = vec![make_table_with_pk("db", "users", vec![("id", true), ("name", false)])];
645+
let mut views = vec![make_view("db", "my_view", vec!["id", "name"])];
646+
let sources = vec![
647+
make_source("db", "my_view", "db", "users", "id"),
648+
make_source("db", "my_view", "db", "users", "name"),
649+
];
650+
resolve_view_primary_keys(&mut views, &sources, &tables);
651+
assert!(views[0].columns[0].is_primary_key);
652+
assert!(!views[0].columns[1].is_primary_key);
653+
}
654+
655+
#[test]
656+
fn test_resolve_pk_no_sources() {
657+
let tables = vec![make_table_with_pk("db", "users", vec![("id", true)])];
658+
let mut views = vec![make_view("db", "my_view", vec!["id"])];
659+
resolve_view_primary_keys(&mut views, &[], &tables);
660+
assert!(!views[0].columns[0].is_primary_key);
661+
}
662+
663+
#[test]
664+
fn test_resolve_pk_no_match() {
665+
let tables = vec![make_table_with_pk("db", "users", vec![("id", true)])];
666+
let mut views = vec![make_view("db", "my_view", vec!["computed"])];
667+
let sources = vec![];
668+
resolve_view_primary_keys(&mut views, &sources, &tables);
669+
assert!(!views[0].columns[0].is_primary_key);
670+
}
568671
}

crates/sqlx_gen/src/introspect/postgres.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
233323
async 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

Comments
 (0)