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