Files
core/test/dever-tests/tests/orm_query.rs
T

550 lines
17 KiB
Rust

use dever_core::source::SourceMap;
use dever_runtime::config::RuntimeProfile;
const MODEL: &str = r#"
public type UserStatus {
Active = "启用"
Disabled = "禁用"
}
public type User {
external_uuid: Uuid default "018f08d8-59b0-7cc2-98c4-dc0c0c07398f"
email: Text(254) unique
display_name: Text(1, 64)
status: UserStatus default UserStatus.Active index
enabled: Bool default true
age: Int default 0
alias: Text?
}
"#;
const SERVICE: &str = r#"
public register() (user: user.model.user.User) {
user = user.create({
external_uuid = "018f08d8-59b0-7cc2-98c4-dc0c0c07398f"
email = "reader@example.com"
display_name = "Reader"
})
}
public active() (users: user.model.user.Page) {
users = user.list({
where = status == user.model.user.UserStatus.Active and enabled == true
order = [created_at.desc]
page = 2
size = 10
})
}
public find(id: user.model.user.id) (user: user.model.user.User) {
user = user.get(id)
}
public disable(id: user.model.user.id) (changed: Int) {
changed = user.update(id, {
status = user.model.user.UserStatus.Disabled
enabled = false
})
}
public remove_disabled() (changed: Int) {
changed = user.delete({
where = status == user.model.user.UserStatus.Disabled
})
}
public bulk() (changed: Int) {
changed = user.create_many([
{
email = "one@example.com"
display_name = "One"
},
{
email = "two@example.com"
display_name = "Two"
}
])
}
public advanced() (users: user.model.user.Page) {
users = user.list({
where = not contains(display_name, "bot") and in(status, [user.model.user.UserStatus.Active, user.model.user.UserStatus.Disabled]) and between(age, 18, 65) and starts_with(email, "reader") and ends_with(email, ".com") and alias == null
})
}
public page_after() (users: user.model.user.Cursor) {
users = user.cursor({
after = null
order = [created_at.desc, id.desc]
size = 10
})
}
show(user: user.model.user.User) () {}
public scan() () {
users = user.stream({ size = 4 })
each(show, users)
}
public save() (saved: user.model.user.User) {
saved = user.upsert(
{ email = "reader@example.com" },
{ display_name = "Reader" },
{ display_name = "Updated" }
)
}
"#;
fn sources(service: &str) -> SourceMap {
let mut sources = SourceMap::default();
sources.add("user/model/user.dever", MODEL);
sources.add("user/service/account.dever", service);
sources
}
fn errors(service: &str) -> String {
let sources = sources(service);
dever_core::check(&sources)
.unwrap_err()
.iter()
.map(|error| error.render(&sources))
.collect()
}
#[test]
fn model_operations_build_static_query_plans_and_parameterized_sql() {
let sources = sources(SERVICE);
let program = dever_core::check(&sources).unwrap();
let api = program.api_snapshot();
assert!(api.contains("function user.service.account.register"));
assert!(api.contains("effects=[database]"), "{api}");
let create =
dever_core::native::emit(&program, &sources, "user.service.account.register").unwrap();
assert!(create.contains("INSERT INTO \\\"user\\\""), "{create}");
assert!(create.contains("VALUES (?1, ?2, ?3)"), "{create}");
assert!(
create.contains("dever_runtime::orm::Uuid::from_bytes"),
"{create}"
);
assert!(
create.contains("dever_runtime::orm::Value::Uuid"),
"{create}"
);
assert!(create.contains("$1::uuid"), "{create}");
assert!(create.contains("decode_model_"), "{create}");
let list = dever_core::native::emit(&program, &sources, "user.service.account.active").unwrap();
assert!(list.contains("SELECT COUNT(*) FROM \\\"user\\\""), "{list}");
assert!(
list.contains("ORDER BY \\\"created_at\\\" DESC, \\\"id\\\" DESC"),
"{list}"
);
assert!(list.contains("LIMIT ?3 OFFSET ?4"), "{list}");
let advanced =
dever_core::native::emit(&program, &sources, "user.service.account.advanced").unwrap();
assert!(
advanced.contains("NOT (instr(\\\"display_name\\\", ?1) > 0)"),
"{advanced}"
);
assert!(
advanced.contains("\\\"status\\\" IN (?2, ?3)"),
"{advanced}"
);
assert!(
advanced.contains("\\\"age\\\" BETWEEN ?4 AND ?5"),
"{advanced}"
);
assert!(advanced.contains("\\\"alias\\\" IS NULL"), "{advanced}");
let cursor =
dever_core::native::emit(&program, &sources, "user.service.account.page_after").unwrap();
assert!(
cursor.contains("(CAST(?1 AS BIGINT) IS NULL OR ((\\\"created_at\\\" < ?1) OR (\\\"created_at\\\" IS ?1 AND \\\"id\\\" < ?2)))"),
"{cursor}"
);
assert!(
cursor.contains("(CAST($1 AS BIGINT) IS NULL OR ((\\\"created_at\\\" < $1) OR (\\\"created_at\\\" IS NOT DISTINCT FROM $1 AND \\\"id\\\" < $2)))"),
"{cursor}"
);
assert!(cursor.contains("LIMIT ?3"), "{cursor}");
assert!(!cursor.contains("COUNT(*)"), "{cursor}");
let bulk = dever_core::native::emit(&program, &sources, "user.service.account.bulk").unwrap();
assert!(bulk.contains("VALUES (?1, ?2), (?3, ?4)"), "{bulk}");
let stream = dever_core::native::emit(&program, &sources, "user.service.account.scan").unwrap();
assert!(stream.contains("database.stream("), "{stream}");
assert!(stream.contains("ORDER BY \\\"id\\\" DESC"), "{stream}");
assert!(
!stream.contains("collect::<Result<Vec<_>, _>>()"),
"{stream}"
);
let upsert = dever_core::native::emit(&program, &sources, "user.service.account.save").unwrap();
assert!(
upsert.contains("ON CONFLICT (\\\"email\\\") DO UPDATE"),
"{upsert}"
);
}
#[test]
fn model_operations_reject_unknown_fields_and_invalid_query_shapes() {
let unknown = SERVICE.replace(
"email = \"reader@example.com\"",
"unknown = \"reader@example.com\"",
);
assert!(errors(&unknown).contains("unknown Model field 'unknown'"));
let generated = SERVICE.replace(
"email = \"reader@example.com\"",
"id = \"reader@example.com\"",
);
assert!(errors(&generated).contains("generated Model field 'id'"));
let unknown_option = SERVICE.replace("page = 2", "offset = 2");
assert!(errors(&unknown_option).contains("unknown query option 'offset'"));
let invalid_in = SERVICE.replace(
"in(status, [user.model.user.UserStatus.Active, user.model.user.UserStatus.Disabled])",
"in(status, [])",
);
assert!(errors(&invalid_in).contains("between 1 and 100 values"));
let invalid_text = SERVICE.replace("contains(display_name, \"bot\")", "contains(age, \"bot\")");
assert!(errors(&invalid_text).contains("requires a Text field"));
let invalid_arity =
SERVICE.replace("contains(display_name, \"bot\")", "contains(display_name)");
assert!(errors(&invalid_arity).contains("where 'contains' expects 2 arguments"));
let invalid_field = SERVICE.replace(
"contains(display_name, \"bot\")",
"contains(\"bot\", display_name)",
);
assert!(errors(&invalid_field).contains("the first 'contains' argument must be a Model field"));
let outside_where = SERVICE.replace(
"user = user.get(id)",
"value = contains(\"reader\", \"read\")\n user = user.get(id)",
);
let outside_where_errors = errors(&outside_where);
assert!(
outside_where_errors.contains("unknown function 'contains' with 2 argument(s)"),
"{outside_where_errors}"
);
let invalid_cursor = SERVICE.replace("order = [created_at.desc, id.desc]", "order = alias.asc");
assert!(errors(&invalid_cursor).contains("does not support nullable Model fields"));
}
#[test]
fn empty_create_many_uses_one_valid_statement_per_dialect() {
let mut sources = SourceMap::default();
sources.add("app/event/model.dever", "type Event {}");
sources.add(
"app/event/app.dever",
"create() (created: Int) { created = model.create_many([{}, {}]) }",
);
let program = dever_core::check(&sources).unwrap();
let native = dever_core::native::emit(&program, &sources, "app.event.create").unwrap();
assert!(
native.contains("sqlite: \"INSERT INTO \\\"event\\\" (\\\"id\\\") VALUES (NULL), (NULL)\""),
"{native}"
);
assert!(
native.contains("postgres: \"INSERT INTO \\\"event\\\" (\\\"created_at\\\") VALUES (DEFAULT), (DEFAULT)\""),
"{native}"
);
}
#[test]
fn transaction_functions_generate_one_shared_database_boundary() {
let service = SERVICE.replacen("register()", "transaction register()", 1);
let sources = sources(&service);
let program = dever_core::check(&sources).unwrap();
let native =
dever_core::native::emit(&program, &sources, "user.service.account.register").unwrap();
assert!(
native.contains("transaction_database.begin().await"),
"{native}"
);
assert!(native.contains("transaction.commit().await"), "{native}");
assert!(native.contains("transaction.rollback().await"), "{native}");
assert!(native.contains("database::Executor::new"), "{native}");
assert!(
native.contains("postgres: \"INSERT INTO \\\"user\\\""),
"{native}"
);
}
#[test]
fn database_effects_flow_through_helpers_and_nested_transactions() {
let helper = r#"
create_user() (user: user.model.user.User) {
user = user.create({
email = "reader@example.com"
display_name = "Reader"
})
}
public transaction register() (user: user.model.user.User) {
user = create_user()
}
"#;
let helper_sources = sources(helper);
let helper_program = dever_core::check(&helper_sources).unwrap();
let helper_native = dever_core::native::emit(
&helper_program,
&helper_sources,
"user.service.account.register",
)
.unwrap();
assert!(
helper_native
.matches("Option<&dever_runtime::database::Transaction>")
.count()
>= 2,
"{helper_native}"
);
assert!(helper_native.contains("(None).await"), "{helper_native}");
assert!(
helper_native.contains("(_database).await"),
"{helper_native}"
);
let nested = helper.replacen(
"create_user() (user: user.model.user.User)",
"transaction create_user() (user: user.model.user.User)",
1,
);
let nested_sources = sources(&nested);
let nested_program = dever_core::check(&nested_sources).unwrap();
let nested_native = dever_core::native::emit(
&nested_program,
&nested_sources,
"user.service.account.register",
)
.unwrap();
assert_eq!(
nested_native
.matches("transaction_database.begin().await")
.count(),
2,
"{nested_native}"
);
assert!(
nested_native.contains("(_database).await"),
"{nested_native}"
);
}
#[test]
fn transaction_functions_require_database_work_and_reject_concurrency() {
let mut empty = SourceMap::default();
empty.add("main.dever", "public transaction save() () {}");
let empty_error = dever_core::check(&empty)
.unwrap_err()
.iter()
.map(|error| error.render(&empty))
.collect::<String>();
assert!(
empty_error.contains("must perform a database operation"),
"{empty_error}"
);
let concurrent = r#"work() () {}
public transaction register() (user: user.model.user.User) {
task = run(work())
user = user.create({
email = "reader@example.com"
display_name = "Reader"
})
wait(task)
}
"#;
assert!(errors(concurrent).contains("transaction functions cannot start concurrent work"));
let streaming = r#"public transaction scan() () {
users = user.stream({ size = 1 })
close(users)
}
"#;
assert!(errors(streaming).contains("transaction functions cannot start concurrent work"));
let structured_blocking = r#"hash(password: Secret) (value: Text) {
value = blocking(dever.crypto.password_hash(password))
}
public transaction register(password: Secret) (user: user.model.user.User) {
password_hash = hash(password)
user = user.create({
email = password_hash
display_name = "Reader"
})
}
"#;
assert!(
errors(structured_blocking)
.contains("transaction functions cannot perform password hashing or verification")
);
let short_transaction = r#"hash(password: Secret) (value: Text) {
value = blocking(dever.crypto.password_hash(password))
}
transaction create_user(email: Text) (user: user.model.user.User) {
user = user.create({
email = email
display_name = "Reader"
})
}
public register(password: Secret) (user: user.model.user.User) {
password_hash = hash(password)
user = create_user(password_hash)
}
"#;
let sources = sources(short_transaction);
dever_core::check(&sources).unwrap_or_else(|errors| {
panic!(
"{}",
errors
.iter()
.map(|error| error.render(&sources))
.collect::<String>()
)
});
}
#[test]
fn nullable_comparisons_use_null_safe_database_operators() {
let service = r#"
find_alias(value: Text?) (users: user.model.user.Page) {
users = user.list({ where = alias == value })
}
public main() (users: user.model.user.Page) {
users = find_alias(null)
}
"#;
let sources = sources(service);
let program = dever_core::check(&sources).unwrap();
let native = dever_core::native::emit(&program, &sources, "user.service.account.main").unwrap();
assert!(native.contains("\\\"alias\\\" IS ?1"), "{native}");
assert!(
native.contains("\\\"alias\\\" IS NOT DISTINCT FROM $1"),
"{native}"
);
}
#[test]
fn database_effects_follow_bound_handlers_inside_transactions() {
let service = r#"
apply(route: handler() (user: user.model.user.User)) (user: user.model.user.User) {
user = route()
}
forward(route: handler() (user: user.model.user.User)) (user: user.model.user.User) {
user = apply(route)
}
create_user() (user: user.model.user.User) {
user = user.create({
email = "reader@example.com"
display_name = "Reader"
})
}
public transaction register() (user: user.model.user.User) {
user = forward(create_user)
}
"#;
let sources = sources(service);
let program = dever_core::check(&sources).unwrap();
let native =
dever_core::native::emit(&program, &sources, "user.service.account.register").unwrap();
assert!(
native.contains("transaction_database.begin().await"),
"{native}"
);
assert!(native.contains("(_database).await"), "{native}");
let concurrent = service.replace(
"create_user() (user: user.model.user.User) {",
"work() () {}\n\ncreate_user() (user: user.model.user.User) {\n task = run(work())\n wait(task)",
);
assert!(errors(&concurrent).contains("transaction functions cannot start concurrent work"));
}
#[test]
fn project_models_generate_scoped_storage_and_controlled_tenant_migration() {
let mut sources = SourceMap::default();
sources.add(
"platform/role/model.dever",
"global type Role { name: Text(1, 32) }",
);
sources.add(
"user/profile/model.dever",
"type Profile {\n role_id: platform.role.model.id\n display_name: Text(1, 64)\n}",
);
sources.add(
"user/profile/app.dever",
"count() (value: Int) { value = model.count() }",
);
sources.add("user/profile/api.dever", "cmd count = app.count");
let program = dever_core::check(&sources).unwrap();
let native = dever_core::native::emit_project(
&program,
&sources,
RuntimeProfile {
sqlite: true,
postgres: false,
},
)
.unwrap();
assert!(!native.contains("MODEL_DATABASE_"), "{native}");
assert!(native.contains("MODEL_STORAGE_"), "{native}");
assert!(
native.contains("OnceLock<dever_runtime::database::StorageBinding>"),
"{native}"
);
assert!(
native.contains("dever_runtime::database::ModelScope::Global"),
"{native}"
);
assert!(
native.contains("dever_runtime::database::ModelScope::Tenant"),
"{native}"
);
assert!(
native.contains("dever_runtime::config::bootstrap_scoped"),
"{native}"
);
assert!(
native.contains("async fn tenant_migrate(tenant_id: i64)"),
"{native}"
);
assert!(native.contains("--dever-tenant-migrate"), "{native}");
assert!(
native.contains("dever_runtime::tenant::begin_migration"),
"{native}"
);
assert!(
native.contains("dever_runtime::tenant::mark_database_ready"),
"{native}"
);
assert!(
native.contains("dever_runtime::tenant::finish_migration"),
"{native}"
);
assert!(!native.contains("REFERENCES \\\"role\\\""), "{native}");
}