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