1mod ids;
2mod queries;
3mod tables;
4#[cfg(test)]
5pub mod tests;
6
7use crate::{executor::Executor, Error, Result};
8use anyhow::anyhow;
9use collections::{BTreeMap, HashMap, HashSet};
10use dashmap::DashMap;
11use futures::StreamExt;
12use rand::{prelude::StdRng, Rng, SeedableRng};
13use rpc::{
14 proto::{self},
15 ConnectionId, ExtensionMetadata,
16};
17use sea_orm::{
18 entity::prelude::*,
19 sea_query::{Alias, Expr, OnConflict},
20 ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr,
21 FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement,
22 TransactionTrait,
23};
24use semantic_version::SemanticVersion;
25use serde::{Deserialize, Serialize};
26use std::ops::RangeInclusive;
27use std::{
28 fmt::Write as _,
29 future::Future,
30 marker::PhantomData,
31 ops::{Deref, DerefMut},
32 rc::Rc,
33 sync::Arc,
34 time::Duration,
35};
36use time::PrimitiveDateTime;
37use tokio::sync::{Mutex, OwnedMutexGuard};
38
39#[cfg(test)]
40pub use tests::TestDb;
41
42pub use ids::*;
43pub use queries::billing_customers::{CreateBillingCustomerParams, UpdateBillingCustomerParams};
44pub use queries::billing_subscriptions::{
45 CreateBillingSubscriptionParams, UpdateBillingSubscriptionParams,
46};
47pub use queries::contributors::ContributorSelector;
48pub use queries::processed_stripe_events::CreateProcessedStripeEventParams;
49pub use sea_orm::ConnectOptions;
50pub use tables::user::Model as User;
51pub use tables::*;
52
53/// Database gives you a handle that lets you access the database.
54/// It handles pooling internally.
55pub struct Database {
56 options: ConnectOptions,
57 pool: DatabaseConnection,
58 rooms: DashMap<RoomId, Arc<Mutex<()>>>,
59 projects: DashMap<ProjectId, Arc<Mutex<()>>>,
60 rng: Mutex<StdRng>,
61 executor: Executor,
62 notification_kinds_by_id: HashMap<NotificationKindId, &'static str>,
63 notification_kinds_by_name: HashMap<String, NotificationKindId>,
64 #[cfg(test)]
65 runtime: Option<tokio::runtime::Runtime>,
66}
67
68// The `Database` type has so many methods that its impl blocks are split into
69// separate files in the `queries` folder.
70impl Database {
71 /// Connects to the database with the given options
72 pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
73 sqlx::any::install_default_drivers();
74 Ok(Self {
75 options: options.clone(),
76 pool: sea_orm::Database::connect(options).await?,
77 rooms: DashMap::with_capacity(16384),
78 projects: DashMap::with_capacity(16384),
79 rng: Mutex::new(StdRng::seed_from_u64(0)),
80 notification_kinds_by_id: HashMap::default(),
81 notification_kinds_by_name: HashMap::default(),
82 executor,
83 #[cfg(test)]
84 runtime: None,
85 })
86 }
87
88 pub fn options(&self) -> &ConnectOptions {
89 &self.options
90 }
91
92 #[cfg(test)]
93 pub fn reset(&self) {
94 self.rooms.clear();
95 self.projects.clear();
96 }
97
98 /// Transaction runs things in a transaction. If you want to call other methods
99 /// and pass the transaction around you need to reborrow the transaction at each
100 /// call site with: `&*tx`.
101 pub async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
102 where
103 F: Send + Fn(TransactionHandle) -> Fut,
104 Fut: Send + Future<Output = Result<T>>,
105 {
106 let body = async {
107 let mut i = 0;
108 loop {
109 let (tx, result) = self.with_transaction(&f).await?;
110 match result {
111 Ok(result) => match tx.commit().await.map_err(Into::into) {
112 Ok(()) => return Ok(result),
113 Err(error) => {
114 if !self.retry_on_serialization_error(&error, i).await {
115 return Err(error);
116 }
117 }
118 },
119 Err(error) => {
120 tx.rollback().await?;
121 if !self.retry_on_serialization_error(&error, i).await {
122 return Err(error);
123 }
124 }
125 }
126 i += 1;
127 }
128 };
129
130 self.run(body).await
131 }
132
133 pub async fn weak_transaction<F, Fut, T>(&self, f: F) -> Result<T>
134 where
135 F: Send + Fn(TransactionHandle) -> Fut,
136 Fut: Send + Future<Output = Result<T>>,
137 {
138 let body = async {
139 let (tx, result) = self.with_weak_transaction(&f).await?;
140 match result {
141 Ok(result) => match tx.commit().await.map_err(Into::into) {
142 Ok(()) => Ok(result),
143 Err(error) => Err(error),
144 },
145 Err(error) => {
146 tx.rollback().await?;
147 Err(error)
148 }
149 }
150 };
151
152 self.run(body).await
153 }
154
155 /// The same as room_transaction, but if you need to only optionally return a Room.
156 async fn optional_room_transaction<F, Fut, T>(
157 &self,
158 f: F,
159 ) -> Result<Option<TransactionGuard<T>>>
160 where
161 F: Send + Fn(TransactionHandle) -> Fut,
162 Fut: Send + Future<Output = Result<Option<(RoomId, T)>>>,
163 {
164 let body = async {
165 let mut i = 0;
166 loop {
167 let (tx, result) = self.with_transaction(&f).await?;
168 match result {
169 Ok(Some((room_id, data))) => {
170 let lock = self.rooms.entry(room_id).or_default().clone();
171 let _guard = lock.lock_owned().await;
172 match tx.commit().await.map_err(Into::into) {
173 Ok(()) => {
174 return Ok(Some(TransactionGuard {
175 data,
176 _guard,
177 _not_send: PhantomData,
178 }));
179 }
180 Err(error) => {
181 if !self.retry_on_serialization_error(&error, i).await {
182 return Err(error);
183 }
184 }
185 }
186 }
187 Ok(None) => match tx.commit().await.map_err(Into::into) {
188 Ok(()) => return Ok(None),
189 Err(error) => {
190 if !self.retry_on_serialization_error(&error, i).await {
191 return Err(error);
192 }
193 }
194 },
195 Err(error) => {
196 tx.rollback().await?;
197 if !self.retry_on_serialization_error(&error, i).await {
198 return Err(error);
199 }
200 }
201 }
202 i += 1;
203 }
204 };
205
206 self.run(body).await
207 }
208
209 async fn project_transaction<F, Fut, T>(
210 &self,
211 project_id: ProjectId,
212 f: F,
213 ) -> Result<TransactionGuard<T>>
214 where
215 F: Send + Fn(TransactionHandle) -> Fut,
216 Fut: Send + Future<Output = Result<T>>,
217 {
218 let room_id = Database::room_id_for_project(self, project_id).await?;
219 let body = async {
220 let mut i = 0;
221 loop {
222 let lock = if let Some(room_id) = room_id {
223 self.rooms.entry(room_id).or_default().clone()
224 } else {
225 self.projects.entry(project_id).or_default().clone()
226 };
227 let _guard = lock.lock_owned().await;
228 let (tx, result) = self.with_transaction(&f).await?;
229 match result {
230 Ok(data) => match tx.commit().await.map_err(Into::into) {
231 Ok(()) => {
232 return Ok(TransactionGuard {
233 data,
234 _guard,
235 _not_send: PhantomData,
236 });
237 }
238 Err(error) => {
239 if !self.retry_on_serialization_error(&error, i).await {
240 return Err(error);
241 }
242 }
243 },
244 Err(error) => {
245 tx.rollback().await?;
246 if !self.retry_on_serialization_error(&error, i).await {
247 return Err(error);
248 }
249 }
250 }
251 i += 1;
252 }
253 };
254
255 self.run(body).await
256 }
257
258 /// room_transaction runs the block in a transaction. It returns a RoomGuard, that keeps
259 /// the database locked until it is dropped. This ensures that updates sent to clients are
260 /// properly serialized with respect to database changes.
261 async fn room_transaction<F, Fut, T>(
262 &self,
263 room_id: RoomId,
264 f: F,
265 ) -> Result<TransactionGuard<T>>
266 where
267 F: Send + Fn(TransactionHandle) -> Fut,
268 Fut: Send + Future<Output = Result<T>>,
269 {
270 let body = async {
271 let mut i = 0;
272 loop {
273 let lock = self.rooms.entry(room_id).or_default().clone();
274 let _guard = lock.lock_owned().await;
275 let (tx, result) = self.with_transaction(&f).await?;
276 match result {
277 Ok(data) => match tx.commit().await.map_err(Into::into) {
278 Ok(()) => {
279 return Ok(TransactionGuard {
280 data,
281 _guard,
282 _not_send: PhantomData,
283 });
284 }
285 Err(error) => {
286 if !self.retry_on_serialization_error(&error, i).await {
287 return Err(error);
288 }
289 }
290 },
291 Err(error) => {
292 tx.rollback().await?;
293 if !self.retry_on_serialization_error(&error, i).await {
294 return Err(error);
295 }
296 }
297 }
298 i += 1;
299 }
300 };
301
302 self.run(body).await
303 }
304
305 async fn with_transaction<F, Fut, T>(&self, f: &F) -> Result<(DatabaseTransaction, Result<T>)>
306 where
307 F: Send + Fn(TransactionHandle) -> Fut,
308 Fut: Send + Future<Output = Result<T>>,
309 {
310 let tx = self
311 .pool
312 .begin_with_config(Some(IsolationLevel::Serializable), None)
313 .await?;
314
315 let mut tx = Arc::new(Some(tx));
316 let result = f(TransactionHandle(tx.clone())).await;
317 let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else {
318 return Err(anyhow!(
319 "couldn't complete transaction because it's still in use"
320 ))?;
321 };
322
323 Ok((tx, result))
324 }
325
326 async fn with_weak_transaction<F, Fut, T>(
327 &self,
328 f: &F,
329 ) -> Result<(DatabaseTransaction, Result<T>)>
330 where
331 F: Send + Fn(TransactionHandle) -> Fut,
332 Fut: Send + Future<Output = Result<T>>,
333 {
334 let tx = self
335 .pool
336 .begin_with_config(Some(IsolationLevel::ReadCommitted), None)
337 .await?;
338
339 let mut tx = Arc::new(Some(tx));
340 let result = f(TransactionHandle(tx.clone())).await;
341 let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else {
342 return Err(anyhow!(
343 "couldn't complete transaction because it's still in use"
344 ))?;
345 };
346
347 Ok((tx, result))
348 }
349
350 async fn run<F, T>(&self, future: F) -> Result<T>
351 where
352 F: Future<Output = Result<T>>,
353 {
354 #[cfg(test)]
355 {
356 if let Executor::Deterministic(executor) = &self.executor {
357 executor.simulate_random_delay().await;
358 }
359
360 self.runtime.as_ref().unwrap().block_on(future)
361 }
362
363 #[cfg(not(test))]
364 {
365 future.await
366 }
367 }
368
369 async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: usize) -> bool {
370 // If the error is due to a failure to serialize concurrent transactions, then retry
371 // this transaction after a delay. With each subsequent retry, double the delay duration.
372 // Also vary the delay randomly in order to ensure different database connections retry
373 // at different times.
374 const SLEEPS: [f32; 10] = [10., 20., 40., 80., 160., 320., 640., 1280., 2560., 5120.];
375 if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() {
376 let base_delay = SLEEPS[prev_attempt_count];
377 let randomized_delay = base_delay * self.rng.lock().await.gen_range(0.5..=2.0);
378 log::warn!(
379 "retrying transaction after serialization error. delay: {} ms.",
380 randomized_delay
381 );
382 self.executor
383 .sleep(Duration::from_millis(randomized_delay as u64))
384 .await;
385 true
386 } else {
387 false
388 }
389 }
390}
391
392fn is_serialization_error(error: &Error) -> bool {
393 const SERIALIZATION_FAILURE_CODE: &str = "40001";
394 match error {
395 Error::Database(
396 DbErr::Exec(sea_orm::RuntimeErr::SqlxError(error))
397 | DbErr::Query(sea_orm::RuntimeErr::SqlxError(error)),
398 ) if error
399 .as_database_error()
400 .and_then(|error| error.code())
401 .as_deref()
402 == Some(SERIALIZATION_FAILURE_CODE) =>
403 {
404 true
405 }
406 _ => false,
407 }
408}
409
410/// A handle to a [`DatabaseTransaction`].
411pub struct TransactionHandle(pub(crate) Arc<Option<DatabaseTransaction>>);
412
413impl Deref for TransactionHandle {
414 type Target = DatabaseTransaction;
415
416 fn deref(&self) -> &Self::Target {
417 self.0.as_ref().as_ref().unwrap()
418 }
419}
420
421/// [`TransactionGuard`] keeps a database transaction alive until it is dropped.
422/// It wraps data that depends on the state of the database and prevents an additional
423/// transaction from starting that would invalidate that data.
424pub struct TransactionGuard<T> {
425 data: T,
426 _guard: OwnedMutexGuard<()>,
427 _not_send: PhantomData<Rc<()>>,
428}
429
430impl<T> Deref for TransactionGuard<T> {
431 type Target = T;
432
433 fn deref(&self) -> &T {
434 &self.data
435 }
436}
437
438impl<T> DerefMut for TransactionGuard<T> {
439 fn deref_mut(&mut self) -> &mut T {
440 &mut self.data
441 }
442}
443
444impl<T> TransactionGuard<T> {
445 /// Returns the inner value of the guard.
446 pub fn into_inner(self) -> T {
447 self.data
448 }
449}
450
451#[derive(Clone, Debug, PartialEq, Eq)]
452pub enum Contact {
453 Accepted { user_id: UserId, busy: bool },
454 Outgoing { user_id: UserId },
455 Incoming { user_id: UserId },
456}
457
458impl Contact {
459 pub fn user_id(&self) -> UserId {
460 match self {
461 Contact::Accepted { user_id, .. } => *user_id,
462 Contact::Outgoing { user_id } => *user_id,
463 Contact::Incoming { user_id, .. } => *user_id,
464 }
465 }
466}
467
468pub type NotificationBatch = Vec<(UserId, proto::Notification)>;
469
470pub struct CreatedChannelMessage {
471 pub message_id: MessageId,
472 pub participant_connection_ids: HashSet<ConnectionId>,
473 pub notifications: NotificationBatch,
474}
475
476pub struct UpdatedChannelMessage {
477 pub message_id: MessageId,
478 pub participant_connection_ids: Vec<ConnectionId>,
479 pub notifications: NotificationBatch,
480 pub reply_to_message_id: Option<MessageId>,
481 pub timestamp: PrimitiveDateTime,
482 pub deleted_mention_notification_ids: Vec<NotificationId>,
483 pub updated_mention_notifications: Vec<rpc::proto::Notification>,
484}
485
486#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)]
487pub struct Invite {
488 pub email_address: String,
489 pub email_confirmation_code: String,
490}
491
492#[derive(Clone, Debug, Deserialize)]
493pub struct NewSignup {
494 pub email_address: String,
495 pub platform_mac: bool,
496 pub platform_windows: bool,
497 pub platform_linux: bool,
498 pub editor_features: Vec<String>,
499 pub programming_languages: Vec<String>,
500 pub device_id: Option<String>,
501 pub added_to_mailing_list: bool,
502 pub created_at: Option<DateTime>,
503}
504
505#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)]
506pub struct WaitlistSummary {
507 pub count: i64,
508 pub linux_count: i64,
509 pub mac_count: i64,
510 pub windows_count: i64,
511 pub unknown_count: i64,
512}
513
514/// The parameters to create a new user.
515#[derive(Debug, Serialize, Deserialize)]
516pub struct NewUserParams {
517 pub github_login: String,
518 pub github_user_id: i32,
519}
520
521/// The result of creating a new user.
522#[derive(Debug)]
523pub struct NewUserResult {
524 pub user_id: UserId,
525 pub metrics_id: String,
526 pub inviting_user_id: Option<UserId>,
527 pub signup_device_id: Option<String>,
528}
529
530/// The result of updating a channel membership.
531#[derive(Debug)]
532pub struct MembershipUpdated {
533 pub channel_id: ChannelId,
534 pub new_channels: ChannelsForUser,
535 pub removed_channels: Vec<ChannelId>,
536}
537
538/// The result of setting a member's role.
539#[derive(Debug)]
540#[allow(clippy::large_enum_variant)]
541pub enum SetMemberRoleResult {
542 InviteUpdated(Channel),
543 MembershipUpdated(MembershipUpdated),
544}
545
546/// The result of inviting a member to a channel.
547#[derive(Debug)]
548pub struct InviteMemberResult {
549 pub channel: Channel,
550 pub notifications: NotificationBatch,
551}
552
553#[derive(Debug)]
554pub struct RespondToChannelInvite {
555 pub membership_update: Option<MembershipUpdated>,
556 pub notifications: NotificationBatch,
557}
558
559#[derive(Debug)]
560pub struct RemoveChannelMemberResult {
561 pub membership_update: MembershipUpdated,
562 pub notification_id: Option<NotificationId>,
563}
564
565#[derive(Debug, PartialEq, Eq, Hash)]
566pub struct Channel {
567 pub id: ChannelId,
568 pub name: String,
569 pub visibility: ChannelVisibility,
570 /// parent_path is the channel ids from the root to this one (not including this one)
571 pub parent_path: Vec<ChannelId>,
572}
573
574impl Channel {
575 pub fn from_model(value: channel::Model) -> Self {
576 Channel {
577 id: value.id,
578 visibility: value.visibility,
579 name: value.clone().name,
580 parent_path: value.ancestors().collect(),
581 }
582 }
583
584 pub fn to_proto(&self) -> proto::Channel {
585 proto::Channel {
586 id: self.id.to_proto(),
587 name: self.name.clone(),
588 visibility: self.visibility.into(),
589 parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(),
590 }
591 }
592}
593
594#[derive(Debug, PartialEq, Eq, Hash)]
595pub struct ChannelMember {
596 pub role: ChannelRole,
597 pub user_id: UserId,
598 pub kind: proto::channel_member::Kind,
599}
600
601impl ChannelMember {
602 pub fn to_proto(&self) -> proto::ChannelMember {
603 proto::ChannelMember {
604 role: self.role.into(),
605 user_id: self.user_id.to_proto(),
606 kind: self.kind.into(),
607 }
608 }
609}
610
611#[derive(Debug, PartialEq)]
612pub struct ChannelsForUser {
613 pub channels: Vec<Channel>,
614 pub channel_memberships: Vec<channel_member::Model>,
615 pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
616 pub hosted_projects: Vec<proto::HostedProject>,
617 pub invited_channels: Vec<Channel>,
618
619 pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
620 pub observed_channel_messages: Vec<proto::ChannelMessageId>,
621 pub latest_buffer_versions: Vec<proto::ChannelBufferVersion>,
622 pub latest_channel_messages: Vec<proto::ChannelMessageId>,
623}
624
625#[derive(Debug)]
626pub struct RejoinedChannelBuffer {
627 pub buffer: proto::RejoinedChannelBuffer,
628 pub old_connection_id: ConnectionId,
629}
630
631#[derive(Clone)]
632pub struct JoinRoom {
633 pub room: proto::Room,
634 pub channel: Option<channel::Model>,
635}
636
637pub struct RejoinedRoom {
638 pub room: proto::Room,
639 pub rejoined_projects: Vec<RejoinedProject>,
640 pub reshared_projects: Vec<ResharedProject>,
641 pub channel: Option<channel::Model>,
642}
643
644pub struct ResharedProject {
645 pub id: ProjectId,
646 pub old_connection_id: ConnectionId,
647 pub collaborators: Vec<ProjectCollaborator>,
648 pub worktrees: Vec<proto::WorktreeMetadata>,
649}
650
651pub struct RejoinedProject {
652 pub id: ProjectId,
653 pub old_connection_id: ConnectionId,
654 pub collaborators: Vec<ProjectCollaborator>,
655 pub worktrees: Vec<RejoinedWorktree>,
656 pub language_servers: Vec<proto::LanguageServer>,
657}
658
659impl RejoinedProject {
660 pub fn to_proto(&self) -> proto::RejoinedProject {
661 proto::RejoinedProject {
662 id: self.id.to_proto(),
663 worktrees: self
664 .worktrees
665 .iter()
666 .map(|worktree| proto::WorktreeMetadata {
667 id: worktree.id,
668 root_name: worktree.root_name.clone(),
669 visible: worktree.visible,
670 abs_path: worktree.abs_path.clone(),
671 })
672 .collect(),
673 collaborators: self
674 .collaborators
675 .iter()
676 .map(|collaborator| collaborator.to_proto())
677 .collect(),
678 language_servers: self.language_servers.clone(),
679 }
680 }
681}
682
683#[derive(Debug)]
684pub struct RejoinedWorktree {
685 pub id: u64,
686 pub abs_path: String,
687 pub root_name: String,
688 pub visible: bool,
689 pub updated_entries: Vec<proto::Entry>,
690 pub removed_entries: Vec<u64>,
691 pub updated_repositories: Vec<proto::RepositoryEntry>,
692 pub removed_repositories: Vec<u64>,
693 pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
694 pub settings_files: Vec<WorktreeSettingsFile>,
695 pub scan_id: u64,
696 pub completed_scan_id: u64,
697}
698
699pub struct LeftRoom {
700 pub room: proto::Room,
701 pub channel: Option<channel::Model>,
702 pub left_projects: HashMap<ProjectId, LeftProject>,
703 pub canceled_calls_to_user_ids: Vec<UserId>,
704 pub deleted: bool,
705}
706
707pub struct RefreshedRoom {
708 pub room: proto::Room,
709 pub channel: Option<channel::Model>,
710 pub stale_participant_user_ids: Vec<UserId>,
711 pub canceled_calls_to_user_ids: Vec<UserId>,
712}
713
714pub struct RefreshedChannelBuffer {
715 pub connection_ids: Vec<ConnectionId>,
716 pub collaborators: Vec<proto::Collaborator>,
717}
718
719pub struct Project {
720 pub id: ProjectId,
721 pub role: ChannelRole,
722 pub collaborators: Vec<ProjectCollaborator>,
723 pub worktrees: BTreeMap<u64, Worktree>,
724 pub language_servers: Vec<proto::LanguageServer>,
725 pub dev_server_project_id: Option<DevServerProjectId>,
726}
727
728pub struct ProjectCollaborator {
729 pub connection_id: ConnectionId,
730 pub user_id: UserId,
731 pub replica_id: ReplicaId,
732 pub is_host: bool,
733}
734
735impl ProjectCollaborator {
736 pub fn to_proto(&self) -> proto::Collaborator {
737 proto::Collaborator {
738 peer_id: Some(self.connection_id.into()),
739 replica_id: self.replica_id.0 as u32,
740 user_id: self.user_id.to_proto(),
741 }
742 }
743}
744
745#[derive(Debug)]
746pub struct LeftProject {
747 pub id: ProjectId,
748 pub should_unshare: bool,
749 pub connection_ids: Vec<ConnectionId>,
750}
751
752pub struct Worktree {
753 pub id: u64,
754 pub abs_path: String,
755 pub root_name: String,
756 pub visible: bool,
757 pub entries: Vec<proto::Entry>,
758 pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
759 pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
760 pub settings_files: Vec<WorktreeSettingsFile>,
761 pub scan_id: u64,
762 pub completed_scan_id: u64,
763}
764
765#[derive(Debug)]
766pub struct WorktreeSettingsFile {
767 pub path: String,
768 pub content: String,
769}
770
771pub struct NewExtensionVersion {
772 pub name: String,
773 pub version: semver::Version,
774 pub description: String,
775 pub authors: Vec<String>,
776 pub repository: String,
777 pub schema_version: i32,
778 pub wasm_api_version: Option<String>,
779 pub published_at: PrimitiveDateTime,
780}
781
782pub struct ExtensionVersionConstraints {
783 pub schema_versions: RangeInclusive<i32>,
784 pub wasm_api_versions: RangeInclusive<SemanticVersion>,
785}