#![cfg(feature = "c-sqlite-tests")]
use asupersync::runtime::RuntimeBuilder;
use asupersync::{Cx, Outcome};
use serde::{Deserialize, Serialize};
use sqlmodel::SchemaBuilder;
use sqlmodel::prelude::*;
use sqlmodel_query::DeleteBuilder;
use sqlmodel_sqlite::SqliteConnection;
fn unwrap_outcome<T>(outcome: Outcome<T, Error>) -> T {
match outcome {
Outcome::Ok(v) => v,
Outcome::Err(e) => panic!("unexpected error: {e}"),
Outcome::Cancelled(r) => panic!("cancelled: {r:?}"),
Outcome::Panicked(p) => panic!("panicked: {p:?}"),
}
}
#[derive(sqlmodel::Model, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[sqlmodel(table = "employees", inheritance = "single", discriminator = "kind")]
struct Employee {
#[sqlmodel(primary_key)]
id: i64,
name: String,
kind: String,
}
#[derive(sqlmodel::Model, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[sqlmodel(inherits = "Employee", discriminator_value = "manager")]
struct Manager {
#[sqlmodel(primary_key)]
id: i64,
name: String,
#[sqlmodel(nullable)]
department: Option<String>,
}
#[derive(sqlmodel::Model, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[sqlmodel(inherits = "Employee", discriminator_value = "engineer")]
struct Engineer {
#[sqlmodel(primary_key)]
id: i64,
name: String,
#[sqlmodel(nullable)]
specialty: Option<String>,
}
async fn count_where(cx: &Cx, conn: &SqliteConnection, predicate: &str) -> i64 {
let rows = unwrap_outcome(
conn.query(
cx,
&format!("SELECT COUNT(*) FROM employees WHERE {predicate}"),
&[],
)
.await,
);
rows[0].get_as::<i64>(0).unwrap()
}
#[test]
fn sti_models_share_one_table_and_children_are_discriminated() {
let rt = RuntimeBuilder::current_thread()
.build()
.expect("create asupersync runtime");
let cx = Cx::for_testing();
rt.block_on(async {
let conn = SqliteConnection::open_memory().expect("open sqlite memory db");
assert_eq!(<Manager as Model>::TABLE_NAME, "employees");
assert_eq!(<Engineer as Model>::TABLE_NAME, "employees");
assert_eq!(Manager::inheritance().discriminator_column, Some("kind"));
assert_eq!(Manager::inheritance().discriminator_value, Some("manager"));
assert_eq!(
Engineer::inheritance().discriminator_value,
Some("engineer")
);
let stmts = SchemaBuilder::new()
.create_table::<Employee>()
.create_table::<Manager>()
.create_table::<Engineer>()
.build();
let creates = stmts
.iter()
.filter(|s| s.starts_with("CREATE TABLE"))
.count();
assert_eq!(
creates, 1,
"STI children must not create their own table: {stmts:?}"
);
assert!(
stmts
.iter()
.any(|s| s.contains("ADD COLUMN") && s.contains("\"department\"")),
"child-only column department must be added to employees: {stmts:?}"
);
assert!(
stmts
.iter()
.any(|s| s.contains("ADD COLUMN") && s.contains("\"specialty\"")),
"child-only column specialty must be added to employees: {stmts:?}"
);
for stmt in &stmts {
unwrap_outcome(conn.execute(&cx, stmt, &[]).await);
}
unwrap_outcome(
insert!(&Employee {
id: 1,
name: "Plain".into(),
kind: "employee".into(),
})
.execute(&cx, &conn)
.await,
);
unwrap_outcome(
insert!(&Manager {
id: 2,
name: "Mia".into(),
department: Some("Platform".into()),
})
.execute(&cx, &conn)
.await,
);
unwrap_outcome(
insert!(&Engineer {
id: 3,
name: "Eli".into(),
specialty: Some("Storage".into()),
})
.execute(&cx, &conn)
.await,
);
unwrap_outcome(
insert!(&Manager {
id: 4,
name: "Max".into(),
department: None,
})
.execute(&cx, &conn)
.await,
);
assert_eq!(count_where(&cx, &conn, "kind = 'manager'").await, 2);
assert_eq!(count_where(&cx, &conn, "kind = 'engineer'").await, 1);
assert_eq!(count_where(&cx, &conn, "kind = 'employee'").await, 1);
assert_eq!(count_where(&cx, &conn, "1 = 1").await, 4);
let managers: Vec<Manager> = unwrap_outcome(
select!(Manager)
.order_by(Expr::col("id").asc())
.all(&cx, &conn)
.await,
);
assert_eq!(
managers.iter().map(|m| m.id).collect::<Vec<_>>(),
vec![2, 4],
"select!(Manager) must return only manager rows"
);
assert_eq!(managers[0].department.as_deref(), Some("Platform"));
assert_eq!(managers[1].department, None);
let engineers: Vec<Engineer> = unwrap_outcome(select!(Engineer).all(&cx, &conn).await);
assert_eq!(engineers.len(), 1);
assert_eq!(engineers[0].specialty.as_deref(), Some("Storage"));
let named: Vec<Manager> = unwrap_outcome(
select!(Manager)
.filter(Expr::col("name").eq("Max"))
.all(&cx, &conn)
.await,
);
assert_eq!(named.len(), 1);
assert_eq!(named[0].id, 4);
let everyone: Vec<Employee> = unwrap_outcome(
select!(Employee)
.order_by(Expr::col("id").asc())
.all(&cx, &conn)
.await,
);
assert_eq!(everyone.len(), 4);
assert_eq!(
everyone.iter().map(|e| e.kind.as_str()).collect::<Vec<_>>(),
vec!["employee", "manager", "engineer", "manager"]
);
let updated = unwrap_outcome(
update!(&Manager {
id: 2,
name: "Mia Renamed".into(),
department: Some("Platform".into()),
})
.execute(&cx, &conn)
.await,
);
assert_eq!(updated, 1);
assert_eq!(
count_where(
&cx,
&conn,
"id = 2 AND kind = 'manager' AND name = 'Mia Renamed'"
)
.await,
1
);
let deleted = unwrap_outcome(
DeleteBuilder::<Manager>::new()
.filter(Expr::col("id").ge(1))
.execute(&cx, &conn)
.await,
);
assert_eq!(
deleted, 2,
"delete!(Manager) with a broad filter must remove only manager rows"
);
assert_eq!(count_where(&cx, &conn, "kind = 'manager'").await, 0);
assert_eq!(
count_where(&cx, &conn, "1 = 1").await,
2,
"the plain employee and the engineer must survive a Manager delete"
);
let deleted_all = unwrap_outcome(
DeleteBuilder::<Employee>::new()
.filter(Expr::col("id").ge(1))
.execute(&cx, &conn)
.await,
);
assert_eq!(deleted_all, 2);
assert_eq!(count_where(&cx, &conn, "1 = 1").await, 0);
});
}