1use crate::{ProgressObserver, ProgressPhase, ProgressState};
11use ssp2::encode_message;
12use std::borrow::Cow;
13#[cfg(test)]
14use std::cell::Cell;
15use std::cell::RefCell;
16use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
17use std::sync::Arc;
18
19use rusqlite::types::{ToSqlOutput, Value as SqlValue, ValueRef};
20use rusqlite::{Connection, OpenFlags, OptionalExtension};
21use serde::{Deserialize, Serialize};
22use serde_json::{json, Map, Value};
23use sha2::{Digest, Sha256};
24use ssp2::decode::WIRE_VERSION;
25use ssp2::model::{
26 FencedRecord, Frame, LiveStatus, MediaType, Message, MsgKind, Op, OpResult, PushStatus,
27 SubStatus,
28};
29use ssp2::primitives::RawJson;
30use ssp2::registration::canonical_scope_json;
31use ssp2::segment::{decode_rows_segment, Column, ColumnType, ColumnValue, Row, RowsSegment};
32use ssp2::{decode_message, encode_presence_publish, parse_control, ControlMessage, PresenceKind};
33
34use crate::api::{
35 AppliedServerCommit, AppliedServerCommitSource, ClientChangeBatch, ClientDiagnosticsHost,
36 ClientDiagnosticsLease, ClientDiagnosticsReplica, ClientDiagnosticsSchema,
37 ClientDiagnosticsSnapshot, ClientDiagnosticsStorage, ClientError, ClientLimits, CommandEffects,
38 CommitDelivery, CommitOperation, CommitOperationOutcome, CommitOutcome, CommitOutcomeQuery,
39 CommitOutcomeResolution, CommitOutcomeStatus, ConflictRecord, CoverageSnapshot,
40 CurrentBaseProof, CurrentBaseProofReason, DiagnosticLastChange, DiagnosticLastRound,
41 DiagnosticQueryFailure, DiagnosticRoundCounters, DiagnosticUnits, EchoRecord, FetchedBlob,
42 LeaseState, LocalDataPurgeInput, LocalDataPurgeResult, LocalDataPurgeTarget,
43 LocalDataRebootstrapInput, LocalDataRebootstrapResult, Mutation, PresencePeer,
44 PreviousVersionStatus, QueryOwner, QueryReadFailure, QueryRow, QuerySnapshot, QueryValue,
45 RejectionDetails, RejectionRecord, ResolveCommitOutcomeInput, RetainedCommitRow,
46 RetainedUniqueConflict, RowState, SchemaFloor, SnapshotRead, SyncIntent, SyncOutcome,
47 SyncReport, SyncStatusSnapshot, TableChange, UnitCatchup, UnitRecord, UnitScopes, UnitStatus,
48 WindowBase, WindowChange, WindowCoverage, WindowState, WindowUnitRef,
49 CLIENT_DIAGNOSTICS_VERSION, MAX_DIAGNOSTIC_QUERY_FAILURES,
50};
51use crate::api::{RealtimePolicy, RealtimeState, REALTIME_UNAVAILABLE_CODE};
52#[cfg(feature = "bench-internals")]
53use crate::bench::{Phase, Recorder};
54use crate::previous_version::{
55 capture_previous_version_from_replica, first_incompatibility,
56 reconcile_previous_version_at_boot, set_local_schema_descriptor,
57 sweep_previous_version_container, PendingCommitAudit, PreviousVersionContextConfig,
58 PreviousVersionLifecycle, PreviousVersionReadSpec, PreviousVersionSnapshot,
59};
60use crate::schema::{parse_schema_json, ClientSchema, FtsIndexSchema, TableSchema};
61use crate::transport::{BlobDownload, SegmentRequest, Transport, TransportError};
62use crate::values::{
63 bytes_to_hex, column_value_to_json, decode_row_bytes, encode_sparse_row_json, full_row_values,
64 is_canonical_bytes_envelope, json_to_column_value, json_to_scope_map, normalize_values_casing,
65 render_row_id_json, scope_map_to_json,
66};
67
68const DEFAULT_ACCEPT: u8 = 0b0111;
72const ACCEPT_INLINE_ROWS: u8 = 1 << 0;
73const ACCEPT_EXTERNAL_ROWS: u8 = 1 << 1;
74const ACCEPT_SQLITE: u8 = 1 << 2;
75const ACCEPT_SIGNED_URLS: u8 = 1 << 3;
76const MAX_DIAGNOSTIC_DOMAINS: usize = 256;
77
78fn recommended_action_of(details: &Option<Value>) -> Option<String> {
80 details
81 .as_ref()?
82 .get("recommendedAction")?
83 .as_str()
84 .map(str::to_owned)
85}
86
87pub(crate) const LOCAL_SCHEMA_VERSION_KEY: &str = "localSchemaVersion";
89const LOCAL_REVISION_KEY: &str = "localRevision";
90const REPLICA_CURSOR_KEY: &str = "replicaCursor";
92const ECHO_DIGEST_KEY: &str = "echoDigest";
94const REGISTERED_DIGEST_KEY: &str = "registrationDigest";
96const OUTBOX_ID_KEY: &str = "outboxId";
98const CLIENT_ID_KEY: &str = "clientId";
99const LEASE_STATE_KEY: &str = "leaseState";
100const SCHEMA_FLOOR_KEY: &str = "schemaFloor";
101pub(crate) const LOG_EPOCH_KEY: &str = "logEpoch";
102const SECURITY_PREFLIGHT_PENDING_KEY: &str = "securityPreflightPending";
107const LOCAL_REBOOTSTRAP_RECEIPT_VERSION: u8 = 2;
108const MAX_JS_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
109const OUTBOX_INCOMPATIBLE_CODE: &str = "sync.outbox_incompatible";
112const ENCRYPT_FAILED_CODE: &str = "client.encrypt_failed";
115const INVALID_REQUEST_CODE: &str = "sync.invalid_request:";
116const LEGACY_VALUE_REFUSAL_MESSAGE: &str =
118 "the persisted commit carries values the current codec refuses";
119const INVALID_STORED_VALUES_REASON: &str = "invalid_stored_values";
121
122#[derive(Debug, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase", deny_unknown_fields)]
124struct PersistedLocalDataRebootstrapReceipt {
125 version: u8,
126 retained_commits: u64,
127 reset_subscriptions: u64,
128}
129
130fn invalid_local_rebootstrap_receipt() -> String {
131 "sync.local_corrupt: persisted local rebootstrap receipt is invalid".to_owned()
132}
133
134fn encode_local_rebootstrap_receipt(
135 retained_commits: usize,
136 reset_subscriptions: usize,
137) -> Result<String, String> {
138 let retained_commits =
139 u64::try_from(retained_commits).map_err(|_| invalid_local_rebootstrap_receipt())?;
140 let reset_subscriptions =
141 u64::try_from(reset_subscriptions).map_err(|_| invalid_local_rebootstrap_receipt())?;
142 if retained_commits > MAX_JS_SAFE_INTEGER || reset_subscriptions > MAX_JS_SAFE_INTEGER {
143 return Err(invalid_local_rebootstrap_receipt());
144 }
145 serde_json::to_string(&PersistedLocalDataRebootstrapReceipt {
146 version: LOCAL_REBOOTSTRAP_RECEIPT_VERSION,
147 retained_commits,
148 reset_subscriptions,
149 })
150 .map_err(|_| invalid_local_rebootstrap_receipt())
151}
152
153fn decode_local_rebootstrap_receipt(value: &str) -> Result<(usize, usize), String> {
154 if value == "v1" {
156 return Ok((0, 0));
157 }
158 let receipt: PersistedLocalDataRebootstrapReceipt =
159 serde_json::from_str(value).map_err(|_| invalid_local_rebootstrap_receipt())?;
160 if receipt.version != LOCAL_REBOOTSTRAP_RECEIPT_VERSION
161 || receipt.retained_commits > MAX_JS_SAFE_INTEGER
162 || receipt.reset_subscriptions > MAX_JS_SAFE_INTEGER
163 {
164 return Err(invalid_local_rebootstrap_receipt());
165 }
166 Ok((
167 usize::try_from(receipt.retained_commits)
168 .map_err(|_| invalid_local_rebootstrap_receipt())?,
169 usize::try_from(receipt.reset_subscriptions)
170 .map_err(|_| invalid_local_rebootstrap_receipt())?,
171 ))
172}
173
174#[cfg(test)]
175mod observation_tests;
176
177#[doc(hidden)]
179pub struct CurrentBasePreparation {
180 client_commit_id: String,
181 sequence: i64,
182 units: Vec<UnitRecord>,
183 transport_epoch: u64,
184 registration_epoch: u64,
185 security_epoch: u64,
186}
187
188#[cfg(test)]
189mod units_tests;
190
191#[derive(Debug, Clone)]
192struct OutboxOp {
193 upsert: bool,
194 table: String,
195 row_id: String,
196 base_version: Option<i64>,
197 values: Option<Map<String, Value>>,
201 insert: bool,
205}
206
207impl OutboxOp {
208 fn presented_values(&self, table: &TableSchema) -> Option<Cow<'_, Map<String, Value>>> {
213 let values = self.values.as_ref()?;
214 if !self.insert || table.columns.iter().all(|c| values.contains_key(&c.name)) {
215 return Some(Cow::Borrowed(values));
216 }
217 full_row_values(table, values.clone()).ok().map(Cow::Owned)
218 }
219}
220
221impl From<&OutboxOp> for CommitOperation {
222 fn from(operation: &OutboxOp) -> Self {
223 Self {
224 table: operation.table.clone(),
225 row_id: operation.row_id.clone(),
226 op: if operation.upsert { "upsert" } else { "delete" }.to_owned(),
227 base_version: operation.base_version,
228 values: operation.values.clone(),
229 insert: operation.insert,
230 }
231 }
232}
233
234#[derive(Debug, Clone)]
235struct OutboxCommit {
236 client_commit_id: String,
237 ops: Vec<OutboxOp>,
238}
239
240fn commit_audit_operations(commit: &OutboxCommit) -> Vec<(&str, Vec<&str>, bool)> {
244 commit
245 .ops
246 .iter()
247 .filter(|operation| operation.upsert)
248 .map(|operation| {
249 (
250 operation.table.as_str(),
251 operation
252 .values
253 .as_ref()
254 .map(|values| values.keys().map(String::as_str).collect())
255 .unwrap_or_default(),
256 operation.insert,
257 )
258 })
259 .collect()
260}
261
262type FailedCommit = (OutboxCommit, Map<String, Value>);
263
264#[derive(Debug, Clone)]
265struct CompiledLocalDataPurgeTarget {
266 table: String,
267 selectors: Vec<(String, Vec<String>)>,
268}
269
270struct StoredCommitOutcomeRow {
271 sequence: i64,
272 client_commit_id: String,
273 status: String,
274 recorded_at_ms: i64,
275 results_json: String,
276 operations_json: Option<String>,
277 resolution: String,
278 resolved_at_ms: Option<i64>,
279 replacement_client_commit_id: Option<String>,
280}
281
282impl StoredCommitOutcomeRow {
283 fn from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Self> {
284 Ok(Self {
285 sequence: row.get(0)?,
286 client_commit_id: row.get(1)?,
287 status: row.get(2)?,
288 recorded_at_ms: row.get(3)?,
289 results_json: row.get(4)?,
290 operations_json: row.get(5)?,
291 resolution: row.get(6)?,
292 resolved_at_ms: row.get(7)?,
293 replacement_client_commit_id: row.get(8)?,
294 })
295 }
296}
297
298struct SectionStart {
302 id: String,
303 status: SubStatus,
304 reason_code: String,
305 bootstrap: bool,
306 fenced: Vec<FencedRecord>,
307}
308
309enum SectionError {
310 FailClosed,
311 Abort(String, String),
312 Transport(TransportError),
313}
314
315#[derive(Debug)]
316pub(crate) struct RequestMeta {
317 pushed_ids: Vec<String>,
318 registration_digest: String,
321 pub(crate) accept: u8,
322 deferred_commits: usize,
326}
327
328#[derive(Default)]
329struct ChangeAccumulator {
330 tables: BTreeMap<String, Option<BTreeSet<String>>>,
332 windows: BTreeMap<String, BTreeSet<String>>,
333 status: bool,
334 conflicts: bool,
335 rejections: bool,
336 outcomes: bool,
337}
338
339impl ChangeAccumulator {
340 fn table(&mut self, table: &str) {
341 self.tables.insert(table.to_owned(), None);
342 }
343
344 fn scope(&mut self, table: &str, key: String) {
345 match self.tables.get_mut(table) {
346 Some(None) => {}
347 Some(Some(keys)) => {
348 keys.insert(key);
349 }
350 None => {
351 self.tables
352 .insert(table.to_owned(), Some(BTreeSet::from([key])));
353 }
354 }
355 }
356
357 fn window(&mut self, base_key: &str, value: &str) {
358 self.windows
359 .entry(base_key.to_owned())
360 .or_default()
361 .insert(value.to_owned());
362 }
363
364 fn touched(&self) -> bool {
365 !self.tables.is_empty()
366 || !self.windows.is_empty()
367 || self.status
368 || self.conflicts
369 || self.rejections
370 || self.outcomes
371 }
372}
373
374const PUSH_OPS_PER_REQUEST: usize = 500;
380const MAX_LOCAL_PURGE_TARGETS: usize = 64;
381const MAX_LOCAL_PURGE_SELECTORS: usize = 8;
382const MAX_LOCAL_PURGE_VALUES: usize = 128;
383const MAX_LOCAL_PURGE_VALUE_LENGTH: usize = 256;
384pub const SECURITY_PREFLIGHT_REQUIRED_CODE: &str = "client.security_preflight_required";
385
386struct OverlayDirty(RefCell<Option<BTreeSet<String>>>);
388
389impl OverlayDirty {
390 fn clean() -> Self {
391 Self(RefCell::new(Some(BTreeSet::new())))
392 }
393 fn get(&self) -> bool {
394 self.0
395 .borrow()
396 .as_ref()
397 .is_none_or(|tables| !tables.is_empty())
398 }
399 fn set(&self, dirty: bool) {
400 *self.0.borrow_mut() = if dirty { None } else { Some(BTreeSet::new()) };
401 }
402 fn table(&self, table: &str) {
403 if let Some(tables) = self.0.borrow_mut().as_mut() {
404 tables.insert(table.to_owned());
405 }
406 }
407 fn snapshot(&self) -> Option<BTreeSet<String>> {
408 self.0.borrow().clone()
409 }
410 fn restore(&self, tables: Option<BTreeSet<String>>) {
411 *self.0.borrow_mut() = tables;
412 }
413}
414
415#[derive(Debug, Clone, Default)]
418struct Shared<T>(Arc<T>);
419
420impl<T> std::ops::Deref for Shared<T> {
421 type Target = T;
422 fn deref(&self) -> &T {
423 &self.0
424 }
425}
426
427impl<T: Clone> std::ops::DerefMut for Shared<T> {
428 fn deref_mut(&mut self) -> &mut T {
429 Arc::make_mut(&mut self.0)
430 }
431}
432
433impl<T> From<T> for Shared<T> {
434 fn from(value: T) -> Self {
435 Self(Arc::new(value))
436 }
437}
438
439#[derive(Debug, Clone)]
443struct Rollback {
444 outbox: Shared<Vec<Arc<OutboxCommit>>>,
445 failed_commits: Shared<Vec<FailedCommit>>,
446 conflicts: Shared<Vec<ConflictRecord>>,
447 rejections: Shared<Vec<RejectionRecord>>,
448 schema_floor: Option<SchemaFloor>,
449 lease_state: Option<LeaseState>,
450 stopped: bool,
451 upgrading: bool,
452 sync_needed: bool,
453 active_round: Option<uuid::Uuid>,
454 overlay_dirty: Option<BTreeSet<String>>,
455 change_queue: usize,
456 sync_intent_queue: usize,
457 last_change: Option<DiagnosticLastChange>,
458}
459
460pub struct SyncClient {
461 #[cfg(feature = "bench-internals")]
462 benchmark_phases: Recorder,
463 conn: Connection,
464 schema: ClientSchema,
465 client_id: String,
466 limits: ClientLimits,
467 outbox: Shared<Vec<Arc<OutboxCommit>>>,
468 failed_commits: Shared<Vec<FailedCommit>>,
469 retain_failed_commits: bool,
470 conflicts: Shared<Vec<ConflictRecord>>,
471 rejections: Shared<Vec<RejectionRecord>>,
472 schema_floor: Option<SchemaFloor>,
473 lease_state: Option<LeaseState>,
475 stopped: bool,
477 upgrading: bool,
479 sync_needed: bool,
481 realtime_policy: RealtimePolicy,
484 realtime_state: RealtimeState,
485 realtime_reason_code: Option<String>,
486 realtime_retry_delay_ms: Option<u64>,
487 presence: HashMap<String, HashMap<String, PresencePeer>>,
489 now_ms: Option<i64>,
492 encryption: crate::values::EncryptionConfig,
496 security_preflight: bool,
499 security_epoch: u64,
500 transport_enabled: bool,
501 transport_epoch: u64,
502 registration_epoch: u64,
505 previous_version: Option<PreviousVersionContextConfig>,
509 insert_sql: RefCell<HashMap<String, String>>,
515 overlay_dirty: OverlayDirty,
518 storage_failure: RefCell<Option<QueryReadFailure>>,
520 #[cfg(test)]
523 overlay_rebuild_count: Cell<usize>,
524 #[cfg(test)]
525 acknowledged_replay_count: Cell<usize>,
526 #[cfg(test)]
529 outcome_prune_count: Cell<usize>,
530 progress: ProgressObserver,
532 change_queue: VecDeque<ClientChangeBatch>,
533 sync_intent_queue: VecDeque<SyncIntent>,
534 retry_delay_ms: u64,
536 round_retry_delay_ms: Option<u64>,
538 round_failure: Option<String>,
543 active_round: Option<uuid::Uuid>,
544 last_round: Option<DiagnosticLastRound>,
545 last_change: Option<DiagnosticLastChange>,
546 query_failures: Vec<DiagnosticQueryFailure>,
548 coalesced: Option<CoalescedApply>,
550 coalesced_failure: Option<String>,
553}
554
555struct CoalescedApply {
559 rows: usize,
560 rollback: Rollback,
561}
562
563const COALESCED_APPLY_ROWS: usize = 1024;
564
565pub(crate) fn quote_ident(name: &str) -> String {
566 format!("\"{}\"", name.replace('"', "\"\""))
567}
568pub(crate) fn meta_get(conn: &Connection, key: &str) -> rusqlite::Result<Option<String>> {
572 conn.query_row(
573 "SELECT value FROM _syncular_meta WHERE key = ?1",
574 rusqlite::params![key],
575 |row| row.get::<_, String>(0),
576 )
577 .optional()
578}
579
580pub(crate) fn meta_set(conn: &Connection, key: &str, value: &str) -> rusqlite::Result<()> {
581 conn.execute(
582 "INSERT OR REPLACE INTO _syncular_meta (key, value) VALUES (?1, ?2)",
583 rusqlite::params![key, value],
584 )
585 .map(|_| ())
586}
587
588pub(crate) fn meta_delete(conn: &Connection, key: &str) -> rusqlite::Result<()> {
589 conn.execute(
590 "DELETE FROM _syncular_meta WHERE key = ?1",
591 rusqlite::params![key],
592 )
593 .map(|_| ())
594}
595
596fn row_id_predicate(table: &TableSchema) -> String {
598 let key = quote_ident(&table.primary_key);
599 let text_match = format!("CAST({key} AS TEXT) = ?1");
600 match table.columns[table.pk_index].ty {
601 ColumnType::String | ColumnType::Json => {
602 format!("{key} = ?1 AND {text_match}")
603 }
604 ColumnType::Integer | ColumnType::Boolean => {
608 format!("{key} = +CAST(?1 AS INTEGER) AND {text_match}")
609 }
610 _ => text_match,
613 }
614}
615
616fn local_scope_predicate(
623 table: &TableSchema,
624 effective: &[(String, Vec<String>)],
625 owners: &[Vec<(String, Vec<String>)>],
626) -> Result<Option<(String, Vec<SqlValue>)>, ()> {
627 if effective.is_empty() || effective.iter().any(|(_, values)| values.is_empty()) {
628 return Ok(None);
629 }
630 let mut clauses = Vec::new();
631 let mut params = Vec::new();
632 for (variable, values) in effective {
633 let name = table.scope_column(variable).ok_or(())?;
634 let column = table
635 .columns
636 .iter()
637 .find(|column| column.name == name)
638 .ok_or(())?;
639 let key = quote_ident(name);
640 let holes = vec!["?"; values.len()].join(", ");
641 clauses.push(if column.ty == ColumnType::Integer {
642 params.extend(values.iter().cloned().map(SqlValue::Text));
643 let seek = vec!["+CAST(? AS INTEGER)"; values.len()].join(", ");
644 format!("{key} IN ({seek}) AND CAST({key} AS TEXT) IN ({holes})")
645 } else {
646 format!("{key} IN ({holes})")
647 });
648 params.extend(values.iter().cloned().map(SqlValue::Text));
649 }
650 for owner in owners {
651 if let Some((sql, owner_params)) = local_scope_predicate(table, owner, &[])? {
652 clauses.push(format!("NOT ({sql})"));
653 params.extend(owner_params);
654 }
655 }
656 Ok(Some((clauses.join(" AND "), params)))
657}
658
659fn owns_local_row(
661 table: &TableSchema,
662 effective: &[(String, Vec<String>)],
663 owners: &[Vec<(String, Vec<String>)>],
664 values: &Map<String, Value>,
665) -> bool {
666 let matches = |scopes: &[(String, Vec<String>)]| {
667 !scopes.is_empty()
668 && scopes.iter().all(|(variable, allowed)| {
669 table
670 .scope_column(variable)
671 .and_then(|column| values.get(column))
672 .filter(|value| !value.is_null())
673 .is_some_and(|value| {
674 allowed.contains(
675 &value
676 .as_str()
677 .map_or_else(|| value.to_string(), str::to_owned),
678 )
679 })
680 })
681 };
682 matches(effective) && !owners.iter().any(|owner| matches(owner))
683}
684
685fn is_local_operation_code_like(value: &str) -> bool {
686 let bytes = value.as_bytes();
687 bytes.first().is_some_and(u8::is_ascii_alphanumeric)
688 && bytes
689 .iter()
690 .all(|byte| byte.is_ascii_alphanumeric() || matches!(*byte, b'.' | b'_' | b':' | b'-'))
691}
692
693fn base_table(name: &str) -> String {
694 quote_ident(&format!("_syncular_base_{name}"))
695}
696
697type PendingEvict = (String, String, Vec<(String, Vec<String>)>);
699
700fn window_base_key(base: &WindowBase) -> String {
702 format!(
703 "{}\0{}",
704 base.variable,
705 ssp2::registration::canonical_unit_json(&scope_pairs(&base.fixed_scopes))
706 )
707}
708
709fn window_unit_scopes(base: &WindowBase, value: &str) -> UnitScopes {
711 let mut scopes = base.fixed_scopes.clone();
712 scopes.insert(base.variable.clone(), value.to_owned());
713 scopes
714}
715
716fn scope_pairs(scopes: &UnitScopes) -> Vec<(String, String)> {
718 let mut pairs: Vec<(String, String)> = scopes
719 .iter()
720 .map(|(variable, value)| (variable.clone(), value.clone()))
721 .collect();
722 pairs.sort_by(|a, b| a.0.encode_utf16().cmp(b.0.encode_utf16()));
723 pairs
724}
725
726fn covers(table: &TableSchema, scopes: &UnitScopes) -> bool {
728 scopes
729 .keys()
730 .all(|variable| table.scope_column(variable).is_some())
731}
732
733fn pair_scopes(
737 scopes: &UnitScopes,
738 table: &str,
739 echo: Option<&[EchoRecord]>,
740) -> Option<Vec<(String, Vec<String>)>> {
741 let fenced = echo.and_then(|echo| echo.iter().find(|record| record.table == table));
742 if fenced.is_some_and(|record| record.status == "revoked") {
743 return None;
744 }
745 let mut effective: BTreeMap<String, Vec<String>> = fenced
746 .map(|record| record.scopes.clone())
747 .unwrap_or_default();
748 for (variable, value) in scopes {
749 effective.insert(variable.clone(), vec![value.clone()]);
750 }
751 Some(effective.into_iter().collect())
752}
753
754fn unit_owners(
759 units: &[UnitRecord],
760 table: &TableSchema,
761 except_id: &str,
762) -> Vec<Vec<(String, Vec<String>)>> {
763 units
764 .iter()
765 .filter(|unit| {
766 unit.id != except_id
767 && unit.status == UnitStatus::Active
768 && unit.echo.is_some()
769 && (!unit.bootstrapping() || unit.seeded.contains(&table.name))
770 && covers(table, &unit.scopes)
771 })
772 .filter_map(|unit| pair_scopes(&unit.scopes, &table.name, unit.echo.as_deref()))
773 .collect()
774}
775
776fn unit_scope_lists(scopes: &UnitScopes) -> Vec<(String, Vec<String>)> {
778 scopes
779 .iter()
780 .map(|(variable, value)| (variable.clone(), vec![value.clone()]))
781 .collect()
782}
783
784const ACKED_ROWS_BY_ROW: &str = "SELECT a.op_json,a.intent_json FROM json_each(?1) k JOIN _syncular_acked_rows a ON a.tbl=json_extract(k.value,'$[0]') AND a.id=json_extract(k.value,'$[1]') WHERE ?2 IS NULL OR a.tbl IN(SELECT value FROM json_each(?2)) ORDER BY a.commit_seq,a.idx";
788
789const UNIT_COLUMNS: &str =
790 "id, scopes, explicit, status, reason_code, cursor, bootstrap_state, seeded, echo";
791
792fn unit_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<UnitRecord> {
795 fn json<T: serde::de::DeserializeOwned>(index: usize, raw: &str) -> rusqlite::Result<T> {
796 serde_json::from_str(raw).map_err(|error| {
797 rusqlite::Error::FromSqlConversionFailure(
798 index,
799 rusqlite::types::Type::Text,
800 Box::new(error),
801 )
802 })
803 }
804 Ok(UnitRecord {
805 id: row.get(0)?,
806 scopes: json(1, &row.get::<_, String>(1)?)?,
807 explicit: row.get::<_, i64>(2)? == 1,
808 status: json(
809 3,
810 &serde_json::to_string(&row.get::<_, String>(3)?).expect("text"),
811 )?,
812 reason_code: row.get(4)?,
813 cursor: row.get(5)?,
814 bootstrap_state: row.get(6)?,
815 seeded: json(7, &row.get::<_, String>(7)?)?,
816 echo: row
817 .get::<_, Option<String>>(8)?
818 .map(|echo| json::<Vec<EchoRecord>>(8, &echo))
819 .transpose()?
820 .map(|echo| {
821 if echo
822 .iter()
823 .all(|record| record.status == "active" || record.status == "revoked")
824 {
825 Ok(echo)
826 } else {
827 Err(rusqlite::Error::FromSqlConversionFailure(
828 8,
829 rusqlite::types::Type::Text,
830 "echo status is neither active nor revoked".into(),
831 ))
832 }
833 })
834 .transpose()?,
835 })
836}
837
838fn load_units(conn: &Connection) -> rusqlite::Result<Vec<UnitRecord>> {
840 conn.prepare_cached(&format!(
841 "SELECT {UNIT_COLUMNS} FROM _syncular_units ORDER BY rowid"
842 ))?
843 .query_map([], unit_from_row)?
844 .collect()
845}
846
847fn get_unit(conn: &Connection, id: &str) -> rusqlite::Result<Option<UnitRecord>> {
848 conn.query_row(
849 &format!("SELECT {UNIT_COLUMNS} FROM _syncular_units WHERE id = ?1"),
850 [id],
851 unit_from_row,
852 )
853 .optional()
854}
855
856fn save_unit(conn: &Connection, unit: &UnitRecord) -> rusqlite::Result<()> {
858 conn.prepare_cached(&format!(
859 "INSERT INTO _syncular_units({UNIT_COLUMNS}) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)
860 ON CONFLICT(id) DO UPDATE SET explicit=excluded.explicit, status=excluded.status,
861 reason_code=excluded.reason_code, cursor=excluded.cursor,
862 bootstrap_state=excluded.bootstrap_state, seeded=excluded.seeded, echo=excluded.echo"
863 ))?
864 .execute(rusqlite::params![
865 unit.id,
866 serde_json::to_string(&unit.scopes).expect("scope map serializes"),
867 i64::from(unit.explicit),
868 unit.status.name(),
869 unit.reason_code,
870 unit.cursor,
871 unit.bootstrap_state,
872 serde_json::to_string(&unit.seeded).expect("table list serializes"),
873 unit.echo
874 .as_ref()
875 .map(|echo| serde_json::to_string(echo).expect("echo serializes")),
876 ])
877 .map(|_| ())
878}
879
880fn window_refs(conn: &Connection, unit_id: &str) -> rusqlite::Result<Vec<(String, String)>> {
882 conn.prepare_cached("SELECT base, unit FROM _syncular_windows WHERE sub_id = ?1")?
883 .query_map([unit_id], |row| Ok((row.get(0)?, row.get(1)?)))?
884 .collect()
885}
886
887fn reset_units(conn: &Connection) -> rusqlite::Result<()> {
891 conn.execute_batch(
892 "UPDATE _syncular_units SET status='active', reason_code=NULL, cursor=-1,
893 bootstrap_state=NULL, seeded='[]', echo=NULL;
894 DELETE FROM _syncular_meta WHERE key IN ('replicaCursor','echoDigest','registrationDigest');",
895 )
896}
897
898fn replica_cursor(conn: &Connection) -> rusqlite::Result<i64> {
900 meta_get(conn, REPLICA_CURSOR_KEY)?.map_or(Ok(-1), |raw| {
901 raw.parse().map_err(|error| {
902 rusqlite::Error::FromSqlConversionFailure(
903 0,
904 rusqlite::types::Type::Text,
905 Box::new(error),
906 )
907 })
908 })
909}
910
911fn visible_table(name: &str) -> String {
912 quote_ident(name)
913}
914
915const FTS_SOURCE_ID_COLUMN: &str = "_syncular_source_id";
916
917fn is_synced_table_name(name: &str) -> bool {
920 if name.starts_with("sqlite_") {
921 return false;
922 }
923 if name.starts_with("_syncular_base_") {
924 return true; }
926 !name.starts_with("_syncular_")
928}
929
930fn blob_id_for(bytes: &[u8]) -> String {
932 let digest = Sha256::digest(bytes);
933 format!("sha256:{}", bytes_to_hex(&digest))
934}
935
936enum RowParam<'a> {
940 Cell(&'a Option<ColumnValue>),
941 Version(i64),
942}
943
944impl rusqlite::ToSql for RowParam<'_> {
945 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
946 Ok(match self {
947 RowParam::Version(v) => ToSqlOutput::Owned(SqlValue::Integer(*v)),
948 RowParam::Cell(cell) => match cell {
949 None => ToSqlOutput::Owned(SqlValue::Null),
950 Some(ColumnValue::String(s)) => ToSqlOutput::Borrowed(ValueRef::Text(s.as_bytes())),
951 Some(ColumnValue::Integer(i)) => ToSqlOutput::Owned(SqlValue::Integer(*i)),
952 Some(ColumnValue::Float(f)) => ToSqlOutput::Owned(SqlValue::Real(*f)),
953 Some(ColumnValue::Boolean(b)) => {
954 ToSqlOutput::Owned(SqlValue::Integer(i64::from(*b)))
955 }
956 Some(ColumnValue::Json(raw)) | Some(ColumnValue::BlobRef(raw)) => {
957 ToSqlOutput::Borrowed(ValueRef::Text(raw.0.as_bytes()))
958 }
959 Some(ColumnValue::Bytes(b)) | Some(ColumnValue::Crdt(b)) => {
961 ToSqlOutput::Borrowed(ValueRef::Blob(b))
962 }
963 },
964 })
965 }
966}
967
968fn owned_sql_value(param: impl rusqlite::ToSql) -> Result<SqlValue, String> {
970 match param.to_sql().map_err(|error| error.to_string())? {
971 ToSqlOutput::Owned(value) => Ok(value),
972 ToSqlOutput::Borrowed(value) => {
973 SqlValue::try_from(value).map_err(|error| error.to_string())
974 }
975 _ => Err("sync.invalid_request: unsupported SQLite key parameter".into()),
976 }
977}
978
979fn image_cell_param<'a>(column: &Column, value: ValueRef<'a>) -> Result<ToSqlOutput<'a>, String> {
987 use ssp2::segment::ColumnType;
988 let mismatch = || {
989 Err(format!(
990 "image column {:?} holds a value of the wrong type",
991 column.name
992 ))
993 };
994 match value {
995 ValueRef::Null => {
996 if !column.nullable {
997 return Err(format!(
998 "image column {:?} is NULL but not nullable",
999 column.name
1000 ));
1001 }
1002 Ok(ToSqlOutput::Owned(SqlValue::Null))
1003 }
1004 ValueRef::Integer(i) => match column.ty {
1005 ColumnType::Integer => Ok(ToSqlOutput::Borrowed(value)),
1006 ColumnType::Boolean => Ok(ToSqlOutput::Owned(SqlValue::Integer(i64::from(i != 0)))),
1007 ColumnType::Float => Ok(ToSqlOutput::Owned(SqlValue::Real(i as f64))),
1008 _ => mismatch(),
1009 },
1010 ValueRef::Real(_) => match column.ty {
1011 ColumnType::Float => Ok(ToSqlOutput::Borrowed(value)),
1012 _ => mismatch(),
1013 },
1014 ValueRef::Text(t) => {
1015 std::str::from_utf8(t)
1016 .map_err(|_| format!("image column {:?} is not UTF-8", column.name))?;
1017 match column.ty {
1018 ColumnType::String | ColumnType::Json | ColumnType::BlobRef => {
1019 Ok(ToSqlOutput::Borrowed(value))
1020 }
1021 _ => mismatch(),
1022 }
1023 }
1024 ValueRef::Blob(_) => match column.ty {
1025 ColumnType::Bytes | ColumnType::Crdt => Ok(ToSqlOutput::Borrowed(value)),
1027 _ => mismatch(),
1028 },
1029 }
1030}
1031
1032pub(crate) fn sql_ref_to_json(column: &Column, value: rusqlite::types::ValueRef<'_>) -> Value {
1033 use rusqlite::types::ValueRef;
1034 match value {
1035 ValueRef::Null => Value::Null,
1036 ValueRef::Integer(i) => match column.ty {
1037 ssp2::segment::ColumnType::Boolean => Value::Bool(i != 0),
1038 ssp2::segment::ColumnType::Float => {
1039 serde_json::Number::from_f64(i as f64).map_or(Value::Null, Value::Number)
1040 }
1041 _ => Value::from(i),
1042 },
1043 ValueRef::Real(f) => serde_json::Number::from_f64(f).map_or(Value::Null, Value::Number),
1044 ValueRef::Text(t) => Value::from(String::from_utf8_lossy(t).into_owned()),
1045 ValueRef::Blob(b) => {
1046 let mut map = Map::new();
1047 map.insert("$bytes".to_owned(), Value::from(bytes_to_hex(b)));
1048 Value::Object(map)
1049 }
1050 }
1051}
1052
1053fn json_param_to_sql(value: &Value) -> Result<SqlValue, String> {
1057 Ok(match value {
1058 Value::Null => SqlValue::Null,
1059 Value::Bool(b) => SqlValue::Integer(i64::from(*b)),
1060 Value::Number(n) => {
1061 if let Some(i) = n.as_i64() {
1062 SqlValue::Integer(i)
1063 } else if let Some(f) = n.as_f64() {
1064 SqlValue::Real(f)
1065 } else {
1066 return Err(format!("query param number {n} is out of range"));
1067 }
1068 }
1069 Value::String(s) => SqlValue::Text(s.clone()),
1070 Value::Object(_) => {
1071 if let Some(hex) = value.get("$bytes").and_then(Value::as_str) {
1072 SqlValue::Blob(crate::values::hex_to_bytes(hex)?)
1073 } else if let Some(decimal) = value.get("$bigint").and_then(Value::as_str) {
1074 SqlValue::Integer(decimal.parse::<i64>().map_err(|_| {
1075 format!("query bigint param {decimal:?} is outside SQLite's i64 range")
1076 })?)
1077 } else {
1078 return Err(
1079 "query object param must be a {$bytes: hex} or {$bigint: decimal} value"
1080 .to_owned(),
1081 );
1082 }
1083 }
1084 Value::Array(_) => return Err("query array params are not supported".to_owned()),
1085 })
1086}
1087
1088fn sql_ref_to_json_dynamic(value: rusqlite::types::ValueRef<'_>) -> Value {
1093 use rusqlite::types::ValueRef;
1094 match value {
1095 ValueRef::Null => Value::Null,
1096 ValueRef::Integer(i) => {
1097 const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
1101 if (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&i) {
1102 Value::from(i)
1103 } else {
1104 let mut map = Map::new();
1105 map.insert("$bigint".to_owned(), Value::from(i.to_string()));
1106 Value::Object(map)
1107 }
1108 }
1109 ValueRef::Real(f) => serde_json::Number::from_f64(f).map_or(Value::Null, Value::Number),
1110 ValueRef::Text(t) => Value::from(String::from_utf8_lossy(t).into_owned()),
1111 ValueRef::Blob(b) => {
1112 let mut map = Map::new();
1113 map.insert("$bytes".to_owned(), Value::from(bytes_to_hex(b)));
1114 Value::Object(map)
1115 }
1116 }
1117}
1118
1119fn query_connection(
1121 conn: &Connection,
1122 sql: &str,
1123 params: &[QueryValue],
1124) -> Result<Vec<QueryRow>, QueryReadFailure> {
1125 let lowered_sql = crate::query_guard::lower_public_query_sql(sql);
1126 let mut stmt = crate::query_guard::prepare_read_only(conn, &lowered_sql)?;
1127 let bound: Vec<SqlValue> = params
1128 .iter()
1129 .map(json_param_to_sql)
1130 .collect::<Result<_, _>>()?;
1131 let columns: Vec<(usize, String)> = stmt
1135 .column_names()
1136 .enumerate()
1137 .filter(|(_, name)| *name != "_syncular_version")
1138 .map(|(i, name)| (i, name.to_owned()))
1139 .collect();
1140 let bound_refs: Vec<&dyn rusqlite::ToSql> =
1141 bound.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
1142 let mut sql_rows = stmt.query(bound_refs.as_slice())?;
1143 let mut out = Vec::new();
1144 while let Some(row) = sql_rows.next()? {
1145 let mut record = Map::new();
1146 for (i, name) in &columns {
1147 let value = row.get_ref(*i)?;
1148 record.insert(name.clone(), sql_ref_to_json_dynamic(value));
1149 }
1150 out.push(record);
1151 }
1152 Ok(out)
1153}
1154
1155fn persisted_window_state(
1156 conn: &Connection,
1157 base: &WindowBase,
1158) -> Result<WindowState, QueryReadFailure> {
1159 let mut stmt = conn.prepare(
1160 "SELECT windows.unit, units.status, units.cursor, units.bootstrap_state
1161 FROM _syncular_windows AS windows
1162 LEFT JOIN _syncular_units AS units ON units.id = windows.sub_id
1163 WHERE windows.base = ?1
1164 ORDER BY windows.unit ASC",
1165 )?;
1166 let rows = stmt.query_map(rusqlite::params![window_base_key(base)], |row| {
1167 Ok((
1168 row.get::<_, String>(0)?,
1169 row.get::<_, Option<String>>(1)?,
1170 row.get::<_, Option<i64>>(2)?,
1171 row.get::<_, Option<String>>(3)?,
1172 ))
1173 })?;
1174 let mut units = Vec::new();
1175 let mut pending = Vec::new();
1176 for row in rows {
1177 let (unit, status, cursor, bootstrap_state) = row?;
1178 if status.as_deref() != Some("active")
1180 || cursor.is_some_and(|cursor| cursor < 0 || bootstrap_state.is_some())
1181 {
1182 pending.push(unit.clone());
1183 }
1184 units.push(unit);
1185 }
1186 Ok(WindowState { units, pending })
1187}
1188
1189#[derive(Debug, Clone, Copy)]
1191pub struct SnapshotStatement<'a> {
1192 pub sql: &'a str,
1193 pub params: &'a [Value],
1194}
1195
1196#[derive(Debug, Clone, Default)]
1200pub struct SnapshotReadRequest<'a> {
1201 pub statements: Vec<SnapshotStatement<'a>>,
1202 pub coverage: &'a [WindowCoverage],
1203 pub units: Vec<String>,
1204 pub commit_ids: Vec<String>,
1205}
1206
1207#[cfg(test)]
1208thread_local! {
1209 static SNAPSHOT_READ_HOOK: std::cell::RefCell<Option<Box<dyn FnOnce()>>> =
1213 std::cell::RefCell::new(None);
1214}
1215
1216#[cfg(test)]
1217fn run_snapshot_read_hook() {
1218 SNAPSHOT_READ_HOOK.with(|slot| {
1219 if let Some(hook) = slot.borrow_mut().take() {
1220 hook();
1221 }
1222 });
1223}
1224
1225fn snapshot_read_connection(
1230 conn: &Connection,
1231 request: &SnapshotReadRequest<'_>,
1232) -> Result<SnapshotRead, QueryReadFailure> {
1233 conn.execute_batch("SAVEPOINT syncular_snapshot_read")?;
1234 let corrupt = |message: &str| QueryReadFailure {
1235 code: Some("sync.local_corrupt"),
1236 sqlite_code: None,
1237 sqlite_message: None,
1238 rollback_failure: None,
1239 message: message.to_owned(),
1240 };
1241 let stored_failure = |error: rusqlite::Error, message: &str| match error {
1242 rusqlite::Error::Utf8Error(..)
1243 | rusqlite::Error::InvalidColumnType(..)
1244 | rusqlite::Error::FromSqlConversionFailure(..)
1245 | rusqlite::Error::IntegralValueOutOfRange(..) => corrupt(message),
1246 _ => QueryReadFailure::from(error),
1247 };
1248 let result: Result<SnapshotRead, QueryReadFailure> = (|| {
1249 let revision: Option<String> = conn
1252 .query_row(
1253 "SELECT value FROM _syncular_meta WHERE key = ?1",
1254 rusqlite::params![LOCAL_REVISION_KEY],
1255 |row| row.get(0),
1256 )
1257 .optional()
1258 .map_err(|error| stored_failure(error, "persisted local revision is invalid"))?;
1259 let revision = match revision {
1260 None => "0".to_owned(),
1261 Some(value)
1262 if value == "0"
1263 || (matches!(value.as_bytes().first(), Some(b'1'..=b'9'))
1264 && value.bytes().all(|b| b.is_ascii_digit())
1265 && value.parse::<u64>().is_ok()) =>
1266 {
1267 value
1268 }
1269 Some(_) => return Err(corrupt("persisted local revision is invalid")),
1270 };
1271 let mut queries = Vec::with_capacity(request.statements.len());
1272 for statement in &request.statements {
1273 queries.push(query_connection(conn, statement.sql, statement.params)?);
1274 #[cfg(test)]
1275 run_snapshot_read_hook();
1276 }
1277 let mut pending = Vec::new();
1278 let mut missing = Vec::new();
1279 for requested in request.coverage {
1280 let base_key = window_base_key(&requested.base);
1281 let state = persisted_window_state(conn, &requested.base)?;
1282 for unit in BTreeSet::from_iter(requested.units.iter().cloned()) {
1283 let reference = WindowUnitRef {
1284 base_key: base_key.clone(),
1285 unit: unit.clone(),
1286 };
1287 if !state.units.iter().any(|held| held == &unit) {
1288 missing.push(reference);
1289 } else if state.pending.iter().any(|held| held == &unit) {
1290 pending.push(reference);
1291 }
1292 }
1293 }
1294 let mut units = Vec::with_capacity(request.units.len());
1295 for id in &request.units {
1296 let exists: bool = conn.query_row(
1297 "SELECT EXISTS(SELECT 1 FROM _syncular_units WHERE id = ?1)",
1298 [id],
1299 |row| row.get(0),
1300 )?;
1301 if !exists {
1302 units.push(UnitCatchup::Unknown { id: id.clone() });
1303 continue;
1304 }
1305 let unit = get_unit(conn, id)
1308 .map_err(|error| stored_failure(error, "persisted unit state is invalid"))?
1309 .ok_or_else(|| corrupt("persisted unit state is invalid"))?;
1310 let cursor = match unit.cursor {
1311 Some(cursor) => cursor,
1312 None => replica_cursor(conn)
1313 .map_err(|error| stored_failure(error, "persisted unit state is invalid"))?,
1314 };
1315 if cursor.unsigned_abs() > MAX_JS_SAFE_INTEGER {
1316 return Err(corrupt("persisted unit state is invalid"));
1317 }
1318 let active = unit.status == UnitStatus::Active;
1319 let pending = unit.bootstrapping();
1320 units.push(UnitCatchup::Known {
1321 id: id.clone(),
1322 status: unit.status,
1323 live: unit.cursor.is_none(),
1324 cursor,
1325 has_resume_token: unit.bootstrap_state.is_some(),
1326 bootstrap_complete: active && !pending,
1327 known_pending_pages: active && pending,
1328 reason_code: unit.reason_code,
1329 });
1330 }
1331 let mut deliveries = Vec::with_capacity(request.commit_ids.len());
1332 for id in &request.commit_ids {
1333 let pending: bool = conn.query_row(
1334 "SELECT EXISTS(SELECT 1 FROM _syncular_outbox WHERE commit_id = ?1)",
1335 rusqlite::params![id],
1336 |row| row.get(0),
1337 )?;
1338 if pending {
1339 deliveries.push(CommitDelivery::Pending {
1340 client_commit_id: id.clone(),
1341 });
1342 continue;
1343 }
1344 let row = conn
1345 .query_row(
1346 "SELECT seq, client_commit_id, status, recorded_at_ms, results_json, operations_json,
1347 resolution, resolved_at_ms, replacement_client_commit_id
1348 FROM _syncular_commit_outcomes WHERE client_commit_id = ?1",
1349 rusqlite::params![id],
1350 StoredCommitOutcomeRow::from_row,
1351 )
1352 .optional()
1353 .map_err(|error| stored_failure(error, "persisted commit outcome is invalid"))?;
1354 deliveries.push(match row {
1355 Some(row) => CommitDelivery::Known {
1356 client_commit_id: id.clone(),
1357 outcome: SyncClient::outcome_from_row(row)
1358 .map_err(|_| corrupt("persisted commit outcome is invalid"))?,
1359 },
1360 None => CommitDelivery::Unknown {
1361 client_commit_id: id.clone(),
1362 },
1363 });
1364 }
1365 Ok(SnapshotRead {
1366 revision,
1367 queries,
1368 coverage: CoverageSnapshot {
1369 complete: pending.is_empty() && missing.is_empty(),
1370 pending,
1371 missing,
1372 },
1373 units,
1374 deliveries,
1375 })
1376 })();
1377 match result {
1378 Ok(read) => {
1379 conn.execute_batch("RELEASE syncular_snapshot_read")?;
1380 Ok(read)
1381 }
1382 Err(mut error) => {
1383 if let Err(rollback) = conn
1384 .execute_batch("ROLLBACK TO syncular_snapshot_read; RELEASE syncular_snapshot_read")
1385 {
1386 error.rollback_failure = Some(Box::new(QueryReadFailure::from(rollback)));
1387 }
1388 Err(error)
1389 }
1390 }
1391}
1392
1393fn snapshot_connection(
1394 conn: &Connection,
1395 sql: &str,
1396 params: &[Value],
1397 coverage: &[WindowCoverage],
1398) -> Result<QuerySnapshot, QueryReadFailure> {
1399 let read = snapshot_read_connection(
1400 conn,
1401 &SnapshotReadRequest {
1402 statements: vec![SnapshotStatement { sql, params }],
1403 coverage,
1404 units: Vec::new(),
1405 commit_ids: Vec::new(),
1406 },
1407 )?;
1408 Ok(QuerySnapshot {
1409 revision: read.revision,
1410 rows: read
1411 .queries
1412 .into_iter()
1413 .next()
1414 .expect("single-query snapshot returns one result"),
1415 coverage: read.coverage,
1416 })
1417}
1418
1419pub struct FileQuerySnapshotReader {
1424 path: String,
1425 conn: Option<Connection>,
1426}
1427
1428impl FileQuerySnapshotReader {
1429 #[must_use]
1430 pub fn new(path: impl Into<String>) -> Self {
1431 Self {
1432 path: path.into(),
1433 conn: None,
1434 }
1435 }
1436
1437 fn connection(&mut self) -> Result<&Connection, QueryReadFailure> {
1438 if self.conn.is_none() {
1439 let conn = Connection::open_with_flags(
1440 &self.path,
1441 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
1442 )
1443 .map_err(QueryReadFailure::from)?;
1444 conn.busy_timeout(std::time::Duration::from_millis(250))
1445 .map_err(QueryReadFailure::from)?;
1446 self.conn = Some(conn);
1447 }
1448 self.conn
1449 .as_ref()
1450 .ok_or_else(|| QueryReadFailure::from("read sidecar connection missing".to_owned()))
1451 }
1452
1453 pub fn snapshot_read(
1456 &mut self,
1457 request: &SnapshotReadRequest<'_>,
1458 ) -> Result<SnapshotRead, QueryReadFailure> {
1459 snapshot_read_connection(self.connection()?, request)
1460 }
1461
1462 pub fn local_revision(&mut self) -> Result<String, QueryReadFailure> {
1465 Ok(self
1466 .snapshot_read(&SnapshotReadRequest::default())?
1467 .revision)
1468 }
1469
1470 pub fn unit_catchup(&mut self, id: &str) -> Result<UnitCatchup, QueryReadFailure> {
1472 let read = self.snapshot_read(&SnapshotReadRequest {
1473 units: vec![id.to_owned()],
1474 ..Default::default()
1475 })?;
1476 Ok(read
1477 .units
1478 .into_iter()
1479 .next()
1480 .expect("one requested catch-up"))
1481 }
1482
1483 pub fn commit_delivery(&mut self, id: &str) -> Result<CommitDelivery, QueryReadFailure> {
1486 let read = self.snapshot_read(&SnapshotReadRequest {
1487 commit_ids: vec![id.to_owned()],
1488 ..Default::default()
1489 })?;
1490 Ok(read
1491 .deliveries
1492 .into_iter()
1493 .next()
1494 .expect("one requested delivery"))
1495 }
1496
1497 pub fn query_snapshot(
1500 &mut self,
1501 sql: &str,
1502 params: &[Value],
1503 coverage: &[WindowCoverage],
1504 ) -> Result<QuerySnapshot, QueryReadFailure> {
1505 snapshot_connection(self.connection()?, sql, params, coverage)
1506 }
1507}
1508
1509fn validate_authority_reads(
1510 schema: &ClientSchema,
1511 declarations: &[crate::api::AuthorityReadDeclaration],
1512) -> Result<(), String> {
1513 let mut seen = BTreeSet::new();
1514 for read in declarations {
1515 let table = schema
1516 .table(&read.table)
1517 .ok_or("client.authority_read_forbidden: authority table is not declared")?;
1518 let plain = |name: &str| {
1519 table.columns.iter().enumerate().any(|(i, c)| {
1520 c.name == name
1521 && name != "authority_version"
1522 && !name.starts_with("_sync")
1523 && !table.encrypted_columns.iter().any(|e| e.index == i)
1524 && matches!(
1525 c.ty,
1526 ssp2::segment::ColumnType::String
1527 | ssp2::segment::ColumnType::Integer
1528 | ssp2::segment::ColumnType::Float
1529 | ssp2::segment::ColumnType::Boolean
1530 | ssp2::segment::ColumnType::Json
1531 )
1532 })
1533 };
1534 if !seen.insert(&read.table)
1535 || read.columns.is_empty()
1536 || read.columns.iter().collect::<BTreeSet<_>>().len() != read.columns.len()
1537 || !read.columns.contains(&table.primary_key)
1538 || !read.columns.iter().all(|c| plain(c))
1539 || read.scopes.is_empty()
1540 || !read.scopes.iter().all(|(v, values)| {
1541 table
1542 .scope_column(v)
1543 .is_some_and(|c| read.columns.iter().any(|column| column == c) && plain(c))
1544 && !values.is_empty()
1545 && values.iter().all(|v| !v.is_empty() && v != "*")
1546 })
1547 {
1548 return Err("client.authority_read_forbidden: authority reads require plain columns, a primary key and nonempty plain scope selectors".into());
1549 }
1550 }
1551 Ok(())
1552}
1553
1554fn schema_marker_read_failure(error: rusqlite::Error) -> String {
1559 let failure = QueryReadFailure::from(error);
1560 if failure.code.is_some() {
1561 failure.to_string()
1562 } else {
1563 "sync.local_corrupt: persisted local schema marker is unreadable or invalid".to_owned()
1564 }
1565}
1566
1567fn validate_client_limits(limits: &crate::api::ClientLimits) -> Result<(), String> {
1571 if limits.outcome_retention_max_entries == Some(0) {
1572 return Err("sync.invalid_request: outcomeRetentionMaxEntries must be positive".to_owned());
1573 }
1574 if [
1575 limits.max_push_commits_per_request,
1576 limits.max_push_operations_per_request,
1577 limits.max_push_request_bytes,
1578 ]
1579 .into_iter()
1580 .flatten()
1581 .any(|value| value == 0 || value > u32::MAX as usize)
1582 {
1583 return Err(
1584 "sync.invalid_request: push limit must be an integer in 1..=4294967295".to_owned(),
1585 );
1586 }
1587 Ok(())
1588}
1589
1590fn read_local_schema_version(conn: &Connection, requested: i32) -> Result<Option<i32>, String> {
1591 let corrupt =
1592 || "sync.local_corrupt: persisted local schema marker is unreadable or invalid".to_owned();
1593 let table_type: Option<String> = conn
1594 .query_row(
1595 "SELECT type FROM sqlite_master WHERE name = '_syncular_meta'",
1596 [],
1597 |row| row.get(0),
1598 )
1599 .optional()
1600 .map_err(schema_marker_read_failure)?;
1601 let Some(table_type) = table_type else {
1602 return Ok(None);
1603 };
1604 if table_type != "table" {
1605 return Err(corrupt());
1606 }
1607 let mut statement = conn
1611 .prepare("SELECT value FROM _syncular_meta WHERE key = ?1")
1612 .map_err(schema_marker_read_failure)?;
1613 let mut rows = statement
1614 .query([LOCAL_SCHEMA_VERSION_KEY])
1615 .map_err(schema_marker_read_failure)?;
1616 let value: Option<String> = rows
1617 .next()
1618 .map_err(schema_marker_read_failure)?
1619 .map(|row| row.get(0))
1620 .transpose()
1621 .map_err(schema_marker_read_failure)?;
1622 if rows.next().map_err(schema_marker_read_failure)?.is_some() {
1623 return Err(corrupt());
1624 }
1625 let Some(value) = value else {
1626 let has_descriptor = conn
1627 .query_row(
1628 "SELECT 1 FROM _syncular_meta WHERE key = ?1 LIMIT 1",
1629 [crate::previous_version::LOCAL_SCHEMA_DESCRIPTOR_KEY],
1630 |_| Ok(()),
1631 )
1632 .optional()
1633 .map_err(schema_marker_read_failure)?
1634 .is_some();
1635 if has_descriptor {
1636 return Err(corrupt());
1637 }
1638 return Ok(None);
1639 };
1640 if !matches!(value.as_bytes().first(), Some(b'1'..=b'9'))
1641 || !value.bytes().all(|b| b.is_ascii_digit())
1642 {
1643 return Err(corrupt());
1644 }
1645 let version = value.parse::<i32>().map_err(|_| corrupt())?;
1646 if version > requested {
1647 return Err(
1648 "client.schema_downgrade: persisted local schema is newer than the requested schema"
1649 .to_owned(),
1650 );
1651 }
1652 Ok(Some(version))
1653}
1654
1655#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1659fn prepare_wasm_replica(path: &str) -> Result<(), String> {
1660 if path.is_empty() || path == ":memory:" {
1661 return Ok(());
1662 }
1663 crate::wasm_vfs::disable_foreign_lock_check()
1664}
1665
1666#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1667fn prepare_wasm_replica(_path: &str) -> Result<(), String> {
1668 Ok(())
1669}
1670
1671impl SyncClient {
1672 pub fn new_with_identity(
1673 client_id: Option<String>,
1674 schema_json: &Value,
1675 limits: ClientLimits,
1676 ) -> Result<Self, String> {
1677 let resolved = client_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
1678 let conn = Connection::open_in_memory().map_err(|e| e.to_string())?;
1679 Self::with_connection(resolved, schema_json, limits, conn)
1680 }
1681
1682 pub fn new(
1683 client_id: String,
1684 schema_json: &Value,
1685 limits: ClientLimits,
1686 ) -> Result<Self, String> {
1687 let conn = Connection::open_in_memory().map_err(|e| e.to_string())?;
1688 Self::with_connection(client_id, schema_json, limits, conn)
1689 }
1690
1691 pub fn open_path(
1697 client_id: String,
1698 schema_json: &Value,
1699 limits: ClientLimits,
1700 path: &str,
1701 ) -> Result<Self, String> {
1702 validate_client_limits(&limits)?;
1703 parse_schema_json(schema_json)?;
1704 prepare_wasm_replica(path)?;
1705 let conn = Connection::open(path).map_err(|e| format!("open db {path:?}: {e}"))?;
1706 Self::with_connection(client_id, schema_json, limits, conn)
1707 }
1708
1709 pub fn open_path_with_identity(
1713 client_id: Option<String>,
1714 schema_json: &Value,
1715 limits: ClientLimits,
1716 path: &str,
1717 ) -> Result<Self, String> {
1718 Self::open_path_with_identity_journal(
1719 client_id,
1720 schema_json,
1721 limits,
1722 path,
1723 !cfg!(all(target_arch = "wasm32", target_os = "unknown")),
1724 )
1725 }
1726
1727 fn open_path_with_identity_journal(
1728 client_id: Option<String>,
1729 schema_json: &Value,
1730 limits: ClientLimits,
1731 path: &str,
1732 require_wal: bool,
1733 ) -> Result<Self, String> {
1734 validate_client_limits(&limits)?;
1735 parse_schema_json(schema_json)?;
1738 prepare_wasm_replica(path)?;
1739 let conn = Connection::open(path).map_err(|e| format!("open db {path:?}: {e}"))?;
1740 conn.busy_timeout(std::time::Duration::from_millis(250))
1748 .map_err(|error| format!("configure db {path:?} busy timeout: {error}"))?;
1749 conn.pragma_update(None, "synchronous", "FULL")
1754 .map_err(|error| format!("configure db {path:?} synchronous: {error}"))?;
1755 let client = Self::with_connection_identity(client_id, schema_json, limits, conn)?;
1756 if require_wal {
1760 let journal_mode: String = client
1761 .conn
1762 .pragma_update_and_check(None, "journal_mode", "WAL", |row| row.get(0))
1763 .map_err(|error| Self::sqlite_failure(&client.storage_failure, error))?;
1764 if journal_mode != "wal" {
1765 return Err(
1766 "sync.invalid_request: file-backed client requires WAL journal mode".to_owned(),
1767 );
1768 }
1769 }
1770 Ok(client)
1771 }
1772
1773 pub fn reset_replica(path: &str) -> Result<(), String> {
1787 if path.is_empty() || path == ":memory:" {
1788 return Err("sync.invalid_request: a memory replica has nothing to reset".to_owned());
1789 }
1790 let vfs = unsafe { rusqlite::ffi::sqlite3_vfs_find(std::ptr::null()) };
1794 if vfs.is_null() {
1795 return Err("client.storage_io: no SQLite VFS is registered".to_owned());
1796 }
1797 let delete = unsafe { (*vfs).xDelete }
1798 .ok_or_else(|| "client.storage_io: the SQLite VFS cannot delete files".to_owned())?;
1799 for suffix in ["-wal", "-shm", "-journal", ""] {
1800 let file_name = format!("{path}{suffix}");
1801 let c_name = std::ffi::CString::new(file_name.as_str()).map_err(|_| {
1802 format!("sync.invalid_request: replica path {file_name:?} contains a NUL byte")
1803 })?;
1804 let code = unsafe { delete(vfs, c_name.as_ptr(), 1) };
1807 if code != rusqlite::ffi::SQLITE_OK && code != rusqlite::ffi::SQLITE_IOERR_DELETE_NOENT
1810 {
1811 return Err(format!(
1812 "client.storage_io: deleting replica file {file_name:?} failed with SQLite code {code}"
1813 ));
1814 }
1815 }
1816 Ok(())
1817 }
1818
1819 #[must_use]
1820 pub fn client_id(&self) -> &str {
1821 &self.client_id
1822 }
1823
1824 pub fn reset_client_id(&mut self, transport: &mut dyn Transport) -> Result<String, String> {
1831 self.disconnect_realtime(transport);
1832 let client_id = uuid::Uuid::new_v4().to_string();
1833 self.set_meta(CLIENT_ID_KEY, &client_id)?;
1834 self.client_id.clone_from(&client_id);
1835 self.round_failure = None;
1836 Ok(client_id)
1837 }
1838
1839 pub fn with_connection(
1846 client_id: String,
1847 schema_json: &Value,
1848 limits: ClientLimits,
1849 conn: Connection,
1850 ) -> Result<Self, String> {
1851 Self::with_connection_identity(Some(client_id), schema_json, limits, conn)
1852 }
1853
1854 fn with_connection_identity(
1857 client_id: Option<String>,
1858 schema_json: &Value,
1859 limits: ClientLimits,
1860 conn: Connection,
1861 ) -> Result<Self, String> {
1862 validate_client_limits(&limits)?;
1863 let schema = parse_schema_json(schema_json)?;
1864 validate_authority_reads(&schema, &limits.authority_reads)?;
1865 let previous_version = match limits.previous_version_context {
1868 Some(config) => {
1869 config.validate()?;
1870 Some(config)
1871 }
1872 None => None,
1873 };
1874 let mut client = SyncClient {
1875 #[cfg(feature = "bench-internals")]
1876 benchmark_phases: Recorder::default(),
1877 conn,
1878 schema,
1879 client_id: client_id
1880 .clone()
1881 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
1882 limits,
1883 outbox: Shared::default(),
1884 failed_commits: Shared::default(),
1885 retain_failed_commits: false,
1886 conflicts: Shared::default(),
1887 rejections: Shared::default(),
1888 schema_floor: None,
1889 lease_state: None,
1890 stopped: false,
1891 upgrading: false,
1892 sync_needed: false,
1893 realtime_policy: RealtimePolicy::Optional,
1894 realtime_state: RealtimeState::Disconnected,
1895 realtime_reason_code: None,
1896 realtime_retry_delay_ms: None,
1897 presence: HashMap::new(),
1898 now_ms: None,
1899 encryption: crate::values::EncryptionConfig::default(),
1900 security_preflight: false,
1901 security_epoch: 0,
1902 transport_enabled: true,
1903 transport_epoch: 0,
1904 registration_epoch: 0,
1905 previous_version,
1906 insert_sql: RefCell::new(HashMap::new()),
1907 overlay_dirty: OverlayDirty::clean(),
1908 storage_failure: RefCell::new(None),
1909 #[cfg(test)]
1910 overlay_rebuild_count: Cell::new(0),
1911 #[cfg(test)]
1912 acknowledged_replay_count: Cell::new(0),
1913 #[cfg(test)]
1914 outcome_prune_count: Cell::new(0),
1915 progress: ProgressObserver::default(),
1916 change_queue: VecDeque::new(),
1917 sync_intent_queue: VecDeque::new(),
1918 retry_delay_ms: 250,
1919 round_retry_delay_ms: None,
1920 round_failure: None,
1921 active_round: None,
1922 last_round: None,
1923 last_change: None,
1924 query_failures: Vec::new(),
1925 coalesced: None,
1926 coalesced_failure: None,
1927 };
1928 client
1932 .conn
1933 .set_prepared_statement_cache_capacity(64.max(client.schema.tables.len() * 4));
1934 client
1940 .conn
1941 .execute_batch("BEGIN IMMEDIATE")
1942 .map_err(|error| Self::sqlite_failure(&client.storage_failure, error))?;
1943 let startup = (|| -> Result<(), String> {
1944 let marker = read_local_schema_version(&client.conn, client.schema.version)?;
1947 client.create_bookkeeping_tables()?;
1950 match client.get_meta(CLIENT_ID_KEY)? {
1951 Some(existing) => match client_id {
1952 Some(requested) if existing != requested => {
1953 return Err(format!(
1954 "client.identity_mismatch: this database belongs to {existing:?}; refusing to rebind it to {requested:?}"
1955 ))
1956 }
1957 _ => client.client_id = existing,
1958 },
1959 None => client.set_meta(CLIENT_ID_KEY, &client.client_id)?,
1960 }
1961 client.restore_persisted_state()?;
1962 client.failed_commits = client.load_failed_commits()?.into();
1963 match marker {
1964 None => {
1965 client.create_synced_tables()?;
1966 client.persist_schema_version()?;
1967 client.save_unit_scope_schema()?;
1968 }
1969 Some(version) if version == client.schema.version => {
1970 client.create_synced_tables()?;
1971 set_local_schema_descriptor(&client.conn, &client.schema)
1975 .map_err(|error| Self::sqlite_failure(&client.storage_failure, error))?;
1976 client.save_unit_scope_schema()?;
1977 }
1978 Some(_) => client.run_schema_reset()?,
1979 }
1980 client.reconcile_previous_version_at_boot()?;
1986 client.clear_satisfied_persisted_schema_floor()?;
1987 client.prune_unknown_units(false)?;
1988 client.drop_unencodable_outbox(false, None)?;
1997 if marker == Some(client.schema.version)
1998 && (!client.outbox.is_empty()
1999 || !client.failed_commits.is_empty()
2000 || client.has_acknowledged_rows()?)
2001 {
2002 client.overlay_dirty.set(true);
2006 client.rebuild_overlay()?;
2007 }
2008 client.enqueue_startup_sync_if_needed()?;
2015 Ok(())
2016 })();
2017 match startup {
2018 Ok(()) => match client.conn.execute_batch("COMMIT") {
2019 Ok(()) => Ok(client),
2020 Err(error) => {
2021 let failure = Self::sqlite_failure(&client.storage_failure, error);
2022 if let Err(rollback) = client.conn.execute_batch("ROLLBACK") {
2023 let _ = Self::sqlite_failure(&client.storage_failure, rollback);
2025 }
2026 Err(failure)
2027 }
2028 },
2029 Err(error) => {
2030 if let Err(rollback) = client.conn.execute_batch("ROLLBACK") {
2031 let _ = Self::sqlite_failure(&client.storage_failure, rollback);
2033 }
2034 Err(error)
2035 }
2036 }
2037 }
2038
2039 pub fn set_now_ms(&mut self, now_ms: i64) {
2042 self.now_ms = Some(now_ms);
2043 }
2044
2045 pub fn set_encryption(&mut self, encryption: crate::values::EncryptionConfig) {
2049 self.encryption = encryption;
2050 }
2051
2052 #[must_use]
2053 pub fn security_lifecycle(&self) -> &'static str {
2054 if self.security_preflight {
2055 "preflight"
2056 } else {
2057 "active"
2058 }
2059 }
2060
2061 #[must_use]
2062 pub fn security_preflight(&self) -> bool {
2063 self.security_preflight
2064 }
2065
2066 pub fn set_transport_enabled(&mut self, transport: &mut dyn Transport, enabled: bool) {
2068 if self.transport_enabled != enabled {
2069 self.transport_enabled = enabled;
2070 self.transport_epoch = self.transport_epoch.wrapping_add(1);
2071 self.sync_intent_queue.clear();
2072 self.sync_intent_queue
2073 .push_back(if enabled && !self.security_preflight {
2074 SyncIntent::Interactive
2075 } else {
2076 SyncIntent::None
2077 });
2078 }
2079 if !enabled && self.active_round.is_none() {
2081 self.disconnect_realtime(transport);
2082 }
2083 }
2084
2085 pub fn transport_enabled(&self) -> bool {
2086 self.transport_enabled
2087 }
2088
2089 pub fn begin_security_preflight(&mut self) -> Result<(), String> {
2096 self.seal_security_on_teardown();
2097 self.set_meta(SECURITY_PREFLIGHT_PENDING_KEY, "1")
2100 }
2101
2102 pub fn seal_security_on_teardown(&mut self) {
2112 self.cancel_sync_round();
2113 self.security_preflight = true;
2114 self.security_epoch = self.security_epoch.wrapping_add(1);
2115 self.encryption = crate::values::EncryptionConfig::default();
2116 self.sync_intent_queue.clear();
2117 }
2118
2119 pub fn activate_security(
2121 &mut self,
2122 encryption: crate::values::EncryptionConfig,
2123 ) -> Result<(), String> {
2124 if !self.security_preflight {
2125 return Err(
2126 "sync.invalid_request: activateSecurity requires security preflight".to_owned(),
2127 );
2128 }
2129 self.enqueue_startup_sync_if_needed()?;
2130 self.delete_meta(SECURITY_PREFLIGHT_PENDING_KEY)?;
2131 self.encryption = encryption;
2132 self.security_preflight = false;
2133 Ok(())
2134 }
2135
2136 fn clock_now_ms(&self) -> i64 {
2137 self.now_ms.unwrap_or_else(crate::wall_clock_ms)
2138 }
2139
2140 fn create_bookkeeping_tables(&self) -> Result<(), String> {
2141 let exists = |name: &str| {
2142 self.conn
2143 .query_row(
2144 "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1)",
2145 [name],
2146 |row| row.get::<_, bool>(0),
2147 )
2148 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
2149 };
2150 let outbox_existed = exists("_syncular_outbox")?;
2151 self.conn
2153 .execute_batch(
2154 "CREATE TABLE IF NOT EXISTS _syncular_outbox (
2155 seq INTEGER PRIMARY KEY AUTOINCREMENT,
2156 commit_id TEXT NOT NULL UNIQUE, ops_json TEXT NOT NULL);
2157 CREATE TABLE IF NOT EXISTS _syncular_row_deliveries (
2158 tbl TEXT NOT NULL, id TEXT NOT NULL, commit_seq INTEGER NOT NULL, PRIMARY KEY(tbl,id));
2159 CREATE TABLE IF NOT EXISTS _syncular_acked_rows (
2160 commit_id TEXT NOT NULL, idx INTEGER NOT NULL, tbl TEXT NOT NULL, id TEXT NOT NULL,
2161 commit_seq INTEGER NOT NULL, op_json TEXT NOT NULL, intent_json TEXT,
2162 PRIMARY KEY(commit_id, idx));
2163 CREATE INDEX IF NOT EXISTS _syncular_acked_rows_row
2164 ON _syncular_acked_rows(tbl, id);
2165 CREATE TABLE IF NOT EXISTS _syncular_failed_commits (
2166 seq INTEGER PRIMARY KEY AUTOINCREMENT, client_commit_id TEXT NOT NULL UNIQUE,
2167 operations_json TEXT NOT NULL, initial_json TEXT NOT NULL);
2168 CREATE TABLE IF NOT EXISTS _syncular_commit_outcomes (
2169 seq INTEGER PRIMARY KEY AUTOINCREMENT,
2170 client_commit_id TEXT NOT NULL UNIQUE,
2171 status TEXT NOT NULL CHECK(status IN ('applied', 'cached', 'conflict', 'rejected')),
2172 recorded_at_ms INTEGER NOT NULL,
2173 results_json TEXT NOT NULL,
2174 operations_json TEXT,
2175 resolution TEXT NOT NULL DEFAULT 'active'
2176 CHECK(resolution IN ('active', 'resolved_keep_server', 'superseded', 'dismissed')),
2177 resolved_at_ms INTEGER,
2178 replacement_client_commit_id TEXT);
2179 CREATE INDEX IF NOT EXISTS _syncular_commit_outcomes_resolution_seq
2180 ON _syncular_commit_outcomes(resolution, seq);
2181 CREATE TABLE IF NOT EXISTS _syncular_units (
2182 id TEXT PRIMARY KEY, scopes TEXT NOT NULL,
2183 explicit INTEGER NOT NULL DEFAULT 0,
2184 status TEXT NOT NULL DEFAULT 'active', reason_code TEXT,
2185 cursor INTEGER DEFAULT -1, bootstrap_state TEXT,
2186 seeded TEXT NOT NULL DEFAULT '[]', echo TEXT);
2187 CREATE TABLE IF NOT EXISTS _syncular_meta (
2188 key TEXT PRIMARY KEY, value TEXT NOT NULL);
2189 CREATE TABLE IF NOT EXISTS _syncular_windows (
2190 base TEXT NOT NULL, unit TEXT NOT NULL, sub_id TEXT NOT NULL,
2191 PRIMARY KEY (base, unit));
2192 CREATE TABLE IF NOT EXISTS _syncular_pending_evictions (
2193 unit_id TEXT NOT NULL, tbl TEXT NOT NULL,
2194 effective_scopes TEXT NOT NULL, PRIMARY KEY (unit_id, tbl));
2195 CREATE TABLE IF NOT EXISTS _syncular_blob_commit_refs(
2196 commit_id TEXT NOT NULL, blob_id TEXT NOT NULL,
2197 PRIMARY KEY(commit_id, blob_id));
2198 CREATE INDEX IF NOT EXISTS _syncular_blob_commit_refs_body
2199 ON _syncular_blob_commit_refs(blob_id);",
2200 )
2201 .map_err(|e| e.to_string())?;
2202 let _ = self
2205 .conn
2206 .execute_batch("ALTER TABLE _syncular_commit_outcomes ADD COLUMN operations_json TEXT");
2207 if self.get_meta(LOCAL_REVISION_KEY)?.is_none() {
2208 self.set_meta(LOCAL_REVISION_KEY, "0")?;
2209 }
2210 if !outbox_existed || self.get_meta(OUTBOX_ID_KEY)?.is_none() {
2213 self.set_meta(OUTBOX_ID_KEY, &uuid::Uuid::new_v4().to_string())?;
2214 }
2215 if exists("_syncular_window_pending_evict")? {
2220 let invalid: bool = self
2221 .conn
2222 .query_row(
2223 "SELECT EXISTS(SELECT 1 FROM _syncular_window_pending_evict
2224 WHERE NOT json_valid(effective_scopes)
2225 OR json_type(effective_scopes) <> 'object'
2226 OR EXISTS(SELECT 1 FROM json_each(effective_scopes)
2227 WHERE type <> 'array'
2228 OR EXISTS(SELECT 1 FROM json_each(value) AS v
2229 WHERE v.type <> 'text')))",
2230 [],
2231 |row| row.get(0),
2232 )
2233 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
2234 if invalid {
2235 return Err(
2236 "sync.local_corrupt: a version 3 pending eviction cannot be upgraded"
2237 .to_owned(),
2238 );
2239 }
2240 self.conn
2241 .execute_batch(
2242 "INSERT OR REPLACE INTO _syncular_pending_evictions
2243 SELECT sub_id, tbl, effective_scopes FROM _syncular_window_pending_evict;
2244 DROP TABLE _syncular_window_pending_evict;
2245 DROP TABLE IF EXISTS _syncular_subscriptions;
2246 DELETE FROM _syncular_windows;",
2247 )
2248 .and_then(|()| reset_units(&self.conn))
2249 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
2250 }
2251 if self.schema_has_blobs() {
2254 if exists("_syncular_blobs")? {
2255 let mut statement = self
2256 .conn
2257 .prepare("PRAGMA table_info(_syncular_blobs)")
2258 .map_err(|e| e.to_string())?;
2259 let mut columns = statement
2260 .query_map([], |row| row.get::<_, String>(1))
2261 .map_err(|e| e.to_string())?
2262 .collect::<Result<Vec<_>, _>>()
2263 .map_err(|e| e.to_string())?;
2264 columns.sort();
2265 let mut expected = vec![
2266 "blob_id",
2267 "byte_length",
2268 "bytes",
2269 "created_at_ms",
2270 "media_type",
2271 ];
2272 expected.sort_unstable();
2273 if columns != expected {
2274 return Err(
2275 "sync.schema_mismatch: Local blob schema is incompatible".to_owned()
2276 );
2277 }
2278 } else {
2279 self.conn
2280 .execute_batch(
2281 "CREATE TABLE _syncular_blobs(
2282 blob_id TEXT PRIMARY KEY,
2283 bytes BLOB NOT NULL,
2284 byte_length INTEGER NOT NULL,
2285 media_type TEXT,
2286 created_at_ms INTEGER NOT NULL);",
2287 )
2288 .map_err(|e| e.to_string())?;
2289 }
2290 self.conn
2291 .execute_batch(
2292 "CREATE TABLE IF NOT EXISTS _syncular_blob_uploads(
2293 blob_id TEXT PRIMARY KEY,
2294 media_type TEXT,
2295 created_at_ms INTEGER NOT NULL);",
2296 )
2297 .map_err(|e| e.to_string())?;
2298 }
2299 Ok(())
2300 }
2301
2302 fn schema_has_blobs(&self) -> bool {
2304 self.schema
2305 .tables
2306 .iter()
2307 .any(|t| t.columns.iter().any(|c| c.ty == ColumnType::BlobRef))
2308 }
2309
2310 fn get_meta(&self, key: &str) -> Result<Option<String>, String> {
2313 meta_get(&self.conn, key)
2314 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
2315 }
2316
2317 fn persist_schema_version(&self) -> Result<(), String> {
2320 self.conn
2321 .execute(
2322 "INSERT OR REPLACE INTO _syncular_meta(key,value) VALUES (?1,?2)",
2323 rusqlite::params![LOCAL_SCHEMA_VERSION_KEY, self.schema.version.to_string()],
2324 )
2325 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
2326 set_local_schema_descriptor(&self.conn, &self.schema)
2327 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
2328 }
2329
2330 pub(crate) fn set_meta(&self, key: &str, value: &str) -> Result<(), String> {
2331 meta_set(&self.conn, key, value)
2332 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
2333 }
2334
2335 fn delete_meta(&self, key: &str) -> Result<(), String> {
2336 meta_delete(&self.conn, key)
2337 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
2338 }
2339
2340 fn load_outbox(&self) -> Result<Vec<Arc<OutboxCommit>>, String> {
2342 let mut stmt = self
2343 .conn
2344 .prepare("SELECT commit_id, ops_json FROM _syncular_outbox ORDER BY seq ASC")
2345 .map_err(|error| error.to_string())?;
2346 let rows = stmt
2347 .query_map([], |row| {
2348 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
2349 })
2350 .map_err(|error| error.to_string())?;
2351 let mut commits = Vec::new();
2352 for row in rows {
2353 let (client_commit_id, raw) = row.map_err(|error| error.to_string())?;
2354 let entries: Vec<Value> = serde_json::from_str(&raw).map_err(|error| {
2355 format!("invalid persisted outbox {client_commit_id:?}: {error}")
2356 })?;
2357 let mut ops = Vec::with_capacity(entries.len());
2358 for entry in entries {
2359 let op = entry.get("op").and_then(Value::as_str).unwrap_or("delete");
2360 ops.push(OutboxOp {
2361 upsert: op == "upsert",
2362 table: entry
2363 .get("table")
2364 .and_then(Value::as_str)
2365 .ok_or_else(|| "persisted outbox operation missing table".to_owned())?
2366 .to_owned(),
2367 row_id: entry
2368 .get("rowId")
2369 .and_then(Value::as_str)
2370 .ok_or_else(|| "persisted outbox operation missing rowId".to_owned())?
2371 .to_owned(),
2372 base_version: entry.get("baseVersion").and_then(Value::as_i64),
2373 values: entry.get("values").and_then(Value::as_object).cloned(),
2376 insert: entry.get("insert") == Some(&Value::Bool(true)),
2377 });
2378 }
2379 commits.push(Arc::new(OutboxCommit {
2380 client_commit_id,
2381 ops,
2382 }));
2383 }
2384 Ok(commits)
2385 }
2386
2387 fn restore_persisted_state(&mut self) -> Result<(), String> {
2388 self.outbox = self.load_outbox()?.into();
2389
2390 self.prune_commit_outcomes()?;
2391 let active = self.commit_outcomes(CommitOutcomeQuery {
2392 active_only: true,
2393 ..CommitOutcomeQuery::default()
2394 })?;
2395 self.conflicts = active
2396 .iter()
2397 .flat_map(|outcome| outcome.results.iter())
2398 .filter_map(|result| match result {
2399 CommitOperationOutcome::Conflict { conflict } => Some(conflict.clone()),
2400 _ => None,
2401 })
2402 .collect::<Vec<_>>()
2403 .into();
2404 self.rejections = active
2405 .iter()
2406 .flat_map(|outcome| outcome.results.iter())
2407 .filter_map(|result| match result {
2408 CommitOperationOutcome::Error { rejection } => Some(rejection.clone()),
2409 _ => None,
2410 })
2411 .collect::<Vec<_>>()
2412 .into();
2413
2414 self.lease_state = self
2415 .get_meta(LEASE_STATE_KEY)?
2416 .map(|raw| serde_json::from_str(&raw))
2417 .transpose()
2418 .map_err(|error| format!("invalid persisted lease state: {error}"))?;
2419 self.schema_floor = self
2420 .get_meta(SCHEMA_FLOOR_KEY)?
2421 .map(|raw| serde_json::from_str(&raw))
2422 .transpose()
2423 .map_err(|error| format!("invalid persisted schema floor: {error}"))?;
2424 self.stopped = self.schema_floor.is_some();
2425 if self.get_meta(SECURITY_PREFLIGHT_PENDING_KEY)?.as_deref() == Some("1") {
2428 self.security_preflight = true;
2429 }
2430 Ok(())
2431 }
2432
2433 fn save_unit_scope_schema(&self) -> Result<(), String> {
2434 let declarations: BTreeMap<_, BTreeMap<_, _>> = self
2435 .schema
2436 .tables
2437 .iter()
2438 .map(|table| {
2439 (
2440 table.name.clone(),
2441 table
2442 .scope_variables
2443 .iter()
2444 .map(|scope| {
2445 (
2446 scope.variable.clone(),
2447 [scope.prefix.clone(), scope.column.clone()],
2448 )
2449 })
2450 .collect(),
2451 )
2452 })
2453 .collect();
2454 self.set_meta(
2455 "subscriptionScopeSchema",
2456 &serde_json::to_string(&declarations).expect("scope schema serialization"),
2457 )
2458 }
2459
2460 fn prune_unknown_units(&mut self, bump: bool) -> Result<(), String> {
2465 let previous: Option<BTreeMap<String, BTreeMap<String, [String; 2]>>> = if bump {
2466 self.get_meta("subscriptionScopeSchema")?
2467 .map(|raw| serde_json::from_str(&raw))
2468 .transpose()
2469 .map_err(|_| "sync.local_corrupt: invalid persisted scope schema".to_owned())?
2470 } else {
2471 None
2472 };
2473 let unchanged = |name: &str, variables: &[&String]| {
2478 let (Some(old), Some(table)) = (
2479 previous.as_ref().and_then(|previous| previous.get(name)),
2480 self.schema.table(name),
2481 ) else {
2482 return false;
2483 };
2484 variables.iter().all(|variable| {
2485 table
2486 .scope_variables
2487 .iter()
2488 .find(|scope| &scope.variable == *variable)
2489 .is_some_and(|scope| {
2490 old.get(*variable) == Some(&[scope.prefix.clone(), scope.column.clone()])
2491 })
2492 })
2493 };
2494 let compatible = |scopes: &UnitScopes| {
2495 if !self.schema.tables.iter().any(|table| covers(table, scopes)) {
2496 return false;
2497 }
2498 if !bump {
2499 return true;
2500 }
2501 let Some(previous) = &previous else {
2502 return false;
2503 };
2504 previous.iter().all(|(name, old)| {
2505 !scopes.keys().all(|variable| old.contains_key(variable))
2506 || unchanged(name, &scopes.keys().collect::<Vec<_>>())
2507 })
2508 };
2509 let mut stale: BTreeSet<String> = self
2510 .stored(load_units(&self.conn))?
2511 .into_iter()
2512 .filter(|unit| !compatible(&unit.scopes))
2513 .map(|unit| unit.id)
2514 .collect();
2515 for (id, table, effective) in self.load_pending_evictions()? {
2520 if self.schema.table(&table).is_none()
2521 || (bump
2522 && !unchanged(
2523 &table,
2524 &effective
2525 .iter()
2526 .map(|(variable, _)| variable)
2527 .collect::<Vec<_>>(),
2528 ))
2529 {
2530 stale.insert(id);
2531 }
2532 }
2533 for id in &stale {
2534 for sql in [
2535 "DELETE FROM _syncular_windows WHERE sub_id = ?1",
2536 "DELETE FROM _syncular_pending_evictions WHERE unit_id = ?1",
2537 "DELETE FROM _syncular_units WHERE id = ?1",
2538 ] {
2539 self.conn
2540 .execute(sql, rusqlite::params![id])
2541 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
2542 }
2543 }
2544 Ok(())
2545 }
2546
2547 pub fn local_revision(&self) -> Result<u64, String> {
2548 Ok(self
2549 .get_meta(LOCAL_REVISION_KEY)?
2550 .and_then(|value| value.parse().ok())
2551 .unwrap_or(0))
2552 }
2553
2554 pub fn snapshot_read(
2560 &mut self,
2561 request: &SnapshotReadRequest<'_>,
2562 owner: Option<QueryOwner<'_>>,
2563 ) -> Result<SnapshotRead, QueryReadFailure> {
2564 let result = snapshot_read_connection(&self.conn, request);
2565 if let Some(owner) = owner {
2566 self.record_query_read(owner, result.as_ref().err());
2567 }
2568 result
2569 }
2570
2571 #[must_use]
2572 pub fn status_snapshot(&self) -> SyncStatusSnapshot {
2573 SyncStatusSnapshot {
2574 current_schema_version: self.schema.version,
2575 outbox: self.outbox.len(),
2576 upgrading: self.upgrading,
2577 lease_state: self.lease_state.clone(),
2578 schema_floor: self.schema_floor.clone(),
2579 sync_needed: self.sync_needed,
2580 previous_version_context: self.previous_version_status(),
2581 }
2582 }
2583
2584 fn previous_version_status(&self) -> PreviousVersionStatus {
2587 let Some(replica_path) = self.previous_version_replica_path() else {
2588 return PreviousVersionStatus {
2589 present: false,
2590 created_at_ms: None,
2591 };
2592 };
2593 match crate::previous_version::read_previous_version_record(&replica_path) {
2594 Ok(Some(record)) => PreviousVersionStatus {
2595 present: true,
2596 created_at_ms: Some(record.created_at_ms),
2597 },
2598 _ => PreviousVersionStatus {
2599 present: std::path::Path::new(
2600 &crate::previous_version::previous_version_container_path(&replica_path),
2601 )
2602 .exists(),
2603 created_at_ms: None,
2604 },
2605 }
2606 }
2607
2608 pub fn previous_version_snapshot(
2614 &self,
2615 spec: &PreviousVersionReadSpec,
2616 ) -> Result<PreviousVersionSnapshot, String> {
2617 let now_ms = self.clock_now_ms();
2618 crate::previous_version::previous_version_snapshot(
2619 &self.conn,
2620 self.previous_version_replica_path().as_deref(),
2621 self.previous_version.as_ref(),
2622 PreviousVersionLifecycle {
2623 lease_inactive: self.previous_version_lease_inactive(now_ms),
2624 scope_revoked: self
2625 .stored(load_units(&self.conn))?
2626 .iter()
2627 .any(|unit| unit.status == UnitStatus::Revoked),
2628 coverage_complete: self.previous_version_coverage_complete()?,
2629 },
2630 now_ms,
2631 self.schema.version,
2632 spec,
2633 )
2634 }
2635
2636 pub fn previous_version_audit(
2638 &self,
2639 ) -> Result<Option<crate::previous_version::PreviousVersionAudit>, String> {
2640 crate::previous_version::stored_previous_version_audit(&self.conn)
2641 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
2642 }
2643
2644 pub fn previous_version_discard(
2648 &self,
2649 ) -> Result<crate::previous_version::PreviousVersionDiscardOutcome, String> {
2650 self.drop_previous_version()
2651 }
2652
2653 fn drop_previous_version(
2654 &self,
2655 ) -> Result<crate::previous_version::PreviousVersionDiscardOutcome, String> {
2656 let Some(replica_path) = self.previous_version_replica_path() else {
2657 return Ok(crate::previous_version::PreviousVersionDiscardOutcome {
2658 present: false,
2659 discarded: false,
2660 });
2661 };
2662 crate::previous_version::discard_previous_version(&self.conn, &replica_path)
2663 }
2664
2665 fn previous_version_lease_inactive(&self, now_ms: i64) -> bool {
2667 let Some(lease) = &self.lease_state else {
2668 return false;
2669 };
2670 if lease.error_code.is_some() {
2671 return true;
2672 }
2673 lease.expires_at_ms.is_some_and(|expires| expires <= now_ms)
2674 }
2675
2676 fn previous_version_coverage_complete(&self) -> Result<bool, String> {
2680 let units = self.stored(load_units(&self.conn))?;
2681 let mut active = units
2682 .iter()
2683 .filter(|unit| unit.status == UnitStatus::Active)
2684 .peekable();
2685 Ok(active.peek().is_some() && !active.any(UnitRecord::bootstrapping))
2686 }
2687
2688 fn previous_version_lifetime_check(&self) {
2692 let Some(config) = self.previous_version.filter(|config| config.enabled) else {
2693 return;
2694 };
2695 let now_ms = self.clock_now_ms();
2696 if self.previous_version_lease_inactive(now_ms) {
2697 let _ = self.drop_previous_version();
2698 return;
2699 }
2700 let Some(replica_path) = self.previous_version_replica_path() else {
2701 return;
2702 };
2703 let Ok(Some(record)) = crate::previous_version::read_previous_version_record(&replica_path)
2704 else {
2705 return;
2706 };
2707 let Ok(units) = load_units(&self.conn) else {
2708 return;
2709 };
2710 if units.iter().any(|unit| unit.status == UnitStatus::Revoked) {
2711 let _ = self.drop_previous_version();
2712 return;
2713 }
2714 if now_ms - record.created_at_ms > config.max_age_ms {
2715 let _ = self.drop_previous_version();
2716 return;
2717 }
2718 if self.previous_version_coverage_complete() == Ok(true) {
2719 let _ = self.drop_previous_version();
2720 }
2721 }
2722
2723 pub fn diagnostics_snapshot(&self) -> Result<ClientDiagnosticsSnapshot, String> {
2724 let registered = self.stored(load_units(&self.conn))?;
2725 let (local_revision, replica_cursor): (Option<String>, Option<String>) = self.stored(
2728 self.conn
2729 .prepare_cached(
2730 "SELECT (SELECT value FROM _syncular_meta WHERE key = ?1),
2731 (SELECT value FROM _syncular_meta WHERE key = ?2)",
2732 )
2733 .and_then(|mut statement| {
2734 statement.query_row([LOCAL_REVISION_KEY, REPLICA_CURSOR_KEY], |row| {
2735 Ok((row.get(0)?, row.get(1)?))
2736 })
2737 }),
2738 )?;
2739 let mut units = DiagnosticUnits::default();
2740 for unit in ®istered {
2741 *match unit.status {
2742 UnitStatus::Revoked => &mut units.revoked,
2743 UnitStatus::Failed => &mut units.failed,
2744 UnitStatus::Active if !unit.bootstrapping() => &mut units.complete,
2745 UnitStatus::Active
2746 if unit.reason_code.as_deref() == Some("sync.cursor_expired") =>
2747 {
2748 &mut units.reset
2749 }
2750 UnitStatus::Active => &mut units.bootstrapping,
2751 } += 1;
2752 }
2753 units.reason_codes = registered
2754 .iter()
2755 .filter_map(|unit| unit.reason_code.as_deref().map(Self::diagnostic_code))
2756 .collect::<BTreeSet<_>>()
2757 .into_iter()
2758 .collect();
2759 let captured_at_ms = self.clock_now_ms();
2760 let lease = if let Some(error_code) = self
2761 .lease_state
2762 .as_ref()
2763 .and_then(|state| state.error_code.clone())
2764 {
2765 ClientDiagnosticsLease {
2766 state: "stopped".to_owned(),
2767 expires_at_ms: self
2768 .lease_state
2769 .as_ref()
2770 .and_then(|state| state.expires_at_ms),
2771 error_code: Some(Self::diagnostic_code(&error_code)),
2772 }
2773 } else if let Some(expires_at_ms) = self
2774 .lease_state
2775 .as_ref()
2776 .and_then(|state| state.expires_at_ms)
2777 {
2778 ClientDiagnosticsLease {
2779 state: if expires_at_ms <= captured_at_ms {
2780 "expired".to_owned()
2781 } else {
2782 "active".to_owned()
2783 },
2784 expires_at_ms: Some(expires_at_ms),
2785 error_code: None,
2786 }
2787 } else {
2788 ClientDiagnosticsLease {
2789 state: "none".to_owned(),
2790 expires_at_ms: None,
2791 error_code: None,
2792 }
2793 };
2794 let connectivity = match self.last_round.as_ref() {
2795 _ if !self.transport_enabled && self.active_round.is_none() => "offline",
2796 Some(round) if round.status == "succeeded" => "online",
2797 Some(round)
2798 if round.status == "failed"
2799 && matches!(
2800 round.cause_kind,
2801 Some(
2802 crate::TransportCauseKind::Network | crate::TransportCauseKind::Connect
2803 )
2804 ) =>
2805 {
2806 "offline"
2807 }
2808 Some(round) if round.status == "failed" && round.http_status.is_some() => "online",
2809 _ => "unknown",
2810 };
2811 Ok(ClientDiagnosticsSnapshot {
2812 version: CLIENT_DIAGNOSTICS_VERSION,
2813 captured_at_ms,
2814 host: ClientDiagnosticsHost {
2815 kind: "direct".to_owned(),
2816 role: "single".to_owned(),
2817 connectivity: connectivity.to_owned(),
2818 realtime: self.realtime_state,
2819 realtime_policy: self.realtime_policy,
2820 realtime_reason_code: self.realtime_reason_code.clone(),
2821 realtime_retry_delay_ms: self.realtime_retry_delay_ms,
2822 },
2823 security_lifecycle: self.security_lifecycle().to_owned(),
2824 schema: ClientDiagnosticsSchema {
2825 current_version: self.schema.version,
2826 upgrading: self.upgrading,
2827 required_version: self
2828 .schema_floor
2829 .as_ref()
2830 .and_then(|floor| floor.required_schema_version),
2831 latest_version: self
2832 .schema_floor
2833 .as_ref()
2834 .and_then(|floor| floor.latest_schema_version),
2835 },
2836 replica: ClientDiagnosticsReplica {
2837 local_revision: local_revision
2838 .and_then(|value| value.parse::<u64>().ok())
2839 .unwrap_or(0)
2840 .to_string(),
2841 sync_needed: self.sync_needed,
2842 pending_outbox: self.outbox.len(),
2843 },
2844 lease,
2845 active_units: registered
2846 .iter()
2847 .filter(|unit| unit.status == UnitStatus::Active)
2848 .count(),
2849 units,
2850 replica_cursor: replica_cursor.map_or(Ok(-1), |raw| {
2851 raw.parse()
2852 .map_err(|_| "sync.local_corrupt: persisted unit state is invalid".to_owned())
2853 })?,
2854 last_round: self.last_round.clone(),
2855 last_change: self.last_change.clone(),
2856 storage: self.diagnostics_storage(),
2857 query_failures: self.query_failures.clone(),
2858 })
2859 }
2860
2861 fn diagnostics_storage(&self) -> ClientDiagnosticsStorage {
2862 let read = || -> Result<ClientDiagnosticsStorage, rusqlite::Error> {
2863 let page_count: i64 = self
2864 .conn
2865 .query_row("PRAGMA page_count", [], |row| row.get(0))?;
2866 let page_size: i64 = self
2867 .conn
2868 .query_row("PRAGMA page_size", [], |row| row.get(0))?;
2869 let (outbox_bytes, outcome_entries, outcome_bytes): (i64, i64, i64) = self
2872 .conn
2873 .prepare_cached(
2874 "SELECT (SELECT COALESCE(SUM(LENGTH(ops_json)), 0) FROM _syncular_outbox),
2875 COUNT(*), COALESCE(SUM(LENGTH(results_json) + COALESCE(LENGTH(operations_json), 0)), 0)
2876 FROM _syncular_commit_outcomes",
2877 )?
2878 .query_row([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?;
2879 let blob_bytes = if self.schema_has_blobs() {
2880 self.conn
2881 .prepare_cached("SELECT COALESCE(SUM(byte_length), 0) FROM _syncular_blobs")?
2882 .query_row([], |row| row.get(0))?
2883 } else {
2884 0
2885 };
2886 let pressure = self
2887 .limits
2888 .blob_cache_max_bytes
2889 .is_some_and(|limit| blob_bytes > limit);
2890 Ok(ClientDiagnosticsStorage {
2891 status: if pressure { "pressure" } else { "healthy" }.to_owned(),
2892 database_bytes_approx: Some(page_count.saturating_mul(page_size).max(0)),
2893 pending_outbox_bytes_approx: Some(outbox_bytes.max(0)),
2894 retained_outcome_bytes_approx: Some(outcome_bytes.max(0)),
2895 retained_outcome_entries: Some(outcome_entries.max(0)),
2896 blob_cache_bytes_approx: Some(blob_bytes.max(0)),
2897 pressure_reason_code: pressure.then(|| "client.blob_cache_over_limit".to_owned()),
2898 })
2899 };
2900 read().unwrap_or_else(|_| ClientDiagnosticsStorage {
2901 status: "unreadable".to_owned(),
2902 database_bytes_approx: None,
2903 pending_outbox_bytes_approx: None,
2904 retained_outcome_bytes_approx: None,
2905 retained_outcome_entries: None,
2906 blob_cache_bytes_approx: None,
2907 pressure_reason_code: None,
2908 })
2909 }
2910
2911 pub fn drain_change_batches(&mut self) -> Vec<ClientChangeBatch> {
2912 self.change_queue.drain(..).collect()
2913 }
2914
2915 pub fn drain_sync_intents(&mut self) -> Vec<SyncIntent> {
2916 let intents = self.sync_intent_queue.drain(..).collect();
2917 if self.transport_enabled {
2918 intents
2919 } else {
2920 vec![SyncIntent::None]
2921 }
2922 }
2923
2924 fn schedule_background_retry(&mut self) -> u64 {
2925 let delay_ms = self.retry_delay_ms;
2926 self.sync_intent_queue
2927 .push_back(SyncIntent::Background { delay_ms });
2928 self.retry_delay_ms = (self.retry_delay_ms * 2).min(30_000);
2929 self.round_retry_delay_ms = Some(delay_ms);
2930 delay_ms
2931 }
2932
2933 fn reset_background_retry(&mut self) {
2934 self.retry_delay_ms = 250;
2935 }
2936
2937 fn retryable_transport_code(code: &str) -> bool {
2938 code == "transport.failed"
2939 || code == "transport.timeout"
2940 || code == "transport.unavailable"
2941 || code == "sync.transport_failed"
2942 }
2943
2944 fn unreachable_transport(details: &Option<serde_json::Value>) -> bool {
2949 matches!(
2950 details
2951 .as_ref()
2952 .and_then(|details| details.get("causeKind"))
2953 .and_then(serde_json::Value::as_str),
2954 Some("connect" | "network")
2955 )
2956 }
2957
2958 fn segment_fetch_failed(&mut self, error: TransportError) -> SectionError {
2963 if ssp2::model::valid_round_failure_code(&error.code) {
2964 self.round_failure = Some(error.code.clone());
2965 }
2966 SectionError::Transport(error)
2967 }
2968
2969 fn retryable_failure_code(code: &str) -> bool {
2974 Self::retryable_transport_code(code)
2975 || matches!(
2976 code,
2977 "sync.auth_required"
2978 | "sync.auth_lease_required"
2979 | "sync.auth_lease_revoked"
2980 | "sync.internal_error"
2981 | "sync.idempotency_cache_miss"
2982 | "sync.schema_not_ready"
2983 | "sync.segment_expired"
2984 | "sync.rate_limited"
2985 | "sync.websocket_connection_limit"
2986 )
2987 }
2988
2989 fn diagnostic_code(code: &str) -> String {
2990 let valid = !code.is_empty()
2991 && code.len() <= 96
2992 && code.contains('.')
2993 && code
2994 .bytes()
2995 .next()
2996 .is_some_and(|byte| byte.is_ascii_lowercase())
2997 && code.bytes().all(|byte| {
2998 byte.is_ascii_lowercase()
2999 || byte.is_ascii_digit()
3000 || matches!(byte, b'.' | b'_' | b'-')
3001 });
3002 if valid {
3003 code.to_owned()
3004 } else {
3005 "client.unknown_failure".to_owned()
3006 }
3007 }
3008
3009 fn set_sync_needed(&mut self, value: bool, interactive: bool) {
3010 if self.sync_needed != value {
3011 if self.begin_observation("syncular_status").is_ok() {
3012 self.sync_needed = value;
3013 let batch = ChangeAccumulator {
3014 status: true,
3015 ..ChangeAccumulator::default()
3016 };
3017 if self.finish_observation("syncular_status", batch).is_err() {
3018 self.rollback_observation("syncular_status");
3019 }
3020 } else {
3021 self.sync_needed = value;
3022 }
3023 }
3024 if value && interactive {
3025 self.sync_intent_queue.push_back(SyncIntent::Interactive);
3026 }
3027 }
3028
3029 fn stored<T>(&self, result: rusqlite::Result<T>) -> Result<T, String> {
3032 result.map_err(|error| match error {
3033 rusqlite::Error::FromSqlConversionFailure(..) => {
3034 "sync.local_corrupt: persisted unit state is invalid".to_owned()
3035 }
3036 error => Self::sqlite_failure(&self.storage_failure, error),
3037 })
3038 }
3039
3040 fn sqlite_failure(
3041 storage_failure: &RefCell<Option<QueryReadFailure>>,
3042 error: rusqlite::Error,
3043 ) -> String {
3044 let message = error.to_string();
3045 let failure = QueryReadFailure::from(error);
3046 let code = failure.code;
3047 if code.is_some() && storage_failure.borrow().is_none() {
3048 *storage_failure.borrow_mut() = Some(failure);
3049 }
3050 match code {
3051 Some(code) => format!("{code}: {message}"),
3052 None => message,
3053 }
3054 }
3055
3056 fn begin_observation(&self, name: &str) -> Result<(), String> {
3057 if let Some(failure) = &self.coalesced_failure {
3058 return Err(failure.clone());
3059 }
3060 if self.coalesced.is_some() && self.conn.is_autocommit() {
3063 return Err("the coalesced apply transaction was rolled back".to_owned());
3064 }
3065 self.conn
3066 .execute_batch(&format!("SAVEPOINT {name}"))
3067 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
3068 }
3069
3070 fn begin_coalesced_apply(&mut self) -> Result<(), String> {
3073 if self.coalesced.is_some() || !self.conn.is_autocommit() {
3074 return Ok(());
3075 }
3076 self.conn
3077 .execute_batch("BEGIN")
3078 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3079 self.coalesced = Some(CoalescedApply {
3080 rows: 0,
3081 rollback: self.capture(),
3082 });
3083 Ok(())
3084 }
3085
3086 fn capture(&self) -> Rollback {
3087 Rollback {
3088 outbox: self.outbox.clone(),
3089 failed_commits: self.failed_commits.clone(),
3090 conflicts: self.conflicts.clone(),
3091 rejections: self.rejections.clone(),
3092 schema_floor: self.schema_floor.clone(),
3093 lease_state: self.lease_state.clone(),
3094 stopped: self.stopped,
3095 upgrading: self.upgrading,
3096 sync_needed: self.sync_needed,
3097 active_round: self.active_round,
3098 overlay_dirty: self.overlay_dirty.snapshot(),
3099 change_queue: self.change_queue.len(),
3100 sync_intent_queue: self.sync_intent_queue.len(),
3101 last_change: self.last_change.clone(),
3102 }
3103 }
3104
3105 fn restore(&mut self, rollback: Rollback) {
3106 self.outbox = rollback.outbox;
3107 self.failed_commits = rollback.failed_commits;
3108 self.conflicts = rollback.conflicts;
3109 self.rejections = rollback.rejections;
3110 self.schema_floor = rollback.schema_floor;
3111 self.lease_state = rollback.lease_state;
3112 self.stopped = rollback.stopped;
3113 self.upgrading = rollback.upgrading;
3114 self.sync_needed = rollback.sync_needed;
3115 self.active_round = rollback.active_round;
3116 self.overlay_dirty.restore(rollback.overlay_dirty);
3117 self.change_queue.truncate(rollback.change_queue);
3118 self.sync_intent_queue.truncate(rollback.sync_intent_queue);
3119 self.last_change = rollback.last_change;
3120 }
3121
3122 fn coalesce_unit(&mut self, rows: usize) -> Result<(), String> {
3125 let Some(open) = &self.coalesced else {
3126 return Ok(());
3127 };
3128 if open.rows > 0 && open.rows + rows > COALESCED_APPLY_ROWS {
3129 self.commit_coalesced_apply()?;
3130 self.begin_coalesced_apply()?;
3131 }
3132 if let Some(open) = &mut self.coalesced {
3133 open.rows += rows;
3134 }
3135 Ok(())
3136 }
3137
3138 fn commit_coalesced_apply(&mut self) -> Result<(), String> {
3143 let Some(open) = self.coalesced.take() else {
3144 return Ok(());
3145 };
3146 let committed = if self.conn.is_autocommit() {
3147 Err("the coalesced apply transaction was rolled back".to_owned())
3148 } else {
3149 self.conn
3150 .execute_batch("COMMIT")
3151 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
3152 };
3153 let Err(error) = committed else {
3154 return Ok(());
3155 };
3156 if !self.conn.is_autocommit() {
3157 if let Err(rollback) = self.conn.execute_batch("ROLLBACK") {
3158 let _ = Self::sqlite_failure(&self.storage_failure, rollback);
3159 }
3160 }
3161 self.restore(open.rollback);
3162 self.coalesced_failure = Some(error.clone());
3163 Err(error)
3164 }
3165
3166 fn finish_coalesced_apply(&mut self) -> Result<(), String> {
3168 let committed = self.commit_coalesced_apply();
3169 match self.coalesced_failure.take() {
3170 Some(failure) => Err(failure),
3171 None => committed,
3172 }
3173 }
3174
3175 fn rollback_observation(&self, name: &str) {
3176 if let Err(error) = self
3177 .conn
3178 .execute_batch(&format!("ROLLBACK TO {name}; RELEASE {name}"))
3179 {
3180 if let Some(failure) = self.storage_failure.borrow_mut().as_mut() {
3181 if failure.rollback_failure.is_none() {
3182 failure.rollback_failure = Some(Box::new(QueryReadFailure::from(error)));
3183 }
3184 }
3185 }
3186 }
3187
3188 fn finish_observation(
3189 &mut self,
3190 name: &str,
3191 mut batch: ChangeAccumulator,
3192 ) -> Result<(), String> {
3193 #[cfg(feature = "bench-internals")]
3194 let _phase = self.benchmark_phases.start(Phase::ObservationCommit);
3195 if self.failed_commits.iter().any(|(commit, _)| {
3196 commit
3197 .ops
3198 .iter()
3199 .any(|operation| batch.tables.contains_key(&operation.table))
3200 }) {
3201 batch.outcomes = true;
3202 }
3203 if !batch.touched() {
3204 self.conn
3205 .execute_batch(&format!("RELEASE {name}"))
3206 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3207 return Ok(());
3208 }
3209 let revision = self
3210 .local_revision()?
3211 .checked_add(1)
3212 .ok_or_else(|| "local revision exhausted u64".to_owned())?;
3213 self.conn
3214 .execute(
3215 "INSERT OR REPLACE INTO _syncular_meta(key, value) VALUES (?1, ?2)",
3216 rusqlite::params![LOCAL_REVISION_KEY, revision.to_string()],
3217 )
3218 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3219 let status = batch.status.then(|| self.status_snapshot());
3220 let event = ClientChangeBatch {
3221 revision: revision.to_string(),
3222 tables: batch
3223 .tables
3224 .into_iter()
3225 .map(|(table, scope_keys)| TableChange {
3226 table,
3227 scope_keys: scope_keys.map(|keys| keys.into_iter().collect()),
3228 })
3229 .collect(),
3230 windows: batch
3231 .windows
3232 .into_iter()
3233 .map(|(base_key, units)| WindowChange {
3234 base_key,
3235 units: units.into_iter().collect(),
3236 })
3237 .collect(),
3238 status,
3239 conflicts_changed: batch.conflicts,
3240 rejections_changed: batch.rejections,
3241 outcomes_changed: batch.outcomes,
3242 applied_commits: Vec::new(),
3243 };
3244 self.conn
3245 .execute_batch(&format!("RELEASE {name}"))
3246 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3247 let mut diagnostic_tables = event
3248 .tables
3249 .iter()
3250 .map(|entry| entry.table.clone())
3251 .collect::<BTreeSet<_>>()
3252 .into_iter()
3253 .collect::<Vec<_>>();
3254 let mut diagnostic_windows = event
3256 .windows
3257 .iter()
3258 .map(|entry| {
3259 entry
3260 .base_key
3261 .split('\0')
3262 .next()
3263 .unwrap_or_default()
3264 .to_owned()
3265 })
3266 .collect::<BTreeSet<_>>()
3267 .into_iter()
3268 .collect::<Vec<_>>();
3269 let domains_truncated = diagnostic_tables.len() > MAX_DIAGNOSTIC_DOMAINS
3270 || diagnostic_windows.len() > MAX_DIAGNOSTIC_DOMAINS;
3271 diagnostic_tables.truncate(MAX_DIAGNOSTIC_DOMAINS);
3272 diagnostic_windows.truncate(MAX_DIAGNOSTIC_DOMAINS);
3273 self.last_change = Some(DiagnosticLastChange {
3274 revision: event.revision.clone(),
3275 recorded_at_ms: self.clock_now_ms(),
3276 tables: diagnostic_tables,
3277 windows: diagnostic_windows,
3278 domains_truncated,
3279 status_changed: event.status.is_some(),
3280 conflicts_changed: event.conflicts_changed,
3281 rejections_changed: event.rejections_changed,
3282 outcomes_changed: event.outcomes_changed,
3283 });
3284 self.change_queue.push_back(event);
3285 Ok(())
3286 }
3287
3288 fn record_scope_map(
3289 &self,
3290 batch: &mut ChangeAccumulator,
3291 table_name: &str,
3292 scopes: &[(String, Vec<String>)],
3293 ) {
3294 let Some(table) = self.schema.table(table_name) else {
3295 return;
3296 };
3297 if table.scope_variables.iter().any(|scope| {
3300 !scopes
3301 .iter()
3302 .any(|(variable, _)| *variable == scope.variable)
3303 }) {
3304 batch.table(table_name);
3305 return;
3306 }
3307 for (variable, values) in scopes {
3308 let Some(scope) = table
3309 .scope_variables
3310 .iter()
3311 .find(|scope| &scope.variable == variable)
3312 else {
3313 continue;
3314 };
3315 for value in values {
3316 batch.scope(table_name, format!("{}:{value}", scope.prefix));
3317 }
3318 }
3319 }
3320
3321 fn record_row_scopes(
3323 &self,
3324 batch: &mut ChangeAccumulator,
3325 table_name: &str,
3326 row_id: &str,
3327 base: bool,
3328 ) -> bool {
3329 let Some(table) = self.schema.table(table_name) else {
3330 return false;
3331 };
3332 if table.scope_variables.is_empty() {
3333 return false;
3334 }
3335 let columns = table
3336 .scope_variables
3337 .iter()
3338 .map(|scope| quote_ident(&scope.column))
3339 .collect::<Vec<_>>()
3340 .join(", ");
3341 let full_table = if base {
3342 base_table(table_name)
3343 } else {
3344 visible_table(table_name)
3345 };
3346 let sql = format!(
3347 "SELECT {columns} FROM {full_table} WHERE {} LIMIT 1",
3348 row_id_predicate(table)
3349 );
3350 let Ok(mut stmt) = self.conn.prepare_cached(&sql) else {
3351 return false;
3352 };
3353 let values = stmt.query_row(rusqlite::params![row_id], |row| {
3354 let mut values = Vec::with_capacity(table.scope_variables.len());
3355 for index in 0..table.scope_variables.len() {
3356 values.push(row.get::<_, Option<String>>(index)?);
3357 }
3358 Ok(values)
3359 });
3360 let Ok(values) = values else {
3361 return false;
3362 };
3363 let mut recorded = false;
3364 for (scope, value) in table.scope_variables.iter().zip(values) {
3365 if let Some(value) = value {
3366 batch.scope(table_name, format!("{}:{value}", scope.prefix));
3367 recorded = true;
3368 }
3369 }
3370 recorded
3371 }
3372
3373 fn record_commit_changes(
3374 &self,
3375 batch: &mut ChangeAccumulator,
3376 tables: &[String],
3377 changes: &[ssp2::model::Change],
3378 ) {
3379 #[cfg(feature = "bench-internals")]
3380 let _phase = self.benchmark_phases.start(Phase::ObservationPrepare);
3381 for change in changes {
3382 let Some(table_name) = tables.get(change.table_index as usize) else {
3383 continue;
3384 };
3385 let mut precise = self.record_row_scopes(batch, table_name, &change.row_id, true);
3386 if let Some(table) = self.schema.table(table_name) {
3387 for (variable, value) in &change.scopes {
3388 if let Some(scope) = table
3389 .scope_variables
3390 .iter()
3391 .find(|scope| &scope.variable == variable)
3392 {
3393 batch.scope(table_name, format!("{}:{value}", scope.prefix));
3394 precise = true;
3395 }
3396 }
3397 }
3398 if !precise {
3399 batch.table(table_name);
3400 }
3401 }
3402 }
3403
3404 fn scoped_rows_exist(
3405 &self,
3406 table_name: &str,
3407 effective: &[(String, Vec<String>)],
3408 owners: &[Vec<(String, Vec<String>)>],
3409 ) -> bool {
3410 let Some(table) = self.schema.table(table_name) else {
3411 return false;
3412 };
3413 let Ok(Some((predicate, params))) = local_scope_predicate(table, effective, owners) else {
3414 return false;
3415 };
3416 let sql = format!(
3417 "SELECT 1 FROM {} WHERE {predicate} LIMIT 1",
3418 base_table(table_name)
3419 );
3420 self.conn
3421 .query_row(&sql, rusqlite::params_from_iter(params), |_| Ok(()))
3422 .is_ok()
3423 }
3424
3425 pub fn upgrading(&self) -> bool {
3427 self.upgrading
3428 }
3429
3430 pub fn recreate_with_schema(&mut self, schema_json: &Value) -> Result<(), String> {
3438 let new_schema = parse_schema_json(schema_json)?;
3439 validate_authority_reads(&new_schema, &self.limits.authority_reads)?;
3440 self.conn
3441 .execute_batch("BEGIN IMMEDIATE")
3442 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3443 let prior_schema = std::mem::replace(&mut self.schema, new_schema);
3444 let rollback = self.capture();
3450 let guarded = (|| -> Result<(), String> {
3451 let marker = read_local_schema_version(&self.conn, self.schema.version)?;
3456 self.create_bookkeeping_tables()?;
3457 match marker {
3458 Some(version) if version < self.schema.version => self.run_schema_reset()?,
3459 None => {
3460 self.create_synced_tables()?;
3461 self.persist_schema_version()?;
3462 }
3463 _ => {}
3464 }
3465 self.outbox = self.load_outbox()?.into();
3470 self.drop_unencodable_outbox(false, None)?;
3475 if marker == Some(self.schema.version)
3476 && (!self.outbox.is_empty()
3477 || !self.failed_commits.is_empty()
3478 || self.has_acknowledged_rows()?)
3479 {
3480 self.overlay_dirty.set(true);
3481 self.rebuild_overlay()?;
3482 }
3483 self.prune_unknown_units(false)?;
3484 self.save_unit_scope_schema()?;
3485 self.enqueue_startup_sync_if_needed()?;
3489 Ok(())
3490 })();
3491 let outcome = match guarded {
3492 Ok(()) => self
3493 .conn
3494 .execute_batch("COMMIT")
3495 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error)),
3496 Err(error) => Err(error),
3497 };
3498 if let Err(error) = outcome {
3499 if let Err(rollback) = self.conn.execute_batch("ROLLBACK") {
3500 let _ = Self::sqlite_failure(&self.storage_failure, rollback);
3502 }
3503 self.schema = prior_schema;
3504 self.restore(rollback);
3505 self.insert_sql.borrow_mut().clear();
3507 return Err(error);
3508 }
3509 Ok(())
3510 }
3511
3512 fn enqueue_startup_sync_if_needed(&mut self) -> Result<(), String> {
3513 let startup_work = !self.stopped
3514 && (!self.outbox.is_empty()
3515 || self.has_acknowledged_rows()?
3516 || !self.load_pending_evictions()?.is_empty()
3517 || self
3518 .stored(load_units(&self.conn))?
3519 .iter()
3520 .any(|unit| unit.status == UnitStatus::Active));
3521 if startup_work {
3522 self.sync_needed = true;
3523 self.sync_intent_queue.push_back(SyncIntent::Interactive);
3524 }
3525 Ok(())
3526 }
3527
3528 fn clear_satisfied_persisted_schema_floor(&mut self) -> Result<(), String> {
3535 let satisfied = self
3536 .schema_floor
3537 .as_ref()
3538 .and_then(|floor| floor.required_schema_version)
3539 .is_some_and(|required| self.schema.version >= required);
3540 if !satisfied {
3541 return Ok(());
3542 }
3543 self.delete_meta(SCHEMA_FLOOR_KEY)?;
3544 self.schema_floor = None;
3545 self.stopped = false;
3546 Ok(())
3547 }
3548
3549 fn reconcile_previous_version_at_boot(&self) -> Result<(), String> {
3554 let Some(replica_path) = self.previous_version_replica_path() else {
3555 return Ok(());
3556 };
3557 let max_age_ms = self
3558 .previous_version
3559 .filter(|config| config.enabled)
3560 .map(|config| config.max_age_ms);
3561 reconcile_previous_version_at_boot(
3562 &self.conn,
3563 &replica_path,
3564 self.schema.version,
3565 max_age_ms,
3566 self.clock_now_ms(),
3567 )
3568 }
3569
3570 fn previous_version_replica_path(&self) -> Option<String> {
3574 let path = self.conn.path()?;
3575 if path.is_empty() || path.starts_with("file:") {
3576 return None;
3577 }
3578 Some(path.to_owned())
3579 }
3580
3581 fn run_schema_reset(&mut self) -> Result<(), String> {
3587 self.begin_observation("syncular_schema_reset")?;
3588 let rollback = self.capture();
3589 let mut batch = ChangeAccumulator::default();
3590 let result = self
3591 .run_schema_reset_observed(&mut batch, true)
3592 .and_then(|()| self.finish_observation("syncular_schema_reset", batch));
3593 if let Err(error) = result {
3594 self.rollback_observation("syncular_schema_reset");
3595 self.restore(rollback);
3596 return Err(error);
3597 }
3598 Ok(())
3599 }
3600
3601 fn run_log_epoch_reset(&mut self, log_epoch: &str) -> Result<Vec<String>, String> {
3602 if self.get_meta(LOG_EPOCH_KEY)?.is_none() {
3603 self.begin_observation("syncular_epoch_acquisition")?;
3604 let rollback = self.capture();
3605 if let Err(error) = self.conn.execute(
3606 "INSERT OR REPLACE INTO _syncular_meta(key,value) VALUES (?1,?2)",
3607 rusqlite::params![LOG_EPOCH_KEY, log_epoch],
3608 ) {
3609 self.rollback_observation("syncular_epoch_acquisition");
3610 return Err(Self::sqlite_failure(&self.storage_failure, error));
3611 }
3612 self.sync_needed = true;
3613 let batch = ChangeAccumulator {
3614 status: true,
3615 ..ChangeAccumulator::default()
3616 };
3617 if let Err(error) = self.finish_observation("syncular_epoch_acquisition", batch) {
3618 self.rollback_observation("syncular_epoch_acquisition");
3619 self.restore(rollback);
3620 return Err(error);
3621 }
3622 self.sync_intent_queue.push_back(SyncIntent::Interactive);
3623 return Ok(Vec::new());
3624 }
3625 let resets = self
3626 .stored(load_units(&self.conn))?
3627 .into_iter()
3628 .map(|unit| unit.id)
3629 .collect();
3630 self.begin_observation("syncular_log_epoch_reset")?;
3631 let rollback = self.capture();
3632 let mut batch = ChangeAccumulator::default();
3633 let result = self
3634 .run_schema_reset_observed(&mut batch, false)
3635 .and_then(|()| {
3636 self.conn
3637 .execute(
3638 "INSERT OR REPLACE INTO _syncular_meta(key,value) VALUES (?1,?2)",
3639 rusqlite::params![LOG_EPOCH_KEY, log_epoch],
3640 )
3641 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3642 self.sync_needed = true;
3643 batch.status = true;
3644 self.finish_observation("syncular_log_epoch_reset", batch)
3645 });
3646 if let Err(error) = result {
3647 self.rollback_observation("syncular_log_epoch_reset");
3648 self.restore(rollback);
3649 return Err(error);
3650 }
3651 self.sync_intent_queue.push_back(SyncIntent::Interactive);
3652 Ok(resets)
3653 }
3654
3655 fn run_schema_reset_observed(
3656 &mut self,
3657 batch: &mut ChangeAccumulator,
3658 capture: bool,
3659 ) -> Result<(), String> {
3660 read_local_schema_version(&self.conn, self.schema.version)?;
3666 self.conn
3667 .execute_batch(
3668 "DELETE FROM _syncular_acked_rows; DELETE FROM _syncular_row_deliveries;",
3669 )
3670 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3671 self.cancel_sync_round();
3672 self.upgrading = true;
3673 batch.status = true;
3674 if self.previous_version.is_some_and(|config| config.enabled) {
3682 let previous_version = self
3683 .get_meta(LOCAL_SCHEMA_VERSION_KEY)?
3684 .and_then(|value| value.parse::<i32>().ok())
3685 .unwrap_or(self.schema.version);
3686 let pending = self
3687 .outbox
3688 .iter()
3689 .map(|commit| PendingCommitAudit {
3690 commit_id: &commit.client_commit_id,
3691 operations: commit_audit_operations(commit),
3692 })
3693 .collect::<Vec<_>>();
3694 crate::previous_version::write_previous_version_audit(
3695 &self.conn,
3696 &crate::previous_version::build_previous_version_audit(
3697 &self.schema,
3698 &pending,
3699 previous_version,
3700 self.schema.version,
3701 self.clock_now_ms(),
3702 ),
3703 )?;
3704 }
3705 if let Some(replica_path) = self.previous_version_replica_path() {
3710 sweep_previous_version_container(&self.conn, &replica_path)?;
3711 if capture {
3712 if let Some(config) = self.previous_version.filter(|config| config.enabled) {
3713 let previous_version = self
3714 .get_meta(LOCAL_SCHEMA_VERSION_KEY)?
3715 .and_then(|value| value.parse::<i32>().ok())
3716 .unwrap_or(self.schema.version);
3717 capture_previous_version_from_replica(
3718 &self.conn,
3719 &replica_path,
3720 previous_version,
3721 self.schema.version,
3722 &config,
3723 self.clock_now_ms(),
3724 )?;
3725 }
3726 }
3727 }
3728 for table in &self.schema.tables {
3729 batch.table(&table.name);
3730 }
3731 for (base_key, value) in self.load_window_registry()? {
3732 batch.window(&base_key, &value);
3733 }
3734 if capture {
3735 self.prune_unknown_units(true)?;
3736 }
3737 self.insert_sql.borrow_mut().clear();
3739 self.overlay_dirty.set(true);
3740 let virtual_tables: Vec<String> = {
3748 let mut stmt = self
3749 .conn
3750 .prepare(
3751 "SELECT name FROM sqlite_master WHERE type = 'table' AND sql LIKE 'CREATE VIRTUAL TABLE%'",
3752 )
3753 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3754 let rows = stmt
3755 .query_map([], |row| row.get::<_, String>(0))
3756 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3757 rows.filter_map(Result::ok)
3758 .filter(|name| is_synced_table_name(name))
3759 .collect()
3760 };
3761 for name in virtual_tables {
3762 self.conn
3763 .execute_batch(&format!(
3764 "DROP TABLE IF EXISTS {}",
3765 quote_ident(&format!("_syncular_fts_{name}"))
3766 ))
3767 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3768 self.conn
3769 .execute(&format!("DROP TABLE IF EXISTS {}", quote_ident(&name)), [])
3770 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3771 }
3772 let existing: Vec<String> = {
3773 let mut stmt = self
3774 .conn
3775 .prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
3776 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3777 let rows = stmt
3778 .query_map([], |row| row.get::<_, String>(0))
3779 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3780 rows.filter_map(Result::ok)
3781 .filter(|name| is_synced_table_name(name))
3782 .collect()
3783 };
3784 for name in &existing {
3785 if let Some(table) = name.strip_prefix("_syncular_base_") {
3786 batch.table(table);
3787 } else if !name.starts_with("_syncular_") {
3788 batch.table(name);
3789 }
3790 }
3791 for name in existing {
3792 self.conn
3793 .execute(&format!("DROP TABLE IF EXISTS {}", quote_ident(&name)), [])
3794 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3795 }
3796 self.create_synced_tables()?;
3798 reset_units(&self.conn)
3800 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3801 self.stopped = false;
3803 self.schema_floor = None;
3804 self.delete_meta(SCHEMA_FLOOR_KEY)?;
3805 self.persist_schema_version()?;
3807 self.save_unit_scope_schema()?;
3808 self.drop_unencodable_outbox(false, Some(batch))?;
3819 self.rebuild_overlay()?;
3821 Ok(())
3822 }
3823
3824 fn drop_incompatible_outbox(&mut self) -> Result<bool, String> {
3833 let incompatible = self
3834 .outbox
3835 .iter()
3836 .filter(|commit| {
3837 first_incompatibility(&self.schema, &commit_audit_operations(commit)).is_some()
3838 })
3839 .cloned()
3840 .collect::<Vec<_>>();
3841 if incompatible.is_empty() {
3842 return Ok(false);
3843 }
3844 self.drop_outbox_commits(
3845 &incompatible,
3846 OUTBOX_INCOMPATIBLE_CODE,
3847 "the persisted commit cannot encode under the current schema",
3848 )?;
3849 Ok(true)
3850 }
3851
3852 fn drop_outbox_commits(
3856 &mut self,
3857 incompatible: &[Arc<OutboxCommit>],
3858 code: &str,
3859 message: &str,
3860 ) -> Result<(), String> {
3861 let rollback = self.capture();
3867 let owns_transaction = self.conn.is_autocommit();
3868 self.begin_observation("syncular_outbox_drop")?;
3869 let mut batch = ChangeAccumulator::default();
3870 let dropped = (|| -> Result<(), String> {
3871 let mut rejections = Vec::new();
3872 for commit in incompatible {
3873 let results = commit
3874 .ops
3875 .iter()
3876 .enumerate()
3877 .map(|(op_index, operation)| {
3878 let rejection = RejectionRecord {
3879 client_commit_id: commit.client_commit_id.clone(),
3880 op_index: op_index as i32,
3881 code: code.to_owned(),
3882 message: message.to_owned(),
3883 retryable: false,
3884 details: None,
3885 operation: Some(CommitOperation::from(operation)),
3886 };
3887 rejections.push(rejection.clone());
3888 CommitOperationOutcome::Error { rejection }
3889 })
3890 .collect::<Vec<_>>();
3891 self.persist_commit_outcome(
3892 &commit.client_commit_id,
3893 CommitOutcomeStatus::Rejected,
3894 &results,
3895 Some(&commit.ops),
3896 )?;
3897 self.delete_outbox_persisted(&commit.client_commit_id)?;
3898 for operation in &commit.ops {
3901 if self.schema.table(&operation.table).is_some() {
3902 batch.table(&operation.table);
3903 }
3904 }
3905 }
3906 self.prune_commit_outcomes()?;
3907 let incompatible_ids = incompatible
3908 .iter()
3909 .map(|commit| commit.client_commit_id.as_str())
3910 .collect::<BTreeSet<_>>();
3911 self.outbox
3912 .retain(|commit| !incompatible_ids.contains(commit.client_commit_id.as_str()));
3913 self.rejections.extend(rejections);
3914 self.overlay_dirty.set(true);
3917 self.rebuild_overlay()?;
3918 batch.status = true;
3919 batch.rejections = true;
3920 batch.outcomes = true;
3921 Ok(())
3922 })();
3923 let result = dropped.and_then(|()| self.finish_observation("syncular_outbox_drop", batch));
3924 if let Err(error) = result {
3925 if owns_transaction {
3929 if let Err(rollback) = self.conn.execute_batch("ROLLBACK") {
3930 let mut failure = self.storage_failure.borrow_mut();
3931 let failure =
3932 failure.get_or_insert_with(|| QueryReadFailure::from(error.clone()));
3933 failure.rollback_failure = Some(Box::new(QueryReadFailure::from(rollback)));
3934 }
3935 } else {
3936 self.rollback_observation("syncular_outbox_drop");
3937 }
3938 self.restore(rollback);
3939 return Err(error);
3940 }
3941 Ok(())
3942 }
3943
3944 fn create_synced_tables(&self) -> Result<(), String> {
3947 for table in &self.schema.tables {
3948 for (full, index_prefix) in [
3952 (base_table(&table.name), "_syncular_base_"),
3953 (visible_table(&table.name), ""),
3954 ] {
3955 let mut cols: Vec<String> =
3956 table.columns.iter().map(|c| quote_ident(&c.name)).collect();
3957 cols.push("\"_syncular_version\" INTEGER NOT NULL".to_owned());
3958 let sql = format!(
3959 "CREATE TABLE IF NOT EXISTS {full} ({} , PRIMARY KEY ({}))",
3960 cols.join(", "),
3961 quote_ident(&table.primary_key)
3962 );
3963 self.conn
3964 .execute(&sql, [])
3965 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3966 for index in &table.indexes {
3970 let unique = if index.unique { "UNIQUE " } else { "" };
3971 let index_name = quote_ident(&format!("{index_prefix}{}", index.name));
3972 let cols_sql = index
3973 .columns
3974 .iter()
3975 .map(|c| quote_ident(c))
3976 .collect::<Vec<_>>()
3977 .join(", ");
3978 let index_sql = format!(
3979 "CREATE {unique}INDEX IF NOT EXISTS {index_name} ON {full} ({cols_sql})"
3980 );
3981 self.conn
3982 .execute(&index_sql, [])
3983 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
3984 }
3985 }
3986 }
3987 for table in &self.schema.tables {
3990 for index in &table.fts_indexes {
3991 self.create_fts_projection(table, index)?;
3992 }
3993 }
3994 Ok(())
3995 }
3996
3997 fn fts_projection_exists(&self, index: &FtsIndexSchema) -> Result<bool, String> {
3998 let count: i64 = self
3999 .conn
4000 .query_row(
4001 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
4002 rusqlite::params![index.name],
4003 |row| row.get(0),
4004 )
4005 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4006 Ok(count > 0)
4007 }
4008
4009 fn drop_fts_triggers(&self, index: &FtsIndexSchema) -> Result<(), String> {
4010 for suffix in ["ai", "ad", "au"] {
4011 self.conn
4012 .execute(
4013 &format!(
4014 "DROP TRIGGER IF EXISTS {}",
4015 quote_ident(&format!("{}_{suffix}", index.name))
4016 ),
4017 [],
4018 )
4019 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4020 }
4021 Ok(())
4022 }
4023
4024 fn create_fts_triggers(
4025 &self,
4026 table: &TableSchema,
4027 index: &FtsIndexSchema,
4028 ) -> Result<(), String> {
4029 let fts = quote_ident(&index.name);
4030 let source = visible_table(&table.name);
4031 let source_id = quote_ident(FTS_SOURCE_ID_COLUMN);
4032 let pk = quote_ident(&table.primary_key);
4033 let projection_columns = std::iter::once(source_id.clone())
4034 .chain(index.columns.iter().map(|column| quote_ident(column)))
4035 .collect::<Vec<_>>()
4036 .join(", ");
4037 let new_values = std::iter::once(format!("CAST(new.{pk} AS TEXT)"))
4038 .chain(
4039 index
4040 .columns
4041 .iter()
4042 .map(|column| format!("new.{}", quote_ident(column))),
4043 )
4044 .collect::<Vec<_>>()
4045 .join(", ");
4046 let mapping = quote_ident(&format!("_syncular_fts_{}", index.name));
4047 let delete_new = format!("DELETE FROM {fts} WHERE rowid = (SELECT id FROM {mapping} WHERE source_id = CAST(new.{pk} AS TEXT)); DELETE FROM {mapping} WHERE source_id = CAST(new.{pk} AS TEXT)");
4048 let delete_old = format!("DELETE FROM {fts} WHERE rowid = (SELECT id FROM {mapping} WHERE source_id = CAST(old.{pk} AS TEXT)); DELETE FROM {mapping} WHERE source_id = CAST(old.{pk} AS TEXT)");
4049 let insert_new = format!("INSERT INTO {mapping}(source_id) VALUES (CAST(new.{pk} AS TEXT)); INSERT INTO {fts} (rowid, {projection_columns}) VALUES ((SELECT id FROM {mapping} WHERE source_id = CAST(new.{pk} AS TEXT)), {new_values})");
4050 let bi = quote_ident(&format!("{}_bi", index.name));
4051 let ai = quote_ident(&format!("{}_ai", index.name));
4052 let ad = quote_ident(&format!("{}_ad", index.name));
4053 let au = quote_ident(&format!("{}_au", index.name));
4054 let replacement_exists = format!("EXISTS (SELECT 1 FROM {source} WHERE {pk} = new.{pk})");
4055 let sql = format!(
4056 "DROP TRIGGER IF EXISTS {bi};
4057 DROP TRIGGER IF EXISTS {ai};
4058 DROP TRIGGER IF EXISTS {ad};
4059 DROP TRIGGER IF EXISTS {au};
4060 CREATE TRIGGER {bi} BEFORE INSERT ON {source} WHEN {replacement_exists} BEGIN {delete_new}; END;
4061 CREATE TRIGGER {ai} AFTER INSERT ON {source} BEGIN {insert_new}; END;
4062 CREATE TRIGGER {ad} AFTER DELETE ON {source} BEGIN {delete_old}; END;
4063 CREATE TRIGGER {au} AFTER UPDATE ON {source} BEGIN {delete_old}; {delete_new}; {insert_new}; END;",
4064 );
4065 self.conn
4066 .execute_batch(&sql)
4067 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
4068 }
4069
4070 fn rebuild_fts_projection(
4071 &self,
4072 table: &TableSchema,
4073 index: &FtsIndexSchema,
4074 ) -> Result<(), String> {
4075 let fts = quote_ident(&index.name);
4076 let source_id = quote_ident(FTS_SOURCE_ID_COLUMN);
4077 let pk = quote_ident(&table.primary_key);
4078 let indexed_columns = index
4079 .columns
4080 .iter()
4081 .map(|column| quote_ident(column))
4082 .collect::<Vec<_>>();
4083 let projection_columns = std::iter::once(source_id.clone())
4084 .chain(indexed_columns.iter().cloned())
4085 .collect::<Vec<_>>()
4086 .join(", ");
4087 let mapping = quote_ident(&format!("_syncular_fts_{}", index.name));
4088 let sql = format!(
4089 "DELETE FROM {fts}; DELETE FROM {mapping}; INSERT INTO {fts} ({projection_columns}) SELECT CAST({pk} AS TEXT), {columns} FROM {source}; INSERT INTO {mapping}(id, source_id) SELECT rowid, {source_id} FROM {fts};",
4090 columns = indexed_columns.join(", "),
4091 source = visible_table(&table.name),
4092 );
4093 self.conn
4094 .execute_batch(&sql)
4095 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
4096 }
4097
4098 fn create_fts_projection(
4099 &self,
4100 table: &TableSchema,
4101 index: &FtsIndexSchema,
4102 ) -> Result<(), String> {
4103 self.begin_observation("syncular_fts_schema")?;
4104 let result = (|| {
4105 let existed = self.fts_projection_exists(index)?;
4106 let tokenizer = index.tokenize.replace('\'', "''");
4107 let columns = index
4108 .columns
4109 .iter()
4110 .map(|column| quote_ident(column))
4111 .collect::<Vec<_>>()
4112 .join(", ");
4113 let sql = format!(
4114 "CREATE VIRTUAL TABLE IF NOT EXISTS {fts} USING fts5({source_id} UNINDEXED, {columns}, tokenize='{tokenizer}')",
4115 fts = quote_ident(&index.name),
4116 source_id = quote_ident(FTS_SOURCE_ID_COLUMN),
4117 );
4118 self.conn.execute(&sql, []).map_err(|error| {
4119 format!(
4120 "cannot create local FTS5 projection {:?}: {error}",
4121 index.name
4122 )
4123 })?;
4124 let mapping = quote_ident(&format!("_syncular_fts_{}", index.name));
4125 let mapped: bool = self
4126 .conn
4127 .query_row(
4128 "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1)",
4129 [format!("_syncular_fts_{}", index.name)],
4130 |row| row.get(0),
4131 )
4132 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4133 self.conn.execute_batch(&format!("CREATE TABLE IF NOT EXISTS {mapping}(id INTEGER PRIMARY KEY, source_id TEXT NOT NULL UNIQUE)")).map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4134 if !mapped && existed {
4135 self.conn
4136 .execute_batch(&format!(
4137 "INSERT INTO {mapping}(id, source_id) SELECT rowid, {} FROM {}",
4138 quote_ident(FTS_SOURCE_ID_COLUMN),
4139 quote_ident(&index.name)
4140 ))
4141 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4142 }
4143 self.create_fts_triggers(table, index)?;
4144 if !existed {
4145 self.rebuild_fts_projection(table, index)?;
4146 }
4147 Ok(())
4148 })();
4149 if let Err(error) = result {
4150 self.rollback_observation("syncular_fts_schema");
4151 return Err(error);
4152 }
4153 if let Err(error) = self.conn.execute_batch("RELEASE syncular_fts_schema") {
4154 self.rollback_observation("syncular_fts_schema");
4155 return Err(error.to_string());
4156 }
4157 Ok(())
4158 }
4159
4160 fn persist_outbox_insert(&self, commit: &OutboxCommit) -> Result<(), String> {
4163 let ops: Vec<Value> = commit
4164 .ops
4165 .iter()
4166 .map(|op| {
4167 let mut entry = serde_json::json!({
4168 "op": if op.upsert { "upsert" } else { "delete" },
4169 "table": op.table,
4170 "rowId": op.row_id,
4171 "baseVersion": op.base_version,
4172 "values": op.values.clone().map(Value::Object),
4173 });
4174 if op.insert {
4175 entry["insert"] = Value::Bool(true);
4176 }
4177 entry
4178 })
4179 .collect();
4180 self.conn
4181 .execute(
4182 "INSERT OR REPLACE INTO _syncular_outbox (commit_id, ops_json) VALUES (?1, ?2)",
4183 rusqlite::params![commit.client_commit_id, Value::Array(ops).to_string()],
4184 )
4185 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4186 for operation in &commit.ops {
4187 let Some(values) = &operation.values else {
4188 continue;
4189 };
4190 let Some(table) = self.schema.table(&operation.table) else {
4191 continue;
4192 };
4193 for column in table
4194 .columns
4195 .iter()
4196 .filter(|column| column.ty == ColumnType::BlobRef)
4197 {
4198 let Some(Value::String(raw)) = values.get(&column.name) else {
4199 continue;
4200 };
4201 let Ok(blob_ref) = serde_json::from_str::<Value>(raw) else {
4202 continue;
4203 };
4204 let Some(blob_id) = blob_ref.get("blobId").and_then(Value::as_str) else {
4205 continue;
4206 };
4207 self.conn
4208 .execute(
4209 "INSERT OR IGNORE INTO _syncular_blob_commit_refs(commit_id, blob_id)
4210 SELECT ?1, ?2 WHERE EXISTS (SELECT 1 FROM _syncular_blobs WHERE blob_id = ?2)",
4211 rusqlite::params![commit.client_commit_id, blob_id],
4212 )
4213 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4214 }
4215 }
4216 Ok(())
4217 }
4218
4219 fn delete_outbox_persisted(&self, client_commit_id: &str) -> Result<(), String> {
4220 self.conn
4221 .execute(
4222 "DELETE FROM _syncular_blob_commit_refs WHERE commit_id = ?1",
4223 rusqlite::params![client_commit_id],
4224 )
4225 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4226 self.conn
4227 .execute(
4228 "DELETE FROM _syncular_outbox WHERE commit_id = ?1",
4229 rusqlite::params![client_commit_id],
4230 )
4231 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4232 self.conn.execute("DELETE FROM _syncular_row_deliveries WHERE NOT EXISTS(SELECT 1 FROM _syncular_outbox,json_each(ops_json) WHERE json_extract(json_each.value,'$.table')=tbl AND json_extract(json_each.value,'$.rowId')=id)", [])
4233 .map(|_| ())
4234 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
4235 }
4236
4237 fn outcome_status_name(status: CommitOutcomeStatus) -> &'static str {
4238 match status {
4239 CommitOutcomeStatus::Applied => "applied",
4240 CommitOutcomeStatus::Cached => "cached",
4241 CommitOutcomeStatus::Conflict => "conflict",
4242 CommitOutcomeStatus::Rejected => "rejected",
4243 }
4244 }
4245
4246 fn outcome_resolution_name(resolution: CommitOutcomeResolution) -> &'static str {
4247 match resolution {
4248 CommitOutcomeResolution::Active => "active",
4249 CommitOutcomeResolution::ResolvedKeepServer => "resolved_keep_server",
4250 CommitOutcomeResolution::Superseded => "superseded",
4251 CommitOutcomeResolution::Dismissed => "dismissed",
4252 }
4253 }
4254
4255 fn parse_outcome_status(value: &str) -> Result<CommitOutcomeStatus, String> {
4256 match value {
4257 "applied" => Ok(CommitOutcomeStatus::Applied),
4258 "cached" => Ok(CommitOutcomeStatus::Cached),
4259 "conflict" => Ok(CommitOutcomeStatus::Conflict),
4260 "rejected" => Ok(CommitOutcomeStatus::Rejected),
4261 _ => Err(format!("invalid persisted commit outcome status {value:?}")),
4262 }
4263 }
4264
4265 fn parse_outcome_resolution(value: &str) -> Result<CommitOutcomeResolution, String> {
4266 match value {
4267 "active" => Ok(CommitOutcomeResolution::Active),
4268 "resolved_keep_server" => Ok(CommitOutcomeResolution::ResolvedKeepServer),
4269 "superseded" => Ok(CommitOutcomeResolution::Superseded),
4270 "dismissed" => Ok(CommitOutcomeResolution::Dismissed),
4271 _ => Err(format!(
4272 "invalid persisted commit outcome resolution {value:?}"
4273 )),
4274 }
4275 }
4276
4277 fn persist_commit_outcome(
4278 &self,
4279 client_commit_id: &str,
4280 status: CommitOutcomeStatus,
4281 results: &[CommitOperationOutcome],
4282 operations: Option<&[OutboxOp]>,
4283 ) -> Result<(), String> {
4284 let results_json = serde_json::to_string(results).map_err(|error| error.to_string())?;
4285 let operations_json = operations
4286 .map(|items| {
4287 serde_json::to_string(&items.iter().map(CommitOperation::from).collect::<Vec<_>>())
4288 })
4289 .transpose()
4290 .map_err(|error| error.to_string())?;
4291 self.conn
4292 .execute(
4293 "INSERT INTO _syncular_commit_outcomes (
4294 client_commit_id, status, recorded_at_ms, results_json,
4295 operations_json, resolution
4296 ) VALUES (?1, ?2, ?3, ?4, ?5, 'active')",
4297 rusqlite::params![
4298 client_commit_id,
4299 Self::outcome_status_name(status),
4300 self.clock_now_ms(),
4301 results_json,
4302 operations_json
4303 ],
4304 )
4305 .map(|_| ())
4306 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
4307 }
4308
4309 fn stored_cell_is_representable(cell: &Value) -> bool {
4312 cell.is_null()
4313 || cell.is_string()
4314 || cell.is_boolean()
4315 || cell.is_number()
4316 || is_canonical_bytes_envelope(cell)
4317 }
4318
4319 fn stored_row_is_representable(value: &Value) -> bool {
4320 value
4321 .as_object()
4322 .is_some_and(|row| row.values().all(Self::stored_cell_is_representable))
4323 }
4324
4325 fn stored_operation_is_representable(value: &Value) -> bool {
4326 value.get("table").is_some_and(Value::is_string)
4327 && value.get("rowId").is_some_and(Value::is_string)
4328 && value.get("baseVersion").is_none_or(|v| {
4329 v.as_i64()
4330 .is_some_and(|n| n.unsigned_abs() <= MAX_JS_SAFE_INTEGER)
4331 })
4332 && match value.get("op").and_then(Value::as_str) {
4333 Some("upsert") => {
4334 value
4335 .get("values")
4336 .is_some_and(Self::stored_row_is_representable)
4337 && value
4338 .get("insert")
4339 .is_none_or(|insert| *insert == Value::Bool(true))
4340 }
4341 Some("delete") => value.get("values").is_none() && value.get("insert").is_none(),
4342 _ => false,
4343 }
4344 }
4345
4346 fn outbox_op_is_journalable(operation: &OutboxOp) -> bool {
4350 serde_json::to_value(CommitOperation::from(operation))
4351 .is_ok_and(|value| Self::stored_operation_is_representable(&value))
4352 }
4353
4354 fn outcome_from_row(row: StoredCommitOutcomeRow) -> Result<CommitOutcome, String> {
4355 let StoredCommitOutcomeRow {
4356 sequence,
4357 client_commit_id,
4358 status,
4359 recorded_at_ms,
4360 results_json,
4361 operations_json,
4362 resolution,
4363 resolved_at_ms,
4364 replacement_client_commit_id,
4365 } = row;
4366 if sequence.unsigned_abs() > MAX_JS_SAFE_INTEGER
4367 || recorded_at_ms.unsigned_abs() > MAX_JS_SAFE_INTEGER
4368 || resolved_at_ms.is_some_and(|value| value.unsigned_abs() > MAX_JS_SAFE_INTEGER)
4369 {
4370 return Err("sync.local_corrupt: persisted commit outcome is invalid".to_owned());
4371 }
4372 let invalid = || "sync.local_corrupt: persisted commit outcome is invalid".to_owned();
4373 let stored: Vec<Value> = serde_json::from_str(&results_json).map_err(|_| invalid())?;
4374 for result in &stored {
4375 let nested = match result.get("status").and_then(Value::as_str) {
4376 Some("conflict") => result.get("conflict"),
4377 Some("error") => result.get("rejection"),
4378 _ => None,
4379 };
4380 if nested
4381 .and_then(|record| record.get("operation"))
4382 .is_some_and(|op| !Self::stored_operation_is_representable(op))
4383 || (result.get("status").and_then(Value::as_str) == Some("conflict")
4384 && nested
4385 .and_then(|record| record.get("serverRow"))
4386 .is_some_and(|row| !Self::stored_row_is_representable(row)))
4387 {
4388 return Err(invalid());
4389 }
4390 }
4391 let results: Vec<CommitOperationOutcome> =
4392 serde_json::from_value(Value::Array(stored)).map_err(|_| invalid())?;
4393 let has_conflict = results
4394 .iter()
4395 .any(|result| matches!(result, CommitOperationOutcome::Conflict { .. }));
4396 let has_error = results
4397 .iter()
4398 .any(|result| matches!(result, CommitOperationOutcome::Error { .. }));
4399 let status = Self::parse_outcome_status(&status).map_err(|_| invalid())?;
4400 let redacted = results.is_empty() && operations_json.is_none();
4401 if match status {
4402 CommitOutcomeStatus::Applied | CommitOutcomeStatus::Cached => has_conflict || has_error,
4403 CommitOutcomeStatus::Conflict => !(has_conflict || redacted),
4404 CommitOutcomeStatus::Rejected => has_conflict || !(has_error || redacted),
4405 } {
4406 return Err(invalid());
4407 }
4408 let operations: Option<Vec<CommitOperation>> = operations_json
4409 .map(|raw| {
4410 let stored: Vec<Value> = serde_json::from_str(&raw).map_err(|_| invalid())?;
4411 if !stored.iter().all(Self::stored_operation_is_representable) {
4412 return Err(invalid());
4413 }
4414 serde_json::from_value(Value::Array(stored)).map_err(|_| invalid())
4415 })
4416 .transpose()?;
4417 for result in &results {
4418 match result {
4419 CommitOperationOutcome::Conflict { conflict }
4420 if conflict.server_version.unsigned_abs() > MAX_JS_SAFE_INTEGER =>
4421 {
4422 return Err("sync.local_corrupt: persisted commit outcome is invalid".to_owned())
4423 }
4424 CommitOperationOutcome::Error { rejection } => {
4425 if let Some(details) = &rejection.details {
4426 let encoded = serde_json::to_string(details).map_err(|_| {
4427 "sync.local_corrupt: persisted commit outcome is invalid".to_owned()
4428 })?;
4429 RejectionDetails::parse(&encoded).map_err(|_| {
4430 "sync.local_corrupt: persisted commit outcome is invalid".to_owned()
4431 })?;
4432 }
4433 }
4434 _ => {}
4435 }
4436 }
4437 Ok(CommitOutcome {
4438 sequence,
4439 client_commit_id,
4440 status,
4441 recorded_at_ms,
4442 results,
4443 operations,
4444 retained_rows: None,
4445 resolution: Self::parse_outcome_resolution(&resolution)?,
4446 resolved_at_ms,
4447 replacement_client_commit_id,
4448 })
4449 }
4450
4451 fn unique_conflicts(
4453 &self,
4454 table: &TableSchema,
4455 values: &Map<String, Value>,
4456 base: bool,
4457 ) -> Result<Vec<RetainedUniqueConflict>, String> {
4458 let source = if base {
4459 base_table(&table.name)
4460 } else {
4461 visible_table(&table.name)
4462 };
4463 let mut conflicts = Vec::new();
4464 for index in table.indexes.iter().filter(|index| index.unique) {
4465 let mut params = vec![json_param_to_sql(
4466 values.get(&table.primary_key).ok_or("sync.local_corrupt")?,
4467 )?];
4468 for name in &index.columns {
4469 let column = table
4470 .columns
4471 .iter()
4472 .find(|column| column.name == *name)
4473 .ok_or("sync.local_corrupt")?;
4474 let cell = json_to_column_value(column, values.get(name))?;
4475 params.push(owned_sql_value(RowParam::Cell(&cell))?);
4476 }
4477 let sql = format!(
4478 "SELECT * FROM {source} WHERE {} != ? AND {}",
4479 quote_ident(&table.primary_key),
4480 index
4481 .columns
4482 .iter()
4483 .map(|column| format!("{} = ?", quote_ident(column)))
4484 .collect::<Vec<_>>()
4485 .join(" AND ")
4486 );
4487 let mut statement = self
4488 .conn
4489 .prepare_cached(&sql)
4490 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4491 let records = statement
4492 .query_map(rusqlite::params_from_iter(params), |record| {
4493 let values = table
4494 .columns
4495 .iter()
4496 .enumerate()
4497 .map(|(index, column)| {
4498 let value = sql_ref_to_json(column, record.get_ref(index)?);
4499 json_to_column_value(column, Some(&value)).map_err(|message| {
4500 rusqlite::Error::ToSqlConversionFailure(message.into())
4501 })
4502 })
4503 .collect::<Result<Row, _>>()?;
4504 Ok((values, record.get::<_, i64>(table.columns.len())?))
4505 })
4506 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4507 for record in records {
4508 let (row, version) =
4509 record.map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4510 let row_id = render_row_id_json(Some(&column_value_to_json(&row[table.pk_index])))?;
4511 conflicts.push(RetainedUniqueConflict {
4512 index: index.name.clone(),
4513 columns: index.columns.clone(),
4514 row_id,
4515 server_row: table
4516 .columns
4517 .iter()
4518 .zip(&row)
4519 .map(|(column, value)| (column.name.clone(), column_value_to_json(value)))
4520 .collect(),
4521 server_version: version,
4522 });
4523 }
4524 }
4525 Ok(conflicts)
4526 }
4527
4528 fn attach_retained_rows(&self, mut outcome: CommitOutcome) -> Result<CommitOutcome, String> {
4529 if let Some((commit, initial)) = self
4530 .failed_commits
4531 .iter()
4532 .find(|(commit, _)| commit.client_commit_id == outcome.client_commit_id)
4533 {
4534 let mut rows = Vec::new();
4535 for operation in &commit.ops {
4536 let Some(table) = self.schema.table(&operation.table) else {
4538 continue;
4539 };
4540 let key =
4541 serde_json::to_string(&(operation.table.clone(), operation.row_id.clone()))
4542 .map_err(|_| "sync.local_corrupt".to_owned())?;
4543 let base = self.stored_row(table, &operation.row_id, true)?;
4544 rows.push(RetainedCommitRow {
4545 table: operation.table.clone(),
4546 row_id: operation.row_id.clone(),
4547 unique_conflicts: if operation.upsert {
4548 self.unique_conflicts(
4549 table,
4550 initial
4551 .get(&key)
4552 .and_then(Value::as_object)
4553 .or(operation.values.as_ref())
4554 .ok_or("sync.local_corrupt")?,
4555 true,
4556 )?
4557 } else {
4558 Vec::new()
4559 },
4560 local_row: if operation.upsert {
4561 initial
4562 .get(&key)
4563 .and_then(Value::as_object)
4564 .cloned()
4565 .or_else(|| operation.values.clone())
4566 } else {
4567 None
4568 },
4569 server_row: base.as_ref().map(|(values, _)| {
4570 table
4571 .columns
4572 .iter()
4573 .zip(values)
4574 .map(|(column, value)| {
4575 (column.name.clone(), column_value_to_json(value))
4576 })
4577 .collect()
4578 }),
4579 server_version: base.map(|(_, version)| version),
4580 });
4581 }
4582 outcome.retained_rows = Some(rows);
4583 }
4584 Ok(outcome)
4585 }
4586
4587 pub fn commit_outcome(&self, client_commit_id: &str) -> Result<Option<CommitOutcome>, String> {
4588 let row = self
4589 .conn
4590 .query_row(
4591 "SELECT seq, client_commit_id, status, recorded_at_ms, results_json, operations_json,
4592 resolution, resolved_at_ms, replacement_client_commit_id
4593 FROM _syncular_commit_outcomes WHERE client_commit_id = ?1",
4594 rusqlite::params![client_commit_id],
4595 |row| {
4596 Ok(StoredCommitOutcomeRow {
4597 sequence: row.get(0)?,
4598 client_commit_id: row.get(1)?,
4599 status: row.get(2)?,
4600 recorded_at_ms: row.get(3)?,
4601 results_json: row.get(4)?,
4602 operations_json: row.get(5)?,
4603 resolution: row.get(6)?,
4604 resolved_at_ms: row.get(7)?,
4605 replacement_client_commit_id: row.get(8)?,
4606 })
4607 },
4608 )
4609 .optional()
4610 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4611 row.map(|row| self.attach_retained_rows(Self::outcome_from_row(row)?))
4612 .transpose()
4613 }
4614
4615 #[doc(hidden)]
4617 pub fn prepare_current_base(
4618 &self,
4619 client_commit_id: &str,
4620 sequence: i64,
4621 ) -> Result<CurrentBasePreparation, CurrentBaseProof> {
4622 let outcome = self
4623 .commit_outcome(client_commit_id)
4624 .map_err(|_| CurrentBaseProof::failure(CurrentBaseProofReason::SnapshotFailed))?;
4625 let valid = outcome.as_ref().is_some_and(|outcome| {
4626 outcome.sequence == sequence
4627 && outcome.resolution == CommitOutcomeResolution::Active
4628 && matches!(
4629 outcome.status,
4630 CommitOutcomeStatus::Conflict | CommitOutcomeStatus::Rejected
4631 )
4632 && outcome
4633 .operations
4634 .as_ref()
4635 .zip(outcome.retained_rows.as_ref())
4636 .is_some_and(|(operations, rows)| {
4637 !rows.is_empty() && operations.len() == rows.len()
4638 })
4639 });
4640 if !valid {
4641 return Err(CurrentBaseProof::failure(
4642 CurrentBaseProofReason::OutcomeUnavailable,
4643 ));
4644 }
4645 if !self.transport_enabled {
4646 return Err(CurrentBaseProof::failure(CurrentBaseProofReason::Offline));
4647 }
4648 Ok(CurrentBasePreparation {
4649 client_commit_id: client_commit_id.to_owned(),
4650 sequence,
4651 units: load_units(&self.conn)
4652 .map_err(|_| CurrentBaseProof::failure(CurrentBaseProofReason::SnapshotFailed))?,
4653 transport_epoch: self.transport_epoch,
4654 registration_epoch: self.registration_epoch,
4655 security_epoch: self.security_epoch,
4656 })
4657 }
4658
4659 #[doc(hidden)]
4661 pub fn finish_current_base(
4662 &mut self,
4663 prepared: CurrentBasePreparation,
4664 drive: SyncOutcome,
4665 ) -> CurrentBaseProof {
4666 if !self.transport_enabled || self.transport_epoch != prepared.transport_epoch {
4667 return CurrentBaseProof::failure(CurrentBaseProofReason::Offline);
4668 }
4669 if self.registration_epoch != prepared.registration_epoch {
4670 return CurrentBaseProof::failure(CurrentBaseProofReason::MissingCoverage);
4671 }
4672 if self.security_preflight || self.security_epoch != prepared.security_epoch {
4673 return CurrentBaseProof::failure(CurrentBaseProofReason::SyncFailed);
4674 }
4675 let report = match drive {
4676 SyncOutcome::Ok(report) => report,
4677 SyncOutcome::BudgetExhausted(_) => {
4678 return CurrentBaseProof::failure(CurrentBaseProofReason::BudgetExhausted)
4679 }
4680 _ => return CurrentBaseProof::failure(CurrentBaseProofReason::SyncFailed),
4681 };
4682 if report.schema_floor.is_some() {
4683 return CurrentBaseProof::failure(CurrentBaseProofReason::SchemaFloor);
4684 }
4685 if self
4686 .conn
4687 .execute_batch("SAVEPOINT syncular_base_proof")
4688 .is_err()
4689 {
4690 return CurrentBaseProof::failure(CurrentBaseProofReason::SnapshotFailed);
4691 }
4692 let proof = (|| {
4693 let snapshot = snapshot_read_connection(
4694 &self.conn,
4695 &SnapshotReadRequest {
4696 units: prepared.units.iter().map(|unit| unit.id.clone()).collect(),
4697 commit_ids: vec![prepared.client_commit_id.clone()],
4698 ..Default::default()
4699 },
4700 )
4701 .map_err(|_| CurrentBaseProofReason::SnapshotFailed)?;
4702 let current = self
4703 .commit_outcome(&prepared.client_commit_id)
4704 .map_err(|_| CurrentBaseProofReason::SnapshotFailed)?;
4705 let unavailable = if report.revoked.is_empty() {
4706 CurrentBaseProofReason::OutcomeUnavailable
4707 } else {
4708 CurrentBaseProofReason::Revoked
4709 };
4710 let Some(outcome) = current else {
4711 return Err(unavailable);
4712 };
4713 if !matches!(
4714 snapshot.deliveries.first(),
4715 Some(CommitDelivery::Known { .. })
4716 ) || outcome.sequence != prepared.sequence
4717 || outcome.resolution != CommitOutcomeResolution::Active
4718 || !matches!(
4719 outcome.status,
4720 CommitOutcomeStatus::Conflict | CommitOutcomeStatus::Rejected
4721 )
4722 {
4723 return Err(unavailable);
4724 }
4725 let (Some(operations), Some(rows)) = (outcome.operations, outcome.retained_rows) else {
4726 return Err(unavailable);
4727 };
4728 if rows.is_empty() || operations.len() != rows.len() {
4729 return Err(unavailable);
4730 }
4731 let after =
4732 load_units(&self.conn).map_err(|_| CurrentBaseProofReason::SnapshotFailed)?;
4733 let complete: HashSet<&str> = snapshot
4734 .units
4735 .iter()
4736 .filter_map(|unit| match unit {
4737 UnitCatchup::Known {
4738 id,
4739 bootstrap_complete: true,
4740 ..
4741 } => Some(id.as_str()),
4742 _ => None,
4743 })
4744 .collect();
4745 let candidates: Vec<&UnitRecord> = after
4748 .iter()
4749 .filter(|unit| {
4750 complete.contains(unit.id.as_str())
4751 && prepared.units.iter().any(|before| before.id == unit.id)
4752 })
4753 .collect();
4754 let covered = |table_name: &str, row: &Map<String, Value>| {
4755 let Some(table) = self.schema.table(table_name) else {
4756 return false;
4757 };
4758 candidates.iter().any(|unit| {
4759 covers(table, &unit.scopes)
4760 && pair_scopes(&unit.scopes, table_name, unit.echo.as_deref()).is_some_and(
4761 |scopes| {
4762 scopes.iter().all(|(variable, allowed)| {
4763 table
4764 .scope_column(variable)
4765 .and_then(|column| match row.get(column) {
4766 Some(Value::String(value)) => Some(value.clone()),
4767 Some(Value::Bool(value)) => Some(value.to_string()),
4768 Some(Value::Number(value)) => {
4769 value.as_f64().map(|number| {
4770 ryu_js::Buffer::new()
4771 .format_finite(number)
4772 .to_owned()
4773 })
4774 }
4775 _ => None,
4776 })
4777 .is_some_and(|value| {
4778 !value.is_empty() && allowed.contains(&value)
4779 })
4780 })
4781 },
4782 )
4783 })
4784 };
4785 for row in &rows {
4786 let Some(base) = row.server_row.as_ref().or(row.local_row.as_ref()) else {
4787 return Err(if report.revoked.is_empty() {
4788 CurrentBaseProofReason::MissingCoverage
4789 } else {
4790 CurrentBaseProofReason::Revoked
4791 });
4792 };
4793 if !covered(&row.table, base)
4794 || row
4795 .unique_conflicts
4796 .iter()
4797 .any(|conflict| !covered(&row.table, &conflict.server_row))
4798 {
4799 return Err(if report.revoked.is_empty() {
4800 CurrentBaseProofReason::MissingCoverage
4801 } else {
4802 CurrentBaseProofReason::Revoked
4803 });
4804 }
4805 }
4806 Ok(rows)
4807 })();
4808 if self
4809 .conn
4810 .execute_batch("RELEASE syncular_base_proof")
4811 .is_err()
4812 {
4813 return CurrentBaseProof::failure(CurrentBaseProofReason::SnapshotFailed);
4814 }
4815 match proof {
4816 Ok(rows) => CurrentBaseProof {
4817 proven: true,
4818 rows: Some(rows),
4819 reason: None,
4820 },
4821 Err(reason) => CurrentBaseProof::failure(reason),
4822 }
4823 }
4824
4825 pub fn prove_current_base(
4826 &mut self,
4827 transport: &mut dyn Transport,
4828 client_commit_id: &str,
4829 sequence: i64,
4830 max_rounds: Option<u32>,
4831 ) -> CurrentBaseProof {
4832 let prepared = match self.prepare_current_base(client_commit_id, sequence) {
4833 Ok(prepared) => prepared,
4834 Err(proof) => return proof,
4835 };
4836 let drive = self.sync_until_idle(transport, max_rounds);
4837 self.finish_current_base(prepared, drive)
4838 }
4839
4840 pub fn commit_outcomes(&self, query: CommitOutcomeQuery) -> Result<Vec<CommitOutcome>, String> {
4841 if query.limit == Some(0) {
4842 return Err("sync.invalid_request: commit outcome limit must be positive".to_owned());
4843 }
4844 let mut sql = String::from(
4845 "SELECT seq, client_commit_id, status, recorded_at_ms, results_json, operations_json,
4846 resolution, resolved_at_ms, replacement_client_commit_id
4847 FROM _syncular_commit_outcomes",
4848 );
4849 if query.active_only {
4850 sql.push_str(" WHERE resolution = 'active' AND status IN ('conflict', 'rejected')");
4851 }
4852 sql.push_str(" ORDER BY seq DESC");
4853 if let Some(limit) = query.limit {
4854 sql.push_str(&format!(" LIMIT {limit}"));
4855 }
4856 let mut stmt = self
4857 .conn
4858 .prepare(&sql)
4859 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4860 let rows = stmt
4861 .query_map([], |row| {
4862 Ok(StoredCommitOutcomeRow {
4863 sequence: row.get(0)?,
4864 client_commit_id: row.get(1)?,
4865 status: row.get(2)?,
4866 recorded_at_ms: row.get(3)?,
4867 results_json: row.get(4)?,
4868 operations_json: row.get(5)?,
4869 resolution: row.get(6)?,
4870 resolved_at_ms: row.get(7)?,
4871 replacement_client_commit_id: row.get(8)?,
4872 })
4873 })
4874 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4875 let mut outcomes = Vec::new();
4876 for row in rows {
4877 outcomes.push(self.attach_retained_rows(Self::outcome_from_row(
4878 row.map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?,
4879 )?)?);
4880 }
4881 Ok(outcomes)
4882 }
4883
4884 fn prune_commit_outcomes(&self) -> Result<(), String> {
4885 #[cfg(test)]
4886 self.outcome_prune_count
4887 .set(self.outcome_prune_count.get() + 1);
4888 let max_entries = self.limits.outcome_retention_max_entries.unwrap_or(1_000);
4889 let count = self
4890 .conn
4891 .query_row(
4892 "SELECT COUNT(*) FROM _syncular_commit_outcomes",
4893 [],
4894 |row| row.get::<_, i64>(0),
4895 )
4896 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
4897 as usize;
4898 let excess = count.saturating_sub(max_entries);
4899 if excess == 0 {
4900 return Ok(());
4901 }
4902 let mut stmt = self
4903 .conn
4904 .prepare(
4905 "SELECT seq FROM _syncular_commit_outcomes
4906 WHERE status IN ('applied', 'cached') OR resolution != 'active'
4907 ORDER BY seq ASC LIMIT ?1",
4908 )
4909 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4910 let rows = stmt
4911 .query_map(rusqlite::params![excess as i64], |row| row.get::<_, i64>(0))
4912 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4913 let sequences = rows
4914 .collect::<Result<Vec<_>, _>>()
4915 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4916 drop(stmt);
4917 for sequence in sequences {
4918 self.conn
4919 .execute(
4920 "DELETE FROM _syncular_commit_outcomes WHERE seq = ?1",
4921 rusqlite::params![sequence],
4922 )
4923 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4924 }
4925 Ok(())
4926 }
4927
4928 pub fn units(&self) -> Result<Vec<UnitRecord>, String> {
4932 self.stored(load_units(&self.conn))
4933 }
4934
4935 pub fn unit(&self, id: &str) -> Result<Option<UnitRecord>, String> {
4936 self.stored(get_unit(&self.conn, id))
4937 }
4938
4939 pub fn subscribe(&mut self, scopes: UnitScopes) -> Result<String, String> {
4941 Ok(self.subscribe_many(vec![scopes])?.remove(0))
4942 }
4943
4944 pub fn subscribe_many(&mut self, units: Vec<UnitScopes>) -> Result<Vec<String>, String> {
4951 self.begin_observation("syncular_subscribe")?;
4952 let mut changed = false;
4953 let registered = (|| {
4954 let mut ids = Vec::with_capacity(units.len());
4955 for scopes in units {
4956 let (id, held) = self.hold_unit(scopes, true)?;
4957 changed |= held;
4958 ids.push(id);
4959 }
4960 self.conn
4961 .execute_batch("RELEASE syncular_subscribe")
4962 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
4963 Ok(ids)
4964 })();
4965 let ids = match registered {
4966 Ok(ids) => ids,
4967 Err(error) => {
4968 self.rollback_observation("syncular_subscribe");
4969 return Err(error);
4970 }
4971 };
4972 if changed {
4973 self.registration_epoch = self.registration_epoch.wrapping_add(1);
4974 self.set_sync_needed(true, true);
4975 }
4976 Ok(ids)
4977 }
4978
4979 pub fn unsubscribe(&mut self, id: &str) -> Result<(), String> {
4982 let Some(unit) = self.stored(get_unit(&self.conn, id))? else {
4983 return Ok(());
4984 };
4985 if !unit.explicit {
4986 return Ok(());
4987 }
4988 if !self.stored(window_refs(&self.conn, id))?.is_empty() {
4989 return self.stored(save_unit(
4990 &self.conn,
4991 &UnitRecord {
4992 explicit: false,
4993 ..unit
4994 },
4995 ));
4996 }
4997 self.conn
4998 .execute("DELETE FROM _syncular_units WHERE id = ?1", [id])
4999 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5000 self.registration_epoch = self.registration_epoch.wrapping_add(1);
5001 if unit.status == UnitStatus::Active {
5002 self.set_sync_needed(true, true);
5003 }
5004 Ok(())
5005 }
5006
5007 fn hold_unit(&mut self, scopes: UnitScopes, explicit: bool) -> Result<(String, bool), String> {
5011 if scopes.is_empty()
5012 || scopes.values().any(|value| value == "*")
5013 || !self
5014 .schema
5015 .tables
5016 .iter()
5017 .any(|table| covers(table, &scopes))
5018 {
5019 return Err("sync.invalid_subscription: a unit names one non-wildcard value per variable and covers a table".to_owned());
5020 }
5021 let id = ssp2::registration::unit_id(&scope_pairs(&scopes));
5022 let existing = self.stored(get_unit(&self.conn, &id))?;
5023 if let Some(existing) = existing
5024 .as_ref()
5025 .filter(|unit| unit.status != UnitStatus::Revoked)
5026 {
5027 if explicit && !existing.explicit {
5028 self.stored(save_unit(
5029 &self.conn,
5030 &UnitRecord {
5031 explicit,
5032 ..existing.clone()
5033 },
5034 ))?;
5035 }
5036 return Ok((id, false));
5037 }
5038 self.delete_pending_evict(&id)?;
5039 self.stored(save_unit(
5040 &self.conn,
5041 &UnitRecord {
5042 id: id.clone(),
5043 scopes,
5044 explicit: explicit || existing.is_some_and(|unit| unit.explicit),
5045 status: UnitStatus::Active,
5046 reason_code: None,
5047 cursor: Some(-1),
5048 bootstrap_state: None,
5049 seeded: Vec::new(),
5050 echo: None,
5051 },
5052 ))?;
5053 Ok((id, true))
5054 }
5055
5056 pub fn set_window(
5064 &mut self,
5065 base: &WindowBase,
5066 units: &[String],
5067 ) -> Result<CommandEffects, String> {
5068 let base_key = window_base_key(base);
5069 let live = self.load_window_units(&base_key)?;
5070 let mut changed = false;
5071 for value in units {
5072 if live.iter().any(|(held, _)| held == value) {
5073 continue;
5074 }
5075 let rollback = self.capture();
5076 self.begin_observation("syncular_window")?;
5077 let widened = (|| {
5078 let (id, registered) = self.hold_unit(window_unit_scopes(base, value), false)?;
5079 self.sync_needed |= registered;
5080 self.conn
5081 .execute(
5082 "INSERT OR REPLACE INTO _syncular_windows(base, unit, sub_id) VALUES (?1, ?2, ?3)",
5083 rusqlite::params![base_key, value, id],
5084 )
5085 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5086 let mut batch = ChangeAccumulator::default();
5087 batch.window(&base_key, value);
5088 batch.status = true;
5089 self.finish_observation("syncular_window", batch)
5090 })();
5091 if let Err(error) = widened {
5092 self.rollback_observation("syncular_window");
5093 self.restore(rollback);
5094 return Err(error);
5095 }
5096 changed = true;
5097 self.registration_epoch = self.registration_epoch.wrapping_add(1);
5098 }
5099 for (value, id) in live {
5100 if units.contains(&value) {
5101 continue;
5102 }
5103 self.rebuild_overlay_if_dirty()?;
5104 let rollback = self.capture();
5105 self.begin_observation("syncular_window")?;
5106 let remaining = self.release_window_unit(&base_key, &value, &id);
5107 let remaining = match remaining {
5108 Ok(remaining) => remaining,
5109 Err(error) => {
5110 self.rollback_observation("syncular_window");
5111 self.restore(rollback);
5112 return Err(error);
5113 }
5114 };
5115 changed = true;
5116 self.registration_epoch = self.registration_epoch.wrapping_add(1);
5117 if remaining {
5118 self.drain_pending_evictions()?;
5119 }
5120 }
5121 Ok(if changed && self.transport_enabled {
5122 CommandEffects::interactive()
5123 } else {
5124 CommandEffects::none()
5125 })
5126 }
5127
5128 pub fn window_state(&self, base: &WindowBase) -> Result<WindowState, String> {
5132 persisted_window_state(&self.conn, base).map_err(|failure| failure.message)
5133 }
5134
5135 fn load_window_units(&self, base_key: &str) -> Result<Vec<(String, String)>, String> {
5137 let mut statement = self
5138 .conn
5139 .prepare_cached(
5140 "SELECT unit, sub_id FROM _syncular_windows WHERE base = ?1 ORDER BY unit ASC",
5141 )
5142 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5143 let rows = statement
5144 .query_map([base_key], |row| Ok((row.get(0)?, row.get(1)?)))
5145 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5146 rows.collect::<Result<_, _>>()
5147 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
5148 }
5149
5150 fn load_window_registry(&self) -> Result<Vec<(String, String)>, String> {
5152 let mut statement = self
5153 .conn
5154 .prepare_cached("SELECT base, unit FROM _syncular_windows ORDER BY base, unit")
5155 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5156 let rows = statement
5157 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
5158 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5159 rows.collect::<Result<_, _>>()
5160 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
5161 }
5162
5163 fn release_window_unit(
5171 &mut self,
5172 base_key: &str,
5173 value: &str,
5174 id: &str,
5175 ) -> Result<bool, String> {
5176 self.conn
5177 .execute(
5178 "DELETE FROM _syncular_windows WHERE base = ?1 AND unit = ?2",
5179 rusqlite::params![base_key, value],
5180 )
5181 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5182 let mut batch = ChangeAccumulator::default();
5183 batch.window(base_key, value);
5184 batch.status = true;
5185 let unit = self.stored(get_unit(&self.conn, id))?;
5186 let mut remaining = false;
5187 if let Some(unit) = unit.filter(|unit| !unit.explicit) {
5188 if self.stored(window_refs(&self.conn, id))?.is_empty() {
5189 let units = self.stored(load_units(&self.conn))?;
5190 self.conn
5191 .execute("DELETE FROM _syncular_units WHERE id = ?1", [id])
5192 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5193 self.sync_needed = true;
5194 for table in self.schema.tables.clone() {
5195 let Some(effective) = unit
5196 .echo
5197 .as_deref()
5198 .filter(|_| covers(&table, &unit.scopes))
5199 .and_then(|echo| pair_scopes(&unit.scopes, &table.name, Some(echo)))
5200 else {
5201 continue;
5202 };
5203 let pinned = self.pinned_row_ids(&table.name);
5204 let owners = unit_owners(&units, &table, id);
5205 let (more, deferred) =
5206 self.evict_scope_rows(&table.name, &effective, &owners, &pinned)?;
5207 remaining |= more;
5208 if deferred {
5209 self.save_pending_evict(id, &table.name, &effective)?;
5210 }
5211 self.record_scope_map(&mut batch, &table.name, &effective);
5212 }
5213 }
5214 }
5215 self.finish_observation("syncular_window", batch)?;
5216 Ok(remaining)
5217 }
5218
5219 fn evict_scope_rows(
5224 &self,
5225 table_name: &str,
5226 effective: &[(String, Vec<String>)],
5227 owners: &[Vec<(String, Vec<String>)>],
5228 pinned: &std::collections::HashSet<String>,
5229 ) -> Result<(bool, bool), String> {
5230 let table = self.schema.table(table_name).ok_or("sync.unknown_table")?;
5231 let Some((scope, mut params)) = local_scope_predicate(table, effective, owners)
5232 .map_err(|()| "sync.scope_revoked".to_owned())?
5233 else {
5234 return Ok((false, false));
5235 };
5236 let scope_params = params.len();
5237 let pk = quote_ident(&table.primary_key);
5238 let predicate = if pinned.is_empty() {
5240 scope.clone()
5241 } else {
5242 params.extend(pinned.iter().cloned().map(SqlValue::Text));
5243 let holes = vec!["?"; pinned.len()].join(", ");
5244 format!("{scope} AND CAST({pk} AS TEXT) NOT IN ({holes})")
5245 };
5246 let base = base_table(table_name);
5247 let visible = visible_table(table_name);
5248 let mut ids: Vec<SqlValue> = Vec::new();
5249 for source in [&base, &visible] {
5250 let mut stmt = self
5251 .conn
5252 .prepare_cached(&format!(
5253 "SELECT {pk} FROM {source} WHERE {predicate} LIMIT 1024"
5254 ))
5255 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5256 ids = stmt
5257 .query_map(rusqlite::params_from_iter(¶ms), |row| row.get(0))
5258 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
5259 .collect::<Result<_, _>>()
5260 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5261 if !ids.is_empty() {
5262 break;
5263 }
5264 }
5265 if !ids.is_empty() {
5266 for id in &ids {
5267 self.conn
5268 .execute(
5269 "DELETE FROM _syncular_acked_rows WHERE tbl=?1 AND id=?2",
5270 rusqlite::params![table_name, id],
5271 )
5272 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5273 }
5274 let holes = vec!["?"; ids.len()].join(", ");
5275 for source in [&base, &visible] {
5276 self.conn
5277 .execute(
5278 &format!("DELETE FROM {source} WHERE {pk} IN ({holes})"),
5279 rusqlite::params_from_iter(&ids),
5280 )
5281 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5282 }
5283 }
5284 let mut remaining = false;
5285 let mut deferred = false;
5286 for source in [&base, &visible] {
5287 remaining |= self
5288 .conn
5289 .query_row(
5290 &format!("SELECT EXISTS(SELECT 1 FROM {source} WHERE {predicate})"),
5291 rusqlite::params_from_iter(¶ms),
5292 |row| row.get::<_, bool>(0),
5293 )
5294 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5295 deferred |= self
5296 .conn
5297 .query_row(
5298 &format!("SELECT EXISTS(SELECT 1 FROM {source} WHERE {scope})"),
5299 rusqlite::params_from_iter(¶ms[..scope_params]),
5300 |row| row.get::<_, bool>(0),
5301 )
5302 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5303 }
5304 Ok((remaining, deferred))
5305 }
5306
5307 fn drain_pending_evictions(&mut self) -> Result<(), String> {
5311 let pending = self.load_pending_evictions()?;
5312 if pending.is_empty() {
5313 return Ok(());
5314 }
5315 self.rebuild_overlay_if_dirty()?;
5316 for (unit_id, table_name, effective) in pending {
5317 let Some(table) = self.schema.table(&table_name).cloned() else {
5318 self.delete_pending_evict_table(&unit_id, &table_name)?;
5319 continue;
5320 };
5321 loop {
5322 std::thread::yield_now();
5323 self.begin_observation("syncular_evict")?;
5324 let chunk = (|| {
5325 let pinned = self.pinned_row_ids(&table_name);
5326 let owners =
5327 unit_owners(&self.stored(load_units(&self.conn))?, &table, &unit_id);
5328 let (remaining, deferred) =
5329 self.evict_scope_rows(&table_name, &effective, &owners, &pinned)?;
5330 if !deferred {
5331 self.delete_pending_evict_table(&unit_id, &table_name)?;
5332 }
5333 let mut batch = ChangeAccumulator::default();
5334 self.record_scope_map(&mut batch, &table_name, &effective);
5335 self.finish_observation("syncular_evict", batch)?;
5336 Ok(remaining)
5337 })();
5338 match chunk {
5339 Ok(true) => {}
5340 Ok(false) => break,
5341 Err(error) => {
5342 self.rollback_observation("syncular_evict");
5343 return Err(error);
5344 }
5345 }
5346 }
5347 }
5348 Ok(())
5349 }
5350
5351 fn pinned_row_ids(&self, table: &str) -> std::collections::HashSet<String> {
5354 let mut pinned = std::collections::HashSet::new();
5355 for commit in self
5356 .outbox
5357 .iter()
5358 .map(|commit| &**commit)
5359 .chain(self.failed_commits.iter().map(|(commit, _)| commit))
5360 {
5361 for op in &commit.ops {
5362 if op.table == table {
5363 pinned.insert(op.row_id.clone());
5364 }
5365 }
5366 }
5367 pinned
5368 }
5369
5370 fn save_pending_evict(
5371 &self,
5372 unit_id: &str,
5373 table: &str,
5374 effective: &[(String, Vec<String>)],
5375 ) -> Result<(), String> {
5376 self.conn.execute(
5377 "INSERT OR REPLACE INTO _syncular_pending_evictions(unit_id, tbl, effective_scopes) VALUES (?1, ?2, ?3)",
5378 rusqlite::params![unit_id, table, scope_map_to_json(effective).to_string()],
5379 ).map(|_| ()).map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
5380 }
5381
5382 fn delete_pending_evict(&self, unit_id: &str) -> Result<(), String> {
5384 self.conn
5385 .execute(
5386 "DELETE FROM _syncular_pending_evictions WHERE unit_id = ?1",
5387 [unit_id],
5388 )
5389 .map(|_| ())
5390 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
5391 }
5392
5393 fn delete_pending_evict_table(&self, unit_id: &str, table: &str) -> Result<(), String> {
5394 self.conn
5395 .execute(
5396 "DELETE FROM _syncular_pending_evictions WHERE unit_id = ?1 AND tbl = ?2",
5397 [unit_id, table],
5398 )
5399 .map(|_| ())
5400 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
5401 }
5402
5403 fn load_pending_evictions(&self) -> Result<Vec<PendingEvict>, String> {
5404 let mut stmt = self
5405 .conn
5406 .prepare("SELECT unit_id, tbl, effective_scopes FROM _syncular_pending_evictions")
5407 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5408 let rows = stmt
5409 .query_map([], |row| {
5410 Ok((
5411 row.get::<_, String>(0)?,
5412 row.get::<_, String>(1)?,
5413 row.get::<_, String>(2)?,
5414 ))
5415 })
5416 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5417 rows.map(|row| {
5418 let (unit_id, table, json) =
5419 row.map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5420 let value: Value = serde_json::from_str(&json)
5421 .map_err(|_| "sync.local_corrupt: invalid pending eviction scopes".to_owned())?;
5422 let effective = json_to_scope_map(&value)
5423 .map_err(|_| "sync.local_corrupt: invalid pending eviction scopes".to_owned())?;
5424 Ok((unit_id, table, effective))
5425 })
5426 .collect()
5427 }
5428
5429 pub fn set_retain_failed_commits(&mut self, enabled: bool) -> Result<(), String> {
5431 self.retain_failed_commits = enabled;
5432 for (commit, _) in self.failed_commits.iter() {
5433 for operation in &commit.ops {
5434 self.overlay_dirty.table(&operation.table);
5435 }
5436 }
5437 self.failed_commits = self.load_failed_commits()?.into();
5438 for (commit, _) in self.failed_commits.iter() {
5439 for operation in &commit.ops {
5440 self.overlay_dirty.table(&operation.table);
5441 }
5442 }
5443 Ok(())
5444 }
5445
5446 fn load_failed_commits(&self) -> Result<Vec<FailedCommit>, String> {
5447 let mut statement = self.conn.prepare("SELECT client_commit_id, operations_json, initial_json FROM _syncular_failed_commits ORDER BY seq")
5448 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5449 let rows = statement
5450 .query_map([], |row| {
5451 Ok((
5452 row.get::<_, String>(0)?,
5453 row.get::<_, String>(1)?,
5454 row.get::<_, String>(2)?,
5455 ))
5456 })
5457 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5458 rows.map(|row| {
5459 let (id, operations, initial) =
5460 row.map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5461 let operations: Vec<CommitOperation> =
5462 serde_json::from_str(&operations).map_err(|_| "sync.local_corrupt".to_owned())?;
5463 let initial =
5464 serde_json::from_str(&initial).map_err(|_| "sync.local_corrupt".to_owned())?;
5465 Ok((
5466 OutboxCommit {
5467 client_commit_id: id,
5468 ops: operations
5469 .into_iter()
5470 .map(|op| OutboxOp {
5471 upsert: op.op == "upsert",
5472 table: op.table,
5473 row_id: op.row_id,
5474 base_version: op.base_version,
5475 values: op.values,
5476 insert: op.insert,
5477 })
5478 .collect(),
5479 },
5480 initial,
5481 ))
5482 })
5483 .collect()
5484 }
5485
5486 fn retain_failed_commit(&mut self, id: &str, operations: &[OutboxOp]) -> Result<(), String> {
5487 let mut initial = Map::new();
5488 for operation in operations {
5489 let Some(table) = self.schema.table(&operation.table) else {
5491 continue;
5492 };
5493 let initial_row = match self.stored_row(table, &operation.row_id, true)? {
5494 Some(row) => Some(row),
5495 None => self.visible_row(table, &operation.row_id)?,
5496 };
5497 if let Some((values, _)) = initial_row {
5498 let mut intended: Map<String, Value> = table
5499 .columns
5500 .iter()
5501 .zip(values.iter())
5502 .map(|(column, value)| (column.name.clone(), column_value_to_json(value)))
5503 .collect();
5504 if let Some(patch) = &operation.values {
5505 intended.extend(patch.clone());
5506 }
5507 initial.insert(
5508 serde_json::to_string(&(operation.table.clone(), operation.row_id.clone()))
5509 .map_err(|_| "sync.local_corrupt".to_owned())?,
5510 Value::Object(intended),
5511 );
5512 }
5513 }
5514 self.conn.execute("INSERT INTO _syncular_failed_commits(client_commit_id, operations_json, initial_json) VALUES (?1, ?2, ?3)", rusqlite::params![id,
5515 serde_json::to_string(&operations.iter().map(CommitOperation::from).collect::<Vec<_>>()).map_err(|_| "sync.local_corrupt".to_owned())?,
5516 serde_json::to_string(&initial).map_err(|_| "sync.local_corrupt".to_owned())?])
5517 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5518 self.failed_commits.push((
5519 OutboxCommit {
5520 client_commit_id: id.to_owned(),
5521 ops: operations.to_vec(),
5522 },
5523 initial,
5524 ));
5525 Ok(())
5526 }
5527
5528 fn drop_failed_commits(
5529 &mut self,
5530 matches: impl Fn(&str, &Map<String, Value>) -> bool,
5531 ) -> Result<Vec<OutboxCommit>, String> {
5532 let mut doomed = self.drop_acknowledged_rows(&matches)?;
5533 for (commit, initial) in self.failed_commits.iter() {
5534 for operation in &commit.ops {
5535 let key =
5536 serde_json::to_string(&(operation.table.clone(), operation.row_id.clone()))
5537 .map_err(|_| "sync.local_corrupt".to_owned())?;
5538 if initial
5539 .get(&key)
5540 .and_then(Value::as_object)
5541 .is_some_and(|values| matches(&operation.table, values))
5542 {
5543 doomed.push(commit.clone());
5544 break;
5545 }
5546 }
5547 }
5548 for commit in &doomed {
5549 self.conn
5550 .execute(
5551 "DELETE FROM _syncular_failed_commits WHERE client_commit_id = ?1",
5552 rusqlite::params![commit.client_commit_id],
5553 )
5554 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5555 }
5556 for commit in &doomed {
5557 self.conn.execute("UPDATE _syncular_commit_outcomes SET operations_json = NULL, results_json = '[]' WHERE client_commit_id = ?1", rusqlite::params![commit.client_commit_id]).map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5558 }
5559 self.conflicts.retain(|record| {
5560 !doomed
5561 .iter()
5562 .any(|commit| commit.client_commit_id == record.client_commit_id)
5563 });
5564 self.rejections.retain(|record| {
5565 !doomed
5566 .iter()
5567 .any(|commit| commit.client_commit_id == record.client_commit_id)
5568 });
5569 self.failed_commits.retain(|(commit, _)| {
5570 !doomed
5571 .iter()
5572 .any(|dropped| dropped.client_commit_id == commit.client_commit_id)
5573 });
5574 for commit in &doomed {
5575 for operation in &commit.ops {
5576 self.overlay_dirty.table(&operation.table);
5577 }
5578 }
5579 Ok(doomed)
5580 }
5581
5582 pub fn mutate(&mut self, mutations: Vec<Mutation>) -> Result<String, ClientError> {
5586 let prior = self.storage_failure.borrow_mut().take();
5587 let outcome = (|| -> Result<String, String> {
5588 if mutations.is_empty() {
5589 return Err(
5590 "sync.invalid_request: a commit must contain at least one operation (§6.1)"
5591 .to_owned(),
5592 );
5593 }
5594 let mut ops = Vec::with_capacity(mutations.len());
5595 for mutation in mutations {
5596 match mutation {
5597 Mutation::Patch {
5598 table,
5599 values,
5600 base_version,
5601 } => {
5602 let schema_table = self
5603 .schema
5604 .table(&table)
5605 .ok_or_else(|| "sync.unknown_table".to_owned())?;
5606 let values = normalize_values_casing(schema_table, values)?;
5607 let row_id = render_row_id_json(values.get(&schema_table.primary_key))?;
5608 ops.push(self.prepare_patch(&table, &row_id, values, base_version)?);
5609 }
5610 Mutation::Upsert {
5611 table,
5612 values,
5613 base_version,
5614 } => {
5615 let schema_table = self.schema.table(&table).ok_or_else(|| {
5616 format!("sync.unknown_table: unknown table {table:?}")
5617 })?;
5618 let values = normalize_values_casing(schema_table, values)?;
5622 let row_id = render_row_id_json(values.get(&schema_table.primary_key))?;
5623 let values = full_row_values(schema_table, values)?;
5626 self.validate_author_encode(schema_table, &row_id, &values)?;
5632 let insert = !self.visible_row_exists(schema_table, &row_id)?;
5636 ops.push(OutboxOp {
5637 upsert: true,
5638 table,
5639 row_id,
5640 base_version,
5641 values: Some(values),
5642 insert,
5643 });
5644 }
5645 Mutation::Delete {
5646 table,
5647 row_id,
5648 base_version,
5649 } => {
5650 if self.schema.table(&table).is_none() {
5651 return Err(format!("sync.unknown_table: unknown table {table:?}"));
5652 }
5653 if row_id.contains('\0') {
5655 return Err(
5656 "sync.invalid_request: rowId holds U+0000 (§2.4)".to_owned()
5657 );
5658 }
5659 ops.push(OutboxOp {
5660 upsert: false,
5661 table,
5662 row_id,
5663 base_version,
5664 values: None,
5665 insert: false,
5666 });
5667 }
5668 }
5669 }
5670 self.record_outbox_commit(ops)
5671 })();
5672 self.finish_authoring(prior, outcome)
5673 }
5674
5675 fn finish_authoring(
5681 &self,
5682 prior: Option<QueryReadFailure>,
5683 outcome: Result<String, String>,
5684 ) -> Result<String, ClientError> {
5685 let fresh = self.storage_failure.borrow_mut().take();
5686 *self.storage_failure.borrow_mut() = fresh.clone().or(prior);
5687 match outcome {
5688 Ok(id) => Ok(id),
5689 Err(message) => Err(fresh
5690 .map(ClientError::from)
5691 .unwrap_or_else(|| ClientError::from(message))),
5692 }
5693 }
5694
5695 fn record_outbox_commit(&mut self, ops: Vec<OutboxOp>) -> Result<String, String> {
5696 let commit = OutboxCommit {
5697 client_commit_id: uuid::Uuid::new_v4().to_string(),
5698 ops,
5699 };
5700 self.begin_observation("syncular_mutation")?;
5701 let mut batch = ChangeAccumulator::default();
5702 for op in &commit.ops {
5703 let mut precise = self.record_row_scopes(&mut batch, &op.table, &op.row_id, false);
5704 if let Some(values) = &op.values {
5705 if let Some(table) = self.schema.table(&op.table) {
5706 for scope in &table.scope_variables {
5707 if let Some(Value::String(value)) = values.get(&scope.column) {
5708 batch.scope(&op.table, format!("{}:{value}", scope.prefix));
5709 precise = true;
5710 }
5711 }
5712 }
5713 }
5714 if !precise {
5715 batch.table(&op.table);
5716 }
5717 }
5718 if let Err(error) = self.persist_outbox_insert(&commit) {
5719 self.rollback_observation("syncular_mutation");
5720 return Err(error);
5721 }
5722 let was_dirty = self.overlay_dirty.snapshot();
5728 let id = commit.client_commit_id.clone();
5729 let appended = self.rebuild_overlay_if_dirty().and_then(|()| {
5730 commit
5731 .ops
5732 .iter()
5733 .try_for_each(|op| self.apply_outbox_op(op))?;
5734 self.outbox.push(Arc::new(commit));
5735 batch.status = true;
5736 self.finish_observation("syncular_mutation", batch)
5737 .inspect_err(|_| drop(self.outbox.pop()))
5738 });
5739 if let Err(error) = appended {
5740 self.rollback_observation("syncular_mutation");
5741 self.overlay_dirty.restore(was_dirty);
5742 return Err(error);
5743 }
5744 Ok(id)
5745 }
5746
5747 pub fn patch(
5753 &mut self,
5754 table: &str,
5755 row_id: &str,
5756 partial: Map<String, Value>,
5757 base_version: Option<i64>,
5758 ) -> Result<String, ClientError> {
5759 let prior = self.storage_failure.borrow_mut().take();
5760 let outcome = self.perform_patch(table, row_id, partial, base_version);
5761 self.finish_authoring(prior, outcome)
5762 }
5763
5764 fn perform_patch(
5767 &mut self,
5768 table: &str,
5769 row_id: &str,
5770 partial: Map<String, Value>,
5771 base_version: Option<i64>,
5772 ) -> Result<String, String> {
5773 let operation = self.prepare_patch(table, row_id, partial, base_version)?;
5774 self.record_outbox_commit(vec![operation])
5775 }
5776
5777 fn prepare_patch(
5778 &self,
5779 table: &str,
5780 row_id: &str,
5781 partial: Map<String, Value>,
5782 base_version: Option<i64>,
5783 ) -> Result<OutboxOp, String> {
5784 let schema_table = self
5785 .schema
5786 .table(table)
5787 .ok_or_else(|| format!("sync.unknown_table: unknown table {table:?}"))?;
5788 let partial = normalize_values_casing(schema_table, partial)?;
5789 if let Some(pk) = partial.get(&schema_table.primary_key) {
5790 if render_row_id_json(Some(pk)).ok().as_deref() != Some(row_id) {
5791 return Err(format!(
5792 "sync.invalid_request: table {table:?}: patch cannot change the primary key"
5793 ));
5794 }
5795 }
5796 if !self.visible_row_exists(schema_table, row_id)? {
5797 return Err("sync.row_missing: a sparse patch requires a local row".to_owned());
5798 }
5799 for scope in &schema_table.scope_variables {
5809 if !partial.contains_key(&scope.column) || scope.column == schema_table.primary_key {
5810 continue;
5811 }
5812 let Some(column) = schema_table
5813 .columns
5814 .iter()
5815 .find(|column| column.name == scope.column)
5816 else {
5817 continue;
5818 };
5819 let stored = self
5820 .conn
5821 .query_row(
5822 &format!(
5823 "SELECT {} FROM {} WHERE {}",
5824 quote_ident(&scope.column),
5825 visible_table(&schema_table.name),
5826 row_id_predicate(schema_table)
5827 ),
5828 rusqlite::params![row_id],
5829 |row| Ok(sql_ref_to_json(column, row.get_ref(0)?)),
5830 )
5831 .optional()
5832 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
5833 let incoming = json_to_column_value(column, partial.get(&scope.column))
5838 .ok()
5839 .map(|value| column_value_to_json(&value));
5840 let equal = stored.as_ref().is_some_and(|stored| {
5844 stored.get("$bytes").is_none() && incoming.as_ref() == Some(stored)
5845 });
5846 if !equal {
5847 return Err(format!(
5848 "sync.invalid_request: table {table:?}: patch cannot write scope column {:?} (§3.4)",
5849 scope.column
5850 ));
5851 }
5852 }
5853 let mut values = partial;
5854 for scope in &schema_table.scope_variables {
5857 values.remove(&scope.column);
5858 }
5859 let pk_value = match schema_table.columns[schema_table.pk_index].ty {
5860 ColumnType::Integer => match row_id.parse::<i64>() {
5862 Ok(v)
5863 if v.to_string() == row_id
5864 && (-ssp2::primitives::I64_SAFE_MAX..=ssp2::primitives::I64_SAFE_MAX)
5865 .contains(&v) =>
5866 {
5867 Value::from(v)
5868 }
5869 _ => {
5870 return Err(format!(
5871 "sync.invalid_request: table {table:?}: invalid integer primary key {row_id:?}"
5872 ))
5873 }
5874 },
5875 ColumnType::Boolean => match row_id {
5876 "true" => Value::Bool(true),
5877 "false" => Value::Bool(false),
5878 _ => {
5879 return Err(format!(
5880 "sync.invalid_request: table {table:?}: invalid boolean primary key {row_id:?}"
5881 ))
5882 }
5883 },
5884 _ => Value::from(row_id.to_owned()),
5885 };
5886 values.insert(schema_table.primary_key.clone(), pk_value);
5887 self.validate_author_encode(schema_table, row_id, &values)?;
5893 Ok(OutboxOp {
5894 upsert: true,
5895 table: table.to_owned(),
5896 row_id: row_id.to_owned(),
5897 base_version,
5898 values: Some(values),
5899 insert: false,
5900 })
5901 }
5902
5903 fn stored_key_fallback(
5908 &self,
5909 table: &TableSchema,
5910 row_id: &str,
5911 ) -> Result<Option<Row>, String> {
5912 let Some(selector) = self.encryption.key_id_columns.get(&table.name) else {
5913 return Ok(None);
5914 };
5915 let Some(index) = table
5916 .columns
5917 .iter()
5918 .position(|column| &column.name == selector)
5919 else {
5920 return Ok(None);
5921 };
5922 let sql = format!(
5923 "SELECT {} FROM {} WHERE {}",
5924 quote_ident(selector),
5925 visible_table(&table.name),
5926 row_id_predicate(table)
5927 );
5928 let key = self
5929 .conn
5930 .query_row(&sql, rusqlite::params![row_id], |row| {
5931 row.get::<_, Option<String>>(0)
5932 })
5933 .optional()
5934 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
5935 .flatten()
5936 .filter(|key| !key.is_empty());
5937 let Some(key) = key else {
5938 return Ok(None);
5939 };
5940 let mut fallback: Row = vec![None; table.columns.len()];
5941 fallback[index] = Some(ColumnValue::String(key));
5942 Ok(Some(fallback))
5943 }
5944
5945 fn validate_author_encode(
5950 &self,
5951 table: &TableSchema,
5952 row_id: &str,
5953 values: &Map<String, Value>,
5954 ) -> Result<(), String> {
5955 match encode_sparse_row_json(table, row_id, values, &self.encryption, None) {
5956 Err(error) if error.starts_with(ENCRYPT_FAILED_CODE) => Ok(()),
5957 result => result.map(|_| ()),
5958 }
5959 }
5960
5961 fn drop_unencodable_outbox(
5975 &mut self,
5976 with_encryption: bool,
5977 batch: Option<&mut ChangeAccumulator>,
5978 ) -> Result<bool, String> {
5979 let mut failures = Vec::new();
5980 for commit in self.outbox.iter() {
5981 for op in &commit.ops {
5982 let Some(table) = self.schema.table(&op.table).filter(|_| op.upsert) else {
5983 continue;
5984 };
5985 let Some(values) = op.presented_values(table) else {
5988 continue;
5989 };
5990 let fallback = self.stored_key_fallback(table, &op.row_id)?;
5991 let failure = match encode_sparse_row_json(
5992 table,
5993 &op.row_id,
5994 &values,
5995 &self.encryption,
5996 fallback.as_ref(),
5997 ) {
5998 Err(error) if error.starts_with(ENCRYPT_FAILED_CODE) => {
5999 with_encryption.then_some((error, ENCRYPT_FAILED_CODE))
6000 }
6001 Err(error) if error.starts_with(INVALID_REQUEST_CODE) => Some((
6006 LEGACY_VALUE_REFUSAL_MESSAGE.to_owned(),
6007 OUTBOX_INCOMPATIBLE_CODE,
6008 )),
6009 _ => None,
6010 };
6011 if let Some((error, code)) = failure {
6012 failures.push((commit.clone(), error, code));
6013 break;
6014 }
6015 }
6016 }
6017 if failures.is_empty() {
6018 return Ok(false);
6019 }
6020 let rollback = self.capture();
6026 let owns_observation = batch.is_none();
6027 let owns_transaction = self.conn.is_autocommit();
6028 let mut domains = ChangeAccumulator::default();
6029 if owns_observation {
6030 self.begin_observation("syncular_value_recovery")?;
6031 }
6032 let dropped = (|| -> Result<(), String> {
6033 for (commit, message, code) in failures {
6034 if self.retain_failed_commits && code == ENCRYPT_FAILED_CODE {
6035 self.retain_failed_commit(&commit.client_commit_id, &commit.ops)?;
6036 }
6037 let representable = commit.ops.iter().all(Self::outbox_op_is_journalable);
6038 let rejection = RejectionRecord {
6039 client_commit_id: commit.client_commit_id.clone(),
6040 op_index: 0,
6041 code: code.to_owned(),
6042 message,
6043 retryable: false,
6044 details: (code == OUTBOX_INCOMPATIBLE_CODE).then(|| RejectionDetails {
6045 field_paths: None,
6046 reason: Some(INVALID_STORED_VALUES_REASON.to_owned()),
6047 required_action: None,
6048 references: None,
6049 }),
6050 operation: representable
6051 .then(|| commit.ops.first())
6052 .flatten()
6053 .map(CommitOperation::from),
6054 };
6055 self.persist_commit_outcome(
6056 &commit.client_commit_id,
6057 CommitOutcomeStatus::Rejected,
6058 &[CommitOperationOutcome::Error {
6059 rejection: rejection.clone(),
6060 }],
6061 representable.then_some(commit.ops.as_slice()),
6062 )?;
6063 self.delete_outbox_persisted(&commit.client_commit_id)?;
6064 self.outbox
6065 .retain(|candidate| candidate.client_commit_id != commit.client_commit_id);
6066 self.rejections.push(rejection);
6067 for operation in &commit.ops {
6068 domains.table(operation.table.as_str());
6069 }
6070 }
6071 domains.status = true;
6072 domains.rejections = true;
6073 domains.outcomes = true;
6074 self.prune_commit_outcomes()?;
6075 self.overlay_dirty.set(true);
6077 self.rebuild_overlay()?;
6078 Ok(())
6079 })();
6080 let result = match batch {
6081 Some(batch) => {
6082 if dropped.is_ok() {
6083 for table in domains.tables.keys() {
6084 batch.table(table.as_str());
6085 }
6086 batch.status |= domains.status;
6087 batch.rejections |= domains.rejections;
6088 batch.outcomes |= domains.outcomes;
6089 }
6090 dropped
6091 }
6092 None => {
6093 dropped.and_then(|()| self.finish_observation("syncular_value_recovery", domains))
6094 }
6095 };
6096 if let Err(error) = result {
6097 if owns_observation {
6098 if owns_transaction {
6099 if let Err(rollback) = self.conn.execute_batch("ROLLBACK") {
6100 let mut failure = self.storage_failure.borrow_mut();
6101 let failure =
6102 failure.get_or_insert_with(|| QueryReadFailure::from(error.clone()));
6103 failure.rollback_failure = Some(Box::new(QueryReadFailure::from(rollback)));
6104 }
6105 } else {
6106 self.rollback_observation("syncular_value_recovery");
6107 }
6108 }
6109 self.restore(rollback);
6110 return Err(error);
6111 }
6112 Ok(true)
6113 }
6114
6115 pub fn pending_commit_ids(&self) -> Vec<String> {
6116 self.outbox
6117 .iter()
6118 .map(|c| c.client_commit_id.clone())
6119 .collect()
6120 }
6121
6122 pub fn pending_payloads(&self) -> Result<Vec<Vec<u8>>, String> {
6126 let mut payloads = Vec::new();
6127 for commit in self.outbox.iter() {
6128 for op in &commit.ops {
6129 let Some(table) = self.schema.table(&op.table).filter(|_| op.upsert) else {
6130 continue;
6131 };
6132 let Some(values) = op.presented_values(table) else {
6133 continue;
6134 };
6135 let fallback = self.stored_key_fallback(table, &op.row_id)?;
6136 if let Ok(payload) = encode_sparse_row_json(
6137 table,
6138 &op.row_id,
6139 &values,
6140 &self.encryption,
6141 fallback.as_ref(),
6142 ) {
6143 payloads.push(payload);
6144 }
6145 }
6146 }
6147 Ok(payloads)
6148 }
6149
6150 pub fn conflicts(&self) -> &[ConflictRecord] {
6151 &self.conflicts
6152 }
6153
6154 pub fn rejections(&self) -> &[RejectionRecord] {
6155 &self.rejections
6156 }
6157
6158 pub fn resolve_commit_outcome(
6159 &mut self,
6160 input: ResolveCommitOutcomeInput,
6161 ) -> Result<CommitOutcome, String> {
6162 let current = self
6163 .commit_outcome(&input.client_commit_id)?
6164 .ok_or_else(|| {
6165 format!(
6166 "sync.outcome_not_found: no durable outcome exists for {:?}",
6167 input.client_commit_id
6168 )
6169 })?;
6170 if current.resolution != CommitOutcomeResolution::Active {
6171 return Ok(current);
6172 }
6173 if input.resolution == CommitOutcomeResolution::Active {
6174 return Err("sync.invalid_request: resolution must leave active state".to_owned());
6175 }
6176 match input.resolution {
6177 CommitOutcomeResolution::Superseded => {
6178 let replacement = input
6179 .replacement_client_commit_id
6180 .as_deref()
6181 .filter(|value| !value.is_empty() && *value != input.client_commit_id)
6182 .ok_or_else(|| {
6183 "sync.invalid_request: superseded outcomes require a distinct replacementClientCommitId"
6184 .to_owned()
6185 })?;
6186 let _ = replacement;
6187 }
6188 _ if input.replacement_client_commit_id.is_some() => {
6189 return Err(
6190 "sync.invalid_request: replacementClientCommitId is valid only for superseded outcomes"
6191 .to_owned(),
6192 );
6193 }
6194 _ => {}
6195 }
6196 let allowed = match current.status {
6197 CommitOutcomeStatus::Conflict => matches!(
6198 input.resolution,
6199 CommitOutcomeResolution::ResolvedKeepServer | CommitOutcomeResolution::Superseded
6200 ),
6201 CommitOutcomeStatus::Rejected => {
6202 matches!(
6203 input.resolution,
6204 CommitOutcomeResolution::Superseded
6205 | CommitOutcomeResolution::ResolvedKeepServer
6206 )
6207 }
6208 CommitOutcomeStatus::Applied | CommitOutcomeStatus::Cached => {
6209 input.resolution == CommitOutcomeResolution::Dismissed
6210 }
6211 };
6212 if !allowed {
6213 return Err(format!(
6214 "sync.invalid_request: resolution {:?} is invalid for {:?} outcome",
6215 input.resolution, current.status
6216 ));
6217 }
6218
6219 let rollback = self.capture();
6220 self.begin_observation("syncular_outcome_resolution")?;
6221 let result = (|| {
6222 self.conn
6223 .execute(
6224 "UPDATE _syncular_commit_outcomes
6225 SET resolution = ?1, resolved_at_ms = ?2,
6226 replacement_client_commit_id = ?3
6227 WHERE client_commit_id = ?4 AND resolution = 'active'",
6228 rusqlite::params![
6229 Self::outcome_resolution_name(input.resolution),
6230 self.clock_now_ms(),
6231 input.replacement_client_commit_id,
6232 input.client_commit_id
6233 ],
6234 )
6235 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
6236 let resolved = self
6237 .commit_outcome(¤t.client_commit_id)?
6238 .ok_or_else(|| "sync.outcome_not_found: outcome disappeared".to_owned())?;
6239 self.conn
6240 .execute(
6241 "DELETE FROM _syncular_failed_commits WHERE client_commit_id = ?1",
6242 rusqlite::params![input.client_commit_id],
6243 )
6244 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
6245 for (commit, _) in self.failed_commits.iter() {
6246 if commit.client_commit_id == input.client_commit_id {
6247 for operation in &commit.ops {
6248 self.overlay_dirty.table(&operation.table);
6249 }
6250 }
6251 }
6252 self.failed_commits
6253 .retain(|(commit, _)| commit.client_commit_id != input.client_commit_id);
6254 self.prune_commit_outcomes()?;
6255 Ok(resolved)
6256 })();
6257 let resolved = match result {
6258 Ok(outcome) => outcome,
6259 Err(error) => {
6260 self.rollback_observation("syncular_outcome_resolution");
6261 self.restore(rollback);
6262 return Err(error);
6263 }
6264 };
6265 self.conflicts
6266 .retain(|record| record.client_commit_id != current.client_commit_id);
6267 self.rejections
6268 .retain(|record| record.client_commit_id != current.client_commit_id);
6269 let mut batch = ChangeAccumulator {
6270 conflicts: current.status == CommitOutcomeStatus::Conflict,
6271 rejections: current.status == CommitOutcomeStatus::Rejected,
6272 outcomes: true,
6273 ..ChangeAccumulator::default()
6274 };
6275 if let Some(operations) = ¤t.operations {
6276 for operation in operations {
6277 batch.table(&operation.table);
6278 }
6279 }
6280 if let Err(error) = self
6281 .rebuild_overlay_if_dirty()
6282 .and_then(|()| self.finish_observation("syncular_outcome_resolution", batch))
6283 {
6284 self.rollback_observation("syncular_outcome_resolution");
6285 self.restore(rollback);
6286 return Err(error);
6287 }
6288 Ok(resolved)
6289 }
6290
6291 pub fn schema_floor(&self) -> Option<&SchemaFloor> {
6292 self.schema_floor.as_ref()
6293 }
6294
6295 pub fn lease_state(&self) -> Option<&LeaseState> {
6297 self.lease_state.as_ref()
6298 }
6299
6300 fn record_lease_error(&mut self, code: &str) -> Result<(), String> {
6303 if code != "sync.auth_lease_required" && code != "sync.auth_lease_revoked" {
6304 return Ok(());
6305 }
6306 let mut next = self.lease_state.clone().unwrap_or_default();
6307 next.error_code = Some(code.to_owned());
6308 self.set_lease_state(Some(next))
6309 }
6310
6311 fn set_lease_state(&mut self, next: Option<LeaseState>) -> Result<(), String> {
6312 if self.lease_state == next {
6313 return Ok(());
6314 }
6315 self.begin_observation("syncular_lease")?;
6316 let rollback = self.capture();
6317 self.lease_state = next;
6318 let persisted = match &self.lease_state {
6319 Some(lease) => serde_json::to_string(lease)
6320 .map_err(|error| error.to_string())
6321 .and_then(|json| self.set_meta(LEASE_STATE_KEY, &json)),
6322 None => self.delete_meta(LEASE_STATE_KEY),
6323 };
6324 let batch = ChangeAccumulator {
6325 status: true,
6326 ..ChangeAccumulator::default()
6327 };
6328 if let Err(error) =
6329 persisted.and_then(|()| self.finish_observation("syncular_lease", batch))
6330 {
6331 self.rollback_observation("syncular_lease");
6332 self.restore(rollback);
6333 return Err(error);
6334 }
6335 Ok(())
6336 }
6337
6338 fn set_schema_floor(&mut self, next: Option<SchemaFloor>) -> Result<(), String> {
6339 if self.schema_floor == next {
6340 return Ok(());
6341 }
6342 self.begin_observation("syncular_schema_floor")?;
6343 let rollback = self.capture();
6344 self.stopped = next.is_some();
6345 self.schema_floor = next;
6346 let persisted = match &self.schema_floor {
6347 Some(floor) => serde_json::to_string(floor)
6348 .map_err(|error| error.to_string())
6349 .and_then(|json| self.set_meta(SCHEMA_FLOOR_KEY, &json)),
6350 None => self.delete_meta(SCHEMA_FLOOR_KEY),
6351 };
6352 let batch = ChangeAccumulator {
6353 status: true,
6354 ..ChangeAccumulator::default()
6355 };
6356 if let Err(error) =
6357 persisted.and_then(|()| self.finish_observation("syncular_schema_floor", batch))
6358 {
6359 self.rollback_observation("syncular_schema_floor");
6360 self.restore(rollback);
6361 return Err(error);
6362 }
6363 Ok(())
6364 }
6365
6366 fn set_upgrading(&mut self, value: bool) {
6367 if self.upgrading == value {
6368 return;
6369 }
6370 if self.begin_observation("syncular_upgrading").is_err() {
6371 return;
6372 }
6373 self.upgrading = value;
6374 let batch = ChangeAccumulator {
6375 status: true,
6376 ..ChangeAccumulator::default()
6377 };
6378 if self
6379 .finish_observation("syncular_upgrading", batch)
6380 .is_err()
6381 {
6382 self.rollback_observation("syncular_upgrading");
6383 }
6384 }
6385
6386 pub fn sync_needed(&self) -> bool {
6387 self.sync_needed
6388 }
6389
6390 pub fn read_rows(&self, table: &str) -> Result<Vec<RowState>, String> {
6391 let schema_table = self
6392 .schema
6393 .table(table)
6394 .ok_or_else(|| format!("unknown table {table:?}"))?;
6395 let sql = format!(
6396 "SELECT * FROM {} ORDER BY {} ASC",
6397 visible_table(table),
6398 quote_ident(&schema_table.primary_key)
6399 );
6400 let mut stmt = self
6401 .conn
6402 .prepare(&sql)
6403 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
6404 let mut rows = stmt
6405 .query([])
6406 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
6407 let mut out = Vec::new();
6408 while let Some(row) = rows
6409 .next()
6410 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
6411 {
6412 let mut values = Map::new();
6413 for (i, column) in schema_table.columns.iter().enumerate() {
6414 let value = row
6415 .get_ref(i)
6416 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
6417 values.insert(column.name.clone(), sql_ref_to_json(column, value));
6418 }
6419 let version: i64 = row
6420 .get(schema_table.columns.len())
6421 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
6422 let row_id = match values.get(&schema_table.primary_key) {
6423 Some(Value::String(s)) => s.clone(),
6424 Some(Value::Number(n)) => n.to_string(),
6425 Some(Value::Bool(b)) => b.to_string(),
6426 other => format!("{}", other.cloned().unwrap_or(Value::Null)),
6427 };
6428 out.push(RowState {
6429 row_id,
6430 version,
6431 values,
6432 });
6433 }
6434 Ok(out)
6435 }
6436
6437 #[cfg(feature = "crdt-yjs")]
6453 fn crdt_column_bytes(
6454 &self,
6455 table: &str,
6456 row_id: &str,
6457 column: &str,
6458 ) -> Result<Option<Vec<u8>>, String> {
6459 let schema_table = self
6460 .schema
6461 .table(table)
6462 .ok_or_else(|| format!("unknown table {table:?}"))?;
6463 let col = schema_table
6464 .columns
6465 .iter()
6466 .find(|c| c.name == column)
6467 .ok_or_else(|| format!("table {table:?} has no column {column:?}"))?;
6468 if col.ty != ColumnType::Crdt {
6469 return Err(format!("column {column:?} is not a crdt column (§5.10.1)"));
6470 }
6471 let sql = format!(
6472 "SELECT {} FROM {} WHERE {}",
6473 quote_ident(column),
6474 visible_table(table),
6475 row_id_predicate(schema_table)
6476 );
6477 let bytes: Option<Vec<u8>> = self
6478 .conn
6479 .prepare_cached(&sql)
6480 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
6481 .query_row(rusqlite::params![row_id], |row| {
6482 row.get::<_, Option<Vec<u8>>>(0)
6483 })
6484 .map_err(|e| match e {
6485 rusqlite::Error::QueryReturnedNoRows => "no such row".to_owned(),
6486 other => Self::sqlite_failure(&self.storage_failure, other),
6487 })?;
6488 Ok(bytes)
6489 }
6490
6491 #[cfg(feature = "crdt-yjs")]
6495 pub fn crdt_text(
6496 &self,
6497 table: &str,
6498 row_id: &str,
6499 column: &str,
6500 name: &str,
6501 ) -> Result<String, String> {
6502 let bytes = self
6503 .crdt_column_bytes(table, row_id, column)?
6504 .unwrap_or_default();
6505 crate::crdt::text(&bytes, name)
6506 }
6507
6508 #[cfg(feature = "crdt-yjs")]
6512 pub fn crdt_insert_text(
6513 &mut self,
6514 table: &str,
6515 row_id: &str,
6516 column: &str,
6517 name: &str,
6518 index: u32,
6519 value: &str,
6520 ) -> Result<String, ClientError> {
6521 let prior = self.storage_failure.borrow_mut().take();
6522 let outcome = (|| -> Result<String, String> {
6523 let current = self
6524 .crdt_column_bytes(table, row_id, column)?
6525 .unwrap_or_default();
6526 let update = crate::crdt::insert_text(¤t, name, index, value)?;
6527 self.perform_crdt_push_update(table, row_id, column, &update)
6528 })();
6529 self.finish_authoring(prior, outcome)
6530 }
6531
6532 #[cfg(feature = "crdt-yjs")]
6536 pub fn crdt_delete_text(
6537 &mut self,
6538 table: &str,
6539 row_id: &str,
6540 column: &str,
6541 name: &str,
6542 index: u32,
6543 len: u32,
6544 ) -> Result<String, ClientError> {
6545 let prior = self.storage_failure.borrow_mut().take();
6546 let outcome = (|| -> Result<String, String> {
6547 let current = self
6548 .crdt_column_bytes(table, row_id, column)?
6549 .unwrap_or_default();
6550 let update = crate::crdt::delete_text(¤t, name, index, len)?;
6551 self.perform_crdt_push_update(table, row_id, column, &update)
6552 })();
6553 self.finish_authoring(prior, outcome)
6554 }
6555
6556 #[cfg(feature = "crdt-yjs")]
6561 pub fn crdt_apply_update(
6562 &mut self,
6563 table: &str,
6564 row_id: &str,
6565 column: &str,
6566 update: &[u8],
6567 ) -> Result<String, ClientError> {
6568 let prior = self.storage_failure.borrow_mut().take();
6569 let outcome = (|| -> Result<String, String> {
6570 let current = self
6571 .crdt_column_bytes(table, row_id, column)?
6572 .unwrap_or_default();
6573 let next = crate::crdt::apply_update(¤t, update)?;
6574 self.perform_crdt_push_update(table, row_id, column, &next)
6575 })();
6576 self.finish_authoring(prior, outcome)
6577 }
6578
6579 #[cfg(feature = "crdt-yjs")]
6586 fn perform_crdt_push_update(
6587 &mut self,
6588 table: &str,
6589 row_id: &str,
6590 column: &str,
6591 crdt_bytes: &[u8],
6592 ) -> Result<String, String> {
6593 let mut bytes_obj = Map::new();
6594 bytes_obj.insert("$bytes".to_owned(), Value::from(bytes_to_hex(crdt_bytes)));
6595 let mut partial = Map::new();
6596 partial.insert(column.to_owned(), Value::Object(bytes_obj));
6597 self.perform_patch(table, row_id, partial, None)
6598 }
6599
6600 pub fn query(&self, sql: &str, params: &[QueryValue]) -> Result<Vec<QueryRow>, String> {
6615 query_connection(&self.conn, sql, params).map_err(|failure| failure.to_string())
6616 }
6617
6618 #[cfg(feature = "bench-internals")]
6620 #[doc(hidden)]
6621 pub fn benchmark_connection(&mut self) -> &mut Connection {
6622 &mut self.conn
6623 }
6624
6625 #[cfg(feature = "bench-internals")]
6627 #[doc(hidden)]
6628 pub fn benchmark_phases(
6629 &self,
6630 enabled: Option<bool>,
6631 reset: bool,
6632 ) -> Result<Option<Value>, String> {
6633 self.benchmark_phases.configure(enabled, reset)?;
6634 Ok(self.benchmark_phases.snapshot())
6635 }
6636
6637 pub fn authority_snapshot(&self) -> Result<Value, String> {
6639 validate_authority_reads(&self.schema, &self.limits.authority_reads)?;
6640 if self.limits.authority_reads.is_empty() {
6641 return Err("client.authority_read_forbidden: no authority reads were declared at client creation".into());
6642 }
6643 self.conn
6644 .execute_batch("SAVEPOINT authority_snapshot")
6645 .map_err(|e| e.to_string())?;
6646 let result = (|| {
6647 let revision = self
6648 .get_meta(LOCAL_REVISION_KEY)?
6649 .ok_or("sync.local_corrupt: authority revision is missing")?;
6650 let parsed_revision = revision
6651 .parse::<u64>()
6652 .map_err(|_| "sync.local_corrupt: authority revision is invalid")?;
6653 if parsed_revision.to_string() != revision {
6654 return Err("sync.local_corrupt: authority revision is invalid".into());
6655 }
6656 let mut tables = Vec::new();
6657 for read in &self.limits.authority_reads {
6658 let table = self
6659 .schema
6660 .table(&read.table)
6661 .ok_or("client.authority_read_forbidden: authority table is not declared")?;
6662 let scalar = |column: &str, value: &Value| -> Result<String, String> {
6663 let ty = table
6664 .columns
6665 .iter()
6666 .find(|c| c.name == column)
6667 .expect("validated authority column")
6668 .ty;
6669 if ty == ColumnType::Boolean {
6670 if let Some(value) = value.as_bool() {
6671 return Ok(value.to_string());
6672 }
6673 return match value.as_i64() {
6674 Some(0) => Ok("false".into()),
6675 Some(1) => Ok("true".into()),
6676 _ => Err("sync.local_corrupt: authority boolean is invalid".into()),
6677 };
6678 }
6679 match value {
6680 Value::String(s) => Ok(s.clone()),
6681 Value::Number(n) => Ok(n.to_string()),
6682 Value::Object(o) if ty == ColumnType::Integer && o.len() == 1 => o
6683 .get("$bigint")
6684 .and_then(Value::as_str)
6685 .map(str::to_owned)
6686 .ok_or("sync.local_corrupt: authority integer is invalid".into()),
6687 _ => {
6688 Err("sync.local_corrupt: authority scalar is missing or invalid".into())
6689 }
6690 }
6691 };
6692 let matches = |row: &QueryRow| -> Result<bool, String> {
6693 for (variable, values) in &read.scopes {
6694 let column = table
6695 .scope_column(variable)
6696 .expect("validated authority scope");
6697 let value = row
6698 .get(column)
6699 .ok_or("sync.local_corrupt: authority scope column is missing")?;
6700 if value.is_null() || !values.contains(&scalar(column, value)?) {
6701 return Ok(false);
6702 }
6703 }
6704 Ok(true)
6705 };
6706 let selected = read
6707 .columns
6708 .iter()
6709 .map(|c| quote_ident(c))
6710 .collect::<Vec<_>>()
6711 .join(",");
6712 let mut intent = BTreeSet::new();
6713 let mut relevant = BTreeSet::new();
6714 for row in query_connection(&self.conn,
6715 "SELECT id,values_json FROM (SELECT json_extract(value,'$.table') AS tbl,json_extract(value,'$.rowId') AS id,json_extract(value,'$.values') AS values_json FROM _syncular_outbox,json_each(ops_json) UNION ALL SELECT json_extract(value,'$.table'),json_extract(value,'$.rowId'),json_extract(value,'$.values') FROM _syncular_failed_commits,json_each(operations_json) UNION ALL SELECT tbl,id,intent_json FROM _syncular_acked_rows) WHERE tbl=?",
6716 &[Value::String(read.table.clone())]).map_err(|e| e.to_string())? {
6717 let id = row.get("id").and_then(Value::as_str).ok_or("sync.local_corrupt: authority intent id is invalid")?.to_owned();
6718 intent.insert(id.clone());
6719 if let Some(raw) = row.get("values_json").and_then(Value::as_str) {
6720 let values: QueryRow = serde_json::from_str(raw).map_err(|_| "sync.local_corrupt: authority intent values are invalid")?;
6721 if read.scopes.keys().all(|v| values.contains_key(table.scope_column(v).expect("validated scope"))) && matches(&values)? {relevant.insert(id);}
6722 }
6723 }
6724 let mut rows = BTreeMap::new();
6725 for full in [base_table(&read.table), visible_table(&read.table)] {
6726 for mut values in query_connection(
6727 &self.conn,
6728 &format!(
6729 "SELECT {selected},_syncular_version AS authority_version FROM {full}"
6730 ),
6731 &[],
6732 )
6733 .map_err(|e| e.to_string())?
6734 {
6735 if !matches(&values)? {
6736 continue;
6737 }
6738 let id = scalar(
6739 &table.primary_key,
6740 values
6741 .get(&table.primary_key)
6742 .expect("selected primary key"),
6743 )?;
6744 relevant.insert(id.clone());
6745 if full == base_table(&read.table) {
6746 let version = values
6747 .remove("authority_version")
6748 .expect("selected version");
6749 let version = version
6750 .as_i64()
6751 .ok_or("sync.local_corrupt: authority server version is invalid")?;
6752 if version >= 0 {
6753 rows.insert(id.clone(), json!({"values": values, "version": version, "hasLocalIntent": intent.contains(&id)}));
6754 }
6755 }
6756 }
6757 }
6758 intent.retain(|id| relevant.contains(id));
6759 let rows = rows.into_values().collect::<Vec<_>>();
6760 let replica = self.stored(replica_cursor(&self.conn))?;
6762 let persisted = self
6763 .stored(load_units(&self.conn))?
6764 .into_iter()
6765 .filter(|unit| covers(table, &unit.scopes))
6766 .map(|unit| {
6767 let effective = unit.echo.as_deref().and_then(|echo| {
6768 pair_scopes(&unit.scopes, &read.table, Some(echo))
6769 });
6770 let requested = read
6771 .scopes
6772 .keys()
6773 .map(|v| (v.clone(), json!(unit.scopes.get(v).into_iter().collect::<Vec<_>>())))
6774 .collect::<Map<_, _>>();
6775 let complete = effective
6776 .as_ref()
6777 .is_some_and(|scopes| scopes.len() == read.scopes.len())
6778 && unit.status == UnitStatus::Active
6779 && !unit.bootstrapping();
6780 let exposed = effective.map(|scopes| {
6781 read.scopes
6782 .keys()
6783 .map(|v| {
6784 let held = scopes.iter().find(|(variable, _)| variable == v);
6785 (v.clone(), json!(held.map_or(&Vec::new(), |(_, values)| values)))
6786 })
6787 .collect::<Map<_, _>>()
6788 });
6789 json!({"requestedScopes": requested, "status": unit.status, "cursor": unit.cursor.unwrap_or(replica), "effectiveScopes": exposed, "complete": complete})
6790 })
6791 .collect::<Vec<_>>();
6792 fn covered(selectors: &[(&String, &Vec<String>)], candidates: &[&Value]) -> bool {
6793 match selectors.split_first() {
6794 None => !candidates.is_empty(),
6795 Some(((variable, values), rest)) => values.iter().all(|value| {
6796 covered(
6797 rest,
6798 &candidates
6799 .iter()
6800 .copied()
6801 .filter(|p| {
6802 p["effectiveScopes"][variable.as_str()]
6803 .as_array()
6804 .is_some_and(|held| {
6805 held.contains(&Value::String(value.clone()))
6806 || held.contains(&Value::String("*".into()))
6807 })
6808 })
6809 .collect::<Vec<_>>(),
6810 )
6811 }),
6812 }
6813 }
6814 let coverage = if covered(
6815 &read.scopes.iter().collect::<Vec<_>>(),
6816 &persisted
6817 .iter()
6818 .filter(|p| p["complete"] == true)
6819 .collect::<Vec<_>>(),
6820 ) {
6821 "complete"
6822 } else if persisted.iter().any(|p| {
6823 p["status"] == "active"
6824 && read.scopes.iter().all(|(v, values)| {
6825 values.iter().any(|value| {
6826 p["requestedScopes"][v].as_array().is_some_and(|held| {
6827 held.contains(&Value::String(value.clone()))
6828 })
6829 })
6830 })
6831 }) {
6832 "pending"
6833 } else {
6834 "missing"
6835 };
6836 tables.push(json!({"table": read.table, "rows": rows, "localIntentRowIds": intent, "scopes": read.scopes, "coverage": coverage, "persisted": persisted}));
6837 }
6838 Ok(
6839 json!({"revision": revision, "complete": tables.iter().all(|t| t["coverage"] == "complete"), "tables": tables}),
6840 )
6841 })();
6842 match result {
6843 Ok(snapshot) => {
6844 self.conn
6845 .execute_batch("RELEASE authority_snapshot")
6846 .map_err(|e| e.to_string())?;
6847 Ok(snapshot)
6848 }
6849 Err(error) => {
6850 self.conn
6851 .execute_batch("ROLLBACK TO authority_snapshot; RELEASE authority_snapshot")
6852 .map_err(|e| format!("{error}; rollback failed: {e}"))?;
6853 Err(error)
6854 }
6855 }
6856 }
6857
6858 pub fn query_snapshot(
6862 &mut self,
6863 sql: &str,
6864 params: &[QueryValue],
6865 coverage: &[WindowCoverage],
6866 owner: Option<QueryOwner<'_>>,
6867 ) -> Result<QuerySnapshot, String> {
6868 let result = snapshot_connection(&self.conn, sql, params, coverage);
6869 if let Some(owner) = owner {
6870 self.record_query_read(owner, result.as_ref().err());
6871 }
6872 result.map_err(|failure| failure.to_string())
6873 }
6874
6875 pub fn record_query_read(&mut self, owner: QueryOwner<'_>, failure: Option<&QueryReadFailure>) {
6879 let position = self
6880 .query_failures
6881 .iter()
6882 .position(|entry| entry.id == owner.id);
6883 let Some(failure) = failure else {
6884 if let Some(index) = position {
6885 self.query_failures.remove(index);
6886 }
6887 return;
6888 };
6889 let tables: Vec<String> = BTreeSet::from_iter(owner.tables.iter().copied())
6890 .into_iter()
6891 .map(str::to_owned)
6892 .collect();
6893 let code = failure.code.unwrap_or("client.query_failed").to_owned();
6894 if position.is_some_and(|index| {
6895 let entry = &self.query_failures[index];
6896 entry.tables == tables && entry.code == code && entry.sqlite_code == failure.sqlite_code
6897 }) {
6898 return;
6899 }
6900 if let Some(index) = position {
6901 self.query_failures.remove(index);
6902 }
6903 self.query_failures.push(DiagnosticQueryFailure {
6904 id: owner.id.to_owned(),
6905 tables,
6906 code,
6907 sqlite_code: failure.sqlite_code,
6908 at_ms: self.clock_now_ms(),
6909 });
6910 if self.query_failures.len() > MAX_DIAGNOSTIC_QUERY_FAILURES {
6911 self.query_failures.remove(0);
6912 }
6913 }
6914
6915 fn build_request(&self, url_capable: bool) -> Result<(Message, RequestMeta), Box<SyncOutcome>> {
6918 #[cfg(feature = "bench-internals")]
6919 let _phase = self.benchmark_phases.start(Phase::RequestPrepare);
6920 let log_epoch = self.get_meta(LOG_EPOCH_KEY).map_err(|message| {
6921 Box::new(SyncOutcome::Failed {
6922 error_code: "storage.failed".into(),
6923 message,
6924 details: None,
6925 recommended_action: None,
6926 })
6927 })?;
6928 let max_commits = self.limits.max_push_commits_per_request;
6929 let max_ops = self
6930 .limits
6931 .max_push_operations_per_request
6932 .unwrap_or(PUSH_OPS_PER_REQUEST);
6933 let byte_cap = self.limits.max_push_request_bytes;
6934 let accept = self.limits.accept.unwrap_or(if url_capable {
6937 DEFAULT_ACCEPT | ACCEPT_SIGNED_URLS
6938 } else {
6939 DEFAULT_ACCEPT
6940 });
6941 let storage_failed = |message: String| {
6942 Box::new(SyncOutcome::Failed {
6943 error_code: "storage.failed".into(),
6944 message,
6945 details: None,
6946 recommended_action: None,
6947 })
6948 };
6949 let header = Frame::ReqHeader {
6950 client_id: self.client_id.clone(),
6951 schema_version: self.schema.version,
6952 log_epoch: log_epoch.clone(),
6953 outbox_id: self.get_meta(OUTBOX_ID_KEY).map_err(storage_failed)?,
6954 };
6955 let mut lead = vec![header];
6959 lead.extend(
6960 self.round_failure
6961 .iter()
6962 .map(|code| Frame::RoundFailure { code: code.clone() }),
6963 );
6964 let registered: Vec<UnitRecord> = self
6965 .stored(load_units(&self.conn))
6966 .map_err(storage_failed)?
6967 .into_iter()
6968 .filter(|unit| unit.status == UnitStatus::Active)
6969 .collect();
6970 let registration_digest = ssp2::registration::registration_digest(
6971 registered
6972 .iter()
6973 .map(|unit| scope_pairs(&unit.scopes))
6974 .collect::<Vec<_>>()
6975 .iter()
6976 .map(Vec::as_slice),
6977 );
6978 let echo_digest = self
6979 .get_meta(ECHO_DIGEST_KEY)
6980 .map_err(storage_failed)?
6981 .filter(|hex| !hex.is_empty())
6982 .map(|hex| {
6983 crate::values::hex_to_bytes(&hex)
6984 .ok()
6985 .and_then(|bytes| <[u8; 32]>::try_from(bytes).ok())
6986 .ok_or_else(|| {
6987 "sync.local_corrupt: persisted echo digest is invalid".to_owned()
6988 })
6989 })
6990 .transpose()
6991 .map_err(storage_failed)?;
6992 let pull = Frame::PullHeader {
6993 limit_commits: self.limits.limit_commits.unwrap_or(0),
6994 limit_snapshot_rows: self.limits.limit_snapshot_rows.unwrap_or(0),
6995 max_snapshot_pages: self.limits.max_snapshot_pages.unwrap_or(0),
6996 accept,
6997 cursor: self
6998 .stored(replica_cursor(&self.conn))
6999 .map_err(storage_failed)?,
7000 registration_digest,
7001 echo_digest,
7002 };
7003 let registration_digest = bytes_to_hex(®istration_digest);
7004 let travels = self
7007 .get_meta(REGISTERED_DIGEST_KEY)
7008 .map_err(storage_failed)?
7009 .as_deref()
7010 != Some(registration_digest.as_str());
7011 let mut sub_frames: Vec<Frame> = Vec::new();
7012 if travels {
7013 sub_frames.extend(registered.iter().map(|unit| Frame::Unit {
7014 scopes: scope_pairs(&unit.scopes),
7015 }));
7016 }
7017 sub_frames.extend(registered.iter().filter_map(|unit| {
7019 Some(Frame::Progress {
7020 id: unit.id.clone(),
7021 cursor: unit.cursor?,
7022 bootstrap_state: unit.bootstrap_state.clone().map(RawJson),
7023 })
7024 }));
7025 let fixed_bytes = match byte_cap {
7028 Some(_) => {
7029 let mut frames = lead.clone();
7030 frames.push(pull.clone());
7031 frames.extend(sub_frames.iter().cloned());
7032 encode_message(&Message {
7033 wire_version: WIRE_VERSION,
7034 msg_kind: MsgKind::Request,
7035 frames,
7036 })
7037 .len()
7038 }
7039 None => 0,
7040 };
7041 if let Some(cap) = byte_cap {
7042 if fixed_bytes > cap {
7043 return Err(Self::push_capacity_outcome("bytes", cap, fixed_bytes, None));
7044 }
7045 }
7046 let base_bytes = match byte_cap {
7047 Some(_) => {
7048 let mut frames = lead.clone();
7049 frames.push(pull.clone());
7050 encode_message(&Message {
7051 wire_version: WIRE_VERSION,
7052 msg_kind: MsgKind::Request,
7053 frames,
7054 })
7055 .len()
7056 }
7057 None => 0,
7058 };
7059 let total_outbox = if log_epoch.is_some() {
7060 self.outbox.len()
7061 } else {
7062 0
7063 };
7064 let mut push_frames: Vec<Frame> = Vec::new();
7065 let mut pushed_ids = Vec::new();
7066 let mut ops_in_request = 0usize;
7067 let mut bytes_in_request = 0usize;
7068 let mut deferred_commits = 0usize;
7069 #[cfg(feature = "bench-internals")]
7070 let outbox_phase = self.benchmark_phases.start(Phase::OutboxEncode);
7071 for (index, commit) in self.outbox.iter().take(total_outbox).enumerate() {
7072 if max_commits.is_some_and(|cap| push_frames.len() >= cap) {
7076 deferred_commits = total_outbox - index;
7077 break;
7078 }
7079 if ops_in_request + commit.ops.len() > max_ops {
7080 if push_frames.is_empty() {
7081 return Err(Self::push_capacity_outcome(
7082 "operations",
7083 max_ops,
7084 commit.ops.len(),
7085 Some(commit.client_commit_id.as_str()),
7086 ));
7087 }
7088 deferred_commits = total_outbox - index;
7089 break;
7090 }
7091 let operations = commit
7092 .ops
7093 .iter()
7094 .map(|op| {
7095 let payload = match self
7096 .schema
7097 .table(&op.table)
7098 .and_then(|table| Some((table, op.presented_values(table)?)))
7099 {
7100 Some((table, values)) => {
7101 let fallback =
7111 self.stored_key_fallback(table, &op.row_id)
7112 .map_err(|message| {
7113 Box::new(SyncOutcome::Failed {
7114 error_code: "storage.failed".into(),
7115 message,
7116 details: None,
7117 recommended_action: None,
7118 })
7119 })?;
7120 encode_sparse_row_json(
7121 table,
7122 &op.row_id,
7123 &values,
7124 &self.encryption,
7125 fallback.as_ref(),
7126 )
7127 .ok()
7128 }
7129 None => None,
7130 };
7131 Ok(ssp2::model::Operation {
7132 table: op.table.clone(),
7133 row_id: op.row_id.clone(),
7134 op: if op.upsert { Op::Upsert } else { Op::Delete },
7135 base_version: op.base_version,
7136 payload,
7137 })
7138 })
7139 .collect::<Result<Vec<_>, Box<SyncOutcome>>>()?;
7140 let client_seq = self
7142 .conn
7143 .prepare_cached("SELECT seq FROM _syncular_outbox WHERE commit_id = ?1")
7144 .and_then(|mut statement| {
7145 statement.query_row([&commit.client_commit_id], |row| row.get(0))
7146 })
7147 .map_err(|error| {
7148 storage_failed(Self::sqlite_failure(&self.storage_failure, error))
7149 })?;
7150 let frame = Frame::PushCommit {
7151 client_commit_id: commit.client_commit_id.clone(),
7152 client_seq,
7153 operations,
7154 };
7155 let frame_bytes = match byte_cap {
7156 Some(_) => {
7157 let mut frames = lead.clone();
7158 frames.push(frame.clone());
7159 frames.push(pull.clone());
7160 encode_message(&Message {
7161 wire_version: WIRE_VERSION,
7162 msg_kind: MsgKind::Request,
7163 frames,
7164 })
7165 .len()
7166 - base_bytes
7167 }
7168 None => 0,
7169 };
7170 if let Some(cap) = byte_cap {
7174 if !push_frames.is_empty() && fixed_bytes + bytes_in_request + frame_bytes > cap {
7175 deferred_commits = total_outbox - index;
7176 break;
7177 }
7178 }
7179 push_frames.push(frame);
7180 pushed_ids.push(commit.client_commit_id.clone());
7181 ops_in_request += commit.ops.len();
7182 bytes_in_request += frame_bytes;
7183 }
7184 #[cfg(feature = "bench-internals")]
7185 drop(outbox_phase);
7186 if let Some(cap) = byte_cap {
7187 if fixed_bytes + bytes_in_request > cap {
7188 let blocked = if push_frames.is_empty() {
7191 None
7192 } else {
7193 self.outbox.first().map(|c| c.client_commit_id.as_str())
7194 };
7195 return Err(Self::push_capacity_outcome(
7196 "bytes",
7197 cap,
7198 fixed_bytes + bytes_in_request,
7199 blocked,
7200 ));
7201 }
7202 }
7203 let mut frames = lead;
7204 frames.extend(push_frames);
7205 frames.push(pull);
7206 frames.extend(sub_frames);
7207 let message = Message {
7208 wire_version: WIRE_VERSION,
7209 msg_kind: MsgKind::Request,
7210 frames,
7211 };
7212 Ok((
7213 message,
7214 RequestMeta {
7215 pushed_ids,
7216 registration_digest,
7217 accept,
7218 deferred_commits,
7219 },
7220 ))
7221 }
7222
7223 fn push_capacity_outcome(
7225 kind: &str,
7226 limit: usize,
7227 size: usize,
7228 client_commit_id: Option<&str>,
7229 ) -> Box<SyncOutcome> {
7230 let mut details = Map::new();
7231 details.insert("kind".to_owned(), Value::from(kind));
7232 details.insert("limit".to_owned(), Value::from(limit as u64));
7233 details.insert("size".to_owned(), Value::from(size as u64));
7234 if let Some(id) = client_commit_id {
7235 details.insert("clientCommitId".to_owned(), Value::from(id));
7236 }
7237 Box::new(SyncOutcome::Failed {
7238 error_code: "client.push_request_too_large".into(),
7239 message: "the push request exceeds a configured capacity limit".into(),
7240 details: Some(Value::Object(details)),
7241 recommended_action: None,
7242 })
7243 }
7244
7245 pub fn progress(&self) -> ProgressObserver {
7249 self.progress.clone()
7250 }
7251
7252 pub fn sync(&mut self, transport: &mut dyn Transport) -> SyncOutcome {
7253 let prepared = match self.prepare_sync_round(transport.supports_url_fetch()) {
7254 Ok(prepared) => prepared,
7255 Err(outcome) => return *outcome,
7256 };
7257 let mut next = prepared;
7262 loop {
7263 match self.apply_sync_round(next.exchange(transport)) {
7264 crate::AppliedSyncRound::Continue(prepared) => next = prepared,
7265 crate::AppliedSyncRound::Complete {
7266 outcome, controls, ..
7267 } => {
7268 if self.transport_enabled {
7269 for text in controls {
7270 let _ = transport.realtime_send(&text);
7271 }
7272 } else {
7273 self.disconnect_realtime(transport);
7274 }
7275 return outcome;
7276 }
7277 }
7278 }
7279 }
7280
7281 fn finish_sync_round(&mut self, started_at_ms: i64, mut outcome: SyncOutcome) -> SyncOutcome {
7282 if let SyncOutcome::Failed {
7283 error_code,
7284 message,
7285 details,
7286 ..
7287 } = &mut outcome
7288 {
7289 if let Some((failure, code)) = self
7290 .storage_failure
7291 .borrow()
7292 .as_ref()
7293 .and_then(|failure| failure.code.map(|code| (failure, code)))
7294 {
7295 *error_code = code.to_owned();
7296 *message = match code {
7297 "client.storage_busy" => "local SQLite storage is busy",
7298 "client.storage_full" => "local SQLite storage is full",
7299 "client.storage_io" => "local SQLite storage I/O failed",
7300 "client.storage_corrupt" => "local SQLite storage is corrupt",
7301 _ => unreachable!("unrecognized classified storage failure"),
7302 }
7303 .to_owned();
7304 *details = failure.details();
7305 }
7306 }
7307 let details = match &outcome {
7308 SyncOutcome::Failed { details, .. } => details.as_ref(),
7309 _ => None,
7310 };
7311 let cause_kind = details
7312 .and_then(|details| details.get("causeKind"))
7313 .and_then(Value::as_str)
7314 .and_then(|cause| match cause {
7315 "network" => Some(crate::TransportCauseKind::Network),
7316 "connect" => Some(crate::TransportCauseKind::Connect),
7317 "tls" => Some(crate::TransportCauseKind::Tls),
7318 "io" => Some(crate::TransportCauseKind::Io),
7319 "request" => Some(crate::TransportCauseKind::Request),
7320 "protocol" => Some(crate::TransportCauseKind::Protocol),
7321 "timeout" => Some(crate::TransportCauseKind::Timeout),
7322 "round_deadline" => Some(crate::TransportCauseKind::RoundDeadline),
7323 "aborted" => Some(crate::TransportCauseKind::Aborted),
7324 "body" => Some(crate::TransportCauseKind::Body),
7325 "status" => Some(crate::TransportCauseKind::Status),
7326 "redirect" => Some(crate::TransportCauseKind::Redirect),
7327 "unknown" => Some(crate::TransportCauseKind::Unknown),
7328 _ => None,
7329 });
7330 let http_status = if matches!(
7331 cause_kind,
7332 Some(
7333 crate::TransportCauseKind::Status
7334 | crate::TransportCauseKind::Body
7335 | crate::TransportCauseKind::Redirect
7336 )
7337 ) {
7338 details
7339 .and_then(|details| details.get("httpStatus"))
7340 .and_then(Value::as_f64)
7341 .filter(|status| status.fract() == 0.0 && (100.0..=599.0).contains(status))
7342 .map(|status| status as u16)
7343 } else {
7344 None
7345 };
7346 let retry_delay_ms = self.round_retry_delay_ms;
7347 self.progress.update(|p| match &outcome {
7348 SyncOutcome::Ok(report) | SyncOutcome::BudgetExhausted(report)
7349 if report.failed.is_empty() =>
7350 {
7351 p.state = ProgressState::Complete
7352 }
7353 SyncOutcome::Ok(_) | SyncOutcome::BudgetExhausted(_) => {
7354 p.state = ProgressState::Failed;
7355 p.error_code = Some("sync.scope_revoked".into());
7356 }
7357 SyncOutcome::Failed { error_code, .. } => {
7358 p.state = ProgressState::Failed;
7359 p.error_code = Some(Self::diagnostic_code(error_code));
7360 p.cause_kind = cause_kind;
7361 p.http_status = http_status;
7362 p.retry_delay_ms = retry_delay_ms;
7363 }
7364 SyncOutcome::RealtimeUnavailable { .. } => {
7365 p.state = ProgressState::Failed;
7366 p.error_code = Some(REALTIME_UNAVAILABLE_CODE.to_owned());
7367 p.retry_delay_ms = retry_delay_ms;
7368 }
7369 });
7370 let completed_at_ms = self.clock_now_ms();
7371 self.last_round = Some(match &outcome {
7372 SyncOutcome::Ok(report) | SyncOutcome::BudgetExhausted(report) => DiagnosticLastRound {
7373 status: "succeeded".to_owned(),
7374 started_at_ms,
7375 completed_at_ms,
7376 duration_ms: completed_at_ms.saturating_sub(started_at_ms).max(0),
7377 counters: Some(DiagnosticRoundCounters {
7378 pushed: report.pushed,
7379 applied: report.applied.len(),
7380 rejected: report.rejected.len(),
7381 retryable: report.retryable.len(),
7382 conflicts: report.conflicts,
7383 commits_applied: report.commits_applied,
7384 segment_rows_applied: report.segment_rows_applied,
7385 bootstrapping: report.bootstrapping.len(),
7386 resets: report.resets.len(),
7387 revoked: report.revoked.len(),
7388 failed: report.failed.len(),
7389 deferred_commits: report.deferred_commits,
7390 }),
7391 error_code: None,
7392 cause_kind: None,
7393 http_status: None,
7394 },
7395 SyncOutcome::Failed { error_code, .. } => DiagnosticLastRound {
7396 status: "failed".to_owned(),
7397 started_at_ms,
7398 completed_at_ms,
7399 duration_ms: completed_at_ms.saturating_sub(started_at_ms).max(0),
7400 counters: None,
7401 error_code: Some(Self::diagnostic_code(error_code)),
7402 cause_kind,
7403 http_status,
7404 },
7405 SyncOutcome::RealtimeUnavailable { .. } => DiagnosticLastRound {
7406 status: "failed".to_owned(),
7407 started_at_ms,
7408 completed_at_ms,
7409 duration_ms: completed_at_ms.saturating_sub(started_at_ms).max(0),
7410 counters: None,
7411 error_code: Some(REALTIME_UNAVAILABLE_CODE.to_owned()),
7412 cause_kind: None,
7413 http_status: None,
7414 },
7415 });
7416 outcome
7417 }
7418
7419 pub fn prepare_sync_round(
7422 &mut self,
7423 url_capable: bool,
7424 ) -> Result<crate::PreparedSyncRound, Box<SyncOutcome>> {
7425 if !self.transport_enabled {
7426 return Err(Box::new(SyncOutcome::Failed {
7427 error_code: "sync.offline".into(),
7428 message: "transport gate is closed".into(),
7429 details: None,
7430 recommended_action: None,
7431 }));
7432 }
7433 if self.active_round.is_some() {
7434 return Err(Box::new(SyncOutcome::Failed {
7435 error_code: "client.round_in_progress".into(),
7436 message: "a sync round is already in flight".into(),
7437 details: None,
7438 recommended_action: None,
7439 }));
7440 }
7441 self.storage_failure.borrow_mut().take();
7442 self.progress.start();
7443 self.round_retry_delay_ms = None;
7444 let started_at_ms = self.clock_now_ms();
7445 let prepared = (|| {
7446 if self.stopped {
7447 return Err(Box::new(SyncOutcome::Ok(SyncReport {
7448 schema_floor: self.schema_floor.clone(),
7449 ..SyncReport::default()
7450 })));
7451 }
7452 if self.security_preflight {
7453 return Err(Box::new(SyncOutcome::Failed {
7454 error_code: SECURITY_PREFLIGHT_REQUIRED_CODE.into(),
7455 message: "security preflight must complete before syncing".into(),
7456 details: None,
7457 recommended_action: None,
7458 }));
7459 }
7460 self.set_sync_needed(false, false);
7461 self.drain_pending_evictions()
7462 .and_then(|_| self.drop_incompatible_outbox())
7463 .and_then(|_| self.drop_unencodable_outbox(true, None))
7464 .map_err(|message| {
7465 Box::new(SyncOutcome::Failed {
7466 error_code: "storage.failed".into(),
7467 message,
7468 details: None,
7469 recommended_action: None,
7470 })
7471 })?;
7472 let has_log_epoch = self.get_meta(LOG_EPOCH_KEY).map_err(|message| {
7473 Box::new(SyncOutcome::Failed {
7474 error_code: "storage.failed".into(),
7475 message,
7476 details: None,
7477 recommended_action: None,
7478 })
7479 })?;
7480 let uploads = if has_log_epoch.is_some() && self.schema_has_blobs() {
7481 self.pending_blob_uploads().map_err(|e| {
7482 Box::new(SyncOutcome::Failed {
7483 error_code: e.code,
7484 message: e.message,
7485 details: e.details,
7486 recommended_action: None,
7487 })
7488 })?
7489 } else {
7490 Vec::new()
7491 };
7492 if self.realtime_state != RealtimeState::Connected
7493 && self.realtime_policy == RealtimePolicy::Required
7494 {
7495 let retry_delay_ms = self.schedule_background_retry();
7496 self.realtime_retry_delay_ms = Some(retry_delay_ms);
7497 return Err(Box::new(SyncOutcome::RealtimeUnavailable {
7498 state: self.realtime_state,
7499 reason_code: self.realtime_reason_code.clone(),
7500 retry_delay_ms,
7501 }));
7502 }
7503 let (message, meta) = self.build_request(url_capable)?;
7504 let id = uuid::Uuid::new_v4();
7505 self.active_round = Some(id);
7506 let phase = if uploads.is_empty() {
7507 crate::round::RoundPhase::Sync
7508 } else {
7509 crate::round::RoundPhase::UploadGrant
7510 };
7511 Ok(crate::PreparedSyncRound {
7512 id,
7513 started_at_ms,
7514 message,
7515 meta,
7516 uploads: uploads.into(),
7517 phase,
7518 encoded: None,
7519 continuation: None,
7520 completed: None,
7521 realtime: self.realtime_state == RealtimeState::Connected,
7522 scopes: self
7524 .stored(load_units(&self.conn))
7525 .map_err(|message| {
7526 Box::new(SyncOutcome::Failed {
7527 error_code: "storage.failed".into(),
7528 message,
7529 details: None,
7530 recommended_action: None,
7531 })
7532 })?
7533 .into_iter()
7534 .map(|unit| {
7535 (
7536 unit.id,
7537 canonical_scope_json(&unit_scope_lists(&unit.scopes)),
7538 )
7539 })
7540 .collect(),
7541 #[cfg(feature = "bench-internals")]
7542 benchmark_phases: self.benchmark_phases.clone(),
7543 progress: self.progress.clone(),
7544 fixed_now: self.now_ms,
7545 round_deadline_at: None,
7546 })
7547 })();
7548 prepared.map_err(|outcome| Box::new(self.finish_sync_round(started_at_ms, *outcome)))
7549 }
7550
7551 pub fn cancel_sync_round(&mut self) {
7554 self.active_round = None;
7555 }
7556
7557 pub fn apply_sync_round(
7560 &mut self,
7561 completed: crate::CompletedSyncRound,
7562 ) -> crate::AppliedSyncRound {
7563 let mut prepared = completed.prepared;
7564 if self.active_round != Some(prepared.id) || self.security_preflight {
7565 return crate::AppliedSyncRound::Complete {
7566 outcome: SyncOutcome::Failed {
7567 error_code: "client.round_cancelled".into(),
7568 message: "sync round belongs to a released client context".into(),
7569 details: None,
7570 recommended_action: None,
7571 },
7572 controls: Vec::new(),
7573 more: false,
7574 bootstrap_advanced: false,
7575 };
7576 }
7577 self.storage_failure.borrow_mut().take();
7578 let (response, mut downloads, transport_failed) = match completed.exchange {
7579 crate::round::ExchangeResult::Upload { id, result } => {
7580 let result = match result {
7581 Err(error)
7586 if error
7587 .details
7588 .as_ref()
7589 .and_then(|details| details.get("retryable"))
7590 == Some(&Value::Bool(false)) =>
7591 {
7592 self.conn
7593 .prepare(
7594 "SELECT commit_id FROM _syncular_blob_commit_refs WHERE blob_id = ?",
7595 )
7596 .and_then(|mut statement| {
7597 statement
7598 .query_map(rusqlite::params![id], |row| row.get::<_, String>(0))?
7599 .collect::<Result<HashSet<_>, _>>()
7600 })
7601 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
7602 .and_then(|refused| {
7603 let commits = self
7604 .outbox
7605 .iter()
7606 .filter(|commit| refused.contains(&commit.client_commit_id))
7607 .cloned()
7608 .collect::<Vec<_>>();
7609 if !commits.is_empty() {
7610 self.drop_outbox_commits(&commits, &error.code, &error.message)?;
7611 }
7612 prepared.message.frames.retain(|frame| {
7613 !matches!(frame, Frame::PushCommit { client_commit_id, .. }
7614 if refused.contains(client_commit_id))
7615 });
7616 prepared.meta.pushed_ids.retain(|id| !refused.contains(id));
7617 prepared.continuation = Some(crate::round::RoundPhase::UploadGrant);
7618 Ok(())
7619 })
7620 .map_err(|message| TransportError::new("client.failed", message))
7621 }
7622 result => result,
7623 };
7624 let result = result.and_then(|()| {
7625 self.conn
7626 .execute(
7627 "DELETE FROM _syncular_blob_uploads WHERE blob_id = ?",
7628 rusqlite::params![id],
7629 )
7630 .map(|_| ())
7631 .map_err(|error| {
7632 TransportError::new(
7633 "client.failed",
7634 Self::sqlite_failure(&self.storage_failure, error),
7635 )
7636 })
7637 });
7638 match result {
7639 Ok(()) => return crate::AppliedSyncRound::Continue(prepared),
7640 Err(TransportError {
7641 code,
7642 message,
7643 details,
7644 }) => {
7645 self.active_round = None;
7646 if Self::unreachable_transport(&details) {
7647 self.set_sync_needed(true, false);
7650 } else if Self::retryable_failure_code(&code) {
7651 self.schedule_background_retry();
7652 }
7653 let recommended_action = recommended_action_of(&details);
7654 let outcome = self.finish_sync_round(
7655 prepared.started_at_ms,
7656 SyncOutcome::Failed {
7657 error_code: code,
7658 message,
7659 details,
7660 recommended_action,
7661 },
7662 );
7663 return crate::AppliedSyncRound::Complete {
7664 outcome,
7665 controls: Vec::new(),
7666 more: false,
7667 bootstrap_advanced: false,
7668 };
7669 }
7670 }
7671 }
7672 crate::round::ExchangeResult::Reply {
7673 response,
7674 downloads,
7675 transport_failed,
7676 } => (response, downloads, transport_failed),
7677 };
7678 self.active_round = None;
7679 let before = self.resume_tokens();
7680 let outcome = (|| {
7681 let response = match response {
7682 Ok(response) => {
7683 self.round_failure = None;
7686 response
7687 }
7688 Err(TransportError {
7689 code,
7690 message,
7691 details,
7692 }) => {
7693 if ssp2::model::valid_round_failure_code(&code) {
7698 self.round_failure = Some(code.clone());
7699 }
7700 if transport_failed {
7701 if prepared.realtime {
7702 self.set_realtime_state(RealtimeState::Lost, Some(&code));
7703 }
7704 if let Err(message) = self.record_lease_error(&code) {
7705 return SyncOutcome::Failed {
7706 error_code: "storage.failed".to_owned(),
7707 message,
7708 details: None,
7709 recommended_action: None,
7710 };
7711 }
7712 }
7713 if Self::retryable_failure_code(&code) && !Self::unreachable_transport(&details)
7714 {
7715 let delay = self.schedule_background_retry();
7716 if prepared.realtime && transport_failed {
7717 self.realtime_retry_delay_ms = Some(delay);
7718 }
7719 }
7720 let recommended_action = recommended_action_of(&details);
7721 return SyncOutcome::Failed {
7722 error_code: code,
7723 message,
7724 details,
7725 recommended_action,
7726 };
7727 }
7728 };
7729 let mut outcome = self.process_response(&mut downloads, response, &prepared.meta);
7730 if let SyncOutcome::Ok(report) = &mut outcome {
7731 report.deferred_commits = prepared.meta.deferred_commits;
7732 }
7733 match &outcome {
7734 SyncOutcome::Ok(_) => self.reset_background_retry(),
7735 SyncOutcome::Failed {
7736 error_code,
7737 details,
7738 ..
7739 } if Self::retryable_failure_code(error_code)
7740 && !Self::unreachable_transport(details) =>
7741 {
7742 self.schedule_background_retry();
7743 }
7744 _ => {}
7745 }
7746 if prepared.meta.deferred_commits > 0 {
7747 self.set_sync_needed(true, true);
7748 }
7749 self.previous_version_lifetime_check();
7750 outcome
7751 })();
7752 match &outcome {
7753 SyncOutcome::Ok(report) if !report.bootstrapping.is_empty() => {
7756 self.set_sync_needed(true, false)
7757 }
7758 SyncOutcome::Failed { details, .. } if Self::unreachable_transport(details) => {
7761 self.set_sync_needed(true, false)
7762 }
7763 _ => {}
7764 }
7765 let more = self.transport_enabled
7766 && matches!(&outcome, SyncOutcome::Ok(report)
7767 if report.schema_floor.is_none()
7768 && (!report.bootstrapping.is_empty()
7769 || report.commits_applied > 0
7770 || report.segment_rows_applied > 0
7771 || !report.resets.is_empty()
7772 || report.deferred_commits > 0
7773 || self.sync_needed));
7774 let bootstrap_advanced = matches!(&outcome, SyncOutcome::Ok(report) if report.segment_rows_applied > 0 && !report.bootstrapping.is_empty())
7775 && before != self.resume_tokens();
7776 crate::AppliedSyncRound::Complete {
7777 outcome: self.finish_sync_round(prepared.started_at_ms, outcome),
7778 controls: if self.transport_enabled {
7779 downloads.controls
7780 } else {
7781 Vec::new()
7782 },
7783 more,
7784 bootstrap_advanced,
7785 }
7786 }
7787
7788 fn resume_tokens(&self) -> Option<Vec<(String, Option<String>)>> {
7790 load_units(&self.conn).ok().map(|units| {
7791 units
7792 .into_iter()
7793 .map(|unit| (unit.id, unit.bootstrap_state))
7794 .collect()
7795 })
7796 }
7797
7798 pub const SYNC_IDLE_TIMEOUT_MS: u64 = 60_000;
7800
7801 pub fn sync_until_idle(
7802 &mut self,
7803 transport: &mut dyn Transport,
7804 max_rounds: Option<u32>,
7805 ) -> SyncOutcome {
7806 self.sync_until_idle_with_deadline(transport, max_rounds, Some(Self::SYNC_IDLE_TIMEOUT_MS))
7807 }
7808
7809 pub fn sync_until_idle_with_deadline(
7814 &mut self,
7815 transport: &mut dyn Transport,
7816 max_rounds: Option<u32>,
7817 timeout_ms: Option<u64>,
7818 ) -> SyncOutcome {
7819 if max_rounds == Some(0) {
7822 return SyncOutcome::Failed {
7823 details: None,
7824 error_code: "sync.invalid_request".into(),
7825 message: "maxRounds must be a positive integer".into(),
7826 recommended_action: None,
7827 };
7828 }
7829 if timeout_ms.is_some_and(|ms| ms == 0 || i64::try_from(ms).is_err()) {
7830 return SyncOutcome::Failed {
7831 details: None,
7832 error_code: "sync.invalid_request".into(),
7833 message: "timeoutMs must be a positive integer".into(),
7834 recommended_action: None,
7835 };
7836 }
7837 let started_at_ms = self.clock_now_ms();
7838 let deadline_ms = timeout_ms.map(|ms| started_at_ms.saturating_add(ms as i64));
7839 self.sync_until_idle_bounded(transport, max_rounds, started_at_ms, deadline_ms)
7840 }
7841
7842 fn sync_until_idle_bounded(
7843 &mut self,
7844 transport: &mut dyn Transport,
7845 max_rounds: Option<u32>,
7846 started_at_ms: i64,
7847 deadline_ms: Option<i64>,
7848 ) -> SyncOutcome {
7849 let rounds = max_rounds.unwrap_or(20);
7850 let mut aggregate = SyncReport::default();
7851 let mut spent = 0;
7852 while spent < rounds {
7853 if let Some(deadline_ms) = deadline_ms {
7854 if self.clock_now_ms() >= deadline_ms {
7855 return SyncOutcome::Failed {
7856 error_code: "sync.idle_timeout".into(),
7857 message: "sync_until_idle exceeded its deadline".into(),
7858 details: Some(serde_json::json!({
7859 "timeoutMs": deadline_ms
7860 .saturating_sub(started_at_ms) as u64,
7861 "rounds": spent,
7862 })),
7863 recommended_action: Some("retryLater".into()),
7864 };
7865 }
7866 }
7867 spent += 1;
7868 let before = self.resume_tokens();
7869 match self.sync(transport) {
7870 SyncOutcome::Failed {
7871 error_code,
7872 message,
7873 details,
7874 recommended_action,
7875 } => {
7876 return SyncOutcome::Failed {
7877 details,
7878 error_code,
7879 message,
7880 recommended_action,
7881 };
7882 }
7883 outcome @ SyncOutcome::RealtimeUnavailable { .. } => return outcome,
7884 SyncOutcome::BudgetExhausted(_) => {
7885 unreachable!("sync() never exhausts a round budget")
7886 }
7887 SyncOutcome::Ok(report) => {
7888 if max_rounds.is_none()
7889 && report.segment_rows_applied > 0
7890 && !report.bootstrapping.is_empty()
7891 && before != self.resume_tokens()
7892 {
7893 spent = 0;
7894 }
7895 let stop = !self.transport_enabled() || report.schema_floor.is_some();
7898 aggregate.merge(&report);
7899 if stop {
7900 return SyncOutcome::Ok(aggregate);
7901 }
7902 let more = !report.bootstrapping.is_empty()
7908 || report.commits_applied > 0
7909 || report.segment_rows_applied > 0
7910 || !report.resets.is_empty()
7911 || report.deferred_commits > 0
7912 || self.sync_needed;
7913 if !more {
7914 return SyncOutcome::Ok(aggregate);
7915 }
7916 }
7917 }
7918 }
7919 SyncOutcome::BudgetExhausted(aggregate)
7922 }
7923
7924 fn process_response(
7925 &mut self,
7926 transport: &mut dyn Transport,
7927 response: Message,
7928 meta: &RequestMeta,
7929 ) -> SyncOutcome {
7930 #[cfg(feature = "bench-internals")]
7931 let _phase = self.benchmark_phases.start(Phase::ResponseApply);
7932 let mut report = SyncReport {
7933 pushed: meta.pushed_ids.len() as u32,
7934 ..SyncReport::default()
7935 };
7936 let mut rejection_details_by_commit: HashMap<String, BTreeMap<i32, RejectionDetails>> =
7937 HashMap::new();
7938 let pushed_ids = meta
7939 .pushed_ids
7940 .iter()
7941 .map(String::as_str)
7942 .collect::<HashSet<_>>();
7943 let mut last_final_push_result_id: Option<String> = None;
7944 for frame in &response.frames {
7945 match frame {
7946 Frame::PushResultDetails {
7947 client_commit_id,
7948 entries,
7949 } => {
7950 let details = rejection_details_by_commit
7951 .entry(client_commit_id.clone())
7952 .or_default();
7953 for entry in entries {
7954 let parsed = match RejectionDetails::parse(&entry.details.0) {
7955 Ok(value) => value,
7956 Err(message) => {
7957 return SyncOutcome::Failed {
7958 details: None,
7959 error_code: "sync.invalid_request".to_owned(),
7960 message,
7961 recommended_action: None,
7962 };
7963 }
7964 };
7965 details.insert(entry.op_index, parsed);
7966 }
7967 }
7968 Frame::PushResult {
7969 client_commit_id,
7970 status,
7971 results,
7972 ..
7973 } if pushed_ids.contains(client_commit_id.as_str())
7974 && Self::push_result_is_final(*status, results) =>
7975 {
7976 last_final_push_result_id = Some(client_commit_id.clone());
7977 }
7978 _ => {}
7979 }
7980 }
7981 let mut frames = response.frames.into_iter().peekable();
7982 match frames.next() {
7983 Some(Frame::RespHeader {
7984 required_schema_version,
7985 latest_schema_version,
7986 log_epoch,
7987 reset_required,
7988 }) => {
7989 if let Some(required) = required_schema_version {
7990 let floor = SchemaFloor {
7993 required_schema_version: Some(required),
7994 latest_schema_version,
7995 };
7996 if let Err(message) = self.set_schema_floor(Some(floor.clone())) {
7997 return SyncOutcome::Failed {
7998 details: None,
7999 error_code: "storage.failed".to_owned(),
8000 message,
8001 recommended_action: None,
8002 };
8003 }
8004 report.schema_floor = Some(floor);
8005 return SyncOutcome::Ok(report);
8006 }
8007 if reset_required {
8008 if frames.next().is_some() {
8009 return SyncOutcome::Failed {
8010 details: None,
8011 error_code: "client.invalid_host_response".to_owned(),
8012 message: "log-epoch reset response contains body frames".to_owned(),
8013 recommended_action: None,
8014 };
8015 }
8016 match self.run_log_epoch_reset(&log_epoch) {
8017 Ok(resets) => {
8018 report.resets = resets;
8019 return SyncOutcome::Ok(report);
8020 }
8021 Err(message) => {
8022 return SyncOutcome::Failed {
8023 details: None,
8024 error_code: "sync.local_corrupt".to_owned(),
8025 message,
8026 recommended_action: None,
8027 };
8028 }
8029 }
8030 }
8031 let stored_epoch = match self.get_meta(LOG_EPOCH_KEY) {
8032 Ok(stored) => stored,
8033 Err(message) => {
8034 return SyncOutcome::Failed {
8035 details: None,
8036 error_code: "storage.failed".to_owned(),
8037 message,
8038 recommended_action: None,
8039 };
8040 }
8041 };
8042 if stored_epoch.as_deref() != Some(log_epoch.as_str()) {
8043 return SyncOutcome::Failed {
8044 details: None,
8045 error_code: "client.invalid_host_response".to_owned(),
8046 message: "server changed logEpoch without requiring a reset".to_owned(),
8047 recommended_action: None,
8048 };
8049 }
8050 }
8051 _ => {
8052 return SyncOutcome::Failed {
8053 details: None,
8054 error_code: "sync.invalid_request".to_owned(),
8055 message: "response does not start with RESP_HEADER".to_owned(),
8056 recommended_action: None,
8057 };
8058 }
8059 }
8060
8061 if let Err(message) = self.begin_coalesced_apply() {
8062 return SyncOutcome::Failed {
8063 details: None,
8064 error_code: "storage.failed".into(),
8065 message,
8066 recommended_action: None,
8067 };
8068 }
8069 let mut failure: Option<(String, String, Option<String>, Option<Value>)> = None;
8070 let mut live: Option<(LiveStatus, String, Vec<UnitRecord>)> = None;
8073 let mut live_ended = false;
8074 while let Some(frame) = frames.next() {
8075 match frame {
8076 frame @ Frame::PushResult { .. } => {
8077 let successful = matches!(
8078 &frame,
8079 Frame::PushResult {
8080 status: PushStatus::Applied | PushStatus::Cached,
8081 ..
8082 }
8083 );
8084 let mut results = vec![frame];
8085 if successful {
8086 while matches!(
8087 frames.peek(),
8088 Some(Frame::PushResult {
8089 status: PushStatus::Applied | PushStatus::Cached,
8090 ..
8091 })
8092 ) {
8093 if let Some(next) = frames.next() {
8094 results.push(next);
8095 }
8096 }
8097 }
8098 if self
8099 .handle_push_results(
8100 &results,
8101 &pushed_ids,
8102 &rejection_details_by_commit,
8103 &mut report,
8104 last_final_push_result_id.as_deref(),
8105 )
8106 .is_err()
8107 {
8108 failure = Some((
8109 "client.outcome_persistence_failed".to_owned(),
8110 "local commit outcome could not be persisted".to_owned(),
8111 None,
8112 None,
8113 ));
8114 break;
8115 }
8116 }
8117 Frame::PushResultDetails { .. } => {}
8118 Frame::SubStart {
8119 id,
8120 status,
8121 reason_code,
8122 bootstrap,
8123 fenced,
8124 } => {
8125 let mut body = Vec::new();
8128 let mut sub_end: Option<(i64, Option<String>)> = None;
8129 let mut error: Option<Frame> = None;
8130 for inner in frames.by_ref() {
8131 match inner {
8132 Frame::SubEnd {
8133 next_cursor,
8134 bootstrap_state,
8135 } => {
8136 sub_end = Some((next_cursor, bootstrap_state.map(|r| r.0)));
8137 break;
8138 }
8139 inner @ Frame::Error { .. } => {
8140 error = Some(inner);
8141 break;
8142 }
8143 Frame::Unknown { .. } => {}
8144 other => body.push(other),
8145 }
8146 }
8147 let ended = sub_end.is_some();
8148 let section = SectionStart {
8149 id,
8150 status,
8151 reason_code,
8152 bootstrap,
8153 fenced,
8154 };
8155 match self.process_section(
8156 transport,
8157 §ion,
8158 body,
8159 sub_end,
8160 meta,
8161 &mut report,
8162 ) {
8163 Err(SectionError::Abort(code, message)) => {
8164 failure = Some((code, message, None, None));
8165 break;
8166 }
8167 Err(SectionError::Transport(error)) => {
8168 failure = Some((error.code, error.message, None, error.details));
8169 break;
8170 }
8171 _ => {}
8172 }
8173 match error {
8174 Some(Frame::Error {
8175 code,
8176 message,
8177 recommended_action,
8178 details,
8179 ..
8180 }) => {
8181 failure = Some(Self::error_frame_failure(
8182 code,
8183 message,
8184 recommended_action,
8185 details,
8186 ));
8187 break;
8188 }
8189 Some(_) => unreachable!("only an ERROR closes a section early"),
8190 None if !ended => {
8191 failure = Some((
8192 "sync.invalid_request".to_owned(),
8193 "unit section without SUB_END".to_owned(),
8194 None,
8195 None,
8196 ));
8197 break;
8198 }
8199 None => {}
8200 }
8201 }
8202 Frame::LiveStart {
8203 status,
8204 reason_code,
8205 } => {
8206 let receivers = match self.stored(load_units(&self.conn)) {
8207 Ok(units) => units
8208 .into_iter()
8209 .filter(|unit| {
8210 unit.status == UnitStatus::Active
8211 && unit.echo.is_some()
8212 && !unit.bootstrapping()
8213 })
8214 .collect(),
8215 Err(message) => {
8216 failure = Some(("storage.failed".to_owned(), message, None, None));
8217 break;
8218 }
8219 };
8220 live = Some((status, reason_code, receivers));
8221 }
8222 Frame::Commit {
8223 commit_seq,
8224 tables,
8225 changes,
8226 ..
8227 } => {
8228 let Some((LiveStatus::Active, _, receivers)) = &live else {
8229 continue;
8230 };
8231 let receivers = receivers.clone();
8232 match self.apply_live_commit(
8233 &receivers,
8234 &tables,
8235 changes,
8236 commit_seq,
8237 AppliedServerCommitSource::Pull,
8238 ) {
8239 Ok(applied) => report.commits_applied += u32::from(applied),
8240 Err((code, message)) => {
8241 failure = Some((code, message, None, None));
8242 break;
8243 }
8244 }
8245 }
8246 Frame::LiveEnd {
8247 through_seq,
8248 echo_digest,
8249 } => {
8250 let Some((status, reason_code, _)) = live.take() else {
8251 continue;
8252 };
8253 live_ended = true;
8254 live = Some((status, String::new(), Vec::new()));
8255 if let Err(message) = self.finish_live_section(
8256 status,
8257 &reason_code,
8258 through_seq,
8259 echo_digest,
8260 &meta.registration_digest,
8261 &mut report,
8262 ) {
8263 failure = Some(("storage.failed".to_owned(), message, None, None));
8264 break;
8265 }
8266 }
8267 Frame::Lease {
8268 lease_id,
8269 expires_at_ms,
8270 } => {
8271 if let Err(message) = self.set_lease_state(Some(LeaseState {
8274 lease_id: Some(lease_id),
8275 expires_at_ms: Some(expires_at_ms),
8276 error_code: None,
8277 })) {
8278 failure = Some(("storage.failed".to_owned(), message, None, None));
8279 break;
8280 }
8281 }
8282 Frame::Error {
8283 code,
8284 message,
8285 recommended_action,
8286 details,
8287 ..
8288 } => {
8289 failure = Some(Self::error_frame_failure(
8292 code,
8293 message,
8294 recommended_action,
8295 details,
8296 ));
8297 break;
8298 }
8299 Frame::Unknown { .. } => {}
8300 _ => {
8301 failure = Some((
8302 "sync.invalid_request".to_owned(),
8303 "unexpected frame in response".to_owned(),
8304 None,
8305 None,
8306 ));
8307 break;
8308 }
8309 }
8310 }
8311
8312 let applied = match failure {
8313 Some(failure) => Err(failure),
8314 None => self
8319 .rebuild_overlay_if_dirty()
8320 .and_then(|()| self.drain_pending_evictions())
8321 .map_err(|message| ("storage.failed".to_owned(), message, None, None)),
8322 };
8323 if let Err(message) = self.finish_coalesced_apply() {
8326 return SyncOutcome::Failed {
8327 details: None,
8328 error_code: "storage.failed".into(),
8329 message,
8330 recommended_action: None,
8331 };
8332 }
8333 if let Err((error_code, message, recommended_action, details)) = applied {
8334 return SyncOutcome::Failed {
8335 details,
8336 error_code,
8337 message,
8338 recommended_action,
8339 };
8340 }
8341 let catching_up = match self.stored(load_units(&self.conn)) {
8342 Ok(units) => units
8343 .into_iter()
8344 .filter(|unit| unit.status == UnitStatus::Active && unit.cursor.is_some())
8345 .map(|unit| unit.id)
8346 .collect::<Vec<_>>(),
8347 Err(message) => {
8348 return SyncOutcome::Failed {
8349 details: None,
8350 error_code: "storage.failed".into(),
8351 message,
8352 recommended_action: None,
8353 }
8354 }
8355 };
8356 let status = live.map(|(status, _, _)| status);
8360 if !live_ended || status != Some(LiveStatus::Active) || !catching_up.is_empty() {
8361 self.set_sync_needed(true, status == Some(LiveStatus::Unregistered));
8362 }
8363 self.ack_after_pull(transport);
8364 if self.upgrading && catching_up.is_empty() {
8367 self.set_upgrading(false);
8368 }
8369 report.bootstrapping = catching_up;
8370 SyncOutcome::Ok(report)
8371 }
8372
8373 fn error_frame_failure(
8375 code: String,
8376 message: String,
8377 recommended_action: String,
8378 details: Option<RawJson>,
8379 ) -> (String, String, Option<String>, Option<Value>) {
8380 let recommended_action = (!recommended_action.is_empty()).then_some(recommended_action);
8381 let details = details
8382 .and_then(|details| serde_json::from_str::<Value>(&details.0).ok())
8383 .filter(Value::is_object);
8384 (code, message, recommended_action, details)
8385 }
8386
8387 fn finish_live_section(
8392 &mut self,
8393 status: LiveStatus,
8394 reason_code: &str,
8395 through_seq: i64,
8396 echo_digest: Option<[u8; 32]>,
8397 registration_digest: &str,
8398 report: &mut SyncReport,
8399 ) -> Result<(), String> {
8400 let rollback = self.capture();
8401 let resets = report.resets.len();
8402 self.begin_observation("syncular_live_end")?;
8403 let finished = (|| {
8404 let mut batch = ChangeAccumulator::default();
8405 let before = self.stored(replica_cursor(&self.conn))?;
8406 let mut cursor = if status == LiveStatus::Unregistered {
8407 before
8408 } else {
8409 -1
8410 };
8411 let joined = before.max(through_seq);
8412 for unit in self.stored(load_units(&self.conn))? {
8413 if unit.status != UnitStatus::Active || unit.bootstrapping() {
8414 continue;
8415 }
8416 match status {
8417 LiveStatus::Reset if unit.cursor.is_none() => {
8420 for (base_key, value) in self.stored(window_refs(&self.conn, &unit.id))? {
8421 batch.window(&base_key, &value);
8422 }
8423 report.resets.push(unit.id.clone());
8424 self.stored(save_unit(
8425 &self.conn,
8426 &UnitRecord {
8427 reason_code: Some(reason_code.to_owned()),
8428 cursor: Some(-1),
8429 seeded: Vec::new(),
8430 ..unit
8431 },
8432 ))?;
8433 }
8434 LiveStatus::Active => {
8436 let next = unit.cursor.unwrap_or(joined).max(through_seq);
8437 if next >= joined {
8438 cursor = joined;
8439 }
8440 if unit.cursor.is_some() {
8441 self.stored(save_unit(
8442 &self.conn,
8443 &UnitRecord {
8444 cursor: (next < joined).then_some(next),
8445 ..unit
8446 },
8447 ))?;
8448 }
8449 }
8450 _ => {}
8451 }
8452 }
8453 self.set_meta(REPLICA_CURSOR_KEY, &cursor.to_string())?;
8454 self.set_meta(
8455 ECHO_DIGEST_KEY,
8456 &echo_digest
8457 .map(|digest| bytes_to_hex(&digest))
8458 .unwrap_or_default(),
8459 )?;
8460 if status == LiveStatus::Unregistered {
8461 self.delete_meta(REGISTERED_DIGEST_KEY)?;
8462 } else {
8463 self.set_meta(REGISTERED_DIGEST_KEY, registration_digest)?;
8464 }
8465 self.finish_observation("syncular_live_end", batch)
8466 })();
8467 if finished.is_err() {
8468 self.rollback_observation("syncular_live_end");
8469 self.restore(rollback);
8470 report.resets.truncate(resets);
8471 }
8472 finished
8473 }
8474
8475 fn handle_push_results(
8478 &mut self,
8479 frames: &[Frame],
8480 pushed_ids: &HashSet<&str>,
8481 rejection_details_by_commit: &HashMap<String, BTreeMap<i32, RejectionDetails>>,
8482 report: &mut SyncReport,
8483 last_final_push_result_id: Option<&str>,
8484 ) -> Result<(), String> {
8485 self.begin_observation("syncular_push_results")?;
8486 let rollback = self.capture();
8487 let conflict_count = self.conflicts.len();
8488 let applied_count = report.applied.len();
8489 let rejected_count = report.rejected.len();
8490 let retryable_count = report.retryable.len();
8491 let mut drained = Vec::new();
8492 let mut batch = ChangeAccumulator::default();
8493 let persisted = (|| {
8494 for frame in frames {
8495 let Frame::PushResult {
8496 client_commit_id,
8497 status,
8498 results,
8499 commit_seq,
8500 } = frame
8501 else {
8502 return Err("unexpected acknowledgement frame".to_owned());
8503 };
8504 if !pushed_ids.contains(client_commit_id.as_str()) {
8505 continue;
8506 }
8507 let Some(index) = self
8508 .outbox
8509 .iter()
8510 .position(|commit| commit.client_commit_id == *client_commit_id)
8511 else {
8512 continue;
8513 };
8514 let status = *status;
8515 let rejection_details = rejection_details_by_commit.get(client_commit_id);
8516 let operations = self.outbox[index].ops.clone();
8517 match status {
8518 PushStatus::Applied | PushStatus::Cached => {
8519 let sequence =
8522 commit_seq.ok_or_else(|| "ACK lacks a commit sequence".to_owned())?;
8523 let affected: HashSet<_> = operations
8524 .iter()
8525 .map(|op| (op.table.as_str(), op.row_id.as_str()))
8526 .collect();
8527 let prior = self.acknowledged_operations(Some(&affected), None)?;
8528 for (op_index, operation) in operations.iter().enumerate() {
8529 let delivered: bool = self.conn.query_row("SELECT EXISTS(SELECT 1 FROM _syncular_row_deliveries WHERE tbl=?1 AND id=?2 AND commit_seq>=?3)", rusqlite::params![operation.table, operation.row_id, sequence], |row| row.get(0))
8530 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
8531 if delivered {
8532 continue;
8533 }
8534 let table = self
8535 .schema
8536 .table(&operation.table)
8537 .ok_or_else(|| "sync.unknown_table".to_owned())?;
8538 let mut intended: Map<String, Value> = self
8539 .stored_row(table, &operation.row_id, true)?
8540 .map(|(row, _)| {
8541 table
8542 .columns
8543 .iter()
8544 .zip(&row)
8545 .map(|(column, value)| {
8546 (column.name.clone(), column_value_to_json(value))
8547 })
8548 .collect()
8549 })
8550 .unwrap_or_default();
8551 let mut scope_image = intended.clone();
8552 for op in prior
8553 .iter()
8554 .chain(
8555 self.failed_commits
8556 .iter()
8557 .flat_map(|(commit, _)| &commit.ops),
8558 )
8559 .chain(
8560 self.outbox
8561 .iter()
8562 .take(index)
8563 .flat_map(|commit| &commit.ops),
8564 )
8565 .chain(operations.iter().take(op_index + 1))
8566 {
8567 if op.table != operation.table || op.row_id != operation.row_id {
8568 continue;
8569 }
8570 if let Some(values) = &op.values {
8571 intended.extend(values.clone());
8572 } else {
8573 scope_image = intended.clone();
8574 intended.clear();
8575 }
8576 }
8577 self.conn.execute("INSERT INTO _syncular_acked_rows(commit_id,idx,tbl,id,commit_seq,op_json,intent_json) VALUES (?1,?2,?3,?4,?5,?6,?7)",
8578 rusqlite::params![client_commit_id, op_index as i64, operation.table, operation.row_id, sequence,
8579 serde_json::to_string(&CommitOperation::from(operation)).map_err(|_| "sync.local_corrupt".to_owned())?,
8580 serde_json::to_string(if operation.upsert { &intended } else { &scope_image }).map_err(|_| "sync.local_corrupt".to_owned())?])
8581 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
8582 }
8583 let journal_results = results
8584 .iter()
8585 .map(|result| {
8586 let op_index = match result {
8587 OpResult::Applied { op_index }
8588 | OpResult::Conflict { op_index, .. }
8589 | OpResult::Error { op_index, .. } => *op_index,
8590 };
8591 CommitOperationOutcome::Applied { op_index }
8592 })
8593 .collect::<Vec<_>>();
8594 let outcome_status = if status == PushStatus::Applied {
8595 CommitOutcomeStatus::Applied
8596 } else {
8597 CommitOutcomeStatus::Cached
8598 };
8599 let persisted = self
8600 .persist_commit_outcome(
8601 client_commit_id,
8602 outcome_status,
8603 &journal_results,
8604 None,
8605 )
8606 .and_then(|()| self.delete_outbox_persisted(client_commit_id));
8607 persisted?;
8608 self.sync_needed = true;
8609 batch.status = true;
8610 batch.outcomes = true;
8611 }
8612 PushStatus::Rejected => {
8613 if results.iter().any(|result| {
8614 matches!(
8615 result,
8616 OpResult::Error {
8617 code,
8618 retryable: true,
8619 ..
8620 } if code == "sync.idempotency_cache_miss"
8621 )
8622 }) {
8623 report.retryable.push(client_commit_id.to_owned());
8626 continue;
8627 }
8628
8629 let mut journal_results = Vec::with_capacity(results.len());
8630 let mut conflicts = Vec::new();
8631 let mut rejections = Vec::new();
8632 for result in results {
8633 match result {
8634 OpResult::Applied { op_index } => {
8635 journal_results.push(CommitOperationOutcome::Applied {
8636 op_index: *op_index,
8637 });
8638 }
8639 OpResult::Conflict {
8640 op_index,
8641 code,
8642 message,
8643 server_version,
8644 server_row,
8645 conflict_columns: conflict_columns_bitmap,
8646 } => {
8647 let operation = operations
8648 .get(*op_index as usize)
8649 .map(CommitOperation::from);
8650 let (table, row_id) = operation
8651 .as_ref()
8652 .map(|op| (op.table.clone(), op.row_id.clone()))
8653 .unwrap_or_default();
8654 let server_row_json = self
8655 .schema
8656 .table(&table)
8657 .and_then(|t| {
8658 decode_row_bytes(t, server_row, &self.encryption)
8659 .ok()
8660 .map(|row| (t, row))
8661 })
8662 .map(|(t, row)| {
8663 let mut map = Map::new();
8664 for (i, column) in t.columns.iter().enumerate() {
8665 map.insert(
8666 column.name.clone(),
8667 column_value_to_json(
8668 row.get(i).unwrap_or(&None),
8669 ),
8670 );
8671 }
8672 map
8673 })
8674 .unwrap_or_default();
8675 let conflict_columns = self
8678 .schema
8679 .table(&table)
8680 .map(|t| {
8681 t.columns
8682 .iter()
8683 .enumerate()
8684 .filter(|(index, _)| {
8685 conflict_columns_bitmap
8686 .get(index / 8)
8687 .is_some_and(|byte| {
8688 byte & (1 << (index % 8)) != 0
8689 })
8690 })
8691 .map(|(_, column)| column.name.clone())
8692 .collect::<Vec<_>>()
8693 })
8694 .unwrap_or_default();
8695 let conflict = ConflictRecord {
8696 client_commit_id: client_commit_id.to_owned(),
8697 op_index: *op_index,
8698 table,
8699 row_id,
8700 code: code.clone(),
8701 message: message.clone(),
8702 server_version: *server_version,
8703 server_row: server_row_json,
8704 conflict_columns,
8705 operation,
8706 };
8707 journal_results.push(CommitOperationOutcome::Conflict {
8708 conflict: conflict.clone(),
8709 });
8710 conflicts.push(conflict);
8711 }
8712 OpResult::Error {
8713 op_index,
8714 code,
8715 message,
8716 retryable,
8717 } => {
8718 let rejection = RejectionRecord {
8719 client_commit_id: client_commit_id.to_owned(),
8720 op_index: *op_index,
8721 code: code.clone(),
8722 message: message.clone(),
8723 retryable: *retryable,
8724 details: rejection_details
8725 .and_then(|details| details.get(op_index))
8726 .cloned(),
8727 operation: operations
8728 .get(*op_index as usize)
8729 .map(CommitOperation::from),
8730 };
8731 journal_results.push(CommitOperationOutcome::Error {
8732 rejection: rejection.clone(),
8733 });
8734 rejections.push(rejection);
8735 }
8736 }
8737 }
8738 if self.retain_failed_commits {
8739 self.retain_failed_commit(client_commit_id, &operations)?;
8740 }
8741 let outcome_status = if conflicts.is_empty() {
8742 CommitOutcomeStatus::Rejected
8743 } else {
8744 CommitOutcomeStatus::Conflict
8745 };
8746 let persisted = self
8747 .persist_commit_outcome(
8748 client_commit_id,
8749 outcome_status,
8750 &journal_results,
8751 Some(&operations),
8752 )
8753 .and_then(|()| self.delete_outbox_persisted(client_commit_id));
8754 persisted?;
8755 batch.conflicts = !conflicts.is_empty();
8756 batch.rejections = !rejections.is_empty();
8757 batch.status = true;
8758 batch.outcomes = true;
8759 self.conflicts.extend(conflicts);
8760 self.rejections.extend(rejections);
8761 }
8762 }
8763 drained.push(self.outbox.remove(index));
8764 if batch.status {
8765 for operation in &operations {
8766 if !self.record_row_scopes(
8767 &mut batch,
8768 &operation.table,
8769 &operation.row_id,
8770 false,
8771 ) {
8772 batch.table(&operation.table);
8773 }
8774 }
8775 }
8776 match status {
8777 PushStatus::Applied | PushStatus::Cached => {
8778 report.applied.push(client_commit_id.to_owned())
8779 }
8780 PushStatus::Rejected => report.rejected.push(client_commit_id.to_owned()),
8781 }
8782 }
8783 if batch.outcomes && frames.iter().any(|frame| matches!(frame, Frame::PushResult { client_commit_id, .. } if Some(client_commit_id.as_str()) == last_final_push_result_id)) {
8784 self.prune_commit_outcomes()?;
8785 }
8786 self.reconcile_rows(&drained)?;
8787 self.rebuild_overlay_if_dirty()?;
8788 self.finish_observation("syncular_push_results", batch)
8789 })();
8790 if let Err(error) = persisted {
8791 self.rollback_observation("syncular_push_results");
8792 self.restore(rollback);
8793 report.applied.truncate(applied_count);
8794 report.rejected.truncate(rejected_count);
8795 report.retryable.truncate(retryable_count);
8796 return Err(error);
8797 }
8798 if !rollback.sync_needed && self.sync_needed {
8799 self.sync_intent_queue.push_back(SyncIntent::Interactive);
8800 }
8801 report.conflicts += (self.conflicts.len() - conflict_count) as u32;
8802 Ok(())
8803 }
8804
8805 fn push_result_is_final(status: PushStatus, results: &[OpResult]) -> bool {
8806 status != PushStatus::Rejected
8807 || !results.iter().any(|result| {
8808 matches!(
8809 result,
8810 OpResult::Error {
8811 code,
8812 retryable: true,
8813 ..
8814 } if code == "sync.idempotency_cache_miss"
8815 )
8816 })
8817 }
8818
8819 fn process_section(
8827 &mut self,
8828 transport: &mut dyn Transport,
8829 start: &SectionStart,
8830 body: Vec<Frame>,
8831 sub_end: Option<(i64, Option<String>)>,
8832 meta: &RequestMeta,
8833 report: &mut SyncReport,
8834 ) -> Result<(), SectionError> {
8835 let storage = |message| SectionError::Abort("storage.failed".to_owned(), message);
8836 let Some(unit) = self
8837 .stored(get_unit(&self.conn, &start.id))
8838 .map_err(storage)?
8839 .filter(|unit| unit.status == UnitStatus::Active)
8840 else {
8841 return Ok(());
8842 };
8843 let echo: Vec<EchoRecord> = start
8844 .fenced
8845 .iter()
8846 .map(|record| EchoRecord {
8847 table: record.table.clone(),
8848 status: record.status.name().to_owned(),
8849 scopes: record.scopes.iter().cloned().collect(),
8850 })
8851 .collect();
8852 let mut seeded: BTreeSet<String> = if unit.bootstrap_state.is_none() {
8854 BTreeSet::new()
8855 } else {
8856 unit.seeded.iter().cloned().collect()
8857 };
8858 let applied = (|| {
8859 for frame in body {
8860 match frame {
8861 Frame::SegmentInline { payload } => {
8862 let segment = decode_rows_segment(&payload).map_err(|e| {
8863 SectionError::Abort(e.code.as_str().to_owned(), e.detail)
8864 })?;
8865 self.progress.update(|p| {
8866 p.phase = ProgressPhase::Import;
8867 p.unit_id = Some(unit.id.clone());
8868 p.table = Some(segment.table.clone());
8869 p.segment_id = None;
8870 p.bytes_received = payload.len() as u64;
8871 p.bytes_total = Some(payload.len() as u64);
8872 p.rows_processed = 0;
8873 p.rows_total =
8874 Some(segment.blocks.iter().map(|block| block.len() as u64).sum());
8875 });
8876 let (pair, clear, effective, owners) =
8877 self.segment_pair(&unit, &echo, &seeded, &segment.table)?;
8878 report.segment_rows_applied +=
8879 self.apply_segment(&pair, &segment, clear, &effective, &owners)?;
8880 seeded.insert(segment.table);
8881 }
8882 Frame::SegmentRef {
8883 segment_id,
8884 byte_length,
8885 media_type,
8886 table,
8887 row_count,
8888 as_of_commit_seq,
8889 scope_digest,
8890 row_cursor,
8891 next_row_cursor,
8892 url,
8893 url_expires_at_ms,
8894 ..
8895 } => {
8896 let advertised = match media_type {
8899 MediaType::Rows => {
8900 meta.accept & ACCEPT_EXTERNAL_ROWS != 0
8901 || meta.accept & ACCEPT_INLINE_ROWS != 0
8902 }
8903 MediaType::Sqlite => meta.accept & ACCEPT_SQLITE != 0,
8904 };
8905 if !advertised {
8906 return Err(SectionError::Abort(
8907 "sync.invalid_request".to_owned(),
8908 format!(
8909 "SEGMENT_REF mediaType {} was not advertised in accept (§4.2)",
8910 media_type.name()
8911 ),
8912 ));
8913 }
8914 self.progress.update(|p| {
8915 p.phase = ProgressPhase::Download;
8916 p.unit_id = Some(unit.id.clone());
8917 p.table = Some(table.clone());
8918 p.segment_id = Some(segment_id.clone());
8919 p.bytes_received = 0;
8920 p.bytes_total = u64::try_from(byte_length).ok();
8921 p.rows_processed = 0;
8922 p.rows_total = u64::try_from(row_count).ok();
8923 });
8924 let observer = self.progress.clone();
8925 let mut on_progress = |bytes| observer.update(|p| p.bytes_received = bytes);
8926 let bytes = if let Some(url) = url {
8927 if meta.accept & ACCEPT_SIGNED_URLS == 0 {
8932 return Err(SectionError::Abort(
8933 "sync.invalid_request".to_owned(),
8934 "SEGMENT_REF carries a url but accept bit 3 was not advertised (§5.4)"
8935 .to_owned(),
8936 ));
8937 }
8938 if url_expires_at_ms.is_some_and(|exp| {
8940 exp <= transport
8941 .segment_fetch_started_at(&url)
8942 .unwrap_or_else(|| self.clock_now_ms())
8943 }) {
8944 return Err(SectionError::Abort(
8945 "sync.segment_expired".to_owned(),
8946 format!(
8947 "signed URL for segment {segment_id} expired before fetch — re-pull mints fresh descriptors (§5.4)"
8948 ),
8949 ));
8950 }
8951 transport
8952 .fetch_url(
8953 &url,
8954 u64::try_from(byte_length).unwrap_or(0),
8955 &mut on_progress,
8956 )
8957 .map_err(|e| self.segment_fetch_failed(e))?
8958 } else {
8959 let requested_scopes_json =
8961 canonical_scope_json(&unit_scope_lists(&unit.scopes));
8962 transport
8963 .download_segment(
8964 &SegmentRequest {
8965 segment_id: segment_id.clone(),
8966 table: table.clone(),
8967 requested_scopes_json,
8968 byte_length: u64::try_from(byte_length).unwrap_or(0),
8969 },
8970 &mut on_progress,
8971 )
8972 .map_err(|e| self.segment_fetch_failed(e))?
8973 };
8974 if bytes.len() as i64 > byte_length {
8978 self.schedule_background_retry();
8979 return Err(SectionError::Abort(
8980 "sync.invalid_request".to_owned(),
8981 "segment exceeds its byteLength".to_owned(),
8982 ));
8983 }
8984 on_progress(bytes.len() as u64);
8985 let digest = Sha256::digest(&bytes);
8986 let expected = segment_id
8987 .strip_prefix("sha256:")
8988 .unwrap_or(segment_id.as_str());
8989 if bytes_to_hex(&digest) != expected {
8990 self.schedule_background_retry();
8992 return Err(SectionError::Abort(
8993 "sync.invalid_request".to_owned(),
8994 "segment bytes do not match the content address (§5.1)".to_owned(),
8995 ));
8996 }
8997 self.progress.update(|p| p.phase = ProgressPhase::Import);
8998 if media_type == MediaType::Sqlite {
8999 if row_cursor.is_some() || next_row_cursor.is_some() {
9002 return Err(SectionError::Abort(
9003 "sync.invalid_request".to_owned(),
9004 "sqlite segments are whole-table: rowCursor/nextRowCursor must be absent (§5.3)"
9005 .to_owned(),
9006 ));
9007 }
9008 let (pair, clear, effective, owners) =
9009 self.segment_pair(&unit, &echo, &seeded, &table)?;
9010 report.segment_rows_applied += self.apply_sqlite_segment(
9011 &pair,
9012 &bytes,
9013 clear,
9014 &effective,
9015 &owners,
9016 row_count,
9017 as_of_commit_seq,
9018 &scope_digest,
9019 )?;
9020 seeded.insert(table);
9021 } else {
9022 let segment = decode_rows_segment(&bytes).map_err(|e| {
9023 SectionError::Abort(e.code.as_str().to_owned(), e.detail)
9024 })?;
9025 let (pair, clear, effective, owners) =
9026 self.segment_pair(&unit, &echo, &seeded, &segment.table)?;
9027 report.segment_rows_applied +=
9028 self.apply_segment(&pair, &segment, clear, &effective, &owners)?;
9029 seeded.insert(segment.table);
9030 }
9031 }
9032 _ => {
9033 return Err(SectionError::Abort(
9034 "sync.invalid_request".to_owned(),
9035 "unexpected frame inside a unit section".to_owned(),
9036 ));
9037 }
9038 }
9039 }
9040 Ok(())
9041 })();
9042 match applied {
9043 Ok(()) => {}
9044 Err(SectionError::FailClosed) => {
9045 self.begin_observation("syncular_section_failure")
9048 .map_err(storage)?;
9049 let failed = (|| {
9050 let mut batch = ChangeAccumulator::default();
9051 for (base_key, value) in self.stored(window_refs(&self.conn, &unit.id))? {
9052 batch.window(&base_key, &value);
9053 }
9054 batch.status = true;
9055 self.stored(save_unit(
9056 &self.conn,
9057 &UnitRecord {
9058 status: UnitStatus::Failed,
9059 reason_code: Some("sync.scope_revoked".to_owned()),
9060 ..unit.clone()
9061 },
9062 ))?;
9063 self.finish_observation("syncular_section_failure", batch)
9064 })();
9065 if let Err(message) = failed {
9066 self.rollback_observation("syncular_section_failure");
9067 return Err(storage(message));
9068 }
9069 report.failed.push(unit.id);
9070 return Ok(());
9071 }
9072 Err(error) => return Err(error),
9073 }
9074 let Some((next_cursor, bootstrap_state)) = sub_end else {
9075 return Ok(());
9076 };
9077 let rollback = self.capture();
9078 let counts = (
9079 report.resets.len(),
9080 report.revoked.len(),
9081 report.failed.len(),
9082 );
9083 self.begin_observation("syncular_sub_end")
9084 .map_err(storage)?;
9085 let finished = self
9086 .finish_section(start, echo, next_cursor, bootstrap_state, seeded, report)
9087 .and_then(|batch| {
9088 self.rebuild_overlay_if_dirty()?;
9089 self.finish_observation("syncular_sub_end", batch)
9090 });
9091 if let Err(message) = finished {
9092 self.rollback_observation("syncular_sub_end");
9093 self.restore(rollback);
9094 report.resets.truncate(counts.0);
9095 report.revoked.truncate(counts.1);
9096 report.failed.truncate(counts.2);
9097 return Err(storage(message));
9098 }
9099 Ok(())
9100 }
9101
9102 #[allow(clippy::type_complexity)]
9105 fn segment_pair(
9106 &self,
9107 unit: &UnitRecord,
9108 echo: &[EchoRecord],
9109 seeded: &BTreeSet<String>,
9110 name: &str,
9111 ) -> Result<
9112 (
9113 TableSchema,
9114 bool,
9115 Vec<(String, Vec<String>)>,
9116 Vec<Vec<(String, Vec<String>)>>,
9117 ),
9118 SectionError,
9119 > {
9120 let table = self.schema.table(name).cloned().ok_or_else(|| {
9121 SectionError::Abort(
9122 "sync.schema_mismatch".to_owned(),
9123 format!("segment table {name:?} is not in the client schema"),
9124 )
9125 })?;
9126 let effective = covers(&table, &unit.scopes)
9127 .then(|| pair_scopes(&unit.scopes, name, Some(echo)))
9128 .flatten()
9129 .ok_or_else(|| {
9130 SectionError::Abort(
9131 "sync.invalid_request".to_owned(),
9132 "a segment names no active pair of its unit".to_owned(),
9133 )
9134 })?;
9135 let units = self
9136 .stored(load_units(&self.conn))
9137 .map_err(|message| SectionError::Abort("storage.failed".to_owned(), message))?;
9138 let owners = unit_owners(&units, &table, &unit.id);
9139 Ok((table, !seeded.contains(name), effective, owners))
9140 }
9141
9142 fn finish_section(
9149 &mut self,
9150 start: &SectionStart,
9151 echo: Vec<EchoRecord>,
9152 next_cursor: i64,
9153 bootstrap_state: Option<String>,
9154 seeded: BTreeSet<String>,
9155 report: &mut SyncReport,
9156 ) -> Result<ChangeAccumulator, String> {
9157 #[cfg(feature = "bench-internals")]
9158 let _phase = self.benchmark_phases.start(Phase::CursorPersist);
9159 let mut batch = ChangeAccumulator::default();
9160 let Some(unit) = self
9162 .stored(get_unit(&self.conn, &start.id))?
9163 .filter(|unit| unit.status == UnitStatus::Active)
9164 else {
9165 return Ok(batch);
9166 };
9167 for (base_key, value) in self.stored(window_refs(&self.conn, &unit.id))? {
9168 batch.window(&base_key, &value);
9169 }
9170 if start.status == SubStatus::Reset {
9171 report.resets.push(unit.id.clone());
9174 self.stored(save_unit(
9175 &self.conn,
9176 &UnitRecord {
9177 reason_code: Some(start.reason_code.clone()),
9178 cursor: Some(-1),
9179 bootstrap_state: None,
9180 seeded: Vec::new(),
9181 ..unit
9182 },
9183 ))?;
9184 return Ok(batch);
9185 }
9186 let revoked = start.status == SubStatus::Revoked;
9187 let units = self.stored(load_units(&self.conn))?;
9188 let mut failed = false;
9189 let mut rearmed = false;
9190 for table in self.schema.tables.clone() {
9191 if !covers(&table, &unit.scopes) {
9192 continue;
9193 }
9194 let last = unit
9195 .echo
9196 .as_deref()
9197 .and_then(|last| pair_scopes(&unit.scopes, &table.name, Some(last)));
9198 let next = (!revoked)
9199 .then(|| pair_scopes(&unit.scopes, &table.name, Some(&echo)))
9200 .flatten();
9201 rearmed |= unit.echo.is_some() && last.is_none() && next.is_some();
9203 if let (Some(last), None) = (&last, &next) {
9206 let owners = unit_owners(&units, &table, &unit.id);
9207 failed |= !self.purge_pair(&mut batch, &table, last, &owners)?;
9208 }
9209 }
9210 if revoked || failed {
9211 if revoked {
9212 report.revoked.push(unit.id.clone());
9213 }
9214 if failed {
9215 report.failed.push(unit.id.clone());
9216 }
9217 batch.status = true;
9218 self.stored(save_unit(
9219 &self.conn,
9220 &UnitRecord {
9221 status: if failed {
9222 UnitStatus::Failed
9223 } else {
9224 UnitStatus::Revoked
9225 },
9226 reason_code: Some(if failed {
9227 "sync.scope_revoked".to_owned()
9228 } else {
9229 start.reason_code.clone()
9230 }),
9231 cursor: Some(-1),
9232 bootstrap_state: None,
9233 seeded: Vec::new(),
9234 echo: None,
9235 ..unit
9236 },
9237 ))?;
9238 return Ok(batch);
9239 }
9240 let completed = start.bootstrap && !rearmed && bootstrap_state.is_none();
9241 let (cursor, bootstrap_state, seeded) = if rearmed {
9242 (Some(-1), None, Vec::new())
9243 } else if start.bootstrap {
9244 let seeded = if bootstrap_state.is_none() {
9245 Vec::new()
9246 } else {
9247 seeded.into_iter().collect()
9248 };
9249 (Some(next_cursor), bootstrap_state, seeded)
9250 } else {
9251 (
9252 unit.cursor,
9253 unit.bootstrap_state.clone(),
9254 unit.seeded.clone(),
9255 )
9256 };
9257 self.stored(save_unit(
9258 &self.conn,
9259 &UnitRecord {
9260 reason_code: None,
9261 echo: Some(echo.clone()),
9262 cursor,
9263 bootstrap_state,
9264 seeded,
9265 ..unit.clone()
9266 },
9267 ))?;
9268 if !completed {
9269 return Ok(batch);
9270 }
9271 for table in self.schema.tables.clone() {
9274 let Some(effective) = covers(&table, &unit.scopes)
9275 .then(|| pair_scopes(&unit.scopes, &table.name, Some(&echo)))
9276 .flatten()
9277 else {
9278 continue;
9279 };
9280 let mut statement = self
9281 .conn
9282 .prepare("SELECT id,intent_json FROM _syncular_acked_rows WHERE tbl=?1 AND commit_seq<=?2")
9283 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
9284 let rows = statement
9285 .query_map(rusqlite::params![table.name, next_cursor], |row| {
9286 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
9287 })
9288 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
9289 let mut covered = HashSet::new();
9290 for row in rows {
9291 let (id, intent) =
9292 row.map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
9293 let values: Map<String, Value> =
9294 serde_json::from_str(&intent).map_err(|_| "sync.local_corrupt".to_owned())?;
9295 if owns_local_row(&table, &effective, &[], &values) {
9296 covered.insert(id);
9297 }
9298 }
9299 drop(statement);
9300 for id in covered {
9301 self.conn
9302 .execute(
9303 "DELETE FROM _syncular_acked_rows WHERE tbl=?1 AND id=?2 AND commit_seq<=?3",
9304 rusqlite::params![table.name, id, next_cursor],
9305 )
9306 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
9307 self.overlay_dirty.table(&table.name);
9308 batch.table(&table.name);
9309 }
9310 }
9311 Ok(batch)
9312 }
9313
9314 fn purge_pair(
9320 &mut self,
9321 batch: &mut ChangeAccumulator,
9322 table: &TableSchema,
9323 last: &[(String, Vec<String>)],
9324 owners: &[Vec<(String, Vec<String>)>],
9325 ) -> Result<bool, String> {
9326 if local_scope_predicate(table, last, owners).is_err() {
9327 return Ok(false);
9328 }
9329 for commit in self.drop_failed_commits(|name, values| {
9330 name == table.name && owns_local_row(table, last, owners, values)
9331 })? {
9332 batch.conflicts = true;
9333 batch.rejections = true;
9334 batch.outcomes = true;
9335 for operation in commit.ops {
9336 batch.table(&operation.table);
9337 }
9338 }
9339 self.purge_scope_rows(&table.name, last, owners)
9340 .map_err(|()| "storage.failed: the revoked scope purge failed".to_owned())?;
9341 self.record_scope_map(batch, &table.name, last);
9342 if self.drop_doomed_outbox(table, last, owners)? {
9343 batch.status = true;
9344 batch.rejections = true;
9345 batch.outcomes = true;
9346 }
9347 self.delete_unreferenced_cached_blobs()?;
9349 Ok(true)
9350 }
9351
9352 fn apply_live_commit(
9357 &mut self,
9358 receivers: &[UnitRecord],
9359 tables: &[String],
9360 changes: Vec<ssp2::model::Change>,
9361 commit_seq: i64,
9362 source: AppliedServerCommitSource,
9363 ) -> Result<bool, (String, String)> {
9364 let changes: Vec<ssp2::model::Change> = changes
9365 .into_iter()
9366 .filter(|change| {
9367 let Some(table) = tables
9369 .get(change.table_index as usize)
9370 .and_then(|name| self.schema.table(name))
9371 else {
9372 return true;
9373 };
9374 receivers.iter().any(|unit| {
9375 covers(table, &unit.scopes)
9376 && pair_scopes(&unit.scopes, &table.name, unit.echo.as_deref()).is_some_and(
9377 |scopes| {
9378 scopes.iter().all(|(variable, values)| {
9379 change
9380 .scopes
9381 .iter()
9382 .find(|(key, _)| key == variable)
9383 .is_some_and(|(_, value)| values.contains(value))
9384 })
9385 },
9386 )
9387 })
9388 })
9389 .collect();
9390 if changes.is_empty() {
9391 return Ok(false);
9392 }
9393 let affected_tables = changes
9394 .iter()
9395 .filter_map(|change| tables.get(change.table_index as usize))
9396 .cloned()
9397 .collect::<BTreeSet<_>>();
9398 let previous_batches = self.change_queue.len();
9399 self.apply_commit_frame(tables, &changes, commit_seq)?;
9400 if self.change_queue.len() > previous_batches {
9401 if let Some(batch) = self.change_queue.back_mut() {
9402 batch
9403 .applied_commits
9404 .extend(
9405 affected_tables
9406 .into_iter()
9407 .map(|table| AppliedServerCommit {
9408 table,
9409 commit_seq,
9410 source,
9411 }),
9412 );
9413 }
9414 }
9415 Ok(true)
9416 }
9417
9418 fn apply_commit_frame(
9420 &mut self,
9421 tables: &[String],
9422 changes: &[ssp2::model::Change],
9423 sequence: i64,
9424 ) -> Result<(), (String, String)> {
9425 #[cfg(feature = "bench-internals")]
9426 let _phase = self.benchmark_phases.start(Phase::CommitApply);
9427 let rollback = self.capture();
9428 self.coalesce_unit(changes.len())
9429 .and_then(|()| self.begin_observation("syncular_commit"))
9430 .map_err(|message| ("storage.failed".into(), message))?;
9431 let applied = (|| {
9432 let mut batch = ChangeAccumulator::default();
9433 self.record_commit_changes(&mut batch, tables, changes);
9434 {
9435 let storage_failed = |error| {
9436 (
9437 "storage.failed".to_owned(),
9438 Self::sqlite_failure(&self.storage_failure, error),
9439 )
9440 };
9441 let pinned: HashSet<(&str, &str)> = self
9442 .outbox
9443 .iter()
9444 .flat_map(|commit| &commit.ops)
9445 .map(|op| (op.table.as_str(), op.row_id.as_str()))
9446 .collect();
9447 for change in changes {
9448 let table = tables.get(change.table_index as usize).ok_or_else(|| {
9449 (
9450 "sync.invalid_request".to_owned(),
9451 "change tableIndex out of range".to_owned(),
9452 )
9453 })?;
9454 if pinned.contains(&(table.as_str(), change.row_id.as_str())) {
9455 self.conn.prepare_cached("INSERT INTO _syncular_row_deliveries(tbl,id,commit_seq) VALUES (?1,?2,?3) ON CONFLICT(tbl,id) DO UPDATE SET commit_seq=MAX(commit_seq,excluded.commit_seq)")
9456 .and_then(|mut statement| statement.execute(rusqlite::params![table, change.row_id, sequence]))
9457 .map_err(storage_failed)?;
9458 }
9459 self.conn.prepare_cached("DELETE FROM _syncular_acked_rows WHERE tbl=?1 AND id=?2 AND commit_seq<=?3")
9460 .and_then(|mut statement| statement.execute(rusqlite::params![table, change.row_id, sequence]))
9461 .map_err(storage_failed)?;
9462 }
9463 }
9464 self.apply_commit_changes(tables, changes)?;
9465 self.rebuild_overlay_if_dirty()
9466 .map_err(|message| ("storage.failed".into(), message))?;
9467 self.finish_observation("syncular_commit", batch)
9468 .map_err(|message| ("storage.failed".into(), message))
9469 })();
9470 if applied.is_err() {
9471 self.rollback_observation("syncular_commit");
9472 self.restore(rollback);
9473 }
9474 applied
9475 }
9476
9477 fn apply_commit_changes(
9478 &mut self,
9479 tables: &[String],
9480 changes: &[ssp2::model::Change],
9481 ) -> Result<(), (String, String)> {
9482 let changed_tables: BTreeSet<&str> = changes
9487 .iter()
9488 .filter_map(|change| tables.get(change.table_index as usize))
9489 .map(String::as_str)
9490 .collect();
9491 let mut mirror_visible = !self.overlay_dirty.get();
9492 for name in &changed_tables {
9493 if mirror_visible
9494 && self
9495 .schema
9496 .table(name)
9497 .is_some_and(|table| table.indexes.iter().any(|index| index.unique))
9498 {
9499 let (pending, protected) = self
9500 .table_intent(name)
9501 .map_err(|message| ("storage.failed".to_owned(), message))?;
9502 mirror_visible = !pending && !protected;
9503 }
9504 }
9505 for change in changes {
9506 let table_name = tables.get(change.table_index as usize).ok_or_else(|| {
9507 (
9508 "sync.invalid_request".to_owned(),
9509 "change tableIndex out of range".to_owned(),
9510 )
9511 })?;
9512 let table = self.schema.table(table_name).ok_or_else(|| {
9513 (
9514 "sync.schema_mismatch".to_owned(),
9515 format!("change targets unknown table {table_name:?}"),
9516 )
9517 })?;
9518 let landed = match change.op {
9519 Op::Upsert => {
9520 let payload = change.row.as_ref().ok_or_else(|| {
9521 (
9522 "sync.invalid_request".to_owned(),
9523 "upsert change without row payload".to_owned(),
9524 )
9525 })?;
9526 let row = {
9528 #[cfg(feature = "bench-internals")]
9529 let _phase = self.benchmark_phases.start(Phase::RowDecode);
9530 decode_row_bytes(table, payload, &self.encryption).map_err(|m| {
9531 match m.strip_prefix("client.decrypt_failed: ") {
9533 Some(detail) => {
9534 ("client.decrypt_failed".to_owned(), detail.to_owned())
9535 }
9536 None => ("sync.invalid_request".to_owned(), m),
9537 }
9538 })?
9539 };
9540 let table_name = table.name.clone();
9541 let landed = self
9543 .write_base_row(&table_name, &row, change.row_version)
9544 .map_err(|m| ("sync.invalid_request".to_owned(), m))?;
9545 if mirror_visible && landed {
9546 self.write_row(
9547 &visible_table(&table_name),
9548 &table_name,
9549 &row,
9550 change.row_version,
9551 )
9552 .map_err(|m| ("sync.invalid_request".to_owned(), m))?;
9553 }
9554 landed
9555 }
9556 Op::Delete => self
9559 .delete_base_row(table_name, &change.row_id, change.row_version)
9560 .map_err(|m| ("sync.invalid_request".to_owned(), m))?,
9561 };
9562 self.overlay_dirty.table(table_name);
9566 if mirror_visible && !(landed && change.op == Op::Upsert) {
9567 let predicate = row_id_predicate(table);
9568 let visible = visible_table(table_name);
9569 let mut statements = vec![format!("DELETE FROM {visible} WHERE {predicate}")];
9570 if !landed {
9571 statements.push(format!(
9572 "INSERT INTO {visible} SELECT * FROM {} WHERE {predicate}",
9573 base_table(table_name)
9574 ));
9575 }
9576 for statement in &statements {
9577 self.conn
9578 .prepare_cached(statement)
9579 .and_then(|mut statement| {
9580 statement.execute(rusqlite::params![change.row_id])
9581 })
9582 .map_err(|error| ("sync.invalid_request".to_owned(), error.to_string()))?;
9583 }
9584 }
9585 }
9586 if mirror_visible {
9587 let changed_rows: HashSet<(&str, &str)> = changes
9588 .iter()
9589 .map(|change| {
9590 (
9591 tables[change.table_index as usize].as_str(),
9592 change.row_id.as_str(),
9593 )
9594 })
9595 .collect();
9596 self.replay_overlay(&changed_tables, Some(&changed_rows))
9597 .map_err(|message| ("storage.failed".to_owned(), message))?;
9598 self.overlay_dirty.set(false);
9599 }
9600 Ok(())
9601 }
9602
9603 fn apply_segment(
9608 &mut self,
9609 table: &TableSchema,
9610 segment: &RowsSegment,
9611 first_page: bool,
9612 effective: &[(String, Vec<String>)],
9613 owners: &[Vec<(String, Vec<String>)>],
9614 ) -> Result<u32, SectionError> {
9615 let matches = segment.table == table.name
9620 && segment.schema_version == self.schema.version
9621 && segment.columns.len() == table.wire_columns.len()
9622 && segment
9623 .columns
9624 .iter()
9625 .zip(table.wire_columns.iter())
9626 .all(|(a, b)| a.name == b.name && a.ty == b.ty && a.nullable == b.nullable);
9627 if !matches {
9628 return Err(SectionError::Abort(
9629 "sync.schema_mismatch".to_owned(),
9630 "segment column table does not match the generated schema (§5.2)".to_owned(),
9631 ));
9632 }
9633 let mut applied = 0u32;
9634 let empty = Vec::new();
9635 let blocks = if segment.blocks.is_empty() {
9636 std::slice::from_ref(&empty)
9637 } else {
9638 &segment.blocks
9639 };
9640 for (index, block) in blocks.iter().enumerate() {
9641 let clear = first_page && index == 0;
9642 if !clear && block.is_empty() {
9643 continue;
9644 }
9645 let rollback = self.capture();
9646 let (pending, protected) = self
9647 .table_intent(&table.name)
9648 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9649 let reconcile_pending =
9650 !protected && pending && table.indexes.iter().any(|index| index.unique);
9651 let mirror_visible = !protected && !reconcile_pending;
9652 self.coalesce_unit(block.len())
9653 .and_then(|()| self.begin_observation("syncular_segment_block"))
9654 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9655 let outcome = (|| {
9656 self.rebuild_overlay_if_dirty()
9657 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9658 let mut changed_keys = Vec::new();
9659 let mut batch = ChangeAccumulator::default();
9660 if !block.is_empty()
9661 || (clear && self.scoped_rows_exist(&table.name, effective, owners))
9662 {
9663 batch.table(&table.name);
9664 }
9665 if clear {
9666 self.purge_scope_rows(&table.name, effective, owners)
9667 .map_err(|()| SectionError::FailClosed)?;
9668 }
9669 for (row_index, row) in block.iter().enumerate() {
9670 let decrypted;
9671 let values = if table.has_encrypted_columns() {
9672 let mut values = row.values.clone();
9673 crate::values::decrypt_segment_row(table, &mut values, &self.encryption)
9674 .map_err(|message| {
9675 SectionError::Abort("client.decrypt_failed".into(), message)
9676 })?;
9677 decrypted = values;
9678 &decrypted
9679 } else {
9680 &row.values
9681 };
9682 if !self
9683 .write_base_row(&table.name, values, row.server_version)
9684 .map_err(|message| {
9685 SectionError::Abort("sync.invalid_request".into(), message)
9686 })?
9687 {
9688 continue;
9689 }
9690 if reconcile_pending {
9691 changed_keys.push(
9692 owned_sql_value(RowParam::Cell(&values[table.pk_index])).map_err(
9693 |message| SectionError::Abort("storage.failed".into(), message),
9694 )?,
9695 );
9696 }
9697 if mirror_visible {
9698 self.write_row(
9699 &visible_table(&table.name),
9700 &table.name,
9701 values,
9702 row.server_version,
9703 )
9704 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9705 }
9706 let processed = u64::from(applied) + row_index as u64 + 1;
9707 if processed.is_multiple_of(1024) {
9708 self.progress.update(|p| p.rows_processed = processed);
9709 }
9710 }
9711 self.reconcile_import(table, &mut changed_keys, protected, reconcile_pending)
9712 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9713 self.finish_observation("syncular_segment_block", batch)
9714 .map_err(|message| SectionError::Abort("storage.failed".into(), message))
9715 })();
9716 if let Err(error) = outcome {
9717 self.rollback_observation("syncular_segment_block");
9718 self.restore(rollback);
9719 return Err(error);
9720 }
9721 applied += block.len() as u32;
9722 self.progress
9723 .update(|p| p.rows_processed = u64::from(applied));
9724 std::thread::yield_now();
9725 }
9726 self.progress
9727 .update(|p| p.rows_processed = u64::from(applied));
9728 Ok(applied)
9729 }
9730
9731 #[allow(clippy::too_many_arguments)]
9739 fn apply_sqlite_segment(
9740 &mut self,
9741 table: &TableSchema,
9742 bytes: &[u8],
9743 first_page: bool,
9744 effective: &[(String, Vec<String>)],
9745 owners: &[Vec<(String, Vec<String>)>],
9746 row_count: i64,
9747 as_of_commit_seq: i64,
9748 scope_digest: &str,
9749 ) -> Result<u32, SectionError> {
9750 let invalid = |detail: &str| {
9751 SectionError::Abort(
9752 "sync.invalid_request".to_owned(),
9753 format!("sqlite segment rejected: {detail} (§5.3)"),
9754 )
9755 };
9756 if table.has_encrypted_columns() {
9759 return Err(invalid("table has encrypted columns"));
9760 }
9761
9762 let directory = self
9766 .previous_version_replica_path()
9767 .and_then(|replica| std::path::Path::new(&replica).parent().map(Into::into))
9768 .unwrap_or_else(std::env::temp_dir);
9769 let path = directory.join(format!(".syncular-image-{}.tmp", uuid::Uuid::new_v4()));
9770 let outcome = (|| {
9771 let mut options = std::fs::OpenOptions::new();
9772 options.write(true).create_new(true);
9773 #[cfg(unix)]
9774 std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
9775 std::io::Write::write_all(
9776 &mut options
9777 .open(&path)
9778 .map_err(|_| invalid("image temp file write failed"))?,
9779 bytes,
9780 )
9781 .map_err(|_| invalid("image temp file write failed"))?;
9782 let img = rusqlite::Connection::open_with_flags(
9783 &path,
9784 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
9785 )
9786 .map_err(|_| invalid("bytes do not open as a SQLite database"))?;
9787 self.apply_sqlite_image(
9788 &img,
9789 table,
9790 first_page,
9791 effective,
9792 owners,
9793 row_count,
9794 as_of_commit_seq,
9795 scope_digest,
9796 )
9797 })();
9798 let _ = std::fs::remove_file(&path);
9799 outcome
9800 }
9801
9802 #[allow(clippy::too_many_arguments)]
9803 fn apply_sqlite_image(
9804 &mut self,
9805 img: &rusqlite::Connection,
9806 table: &crate::schema::TableSchema,
9807 first_fresh_page: bool,
9808 effective: &[(String, Vec<String>)],
9809 owners: &[Vec<(String, Vec<String>)>],
9810 row_count: i64,
9811 as_of_commit_seq: i64,
9812 scope_digest: &str,
9813 ) -> Result<u32, SectionError> {
9814 let invalid = |detail: String| {
9815 SectionError::Abort(
9816 "sync.invalid_request".to_owned(),
9817 format!("sqlite segment rejected: {detail} (§5.3)"),
9818 )
9819 };
9820
9821 type MetaRow = (i64, String, i64, i64, String, i64, i64);
9824 let meta: MetaRow = img
9825 .query_row(
9826 "SELECT format, \"table\", \"schemaVersion\", \"asOfCommitSeq\",
9827 \"scopeDigest\", \"rowCount\",
9828 (SELECT count(*) FROM _syncular_segment)
9829 FROM _syncular_segment",
9830 [],
9831 |row| {
9832 Ok((
9833 row.get(0)?,
9834 row.get(1)?,
9835 row.get(2)?,
9836 row.get(3)?,
9837 row.get(4)?,
9838 row.get(5)?,
9839 row.get(6)?,
9840 ))
9841 },
9842 )
9843 .map_err(|_| invalid("missing or unreadable _syncular_segment metadata".to_owned()))?;
9844 let (format, meta_table, schema_version, pin, digest, meta_rows, meta_count) = meta;
9845 if meta_count != 1 {
9846 return Err(invalid(format!(
9847 "_syncular_segment must contain exactly one row, found {meta_count}"
9848 )));
9849 }
9850 if format != 1 {
9851 return Err(invalid(format!("format {format}")));
9852 }
9853 if meta_table != table.name {
9854 return Err(invalid(format!("image table {meta_table:?}")));
9855 }
9856 if schema_version != i64::from(self.schema.version) {
9857 return Err(invalid(format!("schemaVersion {schema_version}")));
9858 }
9859 if pin != as_of_commit_seq {
9860 return Err(invalid(format!("asOfCommitSeq {pin}")));
9861 }
9862 if digest != scope_digest {
9863 return Err(invalid("scopeDigest mismatch".to_owned()));
9864 }
9865 if meta_rows != row_count {
9866 return Err(invalid(format!("rowCount {meta_rows}")));
9867 }
9868
9869 let mut names: Vec<String> = Vec::new();
9871 let mut primary_keys: Vec<String> = Vec::new();
9872 {
9873 let mut stmt = img
9874 .prepare(&format!("PRAGMA table_info({})", quote_ident(&table.name)))
9875 .map_err(|_| invalid("image data table missing".to_owned()))?;
9876 let mut rows = stmt
9877 .query([])
9878 .map_err(|_| invalid("image data table unreadable".to_owned()))?;
9879 while let Some(row) = rows
9880 .next()
9881 .map_err(|_| invalid("image data table unreadable".to_owned()))?
9882 {
9883 if row
9884 .get::<_, i64>(5)
9885 .map_err(|error| invalid(Self::sqlite_failure(&self.storage_failure, error)))?
9886 > 0
9887 {
9888 primary_keys.push(row.get(1).map_err(|error| {
9889 invalid(Self::sqlite_failure(&self.storage_failure, error))
9890 })?);
9891 }
9892 names.push(
9893 row.get::<_, String>(1)
9894 .map_err(|_| invalid("image data table unreadable".to_owned()))?,
9895 );
9896 }
9897 }
9898 let mut expected: Vec<&str> = table.columns.iter().map(|c| c.name.as_str()).collect();
9899 expected.push("_syncular_version");
9900 if names.len() != expected.len() || names.iter().zip(expected.iter()).any(|(a, b)| a != b) {
9901 return Err(SectionError::Abort(
9902 "sync.schema_mismatch".to_owned(),
9903 "sqlite segment columns do not match the generated schema (§5.3)".to_owned(),
9904 ));
9905 }
9906
9907 if primary_keys != vec![table.primary_key.clone()] {
9908 return Err(invalid(
9909 "image primary key does not match generated schema".into(),
9910 ));
9911 }
9912 let actual_count: i64 = img
9913 .query_row(
9914 &format!("SELECT count(*) FROM {}", quote_ident(&table.name)),
9915 [],
9916 |row| row.get(0),
9917 )
9918 .map_err(|error| invalid(Self::sqlite_failure(&self.storage_failure, error)))?;
9919 if actual_count != row_count {
9920 return Err(invalid("image row count does not match descriptor".into()));
9921 }
9922 let insert = self.insert_row_sql(&base_table(&table.name), table);
9923 let visible_insert = self.insert_row_sql(&visible_table(&table.name), table);
9924 let column_list = names
9925 .iter()
9926 .map(|n| quote_ident(n))
9927 .collect::<Vec<_>>()
9928 .join(", ");
9929 let mut stmt = img
9930 .prepare(&format!(
9931 "SELECT {column_list} FROM {} ORDER BY {}",
9932 quote_ident(&table.name),
9933 quote_ident(&table.primary_key)
9934 ))
9935 .map_err(|error| invalid(Self::sqlite_failure(&self.storage_failure, error)))?;
9936 let mut rows = stmt
9937 .query([])
9938 .map_err(|error| invalid(Self::sqlite_failure(&self.storage_failure, error)))?;
9939 let mut applied = 0u32;
9940 loop {
9941 let clear = applied == 0 && first_fresh_page;
9942 let rollback = self.capture();
9943 let (pending, protected) = self
9944 .table_intent(&table.name)
9945 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9946 let reconcile_pending =
9947 !protected && pending && table.indexes.iter().any(|index| index.unique);
9948 let mirror_visible = !protected && !reconcile_pending;
9949 self.coalesce_unit(1024)
9951 .and_then(|()| self.begin_observation("syncular_image_chunk"))
9952 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9953 let result = (|| {
9954 self.rebuild_overlay_if_dirty()
9955 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9956 let mut changed_keys = Vec::new();
9957 let mut batch = ChangeAccumulator::default();
9958 if clear {
9959 if self.scoped_rows_exist(&table.name, effective, owners) {
9960 batch.table(&table.name);
9961 }
9962 self.purge_scope_rows(&table.name, effective, owners)
9963 .map_err(|()| SectionError::FailClosed)?;
9964 }
9965 let mut processed = 0u32;
9966 {
9967 let mut ins = self.conn.prepare_cached(&insert).map_err(|error| {
9968 invalid(Self::sqlite_failure(&self.storage_failure, error))
9969 })?;
9970 let mut mirror = if mirror_visible {
9971 Some(self.conn.prepare_cached(&visible_insert).map_err(|error| {
9972 invalid(Self::sqlite_failure(&self.storage_failure, error))
9973 })?)
9974 } else {
9975 None
9976 };
9977 for _ in 0..1024 {
9978 let Some(row) = rows.next().map_err(|error| {
9979 invalid(Self::sqlite_failure(&self.storage_failure, error))
9980 })?
9981 else {
9982 break;
9983 };
9984 for (i, column) in table.columns.iter().enumerate() {
9985 let cell = row.get_ref(i).map_err(|error| {
9986 invalid(Self::sqlite_failure(&self.storage_failure, error))
9987 })?;
9988 let param = image_cell_param(column, cell).map_err(invalid)?;
9989 ins.raw_bind_parameter(i + 1, ¶m).map_err(|error| {
9990 invalid(Self::sqlite_failure(&self.storage_failure, error))
9991 })?;
9992 if let Some(mirror) = &mut mirror {
9993 mirror.raw_bind_parameter(i + 1, ¶m).map_err(|error| {
9994 invalid(Self::sqlite_failure(&self.storage_failure, error))
9995 })?;
9996 }
9997 }
9998 let version_index = table.columns.len();
9999 let version: i64 = row.get(version_index).map_err(|error| {
10000 invalid(Self::sqlite_failure(&self.storage_failure, error))
10001 })?;
10002 if version < 1 {
10003 return Err(invalid("image row version must be positive".into()));
10004 }
10005 ins.raw_bind_parameter(version_index + 1, version)
10006 .map_err(|error| {
10007 invalid(Self::sqlite_failure(&self.storage_failure, error))
10008 })?;
10009 let landed = ins.raw_execute().map_err(|error| {
10011 invalid(Self::sqlite_failure(&self.storage_failure, error))
10012 })? > 0;
10013 if !landed {
10014 processed += 1;
10015 continue;
10016 }
10017 if let Some(mirror) = &mut mirror {
10018 mirror
10019 .raw_bind_parameter(version_index + 1, version)
10020 .map_err(|error| {
10021 invalid(Self::sqlite_failure(&self.storage_failure, error))
10022 })?;
10023 mirror.raw_execute().map_err(|error| {
10024 invalid(Self::sqlite_failure(&self.storage_failure, error))
10025 })?;
10026 }
10027 if reconcile_pending {
10028 changed_keys.push(row.get::<_, SqlValue>(table.pk_index).map_err(
10029 |error| invalid(Self::sqlite_failure(&self.storage_failure, error)),
10030 )?);
10031 }
10032 processed += 1;
10033 }
10034 }
10035 if processed > 0 {
10036 batch.table(&table.name);
10037 self.overlay_dirty.table(&table.name);
10038 }
10039 self.reconcile_import(table, &mut changed_keys, protected, reconcile_pending)
10040 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
10041 self.finish_observation("syncular_image_chunk", batch)
10042 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
10043 Ok(processed)
10044 })();
10045 let processed = match result {
10046 Ok(processed) => processed,
10047 Err(error) => {
10048 self.rollback_observation("syncular_image_chunk");
10049 self.restore(rollback);
10050 return Err(error);
10051 }
10052 };
10053 applied += processed;
10054 self.progress
10055 .update(|p| p.rows_processed = u64::from(applied));
10056 std::thread::yield_now();
10057 if processed < 1024 || i64::from(applied) == actual_count {
10058 break;
10059 }
10060 }
10061 if i64::from(applied) != actual_count {
10062 return Err(invalid(format!(
10063 "image yielded {applied} rows, expected {actual_count}"
10064 )));
10065 }
10066 Ok(applied)
10067 }
10068
10069 fn reconcile_rows(&self, drained: &[Arc<OutboxCommit>]) -> Result<(), String> {
10074 let names: BTreeSet<&str> = drained
10075 .iter()
10076 .flat_map(|commit| &commit.ops)
10077 .map(|op| op.table.as_str())
10078 .collect();
10079 for name in names {
10080 let Some(table) = self.schema.table(name) else {
10081 continue;
10082 };
10083 let ops = drained.iter().flat_map(|commit| &commit.ops);
10084 let tables = BTreeSet::from([name]);
10085 let rows: HashSet<_> = ops
10086 .clone()
10087 .map(|op| (op.table.as_str(), op.row_id.as_str()))
10088 .collect();
10089 let unique = table.indexes.iter().any(|index| index.unique);
10090 let acknowledged = if unique {
10091 self.acknowledged_operations(None, Some(&tables))?
10092 } else {
10093 Vec::new()
10094 };
10095 let peers = acknowledged
10096 .iter()
10097 .chain(
10098 self.failed_commits
10099 .iter()
10100 .flat_map(|(commit, _)| &commit.ops),
10101 )
10102 .chain(self.outbox.iter().flat_map(|commit| &commit.ops))
10103 .filter(|_| unique);
10104 self.restore_base_rows(table, &mut Vec::new(), peers.chain(ops.clone()))?;
10105 self.replay_overlay(&tables, (!unique).then_some(&rows))?;
10106 }
10107 Ok(())
10108 }
10109
10110 fn table_intent(&self, table: &str) -> Result<(bool, bool), String> {
10113 let touches = |op: &OutboxOp| op.table == table;
10114 Ok((
10115 self.outbox
10116 .iter()
10117 .flat_map(|commit| &commit.ops)
10118 .any(touches),
10119 self.failed_commits
10120 .iter()
10121 .flat_map(|(commit, _)| &commit.ops)
10122 .any(touches)
10123 || self
10124 .conn
10125 .query_row(
10126 "SELECT EXISTS(SELECT 1 FROM _syncular_acked_rows WHERE tbl=?1)",
10127 [table],
10128 |row| row.get(0),
10129 )
10130 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?,
10131 ))
10132 }
10133
10134 fn reconcile_import(
10138 &mut self,
10139 table: &TableSchema,
10140 keys: &mut Vec<SqlValue>,
10141 protected: bool,
10142 restore: bool,
10143 ) -> Result<(), String> {
10144 if protected {
10145 return self.rebuild_overlay_if_dirty();
10146 }
10147 let pending = self.outbox.iter().flat_map(|commit| &commit.ops);
10148 if restore {
10149 self.restore_base_rows(table, keys, pending.clone())?;
10150 }
10151 self.apply_outbox_ops(pending.filter(|op| op.table == table.name))?;
10152 self.overlay_dirty.set(false);
10153 Ok(())
10154 }
10155
10156 fn restore_base_rows<'a>(
10160 &self,
10161 table: &TableSchema,
10162 keys: &mut Vec<SqlValue>,
10163 ops: impl IntoIterator<Item = &'a OutboxOp>,
10164 ) -> Result<(), String> {
10165 let visible = visible_table(&table.name);
10166 let base = base_table(&table.name);
10167 let pk = quote_ident(&table.primary_key);
10168 let predicate = row_id_predicate(table);
10169 let mut lookup = self.conn.prepare_cached(&format!(
10170 "SELECT {pk} FROM {base} WHERE {predicate} UNION SELECT {pk} FROM {visible} WHERE {predicate}"
10171 )).map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10172 let mut deleted = HashSet::new();
10173 for op in ops.into_iter().filter(|op| op.table == table.name) {
10174 if op.upsert {
10175 if let Some(values) = &op.values {
10176 let key = json_to_column_value(
10177 &table.columns[table.pk_index],
10178 values.get(&table.primary_key),
10179 )?;
10180 keys.push(owned_sql_value(RowParam::Cell(&key))?);
10181 }
10182 } else if deleted.insert(&op.row_id) {
10183 keys.extend(
10184 lookup
10185 .query_map([&op.row_id], |row| row.get::<_, SqlValue>(0))
10186 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
10187 .collect::<Result<Vec<_>, _>>()
10188 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?,
10189 );
10190 }
10191 }
10192 for chunk in keys.chunks(400) {
10195 let holes = vec!["?"; chunk.len()].join(",");
10196 self.conn
10197 .prepare_cached(&format!("DELETE FROM {visible} WHERE {pk} IN ({holes})"))
10198 .and_then(|mut statement| statement.execute(rusqlite::params_from_iter(chunk)))
10199 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10200 }
10201 let updates = table
10202 .columns
10203 .iter()
10204 .map(|column| column.name.as_str())
10205 .chain(std::iter::once("_syncular_version"))
10206 .filter(|name| *name != table.primary_key)
10207 .map(|name| {
10208 let name = quote_ident(name);
10209 format!("{name}=excluded.{name}")
10210 })
10211 .collect::<Vec<_>>()
10212 .join(",");
10213 for chunk in keys.chunks(400) {
10214 let holes = vec!["?"; chunk.len()].join(",");
10215 self.conn.prepare_cached(&format!("INSERT INTO {visible} SELECT * FROM {base} WHERE {pk} IN ({holes}) ON CONFLICT ({pk}) DO UPDATE SET {updates}"))
10216 .and_then(|mut statement| statement.execute(rusqlite::params_from_iter(chunk)))
10217 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10218 }
10219 Ok(())
10220 }
10221
10222 fn compile_local_data_purge(
10225 &self,
10226 input: &LocalDataPurgeInput,
10227 ) -> Result<(Vec<CompiledLocalDataPurgeTarget>, String), String> {
10228 let invalid = |message: String| format!("sync.invalid_request: {message}");
10229 if input.purge_id.is_empty()
10230 || input.purge_id.len() > 128
10231 || !is_local_operation_code_like(&input.purge_id)
10232 {
10233 return Err(invalid(
10234 "local purge purgeId must be a 1–128 character code-like identifier".to_owned(),
10235 ));
10236 }
10237 if input.targets.is_empty() || input.targets.len() > MAX_LOCAL_PURGE_TARGETS {
10238 return Err(invalid(format!(
10239 "local purge needs between 1 and {MAX_LOCAL_PURGE_TARGETS} targets"
10240 )));
10241 }
10242
10243 let mut deduplicated: BTreeMap<
10244 String,
10245 (CompiledLocalDataPurgeTarget, LocalDataPurgeTarget),
10246 > = BTreeMap::new();
10247 for target in &input.targets {
10248 let table = self.schema.table(&target.table).ok_or_else(|| {
10249 invalid(format!(
10250 "local purge names unknown table {:?}",
10251 target.table
10252 ))
10253 })?;
10254 if target.selectors.is_empty() || target.selectors.len() > MAX_LOCAL_PURGE_SELECTORS {
10255 return Err(invalid(format!(
10256 "local purge target {:?} needs between 1 and {MAX_LOCAL_PURGE_SELECTORS} selectors",
10257 target.table
10258 )));
10259 }
10260 let mut selectors = Vec::with_capacity(target.selectors.len());
10261 let mut canonical_selectors = BTreeMap::new();
10262 for (column_name, raw_values) in &target.selectors {
10263 let Some((column_index, column)) = table
10264 .columns
10265 .iter()
10266 .enumerate()
10267 .find(|(_, column)| column.name == *column_name)
10268 else {
10269 return Err(invalid(format!(
10270 "local purge target {:?} names unknown column {:?}",
10271 target.table, column_name
10272 )));
10273 };
10274 let encrypted = table
10275 .encrypted_columns
10276 .iter()
10277 .any(|candidate| candidate.index == column_index);
10278 if column.ty != ColumnType::String || encrypted {
10279 return Err(invalid(format!(
10280 "local purge selector {:?}.{:?} must be a plaintext string column",
10281 target.table, column_name
10282 )));
10283 }
10284 if raw_values.is_empty() || raw_values.len() > MAX_LOCAL_PURGE_VALUES {
10285 return Err(invalid(format!(
10286 "local purge selector {:?}.{:?} needs between 1 and {MAX_LOCAL_PURGE_VALUES} values",
10287 target.table, column_name
10288 )));
10289 }
10290 let mut values = raw_values.clone();
10291 values.sort();
10292 values.dedup();
10293 if values.iter().any(|value| {
10294 value.is_empty()
10295 || value.len() > MAX_LOCAL_PURGE_VALUE_LENGTH
10296 || !is_local_operation_code_like(value)
10297 }) {
10298 return Err(invalid(format!(
10299 "local purge selector values must be 1–{MAX_LOCAL_PURGE_VALUE_LENGTH} character code-like identifiers"
10300 )));
10301 }
10302 selectors.push((column_name.clone(), values.clone()));
10303 canonical_selectors.insert(column_name.clone(), values);
10304 }
10305 let canonical = LocalDataPurgeTarget {
10306 table: target.table.clone(),
10307 selectors: canonical_selectors,
10308 };
10309 let key = serde_json::to_string(&canonical).map_err(|error| error.to_string())?;
10310 deduplicated.insert(
10311 key,
10312 (
10313 CompiledLocalDataPurgeTarget {
10314 table: target.table.clone(),
10315 selectors,
10316 },
10317 canonical,
10318 ),
10319 );
10320 }
10321 let targets = deduplicated
10322 .values()
10323 .map(|(compiled, _)| compiled.clone())
10324 .collect::<Vec<_>>();
10325 let canonical = deduplicated
10326 .values()
10327 .map(|(_, target)| target.clone())
10328 .collect::<Vec<_>>();
10329 let canonical_plan =
10330 serde_json::to_string(&canonical).map_err(|error| error.to_string())?;
10331 Ok((targets, canonical_plan))
10332 }
10333
10334 fn local_purge_base_row_ids(
10335 &self,
10336 targets: &[CompiledLocalDataPurgeTarget],
10337 ) -> Result<BTreeMap<String, BTreeSet<String>>, String> {
10338 let mut by_table: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
10339 for target in targets {
10340 let table = self
10341 .schema
10342 .table(&target.table)
10343 .ok_or_else(|| format!("sync.invalid_request: unknown table {:?}", target.table))?;
10344 let mut clauses = Vec::with_capacity(target.selectors.len());
10345 let mut params = Vec::new();
10346 for (column, values) in &target.selectors {
10347 clauses.push(format!(
10348 "{} IN ({})",
10349 quote_ident(column),
10350 values.iter().map(|_| "?").collect::<Vec<_>>().join(", ")
10351 ));
10352 params.extend(values.iter().cloned().map(SqlValue::Text));
10353 }
10354 let sql = format!(
10355 "SELECT CAST({} AS TEXT) FROM {} WHERE {}",
10356 quote_ident(&table.primary_key),
10357 base_table(&target.table),
10358 clauses.join(" AND ")
10359 );
10360 let mut statement = self
10361 .conn
10362 .prepare(&sql)
10363 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10364 let rows = statement
10365 .query_map(rusqlite::params_from_iter(params), |row| {
10366 let value = sql_ref_to_json(&table.columns[table.pk_index], row.get_ref(0)?);
10367 render_row_id_json(Some(&value))
10368 .map_err(|message| rusqlite::Error::ToSqlConversionFailure(message.into()))
10369 })
10370 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10371 let ids = by_table.entry(target.table.clone()).or_default();
10372 for row in rows {
10373 ids.insert(
10374 row.map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?,
10375 );
10376 }
10377 }
10378 Ok(by_table)
10379 }
10380
10381 fn local_purge_values_match(
10382 target: &CompiledLocalDataPurgeTarget,
10383 values: &Map<String, Value>,
10384 ) -> bool {
10385 target.selectors.iter().all(|(column, allowed)| {
10386 values
10387 .get(column)
10388 .and_then(Value::as_str)
10389 .is_some_and(|value| allowed.iter().any(|candidate| candidate == value))
10390 })
10391 }
10392
10393 pub fn purge_local_data(
10397 &mut self,
10398 input: &LocalDataPurgeInput,
10399 ) -> Result<LocalDataPurgeResult, String> {
10400 let (targets, canonical_plan) = self.compile_local_data_purge(input)?;
10401 let meta_key = format!("localPurge:{}", input.purge_id);
10402 let applied_plan = self
10403 .conn
10404 .query_row(
10405 "SELECT value FROM _syncular_meta WHERE key = ?1",
10406 rusqlite::params![meta_key],
10407 |row| row.get::<_, String>(0),
10408 )
10409 .optional()
10410 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10411 if let Some(applied_plan) = applied_plan {
10412 if applied_plan != canonical_plan {
10413 return Err(format!(
10414 "sync.invalid_request: local purge id {:?} was already used with a different plan",
10415 input.purge_id
10416 ));
10417 }
10418 return Ok(LocalDataPurgeResult {
10419 already_applied: true,
10420 purged_rows: 0,
10421 dropped_commits: 0,
10422 });
10423 }
10424
10425 let _ = self.drop_previous_version();
10429
10430 let rollback = self.capture();
10431 self.begin_observation("syncular_local_purge")?;
10432 let applied = (|| -> Result<(ChangeAccumulator, LocalDataPurgeResult), String> {
10433 let row_ids = self.local_purge_base_row_ids(&targets)?;
10434 let retained_dropped = self.drop_failed_commits(|table, values| {
10435 targets.iter().any(|target| {
10436 target.table == table && Self::local_purge_values_match(target, values)
10437 })
10438 })?;
10439 let doomed = self
10440 .outbox
10441 .iter()
10442 .filter(|commit| {
10443 commit.ops.iter().any(|operation| {
10444 let matching_targets = targets
10445 .iter()
10446 .filter(|target| target.table == operation.table)
10447 .collect::<Vec<_>>();
10448 if matching_targets.is_empty() {
10449 return false;
10450 }
10451 if row_ids
10452 .get(&operation.table)
10453 .is_some_and(|ids| ids.contains(&operation.row_id))
10454 {
10455 return true;
10456 }
10457 operation.values.as_ref().is_some_and(|values| {
10458 matching_targets
10459 .iter()
10460 .any(|target| Self::local_purge_values_match(target, values))
10461 })
10462 })
10463 })
10464 .cloned()
10465 .collect::<Vec<_>>();
10466 let doomed_ids = doomed
10467 .iter()
10468 .map(|commit| commit.client_commit_id.clone())
10469 .collect::<BTreeSet<_>>();
10470 let mut batch = ChangeAccumulator::default();
10471 for commit in &retained_dropped {
10472 batch.conflicts = true;
10473 batch.rejections = true;
10474 batch.outcomes = true;
10475 for operation in &commit.ops {
10476 batch.table(&operation.table);
10477 }
10478 }
10479 let mut rejections = Vec::new();
10480 for commit in &doomed {
10481 for operation in &commit.ops {
10482 batch.table(&operation.table);
10483 self.overlay_dirty.table(&operation.table);
10484 }
10485 let results = commit
10486 .ops
10487 .iter()
10488 .enumerate()
10489 .map(|(op_index, operation)| {
10490 let rejection = RejectionRecord {
10491 client_commit_id: commit.client_commit_id.clone(),
10492 op_index: op_index as i32,
10493 code: "client.local_data_purged".to_owned(),
10494 message: "the commit was dropped by an application-authorized local data purge".to_owned(),
10495 retryable: false,
10496 details: None,
10497 operation: Some(CommitOperation::from(operation)),
10498 };
10499 rejections.push(rejection.clone());
10500 CommitOperationOutcome::Error { rejection }
10501 })
10502 .collect::<Vec<_>>();
10503 self.persist_commit_outcome(
10504 &commit.client_commit_id,
10505 CommitOutcomeStatus::Rejected,
10506 &results,
10507 Some(&commit.ops),
10508 )?;
10509 self.delete_outbox_persisted(&commit.client_commit_id)?;
10510 }
10511 if !doomed.is_empty() {
10512 self.outbox
10513 .retain(|commit| !doomed_ids.contains(&commit.client_commit_id));
10514 self.rejections.extend(rejections);
10515 self.prune_commit_outcomes()?;
10516 batch.status = true;
10517 batch.rejections = true;
10518 batch.outcomes = true;
10519 }
10520
10521 let mut purged_rows = 0usize;
10522 for (table, ids) in &row_ids {
10523 if ids.is_empty() {
10524 continue;
10525 }
10526 batch.table(table);
10527 self.overlay_dirty.table(table);
10528 for row_id in ids {
10529 self.delete_base_row(table, row_id, i64::MAX)?;
10530 purged_rows += 1;
10531 }
10532 }
10533 self.rebuild_overlay_if_dirty()?;
10534 self.delete_unreferenced_cached_blobs()?;
10535 self.conn
10536 .execute(
10537 "INSERT INTO _syncular_meta(key, value) VALUES (?1, ?2)",
10538 rusqlite::params![meta_key, canonical_plan],
10539 )
10540 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10541 Ok((
10542 batch,
10543 LocalDataPurgeResult {
10544 already_applied: false,
10545 purged_rows,
10546 dropped_commits: doomed.len(),
10547 },
10548 ))
10549 })()
10550 .and_then(|(batch, result)| {
10551 self.finish_observation("syncular_local_purge", batch)
10552 .map(|()| result)
10553 });
10554 let result = match applied {
10555 Ok(result) => result,
10556 Err(error) => {
10557 self.rollback_observation("syncular_local_purge");
10558 self.restore(rollback);
10559 return Err(error);
10560 }
10561 };
10562 self.cancel_sync_round();
10563 Ok(result)
10564 }
10565
10566 pub fn rebootstrap_local_data(
10572 &mut self,
10573 input: &LocalDataRebootstrapInput,
10574 ) -> Result<LocalDataRebootstrapResult, String> {
10575 if input.rebootstrap_id.is_empty()
10576 || input.rebootstrap_id.len() > 128
10577 || !is_local_operation_code_like(&input.rebootstrap_id)
10578 {
10579 return Err(
10580 "sync.invalid_request: local rebootstrap rebootstrapId must be a 1–128 character code-like identifier"
10581 .to_owned(),
10582 );
10583 }
10584 let meta_key = format!("localRebootstrap:{}", input.rebootstrap_id);
10585 if let Some(persisted) = meta_get(&self.conn, &meta_key).map_err(|_| {
10586 "sync.local_corrupt: persisted local rebootstrap receipt is unreadable".to_owned()
10587 })? {
10588 let (retained_commits, reset_subscriptions) =
10589 decode_local_rebootstrap_receipt(&persisted)?;
10590 return Ok(LocalDataRebootstrapResult {
10591 already_applied: true,
10592 retained_commits,
10593 reset_subscriptions,
10594 });
10595 }
10596 if self.stopped || self.schema_floor.is_some() {
10597 return Err(
10598 "sync.invalid_request: local rebootstrap cannot bypass an active schema-floor stop; update the application first"
10599 .to_owned(),
10600 );
10601 }
10602
10603 let retained_commits = self.outbox.len();
10604 let reset_subscriptions = self.stored(load_units(&self.conn))?.len();
10605 let receipt = encode_local_rebootstrap_receipt(retained_commits, reset_subscriptions)?;
10606 let rollback = self.capture();
10607 self.begin_observation("syncular_local_rebootstrap")?;
10608 let mut batch = ChangeAccumulator::default();
10609 let applied = (|| -> Result<(), String> {
10610 self.run_schema_reset_observed(&mut batch, false)?;
10611 self.conn
10612 .execute(
10613 "INSERT INTO _syncular_meta(key, value) VALUES (?1, ?2)",
10614 rusqlite::params![meta_key, receipt],
10615 )
10616 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10617 self.sync_needed = true;
10618 self.sync_intent_queue.push_back(SyncIntent::Interactive);
10619 batch.status = true;
10620 Ok(())
10621 })()
10622 .and_then(|()| self.finish_observation("syncular_local_rebootstrap", batch));
10623 if let Err(error) = applied {
10624 self.rollback_observation("syncular_local_rebootstrap");
10625 self.restore(rollback);
10626 return Err(error);
10627 }
10628
10629 Ok(LocalDataRebootstrapResult {
10630 already_applied: false,
10631 retained_commits,
10632 reset_subscriptions,
10633 })
10634 }
10635
10636 fn purge_scope_rows(
10642 &mut self,
10643 table_name: &str,
10644 effective: &[(String, Vec<String>)],
10645 owners: &[Vec<(String, Vec<String>)>],
10646 ) -> Result<(), ()> {
10647 let table = self.schema.table(table_name).ok_or(())?;
10648 let Some((predicate, params)) = local_scope_predicate(table, effective, owners)? else {
10649 return Ok(());
10650 };
10651 self.overlay_dirty.table(table_name);
10652 for source in [base_table(table_name), visible_table(table_name)] {
10653 self.conn
10654 .execute(
10655 &format!("DELETE FROM {source} WHERE {predicate}"),
10656 rusqlite::params_from_iter(¶ms),
10657 )
10658 .map_err(|_| ())?;
10659 }
10660 Ok(())
10661 }
10662
10663 fn drop_doomed_outbox(
10667 &mut self,
10668 table: &TableSchema,
10669 effective: &[(String, Vec<String>)],
10670 owners: &[Vec<(String, Vec<String>)>],
10671 ) -> Result<bool, String> {
10672 let doomed: Vec<_> =
10673 self.outbox
10674 .iter()
10675 .filter(|commit| {
10676 commit.ops.iter().any(|op| {
10677 op.upsert
10678 && op.table == table.name
10679 && op.values.as_ref().is_some_and(|values| {
10680 owns_local_row(table, effective, owners, values)
10681 })
10682 })
10683 })
10684 .cloned()
10685 .collect();
10686 if doomed.is_empty() {
10687 return Ok(false);
10688 }
10689 let mut rejections = Vec::new();
10690 for commit in &doomed {
10691 let results = commit
10692 .ops
10693 .iter()
10694 .enumerate()
10695 .map(|(op_index, operation)| {
10696 self.overlay_dirty.table(&operation.table);
10697 let rejection = RejectionRecord {
10698 client_commit_id: commit.client_commit_id.clone(),
10699 op_index: op_index as i32,
10700 code: "sync.scope_revoked".to_owned(),
10701 message: "the commit was dropped because its effective scope was revoked"
10702 .to_owned(),
10703 retryable: false,
10704 details: None,
10705 operation: Some(CommitOperation::from(operation)),
10706 };
10707 rejections.push(rejection.clone());
10708 CommitOperationOutcome::Error { rejection }
10709 })
10710 .collect::<Vec<_>>();
10711 self.persist_commit_outcome(
10712 &commit.client_commit_id,
10713 CommitOutcomeStatus::Rejected,
10714 &results,
10715 Some(&commit.ops),
10716 )?;
10717 self.delete_outbox_persisted(&commit.client_commit_id)?;
10718 }
10719 self.prune_commit_outcomes()?;
10720 let doomed_ids = doomed
10721 .iter()
10722 .map(|commit| commit.client_commit_id.as_str())
10723 .collect::<BTreeSet<_>>();
10724 self.outbox
10725 .retain(|commit| !doomed_ids.contains(commit.client_commit_id.as_str()));
10726 self.rejections.extend(rejections);
10727 Ok(true)
10728 }
10729
10730 pub fn upload_blob(
10737 &mut self,
10738 bytes: &[u8],
10739 media_type: Option<String>,
10740 name: Option<String>,
10741 ) -> Result<Value, String> {
10742 let blob_id = blob_id_for(bytes);
10743 let now = self.clock_now_ms();
10744 let tx = self
10745 .conn
10746 .transaction()
10747 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10748 tx.execute(
10749 "INSERT INTO _syncular_blobs(blob_id, bytes, byte_length, media_type, created_at_ms) VALUES (?,?,?,?,?)
10750 ON CONFLICT(blob_id) DO NOTHING",
10751 rusqlite::params![blob_id, bytes, bytes.len() as i64, media_type, now],
10752 )
10753 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10754 tx.execute(
10755 "INSERT OR IGNORE INTO _syncular_blob_uploads(blob_id, media_type, created_at_ms) VALUES (?,?,?)",
10756 rusqlite::params![blob_id, media_type, now],
10757 )
10758 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10759 tx.commit()
10760 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10761 self.enforce_blob_cache_cap();
10762 let mut obj = Map::new();
10763 obj.insert("blobId".to_owned(), Value::from(blob_id));
10764 obj.insert("byteLength".to_owned(), Value::from(bytes.len() as i64));
10765 if let Some(mt) = media_type {
10766 obj.insert("mediaType".to_owned(), Value::from(mt));
10767 }
10768 if let Some(n) = name {
10769 obj.insert("name".to_owned(), Value::from(n));
10770 }
10771 Ok(Value::Object(obj))
10772 }
10773
10774 pub fn fetch_blob_bytes(
10778 &mut self,
10779 transport: &mut dyn Transport,
10780 blob_id_or_ref: &str,
10781 ) -> Result<FetchedBlob, (String, String)> {
10782 let simple = |m: String| ("client.failed".to_owned(), m);
10783 let (blob_id, byte_length) = if blob_id_or_ref.starts_with("sha256:") {
10786 (blob_id_or_ref.to_owned(), None)
10787 } else {
10788 let value: Value = serde_json::from_str(blob_id_or_ref)
10789 .map_err(|_| simple("blob ref is not JSON".to_owned()))?;
10790 (
10791 value
10792 .get("blobId")
10793 .and_then(Value::as_str)
10794 .ok_or_else(|| simple("blob ref has no blobId".to_owned()))?
10795 .to_owned(),
10796 value.get("byteLength").and_then(Value::as_u64),
10797 )
10798 };
10799 if let Some(cached) = self.get_cached_blob_bytes(&blob_id).map_err(simple)? {
10800 return Ok(cached);
10801 }
10802 if !self.transport_enabled {
10803 return Err(("sync.offline".into(), "transport gate is closed".into()));
10804 }
10805 #[cfg(feature = "bench-internals")]
10811 let download_phase = self.benchmark_phases.start(Phase::BlobDownload);
10812 let bytes = match transport
10813 .blob_download(&blob_id, byte_length)
10814 .map_err(|e| (e.code, e.message))?
10815 {
10816 BlobDownload::Bytes(bytes) => bytes,
10817 BlobDownload::Url {
10818 url,
10819 url_expires_at_ms,
10820 } => {
10821 if url_expires_at_ms.is_some_and(|exp| exp <= self.clock_now_ms()) {
10823 return Err((
10824 "sync.segment_expired".to_owned(),
10825 format!(
10826 "blob url for {blob_id} expired before fetch — re-request mints a fresh url (§5.9.5)"
10827 ),
10828 ));
10829 }
10830 transport
10831 .fetch_blob_url(&url, byte_length)
10832 .map_err(|e| (e.code, e.message))?
10833 }
10834 };
10835 #[cfg(feature = "bench-internals")]
10836 drop(download_phase);
10837 #[cfg(feature = "bench-internals")]
10838 let validate_phase = self.benchmark_phases.start(Phase::BlobValidate);
10839 if byte_length.is_some_and(|cap| bytes.len() as u64 > cap) {
10842 return Err((
10843 "sync.invalid_request".to_owned(),
10844 "blob exceeds its byteLength".to_owned(),
10845 ));
10846 }
10847 if blob_id_for(&bytes) != blob_id {
10848 return Err(simple(format!(
10849 "blob content address mismatch for {blob_id}"
10850 )));
10851 }
10852 #[cfg(feature = "bench-internals")]
10853 drop(validate_phase);
10854 #[cfg(feature = "bench-internals")]
10855 let insert_phase = self.benchmark_phases.start(Phase::BlobCacheInsert);
10856 let now = self.clock_now_ms();
10857 self.conn
10858 .execute(
10859 "INSERT OR IGNORE INTO _syncular_blobs(blob_id, bytes, byte_length, media_type, created_at_ms) VALUES (?,?,?,NULL,?)",
10860 rusqlite::params![blob_id, bytes, bytes.len() as i64, now],
10861 )
10862 .map_err(|e| simple(e.to_string()))?;
10863 self.conn
10864 .query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()))
10865 .map_err(|e| simple(e.to_string()))?;
10866 self.enforce_blob_cache_cap();
10867 #[cfg(feature = "bench-internals")]
10868 drop(insert_phase);
10869 self.get_cached_blob_bytes(&blob_id)
10870 .map_err(simple)?
10871 .ok_or_else(|| simple("blob cache write failed".to_owned()))
10872 }
10873
10874 fn get_cached_blob_bytes(&self, blob_id: &str) -> Result<Option<FetchedBlob>, String> {
10875 #[cfg(feature = "bench-internals")]
10876 let _phase = self.benchmark_phases.start(Phase::BlobCacheRead);
10877 let mut stmt = self
10878 .conn
10879 .prepare("SELECT bytes, byte_length, media_type FROM _syncular_blobs WHERE blob_id = ?")
10880 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10881 let mut rows = stmt
10882 .query(rusqlite::params![blob_id])
10883 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10884 if let Some(row) = rows
10885 .next()
10886 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
10887 {
10888 let bytes: Vec<u8> = row
10889 .get(0)
10890 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10891 let byte_length: i64 = row
10892 .get(1)
10893 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10894 let media_type: Option<String> = row
10895 .get(2)
10896 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10897 return Ok(Some(FetchedBlob {
10898 blob_id: blob_id.to_owned(),
10899 byte_length,
10900 bytes,
10901 media_type,
10902 }));
10903 }
10904 Ok(None)
10905 }
10906
10907 fn pending_blob_uploads(&self) -> Result<Vec<crate::round::PendingUpload>, TransportError> {
10909 let pending: Vec<(String, Option<String>)> = {
10910 let mut stmt = self
10911 .conn
10912 .prepare(
10913 "SELECT blob_id, media_type FROM (
10914 SELECT blob_id, media_type, created_at_ms FROM _syncular_blob_uploads
10915 UNION ALL
10916 SELECT DISTINCT r.blob_id, b.media_type, 9223372036854775807 AS created_at_ms
10917 FROM _syncular_blob_commit_refs r LEFT JOIN _syncular_blobs b ON b.blob_id = r.blob_id
10918 WHERE NOT EXISTS (SELECT 1 FROM _syncular_blob_uploads u WHERE u.blob_id = r.blob_id)
10919 ) ORDER BY created_at_ms, blob_id",
10920 )
10921 .map_err(|e| TransportError::new("client.failed", e.to_string()))?;
10922 let rows = stmt
10923 .query_map([], |row| {
10924 Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?))
10925 })
10926 .map_err(|e| TransportError::new("client.failed", e.to_string()))?;
10927 rows.collect::<Result<_, _>>()
10928 .map_err(|error| match error {
10929 rusqlite::Error::InvalidColumnType(..)
10930 | rusqlite::Error::FromSqlConversionFailure(..) => TransportError::new(
10931 "sync.local_corrupt",
10932 "Pending blob upload metadata is invalid",
10933 ),
10934 _ => TransportError::new("client.failed", error.to_string()),
10935 })?
10936 };
10937 let mut uploads = Vec::new();
10938 for (blob_id, media_type) in pending {
10939 let bytes = (|| {
10940 let stored: Option<(SqlValue, SqlValue)> = self
10941 .conn
10942 .query_row(
10943 "SELECT bytes, byte_length FROM _syncular_blobs WHERE blob_id = ?",
10944 rusqlite::params![blob_id],
10945 |row| Ok((row.get(0)?, row.get(1)?)),
10946 )
10947 .optional()
10948 .map_err(|e| TransportError::new("client.failed", e.to_string()))?;
10949 let Some((SqlValue::Blob(bytes), SqlValue::Integer(byte_length))) = stored else {
10950 return Err(TransportError::new(
10951 "sync.local_corrupt",
10952 "Pending blob upload body is missing or corrupt",
10953 ));
10954 };
10955 if byte_length != bytes.len() as i64 || blob_id_for(&bytes) != blob_id {
10956 return Err(TransportError::new(
10957 "sync.local_corrupt",
10958 "Pending blob upload body is missing or corrupt",
10959 ));
10960 }
10961 Ok(bytes)
10962 })();
10963 let failed = bytes.is_err();
10964 uploads.push(crate::round::PendingUpload {
10965 id: blob_id,
10966 bytes,
10967 media_type,
10968 });
10969 if failed {
10970 break;
10971 }
10972 }
10973 Ok(uploads)
10974 }
10975
10976 fn visible_blob_ids_sql(&self) -> String {
10977 let mut selects = Vec::new();
10978 for table in &self.schema.tables {
10979 for column in table
10980 .columns
10981 .iter()
10982 .filter(|column| column.ty == ColumnType::BlobRef)
10983 {
10984 let identifier = quote_ident(&column.name);
10985 selects.push(format!(
10986 "SELECT json_extract({identifier}, '$.blobId') AS blob_id FROM {} \
10987 WHERE typeof({identifier}) = 'text' AND json_valid({identifier}) \
10988 AND json_type({identifier}, '$.blobId') = 'text'",
10989 quote_ident(&table.name),
10990 ));
10991 }
10992 }
10993 if selects.is_empty() {
10994 "SELECT NULL AS blob_id WHERE 0".to_owned()
10995 } else {
10996 selects.join(" UNION ")
10997 }
10998 }
10999
11000 fn enforce_blob_cache_cap(&self) {
11003 let Some(max_bytes) = self.limits.blob_cache_max_bytes else {
11004 return;
11005 };
11006 let mut total: i64 = self
11007 .conn
11008 .query_row(
11009 "SELECT COALESCE(SUM(byte_length), 0) FROM _syncular_blobs",
11010 [],
11011 |row| row.get(0),
11012 )
11013 .unwrap_or(0);
11014 if total <= max_bytes {
11015 return;
11016 }
11017 let query = format!(
11018 "SELECT blob_id, byte_length FROM _syncular_blobs
11019 WHERE blob_id NOT IN (SELECT blob_id FROM _syncular_blob_uploads)
11020 AND blob_id NOT IN (SELECT blob_id FROM _syncular_blob_commit_refs)
11021 AND blob_id NOT IN ({})
11022 ORDER BY created_at_ms ASC, blob_id ASC",
11023 self.visible_blob_ids_sql(),
11024 );
11025 let candidates: Vec<(String, i64)> = {
11026 let Ok(mut stmt) = self.conn.prepare(&query) else {
11027 return;
11028 };
11029 let Ok(rows) = stmt.query_map([], |row| {
11030 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
11031 }) else {
11032 return;
11033 };
11034 rows.filter_map(Result::ok).collect()
11035 };
11036 for (blob_id, byte_length) in candidates {
11037 if total <= max_bytes {
11038 break;
11039 }
11040 let _ = self.conn.execute(
11041 "DELETE FROM _syncular_blobs WHERE blob_id = ?",
11042 rusqlite::params![blob_id],
11043 );
11044 total -= byte_length;
11045 }
11046 }
11047
11048 fn delete_unreferenced_cached_blobs(&mut self) -> Result<(), String> {
11049 #[cfg(feature = "bench-internals")]
11050 let _phase = self.benchmark_phases.start(Phase::BlobRetention);
11051 if !self.schema_has_blobs() {
11052 return Ok(());
11053 }
11054 self.rebuild_overlay_if_dirty()?;
11055 self.conn
11056 .execute(
11057 &format!(
11058 "DELETE FROM _syncular_blobs
11059 WHERE blob_id NOT IN (SELECT blob_id FROM _syncular_blob_uploads)
11060 AND blob_id NOT IN (SELECT blob_id FROM _syncular_blob_commit_refs)
11061 AND blob_id NOT IN ({})",
11062 self.visible_blob_ids_sql(),
11063 ),
11064 [],
11065 )
11066 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11067 Ok(())
11068 }
11069
11070 fn insert_row_sql(&self, full_table: &str, table: &crate::schema::TableSchema) -> String {
11075 if let Some(sql) = self.insert_sql.borrow().get(full_table) {
11076 return sql.clone();
11077 }
11078 let mut columns: Vec<String> = table.columns.iter().map(|c| quote_ident(&c.name)).collect();
11079 columns.push(quote_ident("_syncular_version"));
11080 let placeholders: Vec<&str> = columns.iter().map(|_| "?").collect();
11081 let primary_key = quote_ident(&table.primary_key);
11082 let updates = columns
11083 .iter()
11084 .filter(|column| **column != primary_key)
11085 .map(|column| format!("{column}=excluded.{column}"))
11086 .collect::<Vec<_>>()
11087 .join(", ");
11088 let guard = if full_table == base_table(&table.name) {
11091 format!(" WHERE excluded._syncular_version > {full_table}._syncular_version")
11092 } else {
11093 String::new()
11094 };
11095 let sql = format!(
11096 "INSERT INTO {full_table} ({}) VALUES ({}) ON CONFLICT ({primary_key}) DO UPDATE SET {updates}{guard}",
11097 columns.join(", "),
11098 placeholders.join(", ")
11099 );
11100 self.insert_sql
11101 .borrow_mut()
11102 .insert(full_table.to_owned(), sql.clone());
11103 sql
11104 }
11105
11106 fn write_base_row(&self, table_name: &str, row: &Row, version: i64) -> Result<bool, String> {
11109 let landed = self.write_row(&base_table(table_name), table_name, row, version)?;
11110 if landed {
11111 self.overlay_dirty.table(table_name);
11112 }
11113 Ok(landed)
11114 }
11115
11116 fn write_row(
11117 &self,
11118 full_table: &str,
11119 table_name: &str,
11120 row: &Row,
11121 version: i64,
11122 ) -> Result<bool, String> {
11123 #[cfg(feature = "bench-internals")]
11124 let _phase = self.benchmark_phases.start(Phase::RowWrite);
11125 let table = self
11126 .schema
11127 .table(table_name)
11128 .ok_or_else(|| format!("unknown table {table_name:?}"))?;
11129 let sql = self.insert_row_sql(full_table, table);
11130 let mut stmt = self
11131 .conn
11132 .prepare_cached(&sql)
11133 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11134 let params = row
11135 .iter()
11136 .map(RowParam::Cell)
11137 .chain(std::iter::once(RowParam::Version(version)));
11138 stmt.execute(rusqlite::params_from_iter(params))
11139 .map(|changed| changed > 0)
11140 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
11141 }
11142
11143 fn delete_base_row(
11146 &self,
11147 table_name: &str,
11148 row_id: &str,
11149 version: i64,
11150 ) -> Result<bool, String> {
11151 let table = self
11152 .schema
11153 .table(table_name)
11154 .ok_or_else(|| format!("unknown table {table_name:?}"))?;
11155 let sql = format!(
11156 "DELETE FROM {} WHERE {} AND _syncular_version < ?2",
11157 base_table(table_name),
11158 row_id_predicate(table)
11159 );
11160 let mut stmt = self
11161 .conn
11162 .prepare_cached(&sql)
11163 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11164 let landed = stmt
11165 .execute(rusqlite::params![row_id, version])
11166 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
11167 > 0;
11168 if landed {
11169 self.overlay_dirty.table(table_name);
11170 }
11171 Ok(landed)
11172 }
11173
11174 fn has_acknowledged_rows(&self) -> Result<bool, String> {
11175 self.conn
11176 .query_row(
11177 "SELECT EXISTS(SELECT 1 FROM _syncular_acked_rows)",
11178 [],
11179 |row| row.get(0),
11180 )
11181 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
11182 }
11183
11184 fn acknowledged_operations(
11185 &self,
11186 rows: Option<&HashSet<(&str, &str)>>,
11187 tables: Option<&BTreeSet<&str>>,
11188 ) -> Result<Vec<OutboxOp>, String> {
11189 let keys = rows.map(|rows| serde_json::to_string(&rows).expect("row keys"));
11190 let mut statement = self
11191 .conn
11192 .prepare_cached(if keys.is_some() {
11193 ACKED_ROWS_BY_ROW
11194 } else {
11195 "SELECT op_json,intent_json FROM _syncular_acked_rows WHERE ?1 IS NULL AND (?2 IS NULL OR tbl IN(SELECT value FROM json_each(?2))) ORDER BY commit_seq,idx"
11196 })
11197 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11198 let rows = statement
11199 .query_map(
11200 rusqlite::params![
11201 keys,
11202 tables.map(|tables| serde_json::to_string(tables).expect("table keys"))
11203 ],
11204 |row| {
11205 #[cfg(test)]
11206 self.acknowledged_replay_count
11207 .set(self.acknowledged_replay_count.get() + 1);
11208 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
11209 },
11210 )
11211 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11212 rows.map(|row| {
11213 let (op, intent) =
11214 row.map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11215 let op: CommitOperation =
11216 serde_json::from_str(&op).map_err(|_| "sync.local_corrupt".to_owned())?;
11217 let table = self
11218 .schema
11219 .table(&op.table)
11220 .ok_or_else(|| "sync.unknown_table".to_owned())?;
11221 let values = if op.op == "upsert" && self.visible_row(table, &op.row_id)?.is_none() {
11222 Some(serde_json::from_str(&intent).map_err(|_| "sync.local_corrupt".to_owned())?)
11223 } else {
11224 op.values
11225 };
11226 Ok(OutboxOp {
11227 table: op.table,
11228 row_id: op.row_id,
11229 upsert: op.op == "upsert",
11230 base_version: op.base_version,
11231 values,
11232 insert: op.insert,
11233 })
11234 })
11235 .collect()
11236 }
11237
11238 fn drop_acknowledged_rows(
11239 &self,
11240 matches: impl Fn(&str, &Map<String, Value>) -> bool,
11241 ) -> Result<Vec<OutboxCommit>, String> {
11242 let mut statement = self
11243 .conn
11244 .prepare("SELECT commit_id,tbl,intent_json,id FROM _syncular_acked_rows")
11245 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11246 let rows = statement
11247 .query_map([], |row| {
11248 Ok((
11249 row.get::<_, String>(0)?,
11250 row.get::<_, String>(1)?,
11251 row.get::<_, String>(2)?,
11252 row.get::<_, String>(3)?,
11253 ))
11254 })
11255 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11256 let mut doomed = HashSet::new();
11257 for row in rows {
11258 let (id, table, intent, row_id) =
11259 row.map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11260 let values =
11261 serde_json::from_str(&intent).map_err(|_| "sync.local_corrupt".to_owned())?;
11262 let base_matches = self
11263 .schema
11264 .table(&table)
11265 .map(|schema| self.stored_row(schema, &row_id, true))
11266 .transpose()?
11267 .flatten()
11268 .is_some_and(|(base, _)| {
11269 let schema = self.schema.table(&table).expect("acknowledged table");
11270 let values = schema
11271 .columns
11272 .iter()
11273 .zip(base.iter())
11274 .map(|(column, value)| (column.name.clone(), column_value_to_json(value)))
11275 .collect();
11276 matches(&table, &values)
11277 });
11278 if matches(&table, &values) || base_matches {
11279 doomed.insert(id);
11280 }
11281 }
11282 let mut dropped = Vec::new();
11283 for id in doomed {
11284 let mut statement = self
11285 .conn
11286 .prepare("SELECT op_json FROM _syncular_acked_rows WHERE commit_id=?1 ORDER BY idx")
11287 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11288 let operations = statement
11289 .query_map([&id], |row| row.get::<_, String>(0))
11290 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11291 let mut ops = Vec::new();
11292 for operation in operations {
11293 let json = operation
11294 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11295 let op: CommitOperation =
11296 serde_json::from_str(&json).map_err(|_| "sync.local_corrupt".to_owned())?;
11297 self.overlay_dirty.table(&op.table);
11298 ops.push(OutboxOp {
11299 table: op.table,
11300 row_id: op.row_id,
11301 upsert: op.op == "upsert",
11302 base_version: op.base_version,
11303 values: op.values,
11304 insert: op.insert,
11305 });
11306 }
11307 dropped.push(OutboxCommit {
11308 client_commit_id: id.clone(),
11309 ops,
11310 });
11311 self.conn
11312 .execute("DELETE FROM _syncular_acked_rows WHERE commit_id=?1", [id])
11313 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11314 }
11315 Ok(dropped)
11316 }
11317
11318 fn rebuild_overlay_if_dirty(&mut self) -> Result<(), String> {
11321 if self.overlay_dirty.get() {
11322 self.rebuild_overlay()?;
11323 }
11324 Ok(())
11325 }
11326
11327 fn rebuild_overlay(&mut self) -> Result<(), String> {
11331 #[cfg(feature = "bench-internals")]
11332 let _phase = self.benchmark_phases.start(Phase::OverlayRebuild);
11333 #[cfg(test)]
11334 self.overlay_rebuild_count
11335 .set(self.overlay_rebuild_count.get() + 1);
11336 let dirty = self.overlay_dirty.snapshot();
11337 let tables: Vec<_> = self
11338 .schema
11339 .tables
11340 .iter()
11341 .filter(|table| {
11342 dirty
11343 .as_ref()
11344 .is_none_or(|names| names.is_empty() || names.contains(&table.name))
11345 })
11346 .cloned()
11347 .collect();
11348 let names: BTreeSet<_> = tables.iter().map(|table| table.name.as_str()).collect();
11349 self.begin_observation("syncular_overlay")?;
11350 let rebuilt = (|| {
11351 for table in &tables {
11352 for index in &table.fts_indexes {
11353 self.drop_fts_triggers(index)?;
11354 }
11355 let visible = visible_table(&table.name);
11356 let base = base_table(&table.name);
11357 self.conn
11358 .execute_batch(&format!(
11359 "DELETE FROM {visible}; INSERT INTO {visible} SELECT * FROM {base}"
11360 ))
11361 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11362 }
11363 self.replay_overlay(&names, None)?;
11364 for table in &tables {
11365 for index in &table.fts_indexes {
11366 self.rebuild_fts_projection(table, index)?;
11367 self.create_fts_triggers(table, index)?;
11368 }
11369 }
11370 self.conn
11371 .execute_batch("RELEASE syncular_overlay")
11372 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11373 Ok(())
11374 })();
11375 if rebuilt.is_err() {
11376 self.conn
11377 .execute_batch("ROLLBACK TO syncular_overlay; RELEASE syncular_overlay")
11378 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11379 } else {
11380 self.overlay_dirty.set(false);
11381 }
11382 rebuilt
11383 }
11384
11385 fn replay_overlay(
11389 &self,
11390 tables: &BTreeSet<&str>,
11391 rows: Option<&HashSet<(&str, &str)>>,
11392 ) -> Result<(), String> {
11393 let replays = |op: &&OutboxOp| {
11394 tables.contains(op.table.as_str())
11395 && rows.is_none_or(|rows| rows.contains(&(op.table.as_str(), op.row_id.as_str())))
11396 };
11397 self.apply_outbox_ops(&self.acknowledged_operations(rows, Some(tables))?)?;
11398 for (commit, initial) in self.failed_commits.iter() {
11399 for operation in commit.ops.iter().filter(replays) {
11400 let Some(table) = self.schema.table(&operation.table) else {
11401 continue;
11402 };
11403 if operation.upsert
11404 && self.visible_row(table, &operation.row_id)?.is_none()
11405 && operation.presented_values(table).is_some_and(|values| {
11406 table
11407 .columns
11408 .iter()
11409 .all(|column| values.contains_key(&column.name))
11410 })
11411 {
11412 let key = serde_json::to_string(&(&operation.table, &operation.row_id))
11413 .expect("row key serialization");
11414 if let Some(Value::Object(values)) = initial.get(&key) {
11415 self.apply_outbox_ops(&[OutboxOp {
11416 values: Some(values.clone()),
11417 ..operation.clone()
11418 }])?;
11419 }
11420 }
11421 }
11422 self.apply_outbox_ops(commit.ops.iter().filter(replays))?;
11423 }
11424 self.apply_outbox_ops(
11425 self.outbox
11426 .iter()
11427 .flat_map(|commit| &commit.ops)
11428 .filter(replays),
11429 )
11430 }
11431
11432 fn apply_outbox_ops<'a>(
11437 &self,
11438 ops: impl IntoIterator<Item = &'a OutboxOp>,
11439 ) -> Result<(), String> {
11440 #[cfg(feature = "bench-internals")]
11441 let mut phase = self.benchmark_phases.start(Phase::PendingReplay);
11442 for op in ops {
11443 #[cfg(feature = "bench-internals")]
11444 if let Some(phase) = &mut phase {
11445 phase.unit();
11446 }
11447 if op.upsert {
11448 if let Some((table, values)) = self
11449 .schema
11450 .table(&op.table)
11451 .filter(|table| table.indexes.iter().any(|index| index.unique))
11452 .and_then(|table| Some((table, op.presented_values(table)?)))
11453 {
11454 let mut intended: Map<String, Value> = self
11455 .visible_row(table, &op.row_id)?
11456 .map(|(row, _)| {
11457 table
11458 .columns
11459 .iter()
11460 .zip(&row)
11461 .map(|(column, value)| {
11462 (column.name.clone(), column_value_to_json(value))
11463 })
11464 .collect()
11465 })
11466 .unwrap_or_default();
11467 intended.extend(values.into_owned());
11468 if !self.unique_conflicts(table, &intended, false)?.is_empty() {
11469 continue;
11470 }
11471 }
11472 }
11473 self.apply_outbox_op(op)?;
11474 }
11475 Ok(())
11476 }
11477
11478 fn apply_outbox_op(&self, op: &OutboxOp) -> Result<(), String> {
11479 let Some(table) = self.schema.table(&op.table) else {
11480 return Ok(());
11481 };
11482 let visible = visible_table(&table.name);
11483 if !op.upsert {
11484 let sql = format!("DELETE FROM {visible} WHERE {}", row_id_predicate(table));
11485 self.conn
11486 .prepare_cached(&sql)
11487 .and_then(|mut statement| statement.execute(rusqlite::params![op.row_id]))
11488 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11489 return Ok(());
11490 }
11491 let Some(values) = op.presented_values(table) else {
11495 return Ok(());
11496 };
11497 let full = table
11503 .columns
11504 .iter()
11505 .all(|column| values.contains_key(&column.name));
11506 let local = self.visible_row(table, &op.row_id)?;
11507 if local.is_none() && !full {
11508 return Ok(());
11509 }
11510 let mut row: Row = Vec::with_capacity(table.columns.len());
11511 for (index, column) in table.columns.iter().enumerate() {
11512 let incoming = match values.get(&column.name) {
11513 Some(raw) => json_to_column_value(
11514 if raw.get("$bytes").is_some()
11515 && table
11516 .encrypted_columns
11517 .iter()
11518 .any(|encrypted| encrypted.index == index)
11519 {
11520 &table.wire_columns[index]
11521 } else {
11522 column
11523 },
11524 Some(raw),
11525 ),
11526 None => Ok(local
11527 .as_ref()
11528 .and_then(|(values, _)| values.get(index).cloned())
11529 .flatten()),
11530 };
11531 row.push(incoming.map_err(|_| {
11532 "sync.local_corrupt: persisted optimistic row value is invalid".to_owned()
11533 })?);
11534 }
11535 let version = local.as_ref().map_or(-1, |(_, version)| *version);
11536 self.write_row(&visible, &table.name, &row, version)
11537 .map(|_| ())
11538 .map_err(|error| {
11539 if error.starts_with("UNIQUE constraint failed:") {
11540 "sync.constraint_violation: local write violates a unique constraint".to_owned()
11541 } else {
11542 error
11543 }
11544 })
11545 }
11546
11547 fn visible_row_exists(&self, table: &TableSchema, row_id: &str) -> Result<bool, String> {
11551 self.conn
11552 .query_row(
11553 &format!(
11554 "SELECT EXISTS(SELECT 1 FROM {} WHERE {})",
11555 visible_table(&table.name),
11556 row_id_predicate(table)
11557 ),
11558 rusqlite::params![row_id],
11559 |row| row.get(0),
11560 )
11561 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
11562 }
11563
11564 fn visible_row(&self, table: &TableSchema, row_id: &str) -> Result<Option<(Row, i64)>, String> {
11567 self.stored_row(table, row_id, false)
11568 }
11569
11570 fn stored_row(
11571 &self,
11572 table: &TableSchema,
11573 row_id: &str,
11574 base: bool,
11575 ) -> Result<Option<(Row, i64)>, String> {
11576 let sql = format!(
11577 "SELECT * FROM {} WHERE {}",
11578 if base {
11579 base_table(&table.name)
11580 } else {
11581 visible_table(&table.name)
11582 },
11583 row_id_predicate(table)
11584 );
11585 let mut stmt = self
11586 .conn
11587 .prepare_cached(&sql)
11588 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11589 let mut rows = stmt
11590 .query(rusqlite::params![row_id])
11591 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11592 let Some(row) = rows
11593 .next()
11594 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
11595 else {
11596 return Ok(None);
11597 };
11598 let mut values = Vec::with_capacity(table.columns.len());
11599 for (index, column) in table.columns.iter().enumerate() {
11600 let raw = row
11601 .get_ref(index)
11602 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11603 if let rusqlite::types::ValueRef::Blob(bytes) = raw {
11604 if table
11605 .encrypted_columns
11606 .iter()
11607 .any(|encrypted| encrypted.index == index)
11608 {
11609 values.push(Some(ColumnValue::Bytes(bytes.to_vec())));
11610 continue;
11611 }
11612 }
11613 let json = sql_ref_to_json(column, raw);
11614 values.push(json_to_column_value(column, Some(&json)).map_err(|_| {
11615 "sync.local_corrupt: persisted local row value is invalid".to_owned()
11616 })?);
11617 }
11618 let version: i64 = row
11619 .get(table.columns.len())
11620 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11621 Ok(Some((values, version)))
11622 }
11623
11624 #[must_use]
11628 pub fn realtime_policy(&self) -> RealtimePolicy {
11629 self.realtime_policy
11630 }
11631
11632 #[must_use]
11634 pub fn realtime_state(&self) -> RealtimeState {
11635 self.realtime_state
11636 }
11637
11638 pub fn set_realtime_policy(&mut self, policy: RealtimePolicy) -> Result<(), String> {
11641 if self.realtime_state == RealtimeState::Connected && policy != self.realtime_policy {
11642 return Err(
11643 "sync.invalid_request: disconnect realtime before changing the realtime policy"
11644 .to_owned(),
11645 );
11646 }
11647 self.realtime_policy = policy;
11648 match policy {
11649 RealtimePolicy::Off => self.set_realtime_state(RealtimeState::Disabled, None),
11650 RealtimePolicy::Required | RealtimePolicy::Optional
11651 if self.realtime_state == RealtimeState::Disabled =>
11652 {
11653 self.set_realtime_state(RealtimeState::Disconnected, None);
11654 }
11655 RealtimePolicy::Required | RealtimePolicy::Optional => {}
11656 }
11657 Ok(())
11658 }
11659
11660 fn set_realtime_state(&mut self, state: RealtimeState, reason_code: Option<&str>) {
11663 self.realtime_state = state;
11664 self.realtime_reason_code = reason_code.map(Self::diagnostic_code);
11665 if matches!(state, RealtimeState::Connected | RealtimeState::Disabled) {
11666 self.realtime_reason_code = None;
11667 self.realtime_retry_delay_ms = None;
11668 }
11669 }
11670
11671 #[must_use]
11672 fn realtime_connected(&self) -> bool {
11673 self.realtime_state == RealtimeState::Connected
11674 }
11675
11676 pub fn connect_realtime(&mut self, transport: &mut dyn Transport) -> Result<(), String> {
11677 if !self.transport_enabled {
11678 return Err("sync.offline: transport gate is closed".into());
11679 }
11680 if self.realtime_policy == RealtimePolicy::Off {
11681 return Err(
11682 "sync.invalid_request: realtime is off; realtime connect is refused".to_owned(),
11683 );
11684 }
11685 if self.realtime_connected() {
11686 return Ok(());
11687 }
11688 self.realtime_state = RealtimeState::Connecting;
11689 match transport.realtime_connect_for_client(&self.client_id) {
11690 Ok(()) => {
11691 self.set_realtime_state(RealtimeState::Connected, None);
11692 Ok(())
11693 }
11694 Err(error) => {
11695 self.set_realtime_state(RealtimeState::Refused, Some(&error.code));
11696 Err(format!("{}: {}", error.code, error.message))
11697 }
11698 }
11699 }
11700
11701 pub fn disconnect_realtime(&mut self, transport: &mut dyn Transport) {
11702 self.cancel_sync_round();
11703 if self.realtime_state == RealtimeState::Disconnected
11706 || self.realtime_state == RealtimeState::Disabled
11707 {
11708 return;
11709 }
11710 let _ = transport.realtime_close();
11711 self.set_realtime_state(
11712 if self.realtime_policy == RealtimePolicy::Off {
11713 RealtimeState::Disabled
11714 } else {
11715 RealtimeState::Disconnected
11716 },
11717 None,
11718 );
11719 self.presence.clear(); }
11721
11722 pub fn realtime_lost(&mut self, transport: &mut dyn Transport, error: &TransportError) {
11728 if self.realtime_state != RealtimeState::Connected {
11729 return;
11730 }
11731 self.cancel_sync_round();
11732 let _ = transport.realtime_close();
11733 self.set_realtime_state(RealtimeState::Lost, Some(&error.code));
11734 self.realtime_retry_delay_ms = Some(self.schedule_background_retry());
11735 self.presence.clear();
11736 }
11737
11738 pub fn set_presence(
11744 &mut self,
11745 transport: &mut dyn Transport,
11746 scope_key: &str,
11747 doc: Option<&Value>,
11748 ) -> Result<(), String> {
11749 if !self.transport_enabled {
11750 return Err("sync.offline: transport gate is closed".into());
11751 }
11752 if !self.realtime_connected() {
11753 return Err("setPresence requires a connected realtime socket (§8.6)".to_string());
11754 }
11755 let text = encode_presence_publish(scope_key, doc);
11756 transport
11757 .realtime_send(&text)
11758 .map_err(|e| format!("{}: {}", e.code, e.message))
11759 }
11760
11761 pub fn presence(&self, scope_key: &str) -> Vec<PresencePeer> {
11763 self.presence
11764 .get(scope_key)
11765 .map(|peers| peers.values().cloned().collect())
11766 .unwrap_or_default()
11767 }
11768
11769 fn apply_presence(
11771 &mut self,
11772 scope_key: String,
11773 kind: Option<PresenceKind>,
11774 actor_id: Option<String>,
11775 client_id: Option<String>,
11776 doc: Option<Value>,
11777 error: Option<String>,
11778 ) {
11779 if error.is_some() {
11782 return;
11783 }
11784 let (Some(kind), Some(actor_id), Some(client_id)) = (kind, actor_id, client_id) else {
11785 return;
11786 };
11787 let peer_key = format!("{actor_id} {client_id}");
11788 match kind {
11789 PresenceKind::Leave => {
11790 if let Some(peers) = self.presence.get_mut(&scope_key) {
11791 peers.remove(&peer_key);
11792 if peers.is_empty() {
11793 self.presence.remove(&scope_key);
11794 }
11795 }
11796 }
11797 _ => {
11798 let doc = match doc {
11799 Some(Value::Object(_)) => doc.unwrap(),
11800 _ => return,
11801 };
11802 self.presence.entry(scope_key).or_default().insert(
11803 peer_key,
11804 PresencePeer {
11805 actor_id,
11806 client_id,
11807 doc,
11808 },
11809 );
11810 }
11811 }
11812 }
11813
11814 pub fn on_realtime_text(&mut self, text: &str) {
11816 if self.stopped || !self.transport_enabled || self.security_preflight {
11817 return;
11818 }
11819 match parse_control(text) {
11820 Ok(ControlMessage::Hello { requires_sync, .. }) => {
11821 if requires_sync {
11822 self.set_sync_needed(true, true);
11824 }
11825 }
11826 Ok(ControlMessage::Presence {
11827 scope_key,
11828 kind,
11829 actor_id,
11830 client_id,
11831 doc,
11832 error,
11833 ..
11834 }) => {
11835 self.apply_presence(scope_key, kind, actor_id, client_id, doc, error);
11836 }
11837 Ok(ControlMessage::Wake { .. }) => {
11838 self.set_sync_needed(true, true);
11840 }
11841 _ => {}
11842 }
11843 }
11844
11845 pub fn on_realtime_binary_queued(&mut self, bytes: &[u8]) -> Vec<String> {
11849 let mut transport = crate::round::DownloadResults::default();
11850 self.on_realtime_binary(&mut transport, bytes);
11851 transport.controls
11852 }
11853
11854 pub fn on_realtime_binary(&mut self, transport: &mut dyn Transport, bytes: &[u8]) {
11860 if self.stopped || !self.transport_enabled || self.security_preflight {
11861 return;
11862 }
11863 #[cfg(feature = "bench-internals")]
11864 let decode_phase = self.benchmark_phases.start(Phase::ResponseDecode);
11865 let message = match decode_message(bytes) {
11866 Ok(m) if m.msg_kind == MsgKind::Response => m,
11867 _ => {
11868 self.set_sync_needed(true, true);
11869 return;
11870 }
11871 };
11872 #[cfg(feature = "bench-internals")]
11873 drop(decode_phase);
11874 if let Some(Frame::RespHeader {
11877 required_schema_version: Some(required),
11878 latest_schema_version,
11879 ..
11880 }) = message.frames.first()
11881 {
11882 let floor = SchemaFloor {
11883 required_schema_version: Some(*required),
11884 latest_schema_version: *latest_schema_version,
11885 };
11886 if self.set_schema_floor(Some(floor)).is_err() {
11887 self.set_sync_needed(true, true);
11888 }
11889 return;
11890 }
11891 let through = message.frames.iter().find_map(|frame| match frame {
11892 Frame::LiveEnd { through_seq, .. } => Some(*through_seq),
11893 _ => None,
11894 });
11895 let epoch = self.get_meta(LOG_EPOCH_KEY).ok().flatten();
11896 let mut dropped = !matches!(message.frames.first(), Some(Frame::RespHeader {
11897 reset_required: false,
11898 log_epoch,
11899 ..
11900 }) if Some(log_epoch) == epoch.as_ref());
11901 let mut applied: Option<i64> = None;
11902 dropped |= self.begin_coalesced_apply().is_err();
11903 let mut receivers: Option<Vec<UnitRecord>> = None;
11904 for frame in message.frames {
11905 if dropped {
11906 break;
11907 }
11908 match frame {
11909 Frame::LiveStart { status, .. } => {
11910 let Ok(cursor) = self.stored(replica_cursor(&self.conn)) else {
11911 dropped = true;
11912 break;
11913 };
11914 if status != LiveStatus::Active || cursor < 0 {
11915 dropped = true;
11916 } else if !through.is_some_and(|through| through <= cursor) {
11917 receivers = self.stored(load_units(&self.conn)).ok().map(|units| {
11919 units
11920 .into_iter()
11921 .filter(|unit| {
11922 unit.status == UnitStatus::Active
11923 && unit.echo.is_some()
11924 && unit.cursor.is_none()
11925 })
11926 .collect()
11927 });
11928 dropped = receivers.is_none();
11929 }
11930 }
11931 Frame::Commit {
11932 commit_seq,
11933 tables,
11934 changes,
11935 ..
11936 } => {
11937 let Some(receivers) = receivers.clone() else {
11938 continue;
11939 };
11940 dropped = self
11941 .apply_live_commit(
11942 &receivers,
11943 &tables,
11944 changes,
11945 commit_seq,
11946 AppliedServerCommitSource::Delta,
11947 )
11948 .is_err();
11949 }
11950 Frame::LiveEnd { through_seq, .. } if receivers.is_some() => {
11951 dropped = self
11952 .set_meta(REPLICA_CURSOR_KEY, &through_seq.to_string())
11953 .is_err();
11954 applied = Some(through_seq);
11955 }
11956 Frame::Error { .. } => dropped = true,
11957 _ => {}
11958 }
11959 }
11960 if self.finish_coalesced_apply().is_err() || dropped {
11963 applied = None;
11964 dropped = true;
11965 }
11966 if let Some(cursor) = applied {
11967 let ack = format!("{{\"type\":\"ack\",\"cursor\":{cursor}}}");
11969 let _ = transport.realtime_send(&ack);
11970 }
11971 if dropped {
11972 self.set_sync_needed(true, true);
11973 }
11974 }
11975
11976 fn ack_after_pull(&mut self, transport: &mut dyn Transport) {
11979 if !self.realtime_connected() {
11980 return;
11981 }
11982 if let Ok(cursor @ 0..) = self.stored(replica_cursor(&self.conn)) {
11983 let ack = format!("{{\"type\":\"ack\",\"cursor\":{cursor}}}");
11984 let _ = transport.realtime_send(&ack);
11985 }
11986 }
11987}
11988
11989#[cfg(test)]
11990mod previous_version_wiring_tests;
11991
11992#[cfg(test)]
11993mod identity_tests;
11994
11995#[cfg(test)]
11996mod insert_intent_tests;
11997
11998#[cfg(test)]
11999mod robustness_tests;