1use std::collections::BTreeMap;
2use std::path::Path;
3
4use crate::ids::WorkspaceId;
5use anyhow::{Result, bail};
6use sqlx::SqliteConnection;
7use tracing::{info, warn};
8
9use crate::change_log::{ChangeEntity, ChangePayload, append_change, op_type};
10use crate::choices::{TaskPriority, TaskStatus};
11use crate::db::{Database, begin_immediate, set_field_version};
12use crate::ids::{TaskId, new_id, now};
13use crate::labels::resolve_labels_in_workspace;
14use crate::mutation::{set_task_field, set_task_project};
15use crate::projects::resolve_or_create_project_in_workspace;
16use crate::refs::get_task_in_workspace;
17use crate::task_fields::TaskField;
18use crate::types::Task;
19use crate::workspaces::Workspace;
20
21pub struct TaskDraft {
22 pub title: String,
23 pub description: String,
24 pub project: Option<String>,
25 pub status: String,
26 pub priority: String,
27 pub labels: Vec<String>,
28 pub available_at: Option<String>,
29 pub due_on: Option<String>,
30 pub is_epic: bool,
31}
32
33#[derive(Debug)]
34pub struct TaskOutcome {
35 pub task: Task,
36 pub create_change_id: Option<String>,
37 pub attachment_change_ids: Vec<String>,
38}
39
40struct InsertedTask {
41 id: TaskId,
42 change_id: String,
43 project_key: String,
44 label_count: usize,
45}
46
47#[derive(Default)]
48pub struct TaskUpdate {
49 pub title: Option<String>,
50 pub description: Option<String>,
51 pub project: Option<String>,
52 pub status: Option<String>,
53 pub priority: Option<String>,
54 pub available_at: Option<Option<String>>,
55 pub due_on: Option<Option<String>>,
56 pub is_epic: Option<bool>,
57 pub add_labels: Vec<String>,
58 pub remove_labels: Vec<String>,
59}
60
61pub struct TaskUpdateOutcome {
62 pub task: Task,
63 pub changed: bool,
64}
65
66pub struct NoteDeleteOutcome {
67 #[allow(dead_code)]
68 pub task_id: TaskId,
69 #[allow(dead_code)]
70 pub note_id: String,
71 pub changed: bool,
72}
73
74pub struct NoteOutcome {
75 #[allow(dead_code)]
76 pub task_id: TaskId,
77 pub note_id: String,
78 pub change_id: String,
79}
80impl Database {
81 pub async fn create_task(
82 &self,
83 workspace: &Workspace,
84 draft: TaskDraft,
85 ) -> Result<TaskOutcome> {
86 let mut conn = self.acquire().await?;
87 create_task(&mut conn, workspace, draft).await
88 }
89
90 pub async fn create_task_with_attachments(
91 &self,
92 workspace: &Workspace,
93 blob_dir: &Path,
94 lifecycle_policy: crate::attachments::lifecycle::LifecyclePolicy,
95 draft: TaskDraft,
96 attachments: Vec<super::attachments::TaskAttachmentAddInput>,
97 ) -> Result<TaskOutcome> {
98 let mut conn = self.acquire().await?;
99 create_task_with_attachments(
100 &mut conn,
101 workspace,
102 blob_dir,
103 lifecycle_policy,
104 draft,
105 attachments,
106 )
107 .await
108 }
109
110 pub async fn update_task(
111 &self,
112 workspace: &Workspace,
113 task_id: &TaskId,
114 update: TaskUpdate,
115 ) -> Result<TaskUpdateOutcome> {
116 let mut conn = self.acquire().await?;
117 update_task(&mut conn, workspace, task_id, update).await
118 }
119
120 pub async fn update_tasks(
121 &self,
122 workspace: &Workspace,
123 updates: Vec<(TaskId, TaskUpdate)>,
124 ) -> Result<Vec<TaskUpdateOutcome>> {
125 let mut conn = self.acquire().await?;
126 let mut outcomes = Vec::with_capacity(updates.len());
127 for (task_id, update) in updates {
128 outcomes.push(update_task(&mut conn, workspace, &task_id, update).await?);
129 }
130 Ok(outcomes)
131 }
132
133 pub async fn set_task_deleted(
134 &self,
135 workspace: &Workspace,
136 task_id: &TaskId,
137 deleted: bool,
138 ) -> Result<TaskOutcome> {
139 let mut conn = self.acquire().await?;
140 set_task_deleted(&mut conn, workspace, task_id, deleted).await
141 }
142
143 pub async fn add_note(
144 &self,
145 workspace: &Workspace,
146 task_id: &TaskId,
147 body: String,
148 ) -> Result<NoteOutcome> {
149 let mut conn = self.acquire().await?;
150 add_note(&mut conn, workspace, task_id, body).await
151 }
152
153 pub async fn delete_note(
154 &self,
155 workspace: &Workspace,
156 task_id: &TaskId,
157 note_id: &str,
158 ) -> Result<NoteDeleteOutcome> {
159 let mut conn = self.acquire().await?;
160 delete_note(&mut conn, workspace, task_id, note_id).await
161 }
162}
163
164pub async fn create_task(
165 conn: &mut SqliteConnection,
166 workspace: &Workspace,
167 draft: TaskDraft,
168) -> Result<TaskOutcome> {
169 validate_task_draft(&draft)?;
170 let mut tx = begin_immediate(conn).await?;
171 let inserted = insert_task(&mut tx, workspace, draft).await?;
172 tx.commit().await?;
173 info!(
174 task_id = %inserted.id,
175 project_key = %inserted.project_key,
176 label_count = inserted.label_count,
177 "task created"
178 );
179 Ok(TaskOutcome {
180 task: get_task_in_workspace(conn, workspace, &inserted.id).await?,
181 create_change_id: Some(inserted.change_id),
182 attachment_change_ids: Vec::new(),
183 })
184}
185
186pub async fn create_task_with_attachments(
187 conn: &mut SqliteConnection,
188 workspace: &Workspace,
189 blob_dir: &Path,
190 lifecycle_policy: crate::attachments::lifecycle::LifecyclePolicy,
191 draft: TaskDraft,
192 attachments: Vec<super::attachments::TaskAttachmentAddInput>,
193) -> Result<TaskOutcome> {
194 validate_task_draft(&draft)?;
195 let mut prepared = Vec::with_capacity(attachments.len());
196 for attachment in attachments {
197 prepared.push(super::attachments::prepare_task_attachment(attachment).await?);
198 }
199
200 let mut unique = BTreeMap::new();
201 for attachment in &prepared {
202 unique
203 .entry(attachment.sha256.clone())
204 .or_insert_with(|| attachment.clone());
205 }
206
207 let mut capacity_reservations = Vec::new();
208 for attachment in unique.values() {
209 let available: bool = sqlx::query_scalar(
210 "SELECT EXISTS(SELECT 1 FROM blob_inventory WHERE sha256 = ? AND available = 1)",
211 )
212 .bind(&attachment.sha256)
213 .fetch_one(&mut *conn)
214 .await?;
215 if available {
216 continue;
217 }
218 match crate::attachments::lifecycle::ensure_local_capacity(
219 conn,
220 blob_dir,
221 &attachment.sha256,
222 attachment.byte_size,
223 lifecycle_policy,
224 &crate::attachments::lifecycle::SystemClock,
225 )
226 .await
227 {
228 Ok(Some(reservation_id)) => capacity_reservations.push(reservation_id),
229 Ok(None) => {}
230 Err(error) => {
231 for reservation_id in capacity_reservations {
232 let _ =
233 crate::attachments::lifecycle::release_reservation(conn, &reservation_id)
234 .await;
235 }
236 return Err(error);
237 }
238 }
239 }
240
241 let mut staging_leases = Vec::with_capacity(unique.len());
242 for attachment in unique.values() {
243 match crate::attachments::lifecycle::acquire_lease(
244 conn,
245 &attachment.sha256,
246 "staging",
247 &crate::attachments::lifecycle::SystemClock,
248 )
249 .await
250 {
251 Ok(lease_id) => staging_leases.push(lease_id),
252 Err(error) => {
253 for lease_id in staging_leases {
254 let _ = crate::attachments::lifecycle::release_lease(conn, &lease_id).await;
255 }
256 for reservation_id in capacity_reservations {
257 let _ =
258 crate::attachments::lifecycle::release_reservation(conn, &reservation_id)
259 .await;
260 }
261 return Err(error);
262 }
263 }
264 }
265
266 let mut created_hashes = Vec::new();
267 for attachment in unique.values() {
268 match crate::attachments::storage::stage_blob(
269 blob_dir,
270 &attachment.sha256,
271 &attachment.bytes,
272 )
273 .await
274 {
275 Ok(staged) if staged.byte_size == attachment.byte_size => {
276 if staged.created {
277 created_hashes.push(staged.sha256);
278 }
279 }
280 Ok(_) => {
281 cleanup_attachment_guards(conn, &staging_leases, &capacity_reservations).await;
282 cleanup_created_objects(conn, blob_dir, &created_hashes).await;
283 bail!("error attachment-staged-size-mismatch");
284 }
285 Err(error) => {
286 cleanup_attachment_guards(conn, &staging_leases, &capacity_reservations).await;
287 cleanup_created_objects(conn, blob_dir, &created_hashes).await;
288 return Err(error);
289 }
290 }
291 }
292
293 let database_result = async {
294 let mut tx = begin_immediate(conn).await?;
295 for attachment in unique.values() {
296 crate::attachments::storage::upsert_inventory_available(
297 &mut tx,
298 &attachment.sha256,
299 attachment.byte_size,
300 &attachment.facts.media_type,
301 )
302 .await?;
303 }
304 let inserted = insert_task(&mut tx, workspace, draft).await?;
305 let mut attachment_change_ids = Vec::with_capacity(prepared.len());
306 let attachment_base = chrono::DateTime::parse_from_rfc3339(&now())?.to_utc();
307 for (index, attachment) in prepared.iter().enumerate() {
308 let created_at = (attachment_base
309 + chrono::TimeDelta::microseconds(i64::try_from(index)?))
310 .to_rfc3339_opts(chrono::SecondsFormat::Micros, true);
311 attachment_change_ids.push(
312 super::attachments::insert_prepared_attachment(
313 &mut tx,
314 workspace,
315 &inserted.id,
316 attachment,
317 &created_at,
318 )
319 .await?,
320 );
321 }
322 let task = get_task_in_workspace(&mut tx, workspace, &inserted.id).await?;
323 tx.commit().await?;
324 Ok::<_, anyhow::Error>((inserted, attachment_change_ids, task))
325 }
326 .await;
327
328 let (inserted, attachment_change_ids, task) = match database_result {
329 Ok(value) => value,
330 Err(error) => {
331 cleanup_attachment_guards(conn, &staging_leases, &capacity_reservations).await;
332 cleanup_created_objects(conn, blob_dir, &created_hashes).await;
333 return Err(error);
334 }
335 };
336 cleanup_attachment_guards(conn, &staging_leases, &capacity_reservations).await;
337 if let Err(error) = crate::attachments::lifecycle::reconcile_liveness(
338 conn,
339 &crate::attachments::lifecycle::SystemClock,
340 )
341 .await
342 {
343 warn!(%error, "failed to reconcile attachment liveness");
344 }
345 info!(
346 task_id = %inserted.id,
347 project_key = %inserted.project_key,
348 label_count = inserted.label_count,
349 attachment_count = prepared.len(),
350 "task created"
351 );
352 Ok(TaskOutcome {
353 task,
354 create_change_id: Some(inserted.change_id),
355 attachment_change_ids,
356 })
357}
358
359fn validate_task_draft(draft: &TaskDraft) -> Result<()> {
360 TaskStatus::parse(&draft.status)?;
361 TaskPriority::parse(&draft.priority)?;
362 if let Some(available_at) = draft.available_at.as_deref() {
363 crate::time_validation::validate_available_at_value(available_at)?;
364 }
365 if let Some(due_on) = draft.due_on.as_deref() {
366 crate::time_validation::validate_due_on_value(due_on)?;
367 }
368 Ok(())
369}
370
371async fn insert_task(
372 conn: &mut SqliteConnection,
373 workspace: &Workspace,
374 draft: TaskDraft,
375) -> Result<InsertedTask> {
376 let status = TaskStatus::parse(&draft.status)?;
377 let priority = TaskPriority::parse(&draft.priority)?;
378 let available_at = draft.available_at.as_deref().unwrap_or("");
379 let due_on = draft.due_on.as_deref().unwrap_or("");
380 let id = TaskId::new();
381 let ts = now();
382 let project = draft
383 .project
384 .as_deref()
385 .ok_or_else(|| anyhow::anyhow!("error project-required"))?;
386 let project = resolve_or_create_project_in_workspace(conn, &workspace.id, project).await?;
387 let labels = resolve_labels_in_workspace(conn, &workspace.id, &draft.labels).await?;
388 sqlx::query(
389 "INSERT INTO tasks(workspace_id, id, title, description, project_id, status, priority, created_at, updated_at, queue_activity_at, available_at, due_on, is_epic)
390 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
391 )
392 .bind(&workspace.id)
393 .bind(&id)
394 .bind(&draft.title)
395 .bind(&draft.description)
396 .bind(&project.id)
397 .bind(status.as_str())
398 .bind(priority.as_str())
399 .bind(&ts)
400 .bind(&ts)
401 .bind(&ts)
402 .bind(available_at)
403 .bind(due_on)
404 .bind(i64::from(draft.is_epic))
405 .execute(&mut *conn)
406 .await?;
407 for label in &labels {
408 sqlx::query(
409 "INSERT OR IGNORE INTO task_labels(workspace_id, task_id, label) VALUES (?, ?, ?)",
410 )
411 .bind(&workspace.id)
412 .bind(&id)
413 .bind(label)
414 .execute(&mut *conn)
415 .await?;
416 }
417 let change_id = append_change(
418 conn,
419 ChangeEntity::Task,
420 &id,
421 None,
422 op_type::CREATE_TASK,
423 ChangePayload::workspace(workspace)
424 .set("title", draft.title)
425 .set("description", draft.description)
426 .set("project_id", project.id.clone())
427 .set("project_key", project.key.clone())
428 .set("project_name", project.name.clone())
429 .set("project_prefix", project.prefix.clone())
430 .set("status", status.as_str())
431 .set("priority", priority.as_str())
432 .set("available_at", available_at)
433 .set("due_on", due_on)
434 .set("is_epic", if draft.is_epic { "1" } else { "0" })
435 .set("labels", &labels)
436 .set("created_at", ts),
437 )
438 .await?;
439 for field in TaskField::VERSIONED {
440 set_field_version(conn, &id, field.as_str(), &change_id).await?;
441 }
442 Ok(InsertedTask {
443 id,
444 change_id,
445 project_key: project.key,
446 label_count: labels.len(),
447 })
448}
449
450async fn cleanup_attachment_guards(
451 conn: &mut SqliteConnection,
452 leases: &[String],
453 reservations: &[String],
454) {
455 for lease_id in leases {
456 if let Err(error) = crate::attachments::lifecycle::release_lease(conn, lease_id).await {
457 warn!(%error, "failed to release attachment staging lease");
458 }
459 }
460 for reservation_id in reservations {
461 if let Err(error) =
462 crate::attachments::lifecycle::release_reservation(conn, reservation_id).await
463 {
464 warn!(%error, "failed to release attachment capacity reservation");
465 }
466 }
467}
468
469async fn cleanup_created_objects(conn: &mut SqliteConnection, blob_dir: &Path, hashes: &[String]) {
470 for sha256 in hashes {
471 crate::attachments::storage::remove_staged_blob_if_unreferenced(conn, blob_dir, sha256)
472 .await;
473 }
474}
475
476pub async fn update_task(
477 conn: &mut SqliteConnection,
478 workspace: &Workspace,
479 task_id: &crate::ids::TaskId,
480 update: TaskUpdate,
481) -> Result<TaskUpdateOutcome> {
482 if let Some(status) = update.status.as_deref() {
483 TaskStatus::parse(status)?;
484 }
485 if let Some(priority) = update.priority.as_deref() {
486 TaskPriority::parse(priority)?;
487 }
488 if let Some(Some(available_at)) = update.available_at.as_ref() {
489 crate::time_validation::validate_available_at_value(available_at)?;
490 }
491 if let Some(Some(due_on)) = update.due_on.as_ref() {
492 crate::time_validation::validate_due_on_value(due_on)?;
493 }
494 let mut changed = false;
495 let mut tx = begin_immediate(conn).await?;
496 if let Some(title) = update.title {
497 changed |= update_task_field(&mut tx, workspace, task_id, "title", &title).await?;
498 }
499 if let Some(description) = update.description {
500 changed |=
501 update_task_field(&mut tx, workspace, task_id, "description", &description).await?;
502 }
503 if let Some(project) = update.project {
504 let project =
505 resolve_or_create_project_in_workspace(&mut tx, &workspace.id, &project).await?;
506 changed |= set_task_project(&mut tx, workspace, task_id, &project).await?;
507 }
508 if let Some(status) = update.status {
509 changed |= update_task_field(&mut tx, workspace, task_id, "status", &status).await?;
510 }
511 if let Some(priority) = update.priority {
512 changed |= update_task_field(&mut tx, workspace, task_id, "priority", &priority).await?;
513 }
514 if let Some(available_at) = update.available_at {
515 changed |= update_task_field(
516 &mut tx,
517 workspace,
518 task_id,
519 "available_at",
520 available_at.as_deref().unwrap_or(""),
521 )
522 .await?;
523 }
524 if let Some(due_on) = update.due_on {
525 changed |= update_task_field(
526 &mut tx,
527 workspace,
528 task_id,
529 "due_on",
530 due_on.as_deref().unwrap_or(""),
531 )
532 .await?;
533 }
534 if let Some(is_epic) = update.is_epic {
535 if !is_epic {
536 let task = get_task_in_workspace(&mut tx, workspace, task_id).await?;
537 if super::epics::task_has_epic_children(&mut tx, &task.workspace_id, task_id).await? {
538 bail!("error epic-has-children task_id={task_id}");
539 }
540 }
541 changed |= update_task_field(
542 &mut tx,
543 workspace,
544 task_id,
545 "is_epic",
546 if is_epic { "1" } else { "0" },
547 )
548 .await?;
549 }
550 if update_task_labels_in_workspace(
551 &mut tx,
552 &workspace.id,
553 task_id,
554 &update.add_labels,
555 &update.remove_labels,
556 )
557 .await?
558 {
559 changed = true;
560 }
561 tx.commit().await?;
562 info!(task_id = %task_id, changed, "task updated");
563 Ok(TaskUpdateOutcome {
564 task: get_task_in_workspace(conn, workspace, task_id).await?,
565 changed,
566 })
567}
568
569pub async fn update_task_field(
570 conn: &mut SqliteConnection,
571 workspace: &Workspace,
572 task_id: &crate::ids::TaskId,
573 field: &str,
574 value: &str,
575) -> Result<bool> {
576 set_task_field(conn, workspace, task_id, field, value).await
577}
578
579pub async fn update_task_labels_in_workspace(
580 conn: &mut SqliteConnection,
581 workspace_id: &WorkspaceId,
582 task_id: &crate::ids::TaskId,
583 add_labels: &[String],
584 remove_labels: &[String],
585) -> Result<bool> {
586 let workspace = crate::workspaces::workspace_for_id(conn, workspace_id).await?;
587 let mut changed = false;
588 for label in resolve_labels_in_workspace(conn, &workspace.id, add_labels).await? {
589 let rows_affected = sqlx::query(
590 "INSERT OR IGNORE INTO task_labels(workspace_id, task_id, label) VALUES (?, ?, ?)",
591 )
592 .bind(&workspace.id)
593 .bind(task_id)
594 .bind(&label)
595 .execute(&mut *conn)
596 .await?
597 .rows_affected();
598 if rows_affected > 0 {
599 append_change(
600 conn,
601 ChangeEntity::Task,
602 task_id,
603 Some("labels"),
604 op_type::LABEL_ADD,
605 ChangePayload::workspace(&workspace).set("label", label),
606 )
607 .await?;
608 changed = true;
609 }
610 }
611 for label in resolve_labels_in_workspace(conn, &workspace.id, remove_labels).await? {
612 let rows_affected = sqlx::query(
613 "DELETE FROM task_labels WHERE workspace_id = ? AND task_id = ? AND label = ?",
614 )
615 .bind(&workspace.id)
616 .bind(task_id)
617 .bind(&label)
618 .execute(&mut *conn)
619 .await?
620 .rows_affected();
621 if rows_affected > 0 {
622 append_change(
623 conn,
624 ChangeEntity::Task,
625 task_id,
626 Some("labels"),
627 op_type::LABEL_REMOVE,
628 ChangePayload::workspace(&workspace).set("label", label),
629 )
630 .await?;
631 changed = true;
632 }
633 }
634 if changed {
635 info!(
636 task_id = %task_id,
637 added = add_labels.len(),
638 removed = remove_labels.len(),
639 "task labels changed"
640 );
641 }
642 Ok(changed)
643}
644
645pub async fn set_task_deleted(
646 conn: &mut SqliteConnection,
647 workspace: &Workspace,
648 task_id: &crate::ids::TaskId,
649 deleted: bool,
650) -> Result<TaskOutcome> {
651 set_task_field(
652 conn,
653 workspace,
654 task_id,
655 "deleted",
656 if deleted { "1" } else { "0" },
657 )
658 .await?;
659 crate::attachments::lifecycle::reconcile_liveness(
660 conn,
661 &crate::attachments::lifecycle::SystemClock,
662 )
663 .await?;
664 info!(task_id = %task_id, deleted, "task deleted flag changed");
665 Ok(TaskOutcome {
666 task: get_task_in_workspace(conn, workspace, task_id).await?,
667 create_change_id: None,
668 attachment_change_ids: Vec::new(),
669 })
670}
671
672pub async fn add_note(
673 conn: &mut SqliteConnection,
674 workspace: &Workspace,
675 task_id: &crate::ids::TaskId,
676 body: String,
677) -> Result<NoteOutcome> {
678 let note_id = new_id();
679 let ts = now();
680 let mut tx = begin_immediate(conn).await?;
681 let change_id = append_change(
682 &mut tx,
683 ChangeEntity::Task,
684 task_id,
685 Some("notes"),
686 op_type::NOTE_ADD,
687 ChangePayload::workspace(workspace)
688 .set("note_id", ¬e_id)
689 .set("body", &body)
690 .set("created_at", &ts),
691 )
692 .await?;
693 sqlx::query(
694 "INSERT INTO notes(workspace_id, id, task_id, body, created_at, change_id) VALUES (?, ?, ?, ?, ?, ?)",
695 )
696 .bind(&workspace.id)
697 .bind(¬e_id)
698 .bind(task_id)
699 .bind(&body)
700 .bind(&ts)
701 .bind(&change_id)
702 .execute(&mut *tx)
703 .await?;
704 sqlx::query("UPDATE tasks SET queue_activity_at = ? WHERE workspace_id = ? AND id = ?")
705 .bind(&ts)
706 .bind(&workspace.id)
707 .bind(task_id)
708 .execute(&mut *tx)
709 .await?;
710 tx.commit().await?;
711 info!(task_id = %task_id, note_id = %note_id, "note added");
712 Ok(NoteOutcome {
713 task_id: task_id.clone(),
714 note_id,
715 change_id,
716 })
717}
718
719pub async fn delete_note(
720 conn: &mut SqliteConnection,
721 workspace: &Workspace,
722 task_id: &crate::ids::TaskId,
723 note_id: &str,
724) -> Result<NoteDeleteOutcome> {
725 let mut tx = begin_immediate(conn).await?;
726 let deleted_at = now();
727 let deleted =
728 sqlx::query("DELETE FROM notes WHERE workspace_id = ? AND task_id = ? AND id = ?")
729 .bind(&workspace.id)
730 .bind(task_id)
731 .bind(note_id)
732 .execute(&mut *tx)
733 .await?
734 .rows_affected();
735 if deleted > 0 {
736 sqlx::query("UPDATE tasks SET queue_activity_at = ? WHERE workspace_id = ? AND id = ?")
737 .bind(&deleted_at)
738 .bind(&workspace.id)
739 .bind(task_id)
740 .execute(&mut *tx)
741 .await?;
742 append_change(
743 &mut tx,
744 ChangeEntity::Task,
745 task_id,
746 Some("notes"),
747 op_type::NOTE_DELETE,
748 ChangePayload::workspace(workspace)
749 .set("note_id", note_id)
750 .set("deleted_at", deleted_at),
751 )
752 .await?;
753 }
754 tx.commit().await?;
755 if deleted > 0 {
756 info!(task_id = %task_id, note_id = %note_id, "note deleted");
757 }
758 Ok(NoteDeleteOutcome {
759 task_id: task_id.clone(),
760 note_id: note_id.to_string(),
761 changed: deleted > 0,
762 })
763}