1use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11use crate::previous_version::PreviousVersionContextConfig;
12pub type QueryValue = Value;
14pub type QueryRow = Map<String, QueryValue>;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct FetchedBlob {
20 pub blob_id: String,
21 pub byte_length: i64,
22 pub bytes: Vec<u8>,
23 pub media_type: Option<String>,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum TimeBucketUnit {
28 Month,
29}
30
31const MAX_TIME_BUCKET_MS: i64 = 253_402_300_799_999;
32
33fn utc_year_month(timestamp_ms: i64) -> Result<(i64, i64), String> {
34 if !(0..=MAX_TIME_BUCKET_MS).contains(×tamp_ms) {
35 return Err(
36 "sync.invalid_request: time bucket timestamps must fall from 1970 through 9999"
37 .to_owned(),
38 );
39 }
40 let z = timestamp_ms / 86_400_000 + 719_468;
41 let era = z.div_euclid(146_097);
42 let day_of_era = z - era * 146_097;
43 let year_of_era =
44 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
45 let mut year = year_of_era + era * 400;
46 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
47 let month_prime = (5 * day_of_year + 2) / 153;
48 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
49 if month <= 2 {
50 year += 1;
51 }
52 Ok((year, month))
53}
54
55pub fn creation_time_bucket(created_at_ms: i64, unit: TimeBucketUnit) -> Result<String, String> {
57 match unit {
58 TimeBucketUnit::Month => {
59 let (year, month) = utc_year_month(created_at_ms)?;
60 Ok(format!("{year:04}-{month:02}"))
61 }
62 }
63}
64
65pub fn last(count: usize, unit: TimeBucketUnit, now_ms: i64) -> Result<Vec<String>, String> {
67 if !(1..=1_200).contains(&count) {
68 return Err(
69 "sync.invalid_request: time bucket count must be from 1 through 1200".to_owned(),
70 );
71 }
72 match unit {
73 TimeBucketUnit::Month => {
74 let (year, month) = utc_year_month(now_ms)?;
75 let current = year * 12 + month - 1;
76 if current - (count as i64 - 1) < 1970 * 12 {
77 return Err(
78 "sync.invalid_request: every returned UTC month must fall from 1970 through 9999"
79 .to_owned(),
80 );
81 }
82 Ok((0..count)
83 .rev()
84 .map(|offset| {
85 let value = current - offset as i64;
86 format!(
87 "{:04}-{:02}",
88 value.div_euclid(12),
89 value.rem_euclid(12) + 1
90 )
91 })
92 .collect())
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
101pub struct WindowBase {
102 pub table: String,
103 pub variable: String,
104 pub fixed_scopes: Vec<(String, Vec<String>)>,
106 pub params: Option<String>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
116#[serde(rename_all = "camelCase")]
117pub struct WindowState {
118 pub units: Vec<String>,
120 pub pending: Vec<String>,
122}
123
124#[cfg(test)]
125mod time_bucket_tests {
126 use super::{creation_time_bucket, last, TimeBucketUnit};
127
128 #[test]
129 fn utc_month_buckets_cross_year_boundaries() {
130 let now = 1_770_508_800_000; assert_eq!(
132 creation_time_bucket(now, TimeBucketUnit::Month).unwrap(),
133 "2026-02"
134 );
135 assert_eq!(
136 last(3, TimeBucketUnit::Month, now).unwrap(),
137 vec!["2025-12", "2026-01", "2026-02"]
138 );
139 }
140
141 #[test]
142 fn utc_month_buckets_reject_unbounded_input() {
143 assert!(last(0, TimeBucketUnit::Month, 0).is_err());
144 assert!(last(1_201, TimeBucketUnit::Month, 0).is_err());
145 assert!(last(2, TimeBucketUnit::Month, 0).is_err());
146 assert!(creation_time_bucket(-1, TimeBucketUnit::Month).is_err());
147 }
148}
149
150#[derive(Debug, Clone, Serialize)]
151#[serde(rename_all = "camelCase")]
152pub struct TableChange {
153 pub table: String,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub scope_keys: Option<Vec<String>>,
156}
157
158#[derive(Debug, Clone, Serialize)]
159#[serde(rename_all = "camelCase")]
160pub struct WindowChange {
161 pub base_key: String,
162 pub table: String,
163 pub units: Vec<String>,
164}
165
166#[derive(Debug, Clone, Serialize)]
167#[serde(rename_all = "camelCase")]
168pub struct SyncStatusSnapshot {
169 pub current_schema_version: i32,
170 pub outbox: usize,
171 pub upgrading: bool,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub lease_state: Option<LeaseState>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub schema_floor: Option<SchemaFloor>,
176 pub sync_needed: bool,
177 pub previous_version_context: PreviousVersionStatus,
180}
181
182#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)]
184#[serde(rename_all = "camelCase")]
185pub struct PreviousVersionStatus {
186 pub present: bool,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub created_at_ms: Option<i64>,
189}
190
191pub const CLIENT_DIAGNOSTICS_VERSION: u8 = 1;
192pub const MAX_DIAGNOSTIC_EXPECTED_SUBSCRIPTIONS: usize = 256;
193pub const MAX_DIAGNOSTIC_QUERY_FAILURES: usize = 256;
194
195#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
196#[serde(rename_all = "camelCase", deny_unknown_fields)]
197pub struct ExpectedDiagnosticSubscription {
198 pub id: String,
199 pub table: String,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
203#[serde(rename_all = "camelCase", deny_unknown_fields)]
204pub struct ClientDiagnosticsRequest {
205 #[serde(default)]
206 pub expected_subscriptions: Vec<ExpectedDiagnosticSubscription>,
207}
208
209#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
210#[serde(rename_all = "camelCase")]
211pub struct ClientDiagnosticsHost {
212 pub kind: String,
213 pub role: String,
214 pub connectivity: String,
215 pub realtime: String,
216}
217
218#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
219#[serde(rename_all = "camelCase")]
220pub struct DiagnosticSubscription {
221 pub id: String,
222 pub table: String,
223 pub state: String,
224 pub complete: bool,
225 #[serde(skip_serializing_if = "Option::is_none")]
226 pub cursor: Option<i64>,
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub reason_code: Option<String>,
229}
230
231#[derive(Debug, Clone, Serialize, Default, PartialEq, Eq)]
232#[serde(rename_all = "camelCase")]
233pub struct DiagnosticRoundCounters {
234 pub pushed: u32,
235 pub applied: usize,
236 pub rejected: usize,
237 pub retryable: usize,
238 pub conflicts: u32,
239 pub commits_applied: u32,
240 pub segment_rows_applied: u32,
241 pub bootstrapping: usize,
242 pub resets: usize,
243 pub revoked: usize,
244 pub failed: usize,
245 pub deferred_commits: usize,
246}
247
248#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
249#[serde(rename_all = "camelCase")]
250pub struct DiagnosticLastRound {
251 pub status: String,
252 pub started_at_ms: i64,
253 pub completed_at_ms: i64,
254 pub duration_ms: i64,
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub counters: Option<DiagnosticRoundCounters>,
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub error_code: Option<String>,
259}
260
261#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
262#[serde(rename_all = "camelCase")]
263pub struct DiagnosticLastChange {
264 pub revision: String,
265 pub recorded_at_ms: i64,
266 pub tables: Vec<String>,
267 pub windows: Vec<String>,
268 pub domains_truncated: bool,
269 pub status_changed: bool,
270 pub conflicts_changed: bool,
271 pub rejections_changed: bool,
272 pub outcomes_changed: bool,
273}
274
275#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
276#[serde(rename_all = "camelCase")]
277pub struct ClientDiagnosticsStorage {
278 pub status: String,
279 #[serde(skip_serializing_if = "Option::is_none")]
280 pub database_bytes_approx: Option<i64>,
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub pending_outbox_bytes_approx: Option<i64>,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub retained_outcome_bytes_approx: Option<i64>,
285 #[serde(skip_serializing_if = "Option::is_none")]
286 pub retained_outcome_entries: Option<i64>,
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub blob_cache_bytes_approx: Option<i64>,
289 #[serde(skip_serializing_if = "Option::is_none")]
290 pub pressure_reason_code: Option<String>,
291}
292
293#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
295#[serde(rename_all = "camelCase")]
296pub struct DiagnosticQueryFailure {
297 pub id: String,
298 pub tables: Vec<String>,
300 pub code: String,
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub sqlite_code: Option<i32>,
305 pub at_ms: i64,
307}
308
309#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
310#[serde(rename_all = "camelCase")]
311pub struct ClientDiagnosticsSnapshot {
312 pub version: u8,
313 pub captured_at_ms: i64,
314 pub host: ClientDiagnosticsHost,
315 pub security_lifecycle: String,
316 pub schema: ClientDiagnosticsSchema,
317 pub replica: ClientDiagnosticsReplica,
318 pub lease: ClientDiagnosticsLease,
319 pub subscriptions: Vec<DiagnosticSubscription>,
320 pub subscriptions_truncated: bool,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub last_round: Option<DiagnosticLastRound>,
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub last_change: Option<DiagnosticLastChange>,
325 pub storage: ClientDiagnosticsStorage,
326 pub query_failures: Vec<DiagnosticQueryFailure>,
327}
328
329#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
330#[serde(rename_all = "camelCase")]
331pub struct ClientDiagnosticsSchema {
332 pub current_version: i32,
333 pub upgrading: bool,
334 #[serde(skip_serializing_if = "Option::is_none")]
335 pub required_version: Option<i32>,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub latest_version: Option<i32>,
338}
339
340#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
341#[serde(rename_all = "camelCase")]
342pub struct ClientDiagnosticsReplica {
343 pub local_revision: String,
344 pub sync_needed: bool,
345 pub pending_outbox: usize,
346}
347
348#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
349#[serde(rename_all = "camelCase")]
350pub struct ClientDiagnosticsLease {
351 pub state: String,
352 #[serde(skip_serializing_if = "Option::is_none")]
353 pub expires_at_ms: Option<i64>,
354 #[serde(skip_serializing_if = "Option::is_none")]
355 pub error_code: Option<String>,
356}
357
358#[derive(Debug, Clone, Serialize)]
360#[serde(rename_all = "camelCase")]
361pub struct ClientChangeBatch {
362 pub revision: String,
363 pub tables: Vec<TableChange>,
364 pub windows: Vec<WindowChange>,
365 #[serde(skip_serializing_if = "Option::is_none")]
366 pub status: Option<SyncStatusSnapshot>,
367 pub conflicts_changed: bool,
368 pub rejections_changed: bool,
369 pub outcomes_changed: bool,
370}
371
372#[derive(Debug, Clone, Serialize)]
373#[serde(tag = "kind", rename_all = "camelCase")]
374pub enum SyncIntent {
375 None,
376 Interactive,
377 Background {
378 #[serde(rename = "delayMs")]
379 delay_ms: u64,
380 },
381}
382
383#[derive(Debug, Clone, Serialize)]
384pub struct CommandEffects {
385 pub sync: SyncIntent,
386}
387
388impl CommandEffects {
389 #[must_use]
390 pub fn none() -> Self {
391 Self {
392 sync: SyncIntent::None,
393 }
394 }
395
396 #[must_use]
397 pub fn interactive() -> Self {
398 Self {
399 sync: SyncIntent::Interactive,
400 }
401 }
402}
403
404#[derive(Debug, Clone)]
405pub struct WindowCoverage {
406 pub base: WindowBase,
407 pub units: Vec<String>,
408}
409
410#[derive(Debug, Clone, Serialize)]
411#[serde(rename_all = "camelCase")]
412pub struct WindowUnitRef {
413 pub base_key: String,
414 pub unit: String,
415}
416
417#[derive(Debug, Clone, Serialize)]
418#[serde(rename_all = "camelCase")]
419pub struct CoverageSnapshot {
420 pub complete: bool,
421 pub pending: Vec<WindowUnitRef>,
422 pub missing: Vec<WindowUnitRef>,
423}
424
425#[derive(Debug, Clone, Copy)]
428pub struct QueryOwner<'a> {
429 pub id: &'a str,
430 pub tables: &'a [&'a str],
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
437pub struct QueryReadFailure {
438 pub code: Option<&'static str>,
439 pub sqlite_code: Option<i32>,
440 pub message: String,
441}
442
443impl std::fmt::Display for QueryReadFailure {
444 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445 match self.code {
446 Some(code) => write!(f, "{code}: {}", self.message),
447 None => f.write_str(&self.message),
448 }
449 }
450}
451
452impl From<String> for QueryReadFailure {
453 fn from(message: String) -> Self {
454 Self {
455 code: None,
456 sqlite_code: None,
457 message,
458 }
459 }
460}
461
462impl From<rusqlite::Error> for QueryReadFailure {
463 fn from(error: rusqlite::Error) -> Self {
465 let sqlite_code = match &error {
466 rusqlite::Error::SqliteFailure(failure, _) => Some(failure.extended_code),
467 _ => None,
468 };
469 match sqlite_code.map(|code| code & 0xff) {
470 Some(11 | 26) => Self {
471 code: Some("client.storage_corrupt"),
472 sqlite_code,
473 message: "local SQLite storage is corrupt".to_owned(),
474 },
475 Some(10) => Self {
476 code: Some("client.storage_io"),
477 sqlite_code,
478 message: "local SQLite storage I/O failed".to_owned(),
479 },
480 _ => Self {
481 code: None,
482 sqlite_code,
483 message: error.to_string(),
484 },
485 }
486 }
487}
488
489#[derive(Debug, Clone, Serialize)]
490#[serde(rename_all = "camelCase")]
491pub struct QuerySnapshot {
492 pub revision: String,
493 pub rows: Vec<QueryRow>,
494 pub coverage: CoverageSnapshot,
495}
496
497impl WindowState {
498 #[must_use]
500 pub fn complete(&self, unit: &str) -> bool {
501 self.units.iter().any(|u| u == unit) && !self.pending.iter().any(|u| u == unit)
502 }
503}
504
505#[derive(Debug, Clone)]
507pub enum Mutation {
508 Upsert {
509 table: String,
510 values: Map<String, Value>,
511 base_version: Option<i64>,
512 },
513 Delete {
514 table: String,
515 row_id: String,
516 base_version: Option<i64>,
517 },
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
521#[serde(rename_all = "camelCase")]
522pub struct SchemaFloor {
523 #[serde(skip_serializing_if = "Option::is_none")]
524 pub required_schema_version: Option<i32>,
525 #[serde(skip_serializing_if = "Option::is_none")]
526 pub latest_schema_version: Option<i32>,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
533#[serde(rename_all = "camelCase")]
534pub struct LeaseState {
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub lease_id: Option<String>,
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub expires_at_ms: Option<i64>,
539 #[serde(skip_serializing_if = "Option::is_none")]
540 pub error_code: Option<String>,
541}
542
543#[derive(Debug, Clone, Serialize)]
545#[serde(rename_all = "camelCase")]
546pub struct PresencePeer {
547 pub actor_id: String,
548 pub client_id: String,
549 pub doc: serde_json::Value,
550}
551
552#[derive(Debug, Clone, Serialize, Default)]
553#[serde(rename_all = "camelCase")]
554pub struct SyncReport {
555 pub pushed: u32,
556 pub applied: Vec<String>,
557 pub rejected: Vec<String>,
558 pub retryable: Vec<String>,
559 pub conflicts: u32,
560 pub commits_applied: u32,
561 pub segment_rows_applied: u32,
562 pub bootstrapping: Vec<String>,
563 pub resets: Vec<String>,
564 pub revoked: Vec<String>,
565 pub failed: Vec<String>,
566 pub deferred_commits: usize,
567 #[serde(skip_serializing_if = "Option::is_none")]
568 pub schema_floor: Option<SchemaFloor>,
569}
570
571#[derive(Debug, Clone)]
574pub enum SyncOutcome {
575 Ok(SyncReport),
576 Failed { error_code: String, message: String },
577}
578
579impl SyncOutcome {
580 pub fn to_json(&self) -> Value {
581 match self {
582 SyncOutcome::Ok(report) => {
583 let mut map = Map::new();
584 map.insert("ok".to_owned(), Value::Bool(true));
585 map.insert(
586 "report".to_owned(),
587 serde_json::to_value(report).expect("report serializes"),
588 );
589 Value::Object(map)
590 }
591 SyncOutcome::Failed {
592 error_code,
593 message,
594 } => {
595 let mut map = Map::new();
596 map.insert("ok".to_owned(), Value::Bool(false));
597 map.insert("errorCode".to_owned(), Value::from(error_code.clone()));
598 map.insert("message".to_owned(), Value::from(message.clone()));
599 Value::Object(map)
600 }
601 }
602 }
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
606#[serde(rename_all = "camelCase")]
607pub struct ConflictRecord {
608 pub client_commit_id: String,
609 pub op_index: i32,
610 pub table: String,
611 pub row_id: String,
612 pub code: String,
613 pub message: String,
614 pub server_version: i64,
615 pub server_row: Map<String, Value>,
618 #[serde(default, skip_serializing_if = "Vec::is_empty")]
623 pub conflict_columns: Vec<String>,
624 #[serde(skip_serializing_if = "Option::is_none")]
625 pub operation: Option<CommitOperation>,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
631#[serde(rename_all = "camelCase", deny_unknown_fields)]
632pub struct RejectionDetails {
633 #[serde(skip_serializing_if = "Option::is_none")]
634 pub field_paths: Option<Vec<String>>,
635 #[serde(skip_serializing_if = "Option::is_none")]
636 pub reason: Option<String>,
637 #[serde(skip_serializing_if = "Option::is_none")]
638 pub required_action: Option<String>,
639 #[serde(skip_serializing_if = "Option::is_none")]
640 pub references: Option<BTreeMap<String, String>>,
641}
642
643impl RejectionDetails {
644 pub(crate) fn parse(raw: &str) -> Result<Self, String> {
645 if raw.len() > 4_096 {
646 return Err("rejection details exceed 4096 encoded bytes".to_owned());
647 }
648 let details: Self = serde_json::from_str(raw)
649 .map_err(|error| format!("invalid rejection details: {error}"))?;
650 details.validate()?;
651 Ok(details)
652 }
653
654 fn validate(&self) -> Result<(), String> {
655 let mut members = 0;
656 if let Some(paths) = &self.field_paths {
657 members += 1;
658 if paths.is_empty() || paths.len() > 32 {
659 return Err("fieldPaths must contain 1-32 paths".to_owned());
660 }
661 let mut seen = std::collections::BTreeSet::new();
662 for path in paths {
663 if path.len() > 160 || !path.split('.').all(valid_identifier) || !seen.insert(path)
664 {
665 return Err("fieldPaths contains an invalid or duplicate path".to_owned());
666 }
667 }
668 }
669 if let Some(reason) = &self.reason {
670 members += 1;
671 if !valid_token(reason, 96) {
672 return Err("reason must be a lowercase stable token".to_owned());
673 }
674 }
675 if let Some(action) = &self.required_action {
676 members += 1;
677 if !valid_token(action, 96) {
678 return Err("requiredAction must be a lowercase stable token".to_owned());
679 }
680 }
681 if let Some(references) = &self.references {
682 members += 1;
683 if references.is_empty() || references.len() > 16 {
684 return Err("references must contain 1-16 entries".to_owned());
685 }
686 for (key, value) in references {
687 if !valid_token(key, 64)
688 || value.is_empty()
689 || value.len() > 256
690 || value.trim() != value
691 || value.chars().any(char::is_control)
692 {
693 return Err("references contains an invalid key or value".to_owned());
694 }
695 }
696 }
697 if members == 0 {
698 return Err("rejection details must not be empty".to_owned());
699 }
700 Ok(())
701 }
702}
703
704fn valid_identifier(segment: &str) -> bool {
705 let mut chars = segment.chars();
706 matches!(chars.next(), Some(first) if first == '_' || first.is_ascii_alphabetic())
707 && chars.all(|character| character == '_' || character.is_ascii_alphanumeric())
708}
709
710fn valid_token(token: &str, max: usize) -> bool {
711 if token.is_empty() || token.len() > max || !token.starts_with(|c: char| c.is_ascii_lowercase())
712 {
713 return false;
714 }
715 let mut previous_separator = false;
716 for character in token.chars() {
717 if character.is_ascii_lowercase() || character.is_ascii_digit() {
718 previous_separator = false;
719 } else if matches!(character, '.' | '_' | '-') && !previous_separator {
720 previous_separator = true;
721 } else {
722 return false;
723 }
724 }
725 !previous_separator
726}
727
728#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
729#[serde(rename_all = "camelCase")]
730pub struct RejectionRecord {
731 pub client_commit_id: String,
732 pub op_index: i32,
733 pub code: String,
734 pub message: String,
735 pub retryable: bool,
736 #[serde(skip_serializing_if = "Option::is_none")]
737 pub details: Option<RejectionDetails>,
738 #[serde(skip_serializing_if = "Option::is_none")]
739 pub operation: Option<CommitOperation>,
740}
741
742#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
744#[serde(rename_all = "camelCase")]
745pub struct CommitOperation {
746 pub table: String,
747 pub row_id: String,
748 pub op: String,
749 #[serde(skip_serializing_if = "Option::is_none")]
750 pub base_version: Option<i64>,
751 #[serde(skip_serializing_if = "Option::is_none")]
752 pub values: Option<Map<String, Value>>,
753}
754
755#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
756#[serde(rename_all = "snake_case")]
757pub enum CommitOutcomeStatus {
758 Applied,
759 Cached,
760 Conflict,
761 Rejected,
762}
763
764#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
765#[serde(rename_all = "snake_case")]
766pub enum CommitOutcomeResolution {
767 Active,
768 ResolvedKeepServer,
769 Superseded,
770 Dismissed,
771}
772
773#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
774#[serde(tag = "status", rename_all = "snake_case")]
775pub enum CommitOperationOutcome {
776 Applied {
777 #[serde(rename = "opIndex")]
778 op_index: i32,
779 },
780 Conflict {
781 conflict: ConflictRecord,
782 },
783 Error {
784 rejection: RejectionRecord,
785 },
786}
787
788#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
789#[serde(rename_all = "camelCase")]
790pub struct CommitOutcome {
791 pub sequence: i64,
792 pub client_commit_id: String,
793 pub status: CommitOutcomeStatus,
794 pub recorded_at_ms: i64,
795 pub results: Vec<CommitOperationOutcome>,
796 #[serde(skip_serializing_if = "Option::is_none")]
799 pub operations: Option<Vec<CommitOperation>>,
800 pub resolution: CommitOutcomeResolution,
801 #[serde(skip_serializing_if = "Option::is_none")]
802 pub resolved_at_ms: Option<i64>,
803 #[serde(skip_serializing_if = "Option::is_none")]
804 pub replacement_client_commit_id: Option<String>,
805}
806
807#[derive(Debug, Clone, Deserialize, Default)]
808#[serde(rename_all = "camelCase")]
809pub struct CommitOutcomeQuery {
810 pub limit: Option<usize>,
811 #[serde(default)]
812 pub active_only: bool,
813}
814
815#[derive(Debug, Clone, Deserialize)]
816#[serde(rename_all = "camelCase")]
817pub struct ResolveCommitOutcomeInput {
818 pub client_commit_id: String,
819 pub resolution: CommitOutcomeResolution,
820 pub replacement_client_commit_id: Option<String>,
821}
822
823#[derive(Debug, Clone, Serialize)]
824#[serde(rename_all = "camelCase")]
825pub struct RowState {
826 pub row_id: String,
827 pub version: i64,
830 pub values: Map<String, Value>,
831}
832
833#[derive(Debug, Clone, Serialize)]
834#[serde(rename_all = "camelCase")]
835pub struct SubscriptionStateView {
836 pub id: String,
837 pub table: String,
838 pub status: String,
840 pub cursor: i64,
841 pub has_resume_token: bool,
842 #[serde(skip_serializing_if = "Option::is_none")]
843 pub effective_scopes: Option<Value>,
844 #[serde(skip_serializing_if = "Option::is_none")]
845 pub reason_code: Option<String>,
846}
847
848#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
852#[serde(rename_all = "camelCase", deny_unknown_fields)]
853pub struct LocalDataPurgeTarget {
854 pub table: String,
855 pub selectors: BTreeMap<String, Vec<String>>,
856}
857
858#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
861#[serde(rename_all = "camelCase", deny_unknown_fields)]
862pub struct LocalDataPurgeInput {
863 pub purge_id: String,
864 pub targets: Vec<LocalDataPurgeTarget>,
865}
866
867#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
869#[serde(rename_all = "camelCase")]
870pub struct LocalDataPurgeResult {
871 pub already_applied: bool,
872 pub purged_rows: usize,
873 pub dropped_commits: usize,
874}
875
876#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
878#[serde(rename_all = "camelCase", deny_unknown_fields)]
879pub struct LocalDataRebootstrapInput {
880 pub rebootstrap_id: String,
881}
882
883#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
885#[serde(rename_all = "camelCase")]
886pub struct LocalDataRebootstrapResult {
887 pub already_applied: bool,
888 pub retained_commits: usize,
889 pub reset_subscriptions: usize,
890}
891
892#[derive(Debug, Clone, Default)]
894pub struct ClientLimits {
895 pub limit_commits: Option<i32>,
896 pub limit_snapshot_rows: Option<i32>,
897 pub max_snapshot_pages: Option<i32>,
898 pub accept: Option<u8>,
901 pub blob_cache_max_bytes: Option<i64>,
906 pub outcome_retention_max_entries: Option<usize>,
909 pub previous_version_context: Option<PreviousVersionContextConfig>,
913}