feat: release Dever 0.1.5 with direct workers and long transactions

This commit is contained in:
Codex committed 2026-10-11 11:17:33 +00:00
1 parent 89629c2197
commit a900d7e8db
153 files changed
+3676 -3708

No files matched your search

+35 -44
View File
@@ -99,11 +99,13 @@ pub(super) fn validate_rest_bindings(program: &Program, errors: &mut Vec<Diagnos
ExpressionKind::Call {
target: CallTarget::Function(target),
..
} => {
if !rest_function_allowed(program, *target, &model_domain) {
errors.push(Diagnostic::error("C013", "REST field bindings call only pure same-domain Domain functions or pure standard library functions", value.span));
}
} if !rest_function_allowed(program, *target, &model_domain) => {
errors.push(Diagnostic::error("C013", "REST field bindings call only pure same-domain Domain functions or pure standard library functions", value.span));
}
ExpressionKind::Call {
target: CallTarget::Function(_),
..
} => {}
ExpressionKind::Call { .. }
| ExpressionKind::ModelOperation { .. }
| ExpressionKind::JobEnqueue { .. }
@@ -270,57 +272,44 @@ fn rest_intrinsic_allowed(operation: crate::intrinsic::Intrinsic) -> bool {
}
pub(super) fn validate_write_transactions(program: &Program, errors: &mut Vec<Diagnostic>) {
let write_routes = program
let roots = program
.api_routes
.iter()
.filter(|route| route.method != "GET")
.map(|route| route.function)
.map(|route| (route.function, route.long))
.chain(
program
.api_commands
.iter()
.map(|command| (command.function, command.long)),
)
.chain(program.jobs.iter().map(|job| (job.function, job.long)))
.collect::<std::collections::BTreeSet<_>>();
for function in &write_routes {
let root = crate::specialize::Specialization {
function: *function,
handlers: Vec::new(),
};
let password_crypto = crate::specialize::uses_intrinsic(
program,
&root,
crate::intrinsic::Intrinsic::CryptoPasswordVerify,
) || crate::specialize::uses_intrinsic(
program,
&root,
crate::intrinsic::Intrinsic::CryptoPasswordHash,
);
if password_crypto
&& let Some(span) = crate::specialize::write_outside_transaction(program, &root)
{
let mut error = Diagnostic::error(
"C014",
"password crypto API writes must be enclosed in an explicit transaction function",
program.functions[*function].span,
);
error.related.push(crate::diagnostic::Label {
span,
message: "database write occurs outside the short transaction boundary".into(),
});
errors.push(error);
}
}
let roots = write_routes
.into_iter()
.chain(program.api_commands.iter().map(|command| command.function))
.chain(program.jobs.iter().map(|job| job.function))
.collect::<std::collections::BTreeSet<_>>();
for function in roots {
for (function, long) in roots {
let root = crate::specialize::Specialization {
function,
handlers: Vec::new(),
};
if crate::specialize::writes_database(program, &root)
if long {
if let Some(span) = crate::specialize::write_outside_transaction(program, &root) {
let mut error = Diagnostic::error(
"C014",
"long entry database writes must be enclosed in an explicit transaction function",
program.functions[function].span,
);
error.related.push(crate::diagnostic::Label {
span,
message: "database write occurs outside the explicit transaction boundary"
.into(),
});
errors.push(error);
}
} else if crate::specialize::writes_database(program, &root)
&& crate::specialize::starts_transaction_unsafe_work(program, &root)
{
errors.push(Diagnostic::error(
"C014",
"an API, CMD, or Job database transaction cannot start concurrent work",
"automatic entry transactions cannot contain external I/O, Workers, waits, concurrency, or password crypto; declare the entry 'long' and enclose its writes in explicit transaction functions",
program.functions[function].span,
));
}
@@ -526,6 +515,7 @@ pub(super) fn register(
let path = format!("/{prefix}/{}", binding.action.text);
if occupy(&mut routes, (method, path.clone()), binding.span, "HTTP route", errors) {
entries.routes.push(ApiRoute { method, path, function: id, inputs,
long: binding.long,
directory: directory.clone(), anonymous: binding.anonymous,
component: component.clone(), domain: domain.clone(),
action: binding.action.text.clone(), auth: None, permission: None,
@@ -536,7 +526,7 @@ pub(super) fn register(
} else {
let name = format!("{domain_name}.{}", binding.action.text);
if occupy(&mut commands, name.clone(), binding.span, "command", errors) {
entries.commands.push(ApiCommand { name, function: id, inputs, components: Vec::new() });
entries.commands.push(ApiCommand { name, function: id, long: binding.long, inputs, components: Vec::new() });
}
}
}
@@ -981,6 +971,7 @@ fn legacy_routes(
inputs,
directory: Vec::new(),
anonymous: false,
long: false,
span: clause.span,
component: function.name.split('.').next().unwrap_or("app").to_owned(),
domain: function.name.split('.').nth(1).unwrap_or("main").to_owned(),
+14 -11
View File
@@ -113,18 +113,19 @@ pub(super) fn register(
let mut scheduled = BTreeSet::new();
for declaration in &package.declarations {
match declaration {
Declaration::Database(binding) if is_job => {
if connection
.replace(ConnectionSelector::Explicit(binding.name.text.clone()))
.is_some()
{
errors.push(Diagnostic::error(
"C002",
"duplicate Job database binding",
binding.span,
));
}
Declaration::Database(binding)
if is_job
&& connection
.replace(ConnectionSelector::Explicit(binding.name.text.clone()))
.is_some() =>
{
errors.push(Diagnostic::error(
"C002",
"duplicate Job database binding",
binding.span,
));
}
Declaration::Database(_) if is_job => {}
Declaration::Function(clause) => {
let declared = matches!(clause.kind, FunctionKind::Job { .. });
if is_job != declared {
@@ -159,6 +160,7 @@ pub(super) fn register(
let FunctionKind::Job {
attempts,
timeout_ms,
long,
} = entry.kind
else {
continue;
@@ -210,6 +212,7 @@ pub(super) fn register(
};
jobs.push(Job {
function,
long,
connection: connection.clone(),
payload,
attempts,
+12 -16
View File
@@ -525,23 +525,19 @@ fn validate_model_declarations(
let mut sql = BTreeSet::new();
for declaration in &package.declarations {
match declaration {
Declaration::Database(binding) => {
if database.replace(binding.span).is_some() {
errors.push(Diagnostic::error(
"C002",
"a Model may declare only one database binding",
binding.span,
));
}
Declaration::Database(binding) if database.replace(binding.span).is_some() => {
errors.push(Diagnostic::error(
"C002",
"a Model may declare only one database binding",
binding.span,
));
}
Declaration::Seed(value) => {
if seed.replace(value.span).is_some() {
errors.push(Diagnostic::error(
"C002",
"a Model may declare only one seed block",
value.span,
));
}
Declaration::Seed(value) if seed.replace(value.span).is_some() => {
errors.push(Diagnostic::error(
"C002",
"a Model may declare only one seed block",
value.span,
));
}
Declaration::Migration(migration) => {
if !migrations.insert(&migration.name.text) {
+1 -6
View File
@@ -548,12 +548,7 @@ pub(super) fn referenced_types(ty: &Type, visit: &mut impl FnMut(usize)) {
| Type::Stream(element)
| Type::AsyncStream(element)
| Type::RowStream(element)
| Type::Related(element)
| Type::Channel(element) => {
if !matches!(ty, Type::Related(_)) {
referenced_types(element, visit)
}
}
| Type::Channel(element) => referenced_types(element, visit),
Type::Map(key, value) | Type::MapEntry(key, value) => {
referenced_types(key, visit);
referenced_types(value, visit);