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(error) => {
8241 failure = Some((error.code, error.message, None, error.details));
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 p.import_write_ms = None;
8876 p.import_commit_ms = None;
8877 });
8878 let (pair, clear, effective, owners) =
8879 self.segment_pair(&unit, &echo, &seeded, &segment.table)?;
8880 report.segment_rows_applied +=
8881 self.apply_segment(&pair, &segment, clear, &effective, &owners)?;
8882 seeded.insert(segment.table);
8883 }
8884 Frame::SegmentRef {
8885 segment_id,
8886 byte_length,
8887 media_type,
8888 table,
8889 row_count,
8890 as_of_commit_seq,
8891 scope_digest,
8892 row_cursor,
8893 next_row_cursor,
8894 url,
8895 url_expires_at_ms,
8896 ..
8897 } => {
8898 let advertised = match media_type {
8901 MediaType::Rows => {
8902 meta.accept & ACCEPT_EXTERNAL_ROWS != 0
8903 || meta.accept & ACCEPT_INLINE_ROWS != 0
8904 }
8905 MediaType::Sqlite => meta.accept & ACCEPT_SQLITE != 0,
8906 };
8907 if !advertised {
8908 return Err(SectionError::Abort(
8909 "sync.invalid_request".to_owned(),
8910 format!(
8911 "SEGMENT_REF mediaType {} was not advertised in accept (§4.2)",
8912 media_type.name()
8913 ),
8914 ));
8915 }
8916 self.progress.update(|p| {
8917 p.phase = ProgressPhase::Download;
8918 p.unit_id = Some(unit.id.clone());
8919 p.table = Some(table.clone());
8920 p.segment_id = Some(segment_id.clone());
8921 p.bytes_received = 0;
8922 p.bytes_total = u64::try_from(byte_length).ok();
8923 p.rows_processed = 0;
8924 p.rows_total = u64::try_from(row_count).ok();
8925 p.import_write_ms = None;
8926 p.import_commit_ms = None;
8927 });
8928 let observer = self.progress.clone();
8929 let mut on_progress = |bytes| observer.update(|p| p.bytes_received = bytes);
8930 let bytes = if let Some(url) = url {
8931 if meta.accept & ACCEPT_SIGNED_URLS == 0 {
8936 return Err(SectionError::Abort(
8937 "sync.invalid_request".to_owned(),
8938 "SEGMENT_REF carries a url but accept bit 3 was not advertised (§5.4)"
8939 .to_owned(),
8940 ));
8941 }
8942 if url_expires_at_ms.is_some_and(|exp| {
8944 exp <= transport
8945 .segment_fetch_started_at(&url)
8946 .unwrap_or_else(|| self.clock_now_ms())
8947 }) {
8948 return Err(SectionError::Abort(
8949 "sync.segment_expired".to_owned(),
8950 format!(
8951 "signed URL for segment {segment_id} expired before fetch — re-pull mints fresh descriptors (§5.4)"
8952 ),
8953 ));
8954 }
8955 transport
8956 .fetch_url(
8957 &url,
8958 u64::try_from(byte_length).unwrap_or(0),
8959 &mut on_progress,
8960 )
8961 .map_err(|e| self.segment_fetch_failed(e))?
8962 } else {
8963 let requested_scopes_json =
8965 canonical_scope_json(&unit_scope_lists(&unit.scopes));
8966 transport
8967 .download_segment(
8968 &SegmentRequest {
8969 segment_id: segment_id.clone(),
8970 table: table.clone(),
8971 requested_scopes_json,
8972 byte_length: u64::try_from(byte_length).unwrap_or(0),
8973 },
8974 &mut on_progress,
8975 )
8976 .map_err(|e| self.segment_fetch_failed(e))?
8977 };
8978 if bytes.len() as i64 > byte_length {
8982 self.schedule_background_retry();
8983 return Err(SectionError::Abort(
8984 "sync.invalid_request".to_owned(),
8985 "segment exceeds its byteLength".to_owned(),
8986 ));
8987 }
8988 on_progress(bytes.len() as u64);
8989 let digest = Sha256::digest(&bytes);
8990 let expected = segment_id
8991 .strip_prefix("sha256:")
8992 .unwrap_or(segment_id.as_str());
8993 if bytes_to_hex(&digest) != expected {
8994 self.schedule_background_retry();
8996 return Err(SectionError::Abort(
8997 "sync.invalid_request".to_owned(),
8998 "segment bytes do not match the content address (§5.1)".to_owned(),
8999 ));
9000 }
9001 self.progress.update(|p| p.phase = ProgressPhase::Import);
9002 if media_type == MediaType::Sqlite {
9003 if row_cursor.is_some() || next_row_cursor.is_some() {
9006 return Err(SectionError::Abort(
9007 "sync.invalid_request".to_owned(),
9008 "sqlite segments are whole-table: rowCursor/nextRowCursor must be absent (§5.3)"
9009 .to_owned(),
9010 ));
9011 }
9012 let (pair, clear, effective, owners) =
9013 self.segment_pair(&unit, &echo, &seeded, &table)?;
9014 report.segment_rows_applied += self.apply_sqlite_segment(
9015 &pair,
9016 &bytes,
9017 clear,
9018 &effective,
9019 &owners,
9020 row_count,
9021 as_of_commit_seq,
9022 &scope_digest,
9023 )?;
9024 seeded.insert(table);
9025 } else {
9026 let segment = decode_rows_segment(&bytes).map_err(|e| {
9027 SectionError::Abort(e.code.as_str().to_owned(), e.detail)
9028 })?;
9029 let (pair, clear, effective, owners) =
9030 self.segment_pair(&unit, &echo, &seeded, &segment.table)?;
9031 report.segment_rows_applied +=
9032 self.apply_segment(&pair, &segment, clear, &effective, &owners)?;
9033 seeded.insert(segment.table);
9034 }
9035 }
9036 _ => {
9037 return Err(SectionError::Abort(
9038 "sync.invalid_request".to_owned(),
9039 "unexpected frame inside a unit section".to_owned(),
9040 ));
9041 }
9042 }
9043 }
9044 Ok(())
9045 })();
9046 match applied {
9047 Ok(()) => {}
9048 Err(SectionError::FailClosed) => {
9049 self.begin_observation("syncular_section_failure")
9052 .map_err(storage)?;
9053 let failed = (|| {
9054 let mut batch = ChangeAccumulator::default();
9055 for (base_key, value) in self.stored(window_refs(&self.conn, &unit.id))? {
9056 batch.window(&base_key, &value);
9057 }
9058 batch.status = true;
9059 self.stored(save_unit(
9060 &self.conn,
9061 &UnitRecord {
9062 status: UnitStatus::Failed,
9063 reason_code: Some("sync.scope_revoked".to_owned()),
9064 ..unit.clone()
9065 },
9066 ))?;
9067 self.finish_observation("syncular_section_failure", batch)
9068 })();
9069 if let Err(message) = failed {
9070 self.rollback_observation("syncular_section_failure");
9071 return Err(storage(message));
9072 }
9073 report.failed.push(unit.id);
9074 return Ok(());
9075 }
9076 Err(error) => return Err(error),
9077 }
9078 let Some((next_cursor, bootstrap_state)) = sub_end else {
9079 return Ok(());
9080 };
9081 let rollback = self.capture();
9082 let counts = (
9083 report.resets.len(),
9084 report.revoked.len(),
9085 report.failed.len(),
9086 );
9087 self.begin_observation("syncular_sub_end")
9088 .map_err(storage)?;
9089 let finished = self
9090 .finish_section(start, echo, next_cursor, bootstrap_state, seeded, report)
9091 .and_then(|batch| {
9092 self.rebuild_overlay_if_dirty()?;
9093 self.finish_observation("syncular_sub_end", batch)
9094 });
9095 if let Err(message) = finished {
9096 self.rollback_observation("syncular_sub_end");
9097 self.restore(rollback);
9098 report.resets.truncate(counts.0);
9099 report.revoked.truncate(counts.1);
9100 report.failed.truncate(counts.2);
9101 return Err(storage(message));
9102 }
9103 Ok(())
9104 }
9105
9106 #[allow(clippy::type_complexity)]
9109 fn segment_pair(
9110 &self,
9111 unit: &UnitRecord,
9112 echo: &[EchoRecord],
9113 seeded: &BTreeSet<String>,
9114 name: &str,
9115 ) -> Result<
9116 (
9117 TableSchema,
9118 bool,
9119 Vec<(String, Vec<String>)>,
9120 Vec<Vec<(String, Vec<String>)>>,
9121 ),
9122 SectionError,
9123 > {
9124 let table = self.schema.table(name).cloned().ok_or_else(|| {
9125 SectionError::Abort(
9126 "sync.schema_mismatch".to_owned(),
9127 format!("segment table {name:?} is not in the client schema"),
9128 )
9129 })?;
9130 let effective = covers(&table, &unit.scopes)
9131 .then(|| pair_scopes(&unit.scopes, name, Some(echo)))
9132 .flatten()
9133 .ok_or_else(|| {
9134 SectionError::Abort(
9135 "sync.invalid_request".to_owned(),
9136 "a segment names no active pair of its unit".to_owned(),
9137 )
9138 })?;
9139 let units = self
9140 .stored(load_units(&self.conn))
9141 .map_err(|message| SectionError::Abort("storage.failed".to_owned(), message))?;
9142 let owners = unit_owners(&units, &table, &unit.id);
9143 Ok((table, !seeded.contains(name), effective, owners))
9144 }
9145
9146 fn finish_section(
9153 &mut self,
9154 start: &SectionStart,
9155 echo: Vec<EchoRecord>,
9156 next_cursor: i64,
9157 bootstrap_state: Option<String>,
9158 seeded: BTreeSet<String>,
9159 report: &mut SyncReport,
9160 ) -> Result<ChangeAccumulator, String> {
9161 #[cfg(feature = "bench-internals")]
9162 let _phase = self.benchmark_phases.start(Phase::CursorPersist);
9163 let mut batch = ChangeAccumulator::default();
9164 let Some(unit) = self
9166 .stored(get_unit(&self.conn, &start.id))?
9167 .filter(|unit| unit.status == UnitStatus::Active)
9168 else {
9169 return Ok(batch);
9170 };
9171 for (base_key, value) in self.stored(window_refs(&self.conn, &unit.id))? {
9172 batch.window(&base_key, &value);
9173 }
9174 if start.status == SubStatus::Reset {
9175 report.resets.push(unit.id.clone());
9178 self.stored(save_unit(
9179 &self.conn,
9180 &UnitRecord {
9181 reason_code: Some(start.reason_code.clone()),
9182 cursor: Some(-1),
9183 bootstrap_state: None,
9184 seeded: Vec::new(),
9185 ..unit
9186 },
9187 ))?;
9188 return Ok(batch);
9189 }
9190 let revoked = start.status == SubStatus::Revoked;
9191 let units = self.stored(load_units(&self.conn))?;
9192 let mut failed = false;
9193 let mut rearmed = false;
9194 for table in self.schema.tables.clone() {
9195 if !covers(&table, &unit.scopes) {
9196 continue;
9197 }
9198 let last = unit
9199 .echo
9200 .as_deref()
9201 .and_then(|last| pair_scopes(&unit.scopes, &table.name, Some(last)));
9202 let next = (!revoked)
9203 .then(|| pair_scopes(&unit.scopes, &table.name, Some(&echo)))
9204 .flatten();
9205 rearmed |= unit.echo.is_some() && last.is_none() && next.is_some();
9207 if let (Some(last), None) = (&last, &next) {
9210 let owners = unit_owners(&units, &table, &unit.id);
9211 failed |= !self.purge_pair(&mut batch, &table, last, &owners)?;
9212 }
9213 }
9214 if revoked || failed {
9215 if revoked {
9216 report.revoked.push(unit.id.clone());
9217 }
9218 if failed {
9219 report.failed.push(unit.id.clone());
9220 }
9221 batch.status = true;
9222 self.stored(save_unit(
9223 &self.conn,
9224 &UnitRecord {
9225 status: if failed {
9226 UnitStatus::Failed
9227 } else {
9228 UnitStatus::Revoked
9229 },
9230 reason_code: Some(if failed {
9231 "sync.scope_revoked".to_owned()
9232 } else {
9233 start.reason_code.clone()
9234 }),
9235 cursor: Some(-1),
9236 bootstrap_state: None,
9237 seeded: Vec::new(),
9238 echo: None,
9239 ..unit
9240 },
9241 ))?;
9242 return Ok(batch);
9243 }
9244 let completed = start.bootstrap && !rearmed && bootstrap_state.is_none();
9245 let (cursor, bootstrap_state, seeded) = if rearmed {
9246 (Some(-1), None, Vec::new())
9247 } else if start.bootstrap {
9248 let seeded = if bootstrap_state.is_none() {
9249 Vec::new()
9250 } else {
9251 seeded.into_iter().collect()
9252 };
9253 (Some(next_cursor), bootstrap_state, seeded)
9254 } else {
9255 (
9256 unit.cursor,
9257 unit.bootstrap_state.clone(),
9258 unit.seeded.clone(),
9259 )
9260 };
9261 self.stored(save_unit(
9262 &self.conn,
9263 &UnitRecord {
9264 reason_code: None,
9265 echo: Some(echo.clone()),
9266 cursor,
9267 bootstrap_state,
9268 seeded,
9269 ..unit.clone()
9270 },
9271 ))?;
9272 if !completed {
9273 return Ok(batch);
9274 }
9275 for table in self.schema.tables.clone() {
9278 let Some(effective) = covers(&table, &unit.scopes)
9279 .then(|| pair_scopes(&unit.scopes, &table.name, Some(&echo)))
9280 .flatten()
9281 else {
9282 continue;
9283 };
9284 let mut statement = self
9285 .conn
9286 .prepare("SELECT id,intent_json FROM _syncular_acked_rows WHERE tbl=?1 AND commit_seq<=?2")
9287 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
9288 let rows = statement
9289 .query_map(rusqlite::params![table.name, next_cursor], |row| {
9290 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
9291 })
9292 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
9293 let mut covered = HashSet::new();
9294 for row in rows {
9295 let (id, intent) =
9296 row.map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
9297 let values: Map<String, Value> =
9298 serde_json::from_str(&intent).map_err(|_| "sync.local_corrupt".to_owned())?;
9299 if owns_local_row(&table, &effective, &[], &values) {
9300 covered.insert(id);
9301 }
9302 }
9303 drop(statement);
9304 for id in covered {
9305 self.conn
9306 .execute(
9307 "DELETE FROM _syncular_acked_rows WHERE tbl=?1 AND id=?2 AND commit_seq<=?3",
9308 rusqlite::params![table.name, id, next_cursor],
9309 )
9310 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
9311 self.overlay_dirty.table(&table.name);
9312 batch.table(&table.name);
9313 }
9314 }
9315 Ok(batch)
9316 }
9317
9318 fn purge_pair(
9324 &mut self,
9325 batch: &mut ChangeAccumulator,
9326 table: &TableSchema,
9327 last: &[(String, Vec<String>)],
9328 owners: &[Vec<(String, Vec<String>)>],
9329 ) -> Result<bool, String> {
9330 if local_scope_predicate(table, last, owners).is_err() {
9331 return Ok(false);
9332 }
9333 for commit in self.drop_failed_commits(|name, values| {
9334 name == table.name && owns_local_row(table, last, owners, values)
9335 })? {
9336 batch.conflicts = true;
9337 batch.rejections = true;
9338 batch.outcomes = true;
9339 for operation in commit.ops {
9340 batch.table(&operation.table);
9341 }
9342 }
9343 self.purge_scope_rows(&table.name, last, owners)
9344 .map_err(|()| "storage.failed: the revoked scope purge failed".to_owned())?;
9345 self.record_scope_map(batch, &table.name, last);
9346 if self.drop_doomed_outbox(table, last, owners)? {
9347 batch.status = true;
9348 batch.rejections = true;
9349 batch.outcomes = true;
9350 }
9351 self.delete_unreferenced_cached_blobs()?;
9353 Ok(true)
9354 }
9355
9356 fn apply_live_commit(
9361 &mut self,
9362 receivers: &[UnitRecord],
9363 tables: &[String],
9364 changes: Vec<ssp2::model::Change>,
9365 commit_seq: i64,
9366 source: AppliedServerCommitSource,
9367 ) -> Result<bool, TransportError> {
9368 let changes: Vec<ssp2::model::Change> = changes
9369 .into_iter()
9370 .filter(|change| {
9371 let Some(table) = tables
9373 .get(change.table_index as usize)
9374 .and_then(|name| self.schema.table(name))
9375 else {
9376 return true;
9377 };
9378 receivers.iter().any(|unit| {
9379 covers(table, &unit.scopes)
9380 && pair_scopes(&unit.scopes, &table.name, unit.echo.as_deref()).is_some_and(
9381 |scopes| {
9382 scopes.iter().all(|(variable, values)| {
9383 change
9384 .scopes
9385 .iter()
9386 .find(|(key, _)| key == variable)
9387 .is_some_and(|(_, value)| values.contains(value))
9388 })
9389 },
9390 )
9391 })
9392 })
9393 .collect();
9394 if changes.is_empty() {
9395 return Ok(false);
9396 }
9397 let affected_tables = changes
9398 .iter()
9399 .filter_map(|change| tables.get(change.table_index as usize))
9400 .cloned()
9401 .collect::<BTreeSet<_>>();
9402 let previous_batches = self.change_queue.len();
9403 self.apply_commit_frame(tables, &changes, commit_seq)?;
9404 if self.change_queue.len() > previous_batches {
9405 if let Some(batch) = self.change_queue.back_mut() {
9406 batch
9407 .applied_commits
9408 .extend(
9409 affected_tables
9410 .into_iter()
9411 .map(|table| AppliedServerCommit {
9412 table,
9413 commit_seq,
9414 source,
9415 }),
9416 );
9417 }
9418 }
9419 Ok(true)
9420 }
9421
9422 fn apply_commit_frame(
9424 &mut self,
9425 tables: &[String],
9426 changes: &[ssp2::model::Change],
9427 sequence: i64,
9428 ) -> Result<(), TransportError> {
9429 #[cfg(feature = "bench-internals")]
9430 let _phase = self.benchmark_phases.start(Phase::CommitApply);
9431 let rollback = self.capture();
9432 self.coalesce_unit(changes.len())
9433 .and_then(|()| self.begin_observation("syncular_commit"))
9434 .map_err(|message| TransportError::new("storage.failed", message))?;
9435 let applied = (|| {
9436 let mut batch = ChangeAccumulator::default();
9437 self.record_commit_changes(&mut batch, tables, changes);
9438 {
9439 let storage_failed = |error| {
9440 TransportError::new(
9441 "storage.failed",
9442 Self::sqlite_failure(&self.storage_failure, error),
9443 )
9444 };
9445 let pinned: HashSet<(&str, &str)> = self
9446 .outbox
9447 .iter()
9448 .flat_map(|commit| &commit.ops)
9449 .map(|op| (op.table.as_str(), op.row_id.as_str()))
9450 .collect();
9451 for change in changes {
9452 let table = tables.get(change.table_index as usize).ok_or_else(|| {
9453 TransportError::new(
9454 "sync.invalid_request",
9455 "change tableIndex out of range".to_owned(),
9456 )
9457 })?;
9458 if pinned.contains(&(table.as_str(), change.row_id.as_str())) {
9459 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)")
9460 .and_then(|mut statement| statement.execute(rusqlite::params![table, change.row_id, sequence]))
9461 .map_err(storage_failed)?;
9462 }
9463 self.conn.prepare_cached("DELETE FROM _syncular_acked_rows WHERE tbl=?1 AND id=?2 AND commit_seq<=?3")
9464 .and_then(|mut statement| statement.execute(rusqlite::params![table, change.row_id, sequence]))
9465 .map_err(storage_failed)?;
9466 }
9467 }
9468 self.apply_commit_changes(tables, changes)?;
9469 self.rebuild_overlay_if_dirty()
9470 .map_err(|message| TransportError::new("storage.failed", message))?;
9471 self.finish_observation("syncular_commit", batch)
9472 .map_err(|message| TransportError::new("storage.failed", message))
9473 })();
9474 if applied.is_err() {
9475 self.rollback_observation("syncular_commit");
9476 self.restore(rollback);
9477 }
9478 applied
9479 }
9480
9481 fn apply_commit_changes(
9482 &mut self,
9483 tables: &[String],
9484 changes: &[ssp2::model::Change],
9485 ) -> Result<(), TransportError> {
9486 let changed_tables: BTreeSet<&str> = changes
9491 .iter()
9492 .filter_map(|change| tables.get(change.table_index as usize))
9493 .map(String::as_str)
9494 .collect();
9495 let mut mirror_visible = !self.overlay_dirty.get();
9496 for name in &changed_tables {
9497 if mirror_visible
9498 && self
9499 .schema
9500 .table(name)
9501 .is_some_and(|table| table.indexes.iter().any(|index| index.unique))
9502 {
9503 let (pending, protected) = self
9504 .table_intent(name)
9505 .map_err(|message| TransportError::new("storage.failed", message))?;
9506 mirror_visible = !pending && !protected;
9507 }
9508 }
9509 for change in changes {
9510 let table_name = tables.get(change.table_index as usize).ok_or_else(|| {
9511 TransportError::new(
9512 "sync.invalid_request",
9513 "change tableIndex out of range".to_owned(),
9514 )
9515 })?;
9516 let table = self.schema.table(table_name).ok_or_else(|| {
9517 TransportError::new(
9518 "sync.schema_mismatch",
9519 format!("change targets unknown table {table_name:?}"),
9520 )
9521 })?;
9522 let landed = match change.op {
9523 Op::Upsert => {
9524 let payload = change.row.as_ref().ok_or_else(|| {
9525 TransportError::new(
9526 "sync.invalid_request",
9527 "upsert change without row payload".to_owned(),
9528 )
9529 })?;
9530 let row = {
9532 #[cfg(feature = "bench-internals")]
9533 let _phase = self.benchmark_phases.start(Phase::RowDecode);
9534 decode_row_bytes(table, payload, &self.encryption)?
9536 };
9537 let table_name = table.name.clone();
9538 let landed = self
9540 .write_base_row(&table_name, &row, change.row_version)
9541 .map_err(|m| TransportError::new("sync.invalid_request", m))?;
9542 if mirror_visible && landed {
9543 self.write_row(
9544 &visible_table(&table_name),
9545 &table_name,
9546 &row,
9547 change.row_version,
9548 )
9549 .map_err(|m| TransportError::new("sync.invalid_request", m))?;
9550 }
9551 landed
9552 }
9553 Op::Delete => self
9556 .delete_base_row(table_name, &change.row_id, change.row_version)
9557 .map_err(|m| TransportError::new("sync.invalid_request", m))?,
9558 };
9559 self.overlay_dirty.table(table_name);
9563 if mirror_visible && !(landed && change.op == Op::Upsert) {
9564 let predicate = row_id_predicate(table);
9565 let visible = visible_table(table_name);
9566 let mut statements = vec![format!("DELETE FROM {visible} WHERE {predicate}")];
9567 if !landed {
9568 statements.push(format!(
9569 "INSERT INTO {visible} SELECT * FROM {} WHERE {predicate}",
9570 base_table(table_name)
9571 ));
9572 }
9573 for statement in &statements {
9574 self.conn
9575 .prepare_cached(statement)
9576 .and_then(|mut statement| {
9577 statement.execute(rusqlite::params![change.row_id])
9578 })
9579 .map_err(|error| {
9580 TransportError::new("sync.invalid_request", error.to_string())
9581 })?;
9582 }
9583 }
9584 }
9585 if mirror_visible {
9586 let changed_rows: HashSet<(&str, &str)> = changes
9587 .iter()
9588 .map(|change| {
9589 (
9590 tables[change.table_index as usize].as_str(),
9591 change.row_id.as_str(),
9592 )
9593 })
9594 .collect();
9595 self.replay_overlay(&changed_tables, Some(&changed_rows))
9596 .map_err(|message| TransportError::new("storage.failed", message))?;
9597 self.overlay_dirty.set(false);
9598 }
9599 Ok(())
9600 }
9601
9602 fn apply_segment(
9607 &mut self,
9608 table: &TableSchema,
9609 segment: &RowsSegment,
9610 first_page: bool,
9611 effective: &[(String, Vec<String>)],
9612 owners: &[Vec<(String, Vec<String>)>],
9613 ) -> Result<u32, SectionError> {
9614 let column = if segment.columns.len() != table.wire_columns.len() {
9619 Some(Value::from("count"))
9620 } else {
9621 segment
9622 .columns
9623 .iter()
9624 .zip(table.wire_columns.iter())
9625 .position(|(a, b)| a.name != b.name || a.ty != b.ty || a.nullable != b.nullable)
9626 .map(Value::from)
9627 };
9628 if segment.table != table.name
9629 || segment.schema_version != self.schema.version
9630 || column.is_some()
9631 {
9632 let mut details = Map::new();
9633 details.insert("table".to_owned(), Value::from(table.name.as_str()));
9634 if let Some(column) = column {
9635 details.insert("column".to_owned(), column);
9636 }
9637 return Err(SectionError::Transport(TransportError {
9638 code: "sync.schema_mismatch".to_owned(),
9639 message: "rows segment columns do not match the generated schema (§5.2)".to_owned(),
9640 details: Some(Value::Object(details)),
9641 }));
9642 }
9643 let mut applied = 0u32;
9644 let empty = Vec::new();
9645 let blocks = if segment.blocks.is_empty() {
9646 std::slice::from_ref(&empty)
9647 } else {
9648 &segment.blocks
9649 };
9650 for (index, block) in blocks.iter().enumerate() {
9651 let clear = first_page && index == 0;
9652 if !clear && block.is_empty() {
9653 continue;
9654 }
9655 let rollback = self.capture();
9656 let (pending, protected) = self
9657 .table_intent(&table.name)
9658 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9659 let reconcile_pending =
9660 !protected && pending && table.indexes.iter().any(|index| index.unique);
9661 let mirror_visible = !protected && !reconcile_pending;
9662 self.coalesce_unit(block.len())
9663 .and_then(|()| self.begin_observation("syncular_segment_block"))
9664 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9665 let outcome = (|| {
9666 self.rebuild_overlay_if_dirty()
9667 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9668 let mut changed_keys = Vec::new();
9669 let mut batch = ChangeAccumulator::default();
9670 if !block.is_empty()
9671 || (clear && self.scoped_rows_exist(&table.name, effective, owners))
9672 {
9673 batch.table(&table.name);
9674 }
9675 if clear {
9676 self.purge_scope_rows(&table.name, effective, owners)
9677 .map_err(|()| SectionError::FailClosed)?;
9678 }
9679 for (row_index, row) in block.iter().enumerate() {
9680 let decrypted;
9681 let values = if table.has_encrypted_columns() {
9682 let mut values = row.values.clone();
9683 crate::values::decrypt_segment_row(table, &mut values, &self.encryption)
9684 .map_err(SectionError::Transport)?;
9685 decrypted = values;
9686 &decrypted
9687 } else {
9688 &row.values
9689 };
9690 if !self
9691 .write_base_row(&table.name, values, row.server_version)
9692 .map_err(|message| {
9693 SectionError::Abort("sync.invalid_request".into(), message)
9694 })?
9695 {
9696 continue;
9697 }
9698 if reconcile_pending {
9699 changed_keys.push(
9700 owned_sql_value(RowParam::Cell(&values[table.pk_index])).map_err(
9701 |message| SectionError::Abort("storage.failed".into(), message),
9702 )?,
9703 );
9704 }
9705 if mirror_visible {
9706 self.write_row(
9707 &visible_table(&table.name),
9708 &table.name,
9709 values,
9710 row.server_version,
9711 )
9712 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9713 }
9714 let processed = u64::from(applied) + row_index as u64 + 1;
9715 if processed.is_multiple_of(1024) {
9716 self.progress.update(|p| p.rows_processed = processed);
9717 }
9718 }
9719 self.reconcile_import(table, &mut changed_keys, protected, reconcile_pending)
9720 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9721 self.finish_observation("syncular_segment_block", batch)
9722 .map_err(|message| SectionError::Abort("storage.failed".into(), message))
9723 })();
9724 if let Err(error) = outcome {
9725 self.rollback_observation("syncular_segment_block");
9726 self.restore(rollback);
9727 return Err(error);
9728 }
9729 applied += block.len() as u32;
9730 self.progress
9731 .update(|p| p.rows_processed = u64::from(applied));
9732 std::thread::yield_now();
9733 }
9734 self.progress
9735 .update(|p| p.rows_processed = u64::from(applied));
9736 Ok(applied)
9737 }
9738
9739 #[allow(clippy::too_many_arguments)]
9747 fn apply_sqlite_segment(
9748 &mut self,
9749 table: &TableSchema,
9750 bytes: &[u8],
9751 first_page: bool,
9752 effective: &[(String, Vec<String>)],
9753 owners: &[Vec<(String, Vec<String>)>],
9754 row_count: i64,
9755 as_of_commit_seq: i64,
9756 scope_digest: &str,
9757 ) -> Result<u32, SectionError> {
9758 let invalid = |detail: &str| {
9759 SectionError::Abort(
9760 "sync.invalid_request".to_owned(),
9761 format!("sqlite segment rejected: {detail} (§5.3)"),
9762 )
9763 };
9764 if table.has_encrypted_columns() {
9767 return Err(invalid("table has encrypted columns"));
9768 }
9769
9770 let directory = self
9774 .previous_version_replica_path()
9775 .and_then(|replica| std::path::Path::new(&replica).parent().map(Into::into))
9776 .unwrap_or_else(std::env::temp_dir);
9777 let path = directory.join(format!(".syncular-image-{}.tmp", uuid::Uuid::new_v4()));
9778 let outcome = (|| {
9779 let mut options = std::fs::OpenOptions::new();
9780 options.write(true).create_new(true);
9781 #[cfg(unix)]
9782 std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
9783 std::io::Write::write_all(
9784 &mut options
9785 .open(&path)
9786 .map_err(|_| invalid("image temp file write failed"))?,
9787 bytes,
9788 )
9789 .map_err(|_| invalid("image temp file write failed"))?;
9790 let img = rusqlite::Connection::open_with_flags(
9791 &path,
9792 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
9793 )
9794 .map_err(|_| invalid("bytes do not open as a SQLite database"))?;
9795 self.apply_sqlite_image(
9796 &img,
9797 table,
9798 first_page,
9799 effective,
9800 owners,
9801 row_count,
9802 as_of_commit_seq,
9803 scope_digest,
9804 )
9805 })();
9806 let _ = std::fs::remove_file(&path);
9807 outcome
9808 }
9809
9810 #[allow(clippy::too_many_arguments)]
9811 fn apply_sqlite_image(
9812 &mut self,
9813 img: &rusqlite::Connection,
9814 table: &crate::schema::TableSchema,
9815 first_fresh_page: bool,
9816 effective: &[(String, Vec<String>)],
9817 owners: &[Vec<(String, Vec<String>)>],
9818 row_count: i64,
9819 as_of_commit_seq: i64,
9820 scope_digest: &str,
9821 ) -> Result<u32, SectionError> {
9822 let invalid = |detail: String| {
9823 SectionError::Abort(
9824 "sync.invalid_request".to_owned(),
9825 format!("sqlite segment rejected: {detail} (§5.3)"),
9826 )
9827 };
9828
9829 type MetaRow = (i64, String, i64, i64, String, i64, i64);
9832 let meta: MetaRow = img
9833 .query_row(
9834 "SELECT format, \"table\", \"schemaVersion\", \"asOfCommitSeq\",
9835 \"scopeDigest\", \"rowCount\",
9836 (SELECT count(*) FROM _syncular_segment)
9837 FROM _syncular_segment",
9838 [],
9839 |row| {
9840 Ok((
9841 row.get(0)?,
9842 row.get(1)?,
9843 row.get(2)?,
9844 row.get(3)?,
9845 row.get(4)?,
9846 row.get(5)?,
9847 row.get(6)?,
9848 ))
9849 },
9850 )
9851 .map_err(|_| invalid("missing or unreadable _syncular_segment metadata".to_owned()))?;
9852 let (format, meta_table, schema_version, pin, digest, meta_rows, meta_count) = meta;
9853 if meta_count != 1 {
9854 return Err(invalid(format!(
9855 "_syncular_segment must contain exactly one row, found {meta_count}"
9856 )));
9857 }
9858 if format != 1 {
9859 return Err(invalid(format!("format {format}")));
9860 }
9861 if meta_table != table.name {
9862 return Err(invalid(format!("image table {meta_table:?}")));
9863 }
9864 if schema_version != i64::from(self.schema.version) {
9865 return Err(invalid(format!("schemaVersion {schema_version}")));
9866 }
9867 if pin != as_of_commit_seq {
9868 return Err(invalid(format!("asOfCommitSeq {pin}")));
9869 }
9870 if digest != scope_digest {
9871 return Err(invalid("scopeDigest mismatch".to_owned()));
9872 }
9873 if meta_rows != row_count {
9874 return Err(invalid(format!("rowCount {meta_rows}")));
9875 }
9876
9877 let mut names: Vec<String> = Vec::new();
9879 let mut primary_keys: Vec<String> = Vec::new();
9880 {
9881 let mut stmt = img
9882 .prepare(&format!("PRAGMA table_info({})", quote_ident(&table.name)))
9883 .map_err(|_| invalid("image data table missing".to_owned()))?;
9884 let mut rows = stmt
9885 .query([])
9886 .map_err(|_| invalid("image data table unreadable".to_owned()))?;
9887 while let Some(row) = rows
9888 .next()
9889 .map_err(|_| invalid("image data table unreadable".to_owned()))?
9890 {
9891 if row
9892 .get::<_, i64>(5)
9893 .map_err(|error| invalid(Self::sqlite_failure(&self.storage_failure, error)))?
9894 > 0
9895 {
9896 primary_keys.push(row.get(1).map_err(|error| {
9897 invalid(Self::sqlite_failure(&self.storage_failure, error))
9898 })?);
9899 }
9900 names.push(
9901 row.get::<_, String>(1)
9902 .map_err(|_| invalid("image data table unreadable".to_owned()))?,
9903 );
9904 }
9905 }
9906 let mut expected: Vec<&str> = table.columns.iter().map(|c| c.name.as_str()).collect();
9907 expected.push("_syncular_version");
9908 if names.len() != expected.len() || names.iter().zip(expected.iter()).any(|(a, b)| a != b) {
9909 return Err(SectionError::Transport(TransportError {
9910 code: "sync.schema_mismatch".to_owned(),
9911 message: "sqlite segment columns do not match the generated schema (§5.3)"
9912 .to_owned(),
9913 details: Some(serde_json::json!({ "table": table.name })),
9914 }));
9915 }
9916
9917 if primary_keys != vec![table.primary_key.clone()] {
9918 return Err(invalid(
9919 "image primary key does not match generated schema".into(),
9920 ));
9921 }
9922 let actual_count: i64 = img
9923 .query_row(
9924 &format!("SELECT count(*) FROM {}", quote_ident(&table.name)),
9925 [],
9926 |row| row.get(0),
9927 )
9928 .map_err(|error| invalid(Self::sqlite_failure(&self.storage_failure, error)))?;
9929 if actual_count != row_count {
9930 return Err(invalid("image row count does not match descriptor".into()));
9931 }
9932 let insert = self.insert_row_sql(&base_table(&table.name), table);
9933 let visible_insert = self.insert_row_sql(&visible_table(&table.name), table);
9934 let column_list = names
9935 .iter()
9936 .map(|n| quote_ident(n))
9937 .collect::<Vec<_>>()
9938 .join(", ");
9939 let mut stmt = img
9940 .prepare(&format!(
9941 "SELECT {column_list} FROM {} ORDER BY {}",
9942 quote_ident(&table.name),
9943 quote_ident(&table.primary_key)
9944 ))
9945 .map_err(|error| invalid(Self::sqlite_failure(&self.storage_failure, error)))?;
9946 let mut rows = stmt
9947 .query([])
9948 .map_err(|error| invalid(Self::sqlite_failure(&self.storage_failure, error)))?;
9949 let mut applied = 0u32;
9950 let mut limit = 1024u32;
9951 let (mut write, mut commit) = (std::time::Duration::ZERO, std::time::Duration::ZERO);
9952 loop {
9953 let clear = applied == 0 && first_fresh_page;
9954 let rollback = self.capture();
9955 let (pending, protected) = self
9956 .table_intent(&table.name)
9957 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9958 let reconcile_pending =
9959 !protected && pending && table.indexes.iter().any(|index| index.unique);
9960 let mirror_visible = !protected && !reconcile_pending;
9961 let chunk = if pending || protected { 1024 } else { limit };
9964 self.coalesce_unit(chunk as usize)
9965 .and_then(|()| self.begin_observation("syncular_image_chunk"))
9966 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9967 let started = std::time::Instant::now();
9968 let result = (|| {
9969 self.rebuild_overlay_if_dirty()
9970 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
9971 let mut changed_keys = Vec::new();
9972 let mut batch = ChangeAccumulator::default();
9973 if clear {
9974 if self.scoped_rows_exist(&table.name, effective, owners) {
9975 batch.table(&table.name);
9976 }
9977 self.purge_scope_rows(&table.name, effective, owners)
9978 .map_err(|()| SectionError::FailClosed)?;
9979 }
9980 let mut processed = 0u32;
9981 {
9982 let mut ins = self.conn.prepare_cached(&insert).map_err(|error| {
9983 invalid(Self::sqlite_failure(&self.storage_failure, error))
9984 })?;
9985 let mut mirror = if mirror_visible {
9986 Some(self.conn.prepare_cached(&visible_insert).map_err(|error| {
9987 invalid(Self::sqlite_failure(&self.storage_failure, error))
9988 })?)
9989 } else {
9990 None
9991 };
9992 for _ in 0..chunk {
9993 let Some(row) = rows.next().map_err(|error| {
9994 invalid(Self::sqlite_failure(&self.storage_failure, error))
9995 })?
9996 else {
9997 break;
9998 };
9999 for (i, column) in table.columns.iter().enumerate() {
10000 let cell = row.get_ref(i).map_err(|error| {
10001 invalid(Self::sqlite_failure(&self.storage_failure, error))
10002 })?;
10003 let param = image_cell_param(column, cell).map_err(invalid)?;
10004 ins.raw_bind_parameter(i + 1, ¶m).map_err(|error| {
10005 invalid(Self::sqlite_failure(&self.storage_failure, error))
10006 })?;
10007 if let Some(mirror) = &mut mirror {
10008 mirror.raw_bind_parameter(i + 1, ¶m).map_err(|error| {
10009 invalid(Self::sqlite_failure(&self.storage_failure, error))
10010 })?;
10011 }
10012 }
10013 let version_index = table.columns.len();
10014 let version: i64 = row.get(version_index).map_err(|error| {
10015 invalid(Self::sqlite_failure(&self.storage_failure, error))
10016 })?;
10017 if version < 1 {
10018 return Err(invalid("image row version must be positive".into()));
10019 }
10020 ins.raw_bind_parameter(version_index + 1, version)
10021 .map_err(|error| {
10022 invalid(Self::sqlite_failure(&self.storage_failure, error))
10023 })?;
10024 let landed = ins.raw_execute().map_err(|error| {
10026 invalid(Self::sqlite_failure(&self.storage_failure, error))
10027 })? > 0;
10028 processed += 1;
10029 if processed.is_multiple_of(1024) && processed < chunk {
10030 self.progress
10031 .update(|p| p.rows_processed = u64::from(applied + processed));
10032 }
10033 if !landed {
10034 continue;
10035 }
10036 if let Some(mirror) = &mut mirror {
10037 mirror
10038 .raw_bind_parameter(version_index + 1, version)
10039 .map_err(|error| {
10040 invalid(Self::sqlite_failure(&self.storage_failure, error))
10041 })?;
10042 mirror.raw_execute().map_err(|error| {
10043 invalid(Self::sqlite_failure(&self.storage_failure, error))
10044 })?;
10045 }
10046 if reconcile_pending {
10047 changed_keys.push(row.get::<_, SqlValue>(table.pk_index).map_err(
10048 |error| invalid(Self::sqlite_failure(&self.storage_failure, error)),
10049 )?);
10050 }
10051 }
10052 }
10053 if processed > 0 {
10054 batch.table(&table.name);
10055 self.overlay_dirty.table(&table.name);
10056 }
10057 self.reconcile_import(table, &mut changed_keys, protected, reconcile_pending)
10058 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
10059 let written = std::time::Instant::now();
10060 self.finish_observation("syncular_image_chunk", batch)
10061 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
10062 Ok((processed, written))
10063 })();
10064 let (processed, written) = match result {
10065 Ok(done) => done,
10066 Err(error) => {
10067 self.rollback_observation("syncular_image_chunk");
10068 self.restore(rollback);
10069 return Err(error);
10070 }
10071 };
10072 let coalesced = self.coalesced.is_some();
10074 if coalesced {
10075 self.commit_coalesced_apply()
10076 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
10077 }
10078 write += written - started;
10079 commit += written.elapsed();
10080 applied += processed;
10081 let last = processed < chunk || i64::from(applied) == actual_count;
10082 if last && self.conn.is_autocommit() {
10084 self.conn
10085 .query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()))
10086 .map_err(|error| invalid(Self::sqlite_failure(&self.storage_failure, error)))?;
10087 }
10088 if coalesced {
10089 self.begin_coalesced_apply()
10090 .map_err(|message| SectionError::Abort("storage.failed".into(), message))?;
10091 }
10092 self.progress.update(|p| {
10093 p.rows_processed = u64::from(applied);
10094 p.import_write_ms = u64::try_from(write.as_millis()).ok();
10095 p.import_commit_ms = u64::try_from(commit.as_millis()).ok();
10096 });
10097 std::thread::yield_now();
10098 if last {
10099 break;
10100 }
10101 limit = (limit * 2).min(65_536);
10102 }
10103 if i64::from(applied) != actual_count {
10104 return Err(invalid(format!(
10105 "image yielded {applied} rows, expected {actual_count}"
10106 )));
10107 }
10108 Ok(applied)
10109 }
10110
10111 fn reconcile_rows(&self, drained: &[Arc<OutboxCommit>]) -> Result<(), String> {
10116 let names: BTreeSet<&str> = drained
10117 .iter()
10118 .flat_map(|commit| &commit.ops)
10119 .map(|op| op.table.as_str())
10120 .collect();
10121 for name in names {
10122 let Some(table) = self.schema.table(name) else {
10123 continue;
10124 };
10125 let ops = drained.iter().flat_map(|commit| &commit.ops);
10126 let tables = BTreeSet::from([name]);
10127 let rows: HashSet<_> = ops
10128 .clone()
10129 .map(|op| (op.table.as_str(), op.row_id.as_str()))
10130 .collect();
10131 let unique = table.indexes.iter().any(|index| index.unique);
10132 let acknowledged = if unique {
10133 self.acknowledged_operations(None, Some(&tables))?
10134 } else {
10135 Vec::new()
10136 };
10137 let peers = acknowledged
10138 .iter()
10139 .chain(
10140 self.failed_commits
10141 .iter()
10142 .flat_map(|(commit, _)| &commit.ops),
10143 )
10144 .chain(self.outbox.iter().flat_map(|commit| &commit.ops))
10145 .filter(|_| unique);
10146 self.restore_base_rows(table, &mut Vec::new(), peers.chain(ops.clone()))?;
10147 self.replay_overlay(&tables, (!unique).then_some(&rows))?;
10148 }
10149 Ok(())
10150 }
10151
10152 fn table_intent(&self, table: &str) -> Result<(bool, bool), String> {
10155 let touches = |op: &OutboxOp| op.table == table;
10156 Ok((
10157 self.outbox
10158 .iter()
10159 .flat_map(|commit| &commit.ops)
10160 .any(touches),
10161 self.failed_commits
10162 .iter()
10163 .flat_map(|(commit, _)| &commit.ops)
10164 .any(touches)
10165 || self
10166 .conn
10167 .query_row(
10168 "SELECT EXISTS(SELECT 1 FROM _syncular_acked_rows WHERE tbl=?1)",
10169 [table],
10170 |row| row.get(0),
10171 )
10172 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?,
10173 ))
10174 }
10175
10176 fn reconcile_import(
10180 &mut self,
10181 table: &TableSchema,
10182 keys: &mut Vec<SqlValue>,
10183 protected: bool,
10184 restore: bool,
10185 ) -> Result<(), String> {
10186 if protected {
10187 return self.rebuild_overlay_if_dirty();
10188 }
10189 let pending = self.outbox.iter().flat_map(|commit| &commit.ops);
10190 if restore {
10191 self.restore_base_rows(table, keys, pending.clone())?;
10192 }
10193 self.apply_outbox_ops(pending.filter(|op| op.table == table.name))?;
10194 self.overlay_dirty.set(false);
10195 Ok(())
10196 }
10197
10198 fn restore_base_rows<'a>(
10202 &self,
10203 table: &TableSchema,
10204 keys: &mut Vec<SqlValue>,
10205 ops: impl IntoIterator<Item = &'a OutboxOp>,
10206 ) -> Result<(), String> {
10207 let visible = visible_table(&table.name);
10208 let base = base_table(&table.name);
10209 let pk = quote_ident(&table.primary_key);
10210 let predicate = row_id_predicate(table);
10211 let mut lookup = self.conn.prepare_cached(&format!(
10212 "SELECT {pk} FROM {base} WHERE {predicate} UNION SELECT {pk} FROM {visible} WHERE {predicate}"
10213 )).map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10214 let mut deleted = HashSet::new();
10215 for op in ops.into_iter().filter(|op| op.table == table.name) {
10216 if op.upsert {
10217 if let Some(values) = &op.values {
10218 let key = json_to_column_value(
10219 &table.columns[table.pk_index],
10220 values.get(&table.primary_key),
10221 )?;
10222 keys.push(owned_sql_value(RowParam::Cell(&key))?);
10223 }
10224 } else if deleted.insert(&op.row_id) {
10225 keys.extend(
10226 lookup
10227 .query_map([&op.row_id], |row| row.get::<_, SqlValue>(0))
10228 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
10229 .collect::<Result<Vec<_>, _>>()
10230 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?,
10231 );
10232 }
10233 }
10234 for chunk in keys.chunks(400) {
10237 let holes = vec!["?"; chunk.len()].join(",");
10238 self.conn
10239 .prepare_cached(&format!("DELETE FROM {visible} WHERE {pk} IN ({holes})"))
10240 .and_then(|mut statement| statement.execute(rusqlite::params_from_iter(chunk)))
10241 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10242 }
10243 let updates = table
10244 .columns
10245 .iter()
10246 .map(|column| column.name.as_str())
10247 .chain(std::iter::once("_syncular_version"))
10248 .filter(|name| *name != table.primary_key)
10249 .map(|name| {
10250 let name = quote_ident(name);
10251 format!("{name}=excluded.{name}")
10252 })
10253 .collect::<Vec<_>>()
10254 .join(",");
10255 for chunk in keys.chunks(400) {
10256 let holes = vec!["?"; chunk.len()].join(",");
10257 self.conn.prepare_cached(&format!("INSERT INTO {visible} SELECT * FROM {base} WHERE {pk} IN ({holes}) ON CONFLICT ({pk}) DO UPDATE SET {updates}"))
10258 .and_then(|mut statement| statement.execute(rusqlite::params_from_iter(chunk)))
10259 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10260 }
10261 Ok(())
10262 }
10263
10264 fn compile_local_data_purge(
10267 &self,
10268 input: &LocalDataPurgeInput,
10269 ) -> Result<(Vec<CompiledLocalDataPurgeTarget>, String), String> {
10270 let invalid = |message: String| format!("sync.invalid_request: {message}");
10271 if input.purge_id.is_empty()
10272 || input.purge_id.len() > 128
10273 || !is_local_operation_code_like(&input.purge_id)
10274 {
10275 return Err(invalid(
10276 "local purge purgeId must be a 1–128 character code-like identifier".to_owned(),
10277 ));
10278 }
10279 if input.targets.is_empty() || input.targets.len() > MAX_LOCAL_PURGE_TARGETS {
10280 return Err(invalid(format!(
10281 "local purge needs between 1 and {MAX_LOCAL_PURGE_TARGETS} targets"
10282 )));
10283 }
10284
10285 let mut deduplicated: BTreeMap<
10286 String,
10287 (CompiledLocalDataPurgeTarget, LocalDataPurgeTarget),
10288 > = BTreeMap::new();
10289 for target in &input.targets {
10290 let table = self.schema.table(&target.table).ok_or_else(|| {
10291 invalid(format!(
10292 "local purge names unknown table {:?}",
10293 target.table
10294 ))
10295 })?;
10296 if target.selectors.is_empty() || target.selectors.len() > MAX_LOCAL_PURGE_SELECTORS {
10297 return Err(invalid(format!(
10298 "local purge target {:?} needs between 1 and {MAX_LOCAL_PURGE_SELECTORS} selectors",
10299 target.table
10300 )));
10301 }
10302 let mut selectors = Vec::with_capacity(target.selectors.len());
10303 let mut canonical_selectors = BTreeMap::new();
10304 for (column_name, raw_values) in &target.selectors {
10305 let Some((column_index, column)) = table
10306 .columns
10307 .iter()
10308 .enumerate()
10309 .find(|(_, column)| column.name == *column_name)
10310 else {
10311 return Err(invalid(format!(
10312 "local purge target {:?} names unknown column {:?}",
10313 target.table, column_name
10314 )));
10315 };
10316 let encrypted = table
10317 .encrypted_columns
10318 .iter()
10319 .any(|candidate| candidate.index == column_index);
10320 if column.ty != ColumnType::String || encrypted {
10321 return Err(invalid(format!(
10322 "local purge selector {:?}.{:?} must be a plaintext string column",
10323 target.table, column_name
10324 )));
10325 }
10326 if raw_values.is_empty() || raw_values.len() > MAX_LOCAL_PURGE_VALUES {
10327 return Err(invalid(format!(
10328 "local purge selector {:?}.{:?} needs between 1 and {MAX_LOCAL_PURGE_VALUES} values",
10329 target.table, column_name
10330 )));
10331 }
10332 let mut values = raw_values.clone();
10333 values.sort();
10334 values.dedup();
10335 if values.iter().any(|value| {
10336 value.is_empty()
10337 || value.len() > MAX_LOCAL_PURGE_VALUE_LENGTH
10338 || !is_local_operation_code_like(value)
10339 }) {
10340 return Err(invalid(format!(
10341 "local purge selector values must be 1–{MAX_LOCAL_PURGE_VALUE_LENGTH} character code-like identifiers"
10342 )));
10343 }
10344 selectors.push((column_name.clone(), values.clone()));
10345 canonical_selectors.insert(column_name.clone(), values);
10346 }
10347 let canonical = LocalDataPurgeTarget {
10348 table: target.table.clone(),
10349 selectors: canonical_selectors,
10350 };
10351 let key = serde_json::to_string(&canonical).map_err(|error| error.to_string())?;
10352 deduplicated.insert(
10353 key,
10354 (
10355 CompiledLocalDataPurgeTarget {
10356 table: target.table.clone(),
10357 selectors,
10358 },
10359 canonical,
10360 ),
10361 );
10362 }
10363 let targets = deduplicated
10364 .values()
10365 .map(|(compiled, _)| compiled.clone())
10366 .collect::<Vec<_>>();
10367 let canonical = deduplicated
10368 .values()
10369 .map(|(_, target)| target.clone())
10370 .collect::<Vec<_>>();
10371 let canonical_plan =
10372 serde_json::to_string(&canonical).map_err(|error| error.to_string())?;
10373 Ok((targets, canonical_plan))
10374 }
10375
10376 fn local_purge_base_row_ids(
10377 &self,
10378 targets: &[CompiledLocalDataPurgeTarget],
10379 ) -> Result<BTreeMap<String, BTreeSet<String>>, String> {
10380 let mut by_table: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
10381 for target in targets {
10382 let table = self
10383 .schema
10384 .table(&target.table)
10385 .ok_or_else(|| format!("sync.invalid_request: unknown table {:?}", target.table))?;
10386 let mut clauses = Vec::with_capacity(target.selectors.len());
10387 let mut params = Vec::new();
10388 for (column, values) in &target.selectors {
10389 clauses.push(format!(
10390 "{} IN ({})",
10391 quote_ident(column),
10392 values.iter().map(|_| "?").collect::<Vec<_>>().join(", ")
10393 ));
10394 params.extend(values.iter().cloned().map(SqlValue::Text));
10395 }
10396 let sql = format!(
10397 "SELECT CAST({} AS TEXT) FROM {} WHERE {}",
10398 quote_ident(&table.primary_key),
10399 base_table(&target.table),
10400 clauses.join(" AND ")
10401 );
10402 let mut statement = self
10403 .conn
10404 .prepare(&sql)
10405 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10406 let rows = statement
10407 .query_map(rusqlite::params_from_iter(params), |row| {
10408 let value = sql_ref_to_json(&table.columns[table.pk_index], row.get_ref(0)?);
10409 render_row_id_json(Some(&value))
10410 .map_err(|message| rusqlite::Error::ToSqlConversionFailure(message.into()))
10411 })
10412 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10413 let ids = by_table.entry(target.table.clone()).or_default();
10414 for row in rows {
10415 ids.insert(
10416 row.map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?,
10417 );
10418 }
10419 }
10420 Ok(by_table)
10421 }
10422
10423 fn local_purge_values_match(
10424 target: &CompiledLocalDataPurgeTarget,
10425 values: &Map<String, Value>,
10426 ) -> bool {
10427 target.selectors.iter().all(|(column, allowed)| {
10428 values
10429 .get(column)
10430 .and_then(Value::as_str)
10431 .is_some_and(|value| allowed.iter().any(|candidate| candidate == value))
10432 })
10433 }
10434
10435 pub fn purge_local_data(
10439 &mut self,
10440 input: &LocalDataPurgeInput,
10441 ) -> Result<LocalDataPurgeResult, String> {
10442 let (targets, canonical_plan) = self.compile_local_data_purge(input)?;
10443 let meta_key = format!("localPurge:{}", input.purge_id);
10444 let applied_plan = self
10445 .conn
10446 .query_row(
10447 "SELECT value FROM _syncular_meta WHERE key = ?1",
10448 rusqlite::params![meta_key],
10449 |row| row.get::<_, String>(0),
10450 )
10451 .optional()
10452 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10453 if let Some(applied_plan) = applied_plan {
10454 if applied_plan != canonical_plan {
10455 return Err(format!(
10456 "sync.invalid_request: local purge id {:?} was already used with a different plan",
10457 input.purge_id
10458 ));
10459 }
10460 return Ok(LocalDataPurgeResult {
10461 already_applied: true,
10462 purged_rows: 0,
10463 dropped_commits: 0,
10464 });
10465 }
10466
10467 let _ = self.drop_previous_version();
10471
10472 let rollback = self.capture();
10473 self.begin_observation("syncular_local_purge")?;
10474 let applied = (|| -> Result<(ChangeAccumulator, LocalDataPurgeResult), String> {
10475 let row_ids = self.local_purge_base_row_ids(&targets)?;
10476 let retained_dropped = self.drop_failed_commits(|table, values| {
10477 targets.iter().any(|target| {
10478 target.table == table && Self::local_purge_values_match(target, values)
10479 })
10480 })?;
10481 let doomed = self
10482 .outbox
10483 .iter()
10484 .filter(|commit| {
10485 commit.ops.iter().any(|operation| {
10486 let matching_targets = targets
10487 .iter()
10488 .filter(|target| target.table == operation.table)
10489 .collect::<Vec<_>>();
10490 if matching_targets.is_empty() {
10491 return false;
10492 }
10493 if row_ids
10494 .get(&operation.table)
10495 .is_some_and(|ids| ids.contains(&operation.row_id))
10496 {
10497 return true;
10498 }
10499 operation.values.as_ref().is_some_and(|values| {
10500 matching_targets
10501 .iter()
10502 .any(|target| Self::local_purge_values_match(target, values))
10503 })
10504 })
10505 })
10506 .cloned()
10507 .collect::<Vec<_>>();
10508 let doomed_ids = doomed
10509 .iter()
10510 .map(|commit| commit.client_commit_id.clone())
10511 .collect::<BTreeSet<_>>();
10512 let mut batch = ChangeAccumulator::default();
10513 for commit in &retained_dropped {
10514 batch.conflicts = true;
10515 batch.rejections = true;
10516 batch.outcomes = true;
10517 for operation in &commit.ops {
10518 batch.table(&operation.table);
10519 }
10520 }
10521 let mut rejections = Vec::new();
10522 for commit in &doomed {
10523 for operation in &commit.ops {
10524 batch.table(&operation.table);
10525 self.overlay_dirty.table(&operation.table);
10526 }
10527 let results = commit
10528 .ops
10529 .iter()
10530 .enumerate()
10531 .map(|(op_index, operation)| {
10532 let rejection = RejectionRecord {
10533 client_commit_id: commit.client_commit_id.clone(),
10534 op_index: op_index as i32,
10535 code: "client.local_data_purged".to_owned(),
10536 message: "the commit was dropped by an application-authorized local data purge".to_owned(),
10537 retryable: false,
10538 details: None,
10539 operation: Some(CommitOperation::from(operation)),
10540 };
10541 rejections.push(rejection.clone());
10542 CommitOperationOutcome::Error { rejection }
10543 })
10544 .collect::<Vec<_>>();
10545 self.persist_commit_outcome(
10546 &commit.client_commit_id,
10547 CommitOutcomeStatus::Rejected,
10548 &results,
10549 Some(&commit.ops),
10550 )?;
10551 self.delete_outbox_persisted(&commit.client_commit_id)?;
10552 }
10553 if !doomed.is_empty() {
10554 self.outbox
10555 .retain(|commit| !doomed_ids.contains(&commit.client_commit_id));
10556 self.rejections.extend(rejections);
10557 self.prune_commit_outcomes()?;
10558 batch.status = true;
10559 batch.rejections = true;
10560 batch.outcomes = true;
10561 }
10562
10563 let mut purged_rows = 0usize;
10564 for (table, ids) in &row_ids {
10565 if ids.is_empty() {
10566 continue;
10567 }
10568 batch.table(table);
10569 self.overlay_dirty.table(table);
10570 for row_id in ids {
10571 self.delete_base_row(table, row_id, i64::MAX)?;
10572 purged_rows += 1;
10573 }
10574 }
10575 self.rebuild_overlay_if_dirty()?;
10576 self.delete_unreferenced_cached_blobs()?;
10577 self.conn
10578 .execute(
10579 "INSERT INTO _syncular_meta(key, value) VALUES (?1, ?2)",
10580 rusqlite::params![meta_key, canonical_plan],
10581 )
10582 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10583 Ok((
10584 batch,
10585 LocalDataPurgeResult {
10586 already_applied: false,
10587 purged_rows,
10588 dropped_commits: doomed.len(),
10589 },
10590 ))
10591 })()
10592 .and_then(|(batch, result)| {
10593 self.finish_observation("syncular_local_purge", batch)
10594 .map(|()| result)
10595 });
10596 let result = match applied {
10597 Ok(result) => result,
10598 Err(error) => {
10599 self.rollback_observation("syncular_local_purge");
10600 self.restore(rollback);
10601 return Err(error);
10602 }
10603 };
10604 self.cancel_sync_round();
10605 Ok(result)
10606 }
10607
10608 pub fn rebootstrap_local_data(
10614 &mut self,
10615 input: &LocalDataRebootstrapInput,
10616 ) -> Result<LocalDataRebootstrapResult, String> {
10617 if input.rebootstrap_id.is_empty()
10618 || input.rebootstrap_id.len() > 128
10619 || !is_local_operation_code_like(&input.rebootstrap_id)
10620 {
10621 return Err(
10622 "sync.invalid_request: local rebootstrap rebootstrapId must be a 1–128 character code-like identifier"
10623 .to_owned(),
10624 );
10625 }
10626 let meta_key = format!("localRebootstrap:{}", input.rebootstrap_id);
10627 if let Some(persisted) = meta_get(&self.conn, &meta_key).map_err(|_| {
10628 "sync.local_corrupt: persisted local rebootstrap receipt is unreadable".to_owned()
10629 })? {
10630 let (retained_commits, reset_subscriptions) =
10631 decode_local_rebootstrap_receipt(&persisted)?;
10632 return Ok(LocalDataRebootstrapResult {
10633 already_applied: true,
10634 retained_commits,
10635 reset_subscriptions,
10636 });
10637 }
10638 if self.stopped || self.schema_floor.is_some() {
10639 return Err(
10640 "sync.invalid_request: local rebootstrap cannot bypass an active schema-floor stop; update the application first"
10641 .to_owned(),
10642 );
10643 }
10644
10645 let retained_commits = self.outbox.len();
10646 let reset_subscriptions = self.stored(load_units(&self.conn))?.len();
10647 let receipt = encode_local_rebootstrap_receipt(retained_commits, reset_subscriptions)?;
10648 let rollback = self.capture();
10649 self.begin_observation("syncular_local_rebootstrap")?;
10650 let mut batch = ChangeAccumulator::default();
10651 let applied = (|| -> Result<(), String> {
10652 self.run_schema_reset_observed(&mut batch, false)?;
10653 self.conn
10654 .execute(
10655 "INSERT INTO _syncular_meta(key, value) VALUES (?1, ?2)",
10656 rusqlite::params![meta_key, receipt],
10657 )
10658 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10659 self.sync_needed = true;
10660 self.sync_intent_queue.push_back(SyncIntent::Interactive);
10661 batch.status = true;
10662 Ok(())
10663 })()
10664 .and_then(|()| self.finish_observation("syncular_local_rebootstrap", batch));
10665 if let Err(error) = applied {
10666 self.rollback_observation("syncular_local_rebootstrap");
10667 self.restore(rollback);
10668 return Err(error);
10669 }
10670
10671 Ok(LocalDataRebootstrapResult {
10672 already_applied: false,
10673 retained_commits,
10674 reset_subscriptions,
10675 })
10676 }
10677
10678 fn purge_scope_rows(
10684 &mut self,
10685 table_name: &str,
10686 effective: &[(String, Vec<String>)],
10687 owners: &[Vec<(String, Vec<String>)>],
10688 ) -> Result<(), ()> {
10689 let table = self.schema.table(table_name).ok_or(())?;
10690 let Some((predicate, params)) = local_scope_predicate(table, effective, owners)? else {
10691 return Ok(());
10692 };
10693 self.overlay_dirty.table(table_name);
10694 for source in [base_table(table_name), visible_table(table_name)] {
10695 self.conn
10696 .execute(
10697 &format!("DELETE FROM {source} WHERE {predicate}"),
10698 rusqlite::params_from_iter(¶ms),
10699 )
10700 .map_err(|_| ())?;
10701 }
10702 Ok(())
10703 }
10704
10705 fn drop_doomed_outbox(
10709 &mut self,
10710 table: &TableSchema,
10711 effective: &[(String, Vec<String>)],
10712 owners: &[Vec<(String, Vec<String>)>],
10713 ) -> Result<bool, String> {
10714 let doomed: Vec<_> =
10715 self.outbox
10716 .iter()
10717 .filter(|commit| {
10718 commit.ops.iter().any(|op| {
10719 op.upsert
10720 && op.table == table.name
10721 && op.values.as_ref().is_some_and(|values| {
10722 owns_local_row(table, effective, owners, values)
10723 })
10724 })
10725 })
10726 .cloned()
10727 .collect();
10728 if doomed.is_empty() {
10729 return Ok(false);
10730 }
10731 let mut rejections = Vec::new();
10732 for commit in &doomed {
10733 let results = commit
10734 .ops
10735 .iter()
10736 .enumerate()
10737 .map(|(op_index, operation)| {
10738 self.overlay_dirty.table(&operation.table);
10739 let rejection = RejectionRecord {
10740 client_commit_id: commit.client_commit_id.clone(),
10741 op_index: op_index as i32,
10742 code: "sync.scope_revoked".to_owned(),
10743 message: "the commit was dropped because its effective scope was revoked"
10744 .to_owned(),
10745 retryable: false,
10746 details: None,
10747 operation: Some(CommitOperation::from(operation)),
10748 };
10749 rejections.push(rejection.clone());
10750 CommitOperationOutcome::Error { rejection }
10751 })
10752 .collect::<Vec<_>>();
10753 self.persist_commit_outcome(
10754 &commit.client_commit_id,
10755 CommitOutcomeStatus::Rejected,
10756 &results,
10757 Some(&commit.ops),
10758 )?;
10759 self.delete_outbox_persisted(&commit.client_commit_id)?;
10760 }
10761 self.prune_commit_outcomes()?;
10762 let doomed_ids = doomed
10763 .iter()
10764 .map(|commit| commit.client_commit_id.as_str())
10765 .collect::<BTreeSet<_>>();
10766 self.outbox
10767 .retain(|commit| !doomed_ids.contains(commit.client_commit_id.as_str()));
10768 self.rejections.extend(rejections);
10769 Ok(true)
10770 }
10771
10772 pub fn upload_blob(
10779 &mut self,
10780 bytes: &[u8],
10781 media_type: Option<String>,
10782 name: Option<String>,
10783 ) -> Result<Value, String> {
10784 let blob_id = blob_id_for(bytes);
10785 let now = self.clock_now_ms();
10786 let tx = self
10787 .conn
10788 .transaction()
10789 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10790 tx.execute(
10791 "INSERT INTO _syncular_blobs(blob_id, bytes, byte_length, media_type, created_at_ms) VALUES (?,?,?,?,?)
10792 ON CONFLICT(blob_id) DO NOTHING",
10793 rusqlite::params![blob_id, bytes, bytes.len() as i64, media_type, now],
10794 )
10795 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10796 tx.execute(
10797 "INSERT OR IGNORE INTO _syncular_blob_uploads(blob_id, media_type, created_at_ms) VALUES (?,?,?)",
10798 rusqlite::params![blob_id, media_type, now],
10799 )
10800 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10801 tx.commit()
10802 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10803 self.enforce_blob_cache_cap();
10804 let mut obj = Map::new();
10805 obj.insert("blobId".to_owned(), Value::from(blob_id));
10806 obj.insert("byteLength".to_owned(), Value::from(bytes.len() as i64));
10807 if let Some(mt) = media_type {
10808 obj.insert("mediaType".to_owned(), Value::from(mt));
10809 }
10810 if let Some(n) = name {
10811 obj.insert("name".to_owned(), Value::from(n));
10812 }
10813 Ok(Value::Object(obj))
10814 }
10815
10816 pub fn fetch_blob_bytes(
10820 &mut self,
10821 transport: &mut dyn Transport,
10822 blob_id_or_ref: &str,
10823 ) -> Result<FetchedBlob, (String, String)> {
10824 let simple = |m: String| ("client.failed".to_owned(), m);
10825 let (blob_id, byte_length) = if blob_id_or_ref.starts_with("sha256:") {
10828 (blob_id_or_ref.to_owned(), None)
10829 } else {
10830 let value: Value = serde_json::from_str(blob_id_or_ref)
10831 .map_err(|_| simple("blob ref is not JSON".to_owned()))?;
10832 (
10833 value
10834 .get("blobId")
10835 .and_then(Value::as_str)
10836 .ok_or_else(|| simple("blob ref has no blobId".to_owned()))?
10837 .to_owned(),
10838 value.get("byteLength").and_then(Value::as_u64),
10839 )
10840 };
10841 if let Some(cached) = self.get_cached_blob_bytes(&blob_id).map_err(simple)? {
10842 return Ok(cached);
10843 }
10844 if !self.transport_enabled {
10845 return Err(("sync.offline".into(), "transport gate is closed".into()));
10846 }
10847 #[cfg(feature = "bench-internals")]
10853 let download_phase = self.benchmark_phases.start(Phase::BlobDownload);
10854 let bytes = match transport
10855 .blob_download(&blob_id, byte_length)
10856 .map_err(|e| (e.code, e.message))?
10857 {
10858 BlobDownload::Bytes(bytes) => bytes,
10859 BlobDownload::Url {
10860 url,
10861 url_expires_at_ms,
10862 } => {
10863 if url_expires_at_ms.is_some_and(|exp| exp <= self.clock_now_ms()) {
10865 return Err((
10866 "sync.segment_expired".to_owned(),
10867 format!(
10868 "blob url for {blob_id} expired before fetch — re-request mints a fresh url (§5.9.5)"
10869 ),
10870 ));
10871 }
10872 transport
10873 .fetch_blob_url(&url, byte_length)
10874 .map_err(|e| (e.code, e.message))?
10875 }
10876 };
10877 #[cfg(feature = "bench-internals")]
10878 drop(download_phase);
10879 #[cfg(feature = "bench-internals")]
10880 let validate_phase = self.benchmark_phases.start(Phase::BlobValidate);
10881 if byte_length.is_some_and(|cap| bytes.len() as u64 > cap) {
10884 return Err((
10885 "sync.invalid_request".to_owned(),
10886 "blob exceeds its byteLength".to_owned(),
10887 ));
10888 }
10889 if blob_id_for(&bytes) != blob_id {
10890 return Err(simple(format!(
10891 "blob content address mismatch for {blob_id}"
10892 )));
10893 }
10894 #[cfg(feature = "bench-internals")]
10895 drop(validate_phase);
10896 #[cfg(feature = "bench-internals")]
10897 let insert_phase = self.benchmark_phases.start(Phase::BlobCacheInsert);
10898 let now = self.clock_now_ms();
10899 self.conn
10900 .execute(
10901 "INSERT OR IGNORE INTO _syncular_blobs(blob_id, bytes, byte_length, media_type, created_at_ms) VALUES (?,?,?,NULL,?)",
10902 rusqlite::params![blob_id, bytes, bytes.len() as i64, now],
10903 )
10904 .map_err(|e| simple(e.to_string()))?;
10905 self.conn
10906 .query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()))
10907 .map_err(|e| simple(e.to_string()))?;
10908 self.enforce_blob_cache_cap();
10909 #[cfg(feature = "bench-internals")]
10910 drop(insert_phase);
10911 self.get_cached_blob_bytes(&blob_id)
10912 .map_err(simple)?
10913 .ok_or_else(|| simple("blob cache write failed".to_owned()))
10914 }
10915
10916 fn get_cached_blob_bytes(&self, blob_id: &str) -> Result<Option<FetchedBlob>, String> {
10917 #[cfg(feature = "bench-internals")]
10918 let _phase = self.benchmark_phases.start(Phase::BlobCacheRead);
10919 let mut stmt = self
10920 .conn
10921 .prepare("SELECT bytes, byte_length, media_type FROM _syncular_blobs WHERE blob_id = ?")
10922 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10923 let mut rows = stmt
10924 .query(rusqlite::params![blob_id])
10925 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10926 if let Some(row) = rows
10927 .next()
10928 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
10929 {
10930 let bytes: Vec<u8> = row
10931 .get(0)
10932 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10933 let byte_length: i64 = row
10934 .get(1)
10935 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10936 let media_type: Option<String> = row
10937 .get(2)
10938 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
10939 return Ok(Some(FetchedBlob {
10940 blob_id: blob_id.to_owned(),
10941 byte_length,
10942 bytes,
10943 media_type,
10944 }));
10945 }
10946 Ok(None)
10947 }
10948
10949 fn pending_blob_uploads(&self) -> Result<Vec<crate::round::PendingUpload>, TransportError> {
10951 let pending: Vec<(String, Option<String>)> = {
10952 let mut stmt = self
10953 .conn
10954 .prepare(
10955 "SELECT blob_id, media_type FROM (
10956 SELECT blob_id, media_type, created_at_ms FROM _syncular_blob_uploads
10957 UNION ALL
10958 SELECT DISTINCT r.blob_id, b.media_type, 9223372036854775807 AS created_at_ms
10959 FROM _syncular_blob_commit_refs r LEFT JOIN _syncular_blobs b ON b.blob_id = r.blob_id
10960 WHERE NOT EXISTS (SELECT 1 FROM _syncular_blob_uploads u WHERE u.blob_id = r.blob_id)
10961 ) ORDER BY created_at_ms, blob_id",
10962 )
10963 .map_err(|e| TransportError::new("client.failed", e.to_string()))?;
10964 let rows = stmt
10965 .query_map([], |row| {
10966 Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?))
10967 })
10968 .map_err(|e| TransportError::new("client.failed", e.to_string()))?;
10969 rows.collect::<Result<_, _>>()
10970 .map_err(|error| match error {
10971 rusqlite::Error::InvalidColumnType(..)
10972 | rusqlite::Error::FromSqlConversionFailure(..) => TransportError::new(
10973 "sync.local_corrupt",
10974 "Pending blob upload metadata is invalid",
10975 ),
10976 _ => TransportError::new("client.failed", error.to_string()),
10977 })?
10978 };
10979 let mut uploads = Vec::new();
10980 for (blob_id, media_type) in pending {
10981 let bytes = (|| {
10982 let stored: Option<(SqlValue, SqlValue)> = self
10983 .conn
10984 .query_row(
10985 "SELECT bytes, byte_length FROM _syncular_blobs WHERE blob_id = ?",
10986 rusqlite::params![blob_id],
10987 |row| Ok((row.get(0)?, row.get(1)?)),
10988 )
10989 .optional()
10990 .map_err(|e| TransportError::new("client.failed", e.to_string()))?;
10991 let Some((SqlValue::Blob(bytes), SqlValue::Integer(byte_length))) = stored else {
10992 return Err(TransportError::new(
10993 "sync.local_corrupt",
10994 "Pending blob upload body is missing or corrupt",
10995 ));
10996 };
10997 if byte_length != bytes.len() as i64 || blob_id_for(&bytes) != blob_id {
10998 return Err(TransportError::new(
10999 "sync.local_corrupt",
11000 "Pending blob upload body is missing or corrupt",
11001 ));
11002 }
11003 Ok(bytes)
11004 })();
11005 let failed = bytes.is_err();
11006 uploads.push(crate::round::PendingUpload {
11007 id: blob_id,
11008 bytes,
11009 media_type,
11010 });
11011 if failed {
11012 break;
11013 }
11014 }
11015 Ok(uploads)
11016 }
11017
11018 fn visible_blob_ids_sql(&self) -> String {
11019 let mut selects = Vec::new();
11020 for table in &self.schema.tables {
11021 for column in table
11022 .columns
11023 .iter()
11024 .filter(|column| column.ty == ColumnType::BlobRef)
11025 {
11026 let identifier = quote_ident(&column.name);
11027 selects.push(format!(
11028 "SELECT json_extract({identifier}, '$.blobId') AS blob_id FROM {} \
11029 WHERE typeof({identifier}) = 'text' AND json_valid({identifier}) \
11030 AND json_type({identifier}, '$.blobId') = 'text'",
11031 quote_ident(&table.name),
11032 ));
11033 }
11034 }
11035 if selects.is_empty() {
11036 "SELECT NULL AS blob_id WHERE 0".to_owned()
11037 } else {
11038 selects.join(" UNION ")
11039 }
11040 }
11041
11042 fn enforce_blob_cache_cap(&self) {
11045 let Some(max_bytes) = self.limits.blob_cache_max_bytes else {
11046 return;
11047 };
11048 let mut total: i64 = self
11049 .conn
11050 .query_row(
11051 "SELECT COALESCE(SUM(byte_length), 0) FROM _syncular_blobs",
11052 [],
11053 |row| row.get(0),
11054 )
11055 .unwrap_or(0);
11056 if total <= max_bytes {
11057 return;
11058 }
11059 let query = format!(
11060 "SELECT blob_id, byte_length FROM _syncular_blobs
11061 WHERE blob_id NOT IN (SELECT blob_id FROM _syncular_blob_uploads)
11062 AND blob_id NOT IN (SELECT blob_id FROM _syncular_blob_commit_refs)
11063 AND blob_id NOT IN ({})
11064 ORDER BY created_at_ms ASC, blob_id ASC",
11065 self.visible_blob_ids_sql(),
11066 );
11067 let candidates: Vec<(String, i64)> = {
11068 let Ok(mut stmt) = self.conn.prepare(&query) else {
11069 return;
11070 };
11071 let Ok(rows) = stmt.query_map([], |row| {
11072 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
11073 }) else {
11074 return;
11075 };
11076 rows.filter_map(Result::ok).collect()
11077 };
11078 for (blob_id, byte_length) in candidates {
11079 if total <= max_bytes {
11080 break;
11081 }
11082 let _ = self.conn.execute(
11083 "DELETE FROM _syncular_blobs WHERE blob_id = ?",
11084 rusqlite::params![blob_id],
11085 );
11086 total -= byte_length;
11087 }
11088 }
11089
11090 fn delete_unreferenced_cached_blobs(&mut self) -> Result<(), String> {
11091 #[cfg(feature = "bench-internals")]
11092 let _phase = self.benchmark_phases.start(Phase::BlobRetention);
11093 if !self.schema_has_blobs() {
11094 return Ok(());
11095 }
11096 self.rebuild_overlay_if_dirty()?;
11097 self.conn
11098 .execute(
11099 &format!(
11100 "DELETE FROM _syncular_blobs
11101 WHERE blob_id NOT IN (SELECT blob_id FROM _syncular_blob_uploads)
11102 AND blob_id NOT IN (SELECT blob_id FROM _syncular_blob_commit_refs)
11103 AND blob_id NOT IN ({})",
11104 self.visible_blob_ids_sql(),
11105 ),
11106 [],
11107 )
11108 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11109 Ok(())
11110 }
11111
11112 fn insert_row_sql(&self, full_table: &str, table: &crate::schema::TableSchema) -> String {
11117 if let Some(sql) = self.insert_sql.borrow().get(full_table) {
11118 return sql.clone();
11119 }
11120 let mut columns: Vec<String> = table.columns.iter().map(|c| quote_ident(&c.name)).collect();
11121 columns.push(quote_ident("_syncular_version"));
11122 let placeholders: Vec<&str> = columns.iter().map(|_| "?").collect();
11123 let primary_key = quote_ident(&table.primary_key);
11124 let updates = columns
11125 .iter()
11126 .filter(|column| **column != primary_key)
11127 .map(|column| format!("{column}=excluded.{column}"))
11128 .collect::<Vec<_>>()
11129 .join(", ");
11130 let guard = if full_table == base_table(&table.name) {
11133 format!(" WHERE excluded._syncular_version > {full_table}._syncular_version")
11134 } else {
11135 String::new()
11136 };
11137 let sql = format!(
11138 "INSERT INTO {full_table} ({}) VALUES ({}) ON CONFLICT ({primary_key}) DO UPDATE SET {updates}{guard}",
11139 columns.join(", "),
11140 placeholders.join(", ")
11141 );
11142 self.insert_sql
11143 .borrow_mut()
11144 .insert(full_table.to_owned(), sql.clone());
11145 sql
11146 }
11147
11148 fn write_base_row(&self, table_name: &str, row: &Row, version: i64) -> Result<bool, String> {
11151 let landed = self.write_row(&base_table(table_name), table_name, row, version)?;
11152 if landed {
11153 self.overlay_dirty.table(table_name);
11154 }
11155 Ok(landed)
11156 }
11157
11158 fn write_row(
11159 &self,
11160 full_table: &str,
11161 table_name: &str,
11162 row: &Row,
11163 version: i64,
11164 ) -> Result<bool, String> {
11165 #[cfg(feature = "bench-internals")]
11166 let _phase = self.benchmark_phases.start(Phase::RowWrite);
11167 let table = self
11168 .schema
11169 .table(table_name)
11170 .ok_or_else(|| format!("unknown table {table_name:?}"))?;
11171 let sql = self.insert_row_sql(full_table, table);
11172 let mut stmt = self
11173 .conn
11174 .prepare_cached(&sql)
11175 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11176 let params = row
11177 .iter()
11178 .map(RowParam::Cell)
11179 .chain(std::iter::once(RowParam::Version(version)));
11180 stmt.execute(rusqlite::params_from_iter(params))
11181 .map(|changed| changed > 0)
11182 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
11183 }
11184
11185 fn delete_base_row(
11188 &self,
11189 table_name: &str,
11190 row_id: &str,
11191 version: i64,
11192 ) -> Result<bool, String> {
11193 let table = self
11194 .schema
11195 .table(table_name)
11196 .ok_or_else(|| format!("unknown table {table_name:?}"))?;
11197 let sql = format!(
11198 "DELETE FROM {} WHERE {} AND _syncular_version < ?2",
11199 base_table(table_name),
11200 row_id_predicate(table)
11201 );
11202 let mut stmt = self
11203 .conn
11204 .prepare_cached(&sql)
11205 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11206 let landed = stmt
11207 .execute(rusqlite::params![row_id, version])
11208 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
11209 > 0;
11210 if landed {
11211 self.overlay_dirty.table(table_name);
11212 }
11213 Ok(landed)
11214 }
11215
11216 fn has_acknowledged_rows(&self) -> Result<bool, String> {
11217 self.conn
11218 .query_row(
11219 "SELECT EXISTS(SELECT 1 FROM _syncular_acked_rows)",
11220 [],
11221 |row| row.get(0),
11222 )
11223 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
11224 }
11225
11226 fn acknowledged_operations(
11227 &self,
11228 rows: Option<&HashSet<(&str, &str)>>,
11229 tables: Option<&BTreeSet<&str>>,
11230 ) -> Result<Vec<OutboxOp>, String> {
11231 let keys = rows.map(|rows| serde_json::to_string(&rows).expect("row keys"));
11232 let mut statement = self
11233 .conn
11234 .prepare_cached(if keys.is_some() {
11235 ACKED_ROWS_BY_ROW
11236 } else {
11237 "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"
11238 })
11239 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11240 let rows = statement
11241 .query_map(
11242 rusqlite::params![
11243 keys,
11244 tables.map(|tables| serde_json::to_string(tables).expect("table keys"))
11245 ],
11246 |row| {
11247 #[cfg(test)]
11248 self.acknowledged_replay_count
11249 .set(self.acknowledged_replay_count.get() + 1);
11250 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
11251 },
11252 )
11253 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11254 rows.map(|row| {
11255 let (op, intent) =
11256 row.map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11257 let op: CommitOperation =
11258 serde_json::from_str(&op).map_err(|_| "sync.local_corrupt".to_owned())?;
11259 let table = self
11260 .schema
11261 .table(&op.table)
11262 .ok_or_else(|| "sync.unknown_table".to_owned())?;
11263 let values = if op.op == "upsert" && self.visible_row(table, &op.row_id)?.is_none() {
11264 Some(serde_json::from_str(&intent).map_err(|_| "sync.local_corrupt".to_owned())?)
11265 } else {
11266 op.values
11267 };
11268 Ok(OutboxOp {
11269 table: op.table,
11270 row_id: op.row_id,
11271 upsert: op.op == "upsert",
11272 base_version: op.base_version,
11273 values,
11274 insert: op.insert,
11275 })
11276 })
11277 .collect()
11278 }
11279
11280 fn drop_acknowledged_rows(
11281 &self,
11282 matches: impl Fn(&str, &Map<String, Value>) -> bool,
11283 ) -> Result<Vec<OutboxCommit>, String> {
11284 let mut statement = self
11285 .conn
11286 .prepare("SELECT commit_id,tbl,intent_json,id FROM _syncular_acked_rows")
11287 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11288 let rows = statement
11289 .query_map([], |row| {
11290 Ok((
11291 row.get::<_, String>(0)?,
11292 row.get::<_, String>(1)?,
11293 row.get::<_, String>(2)?,
11294 row.get::<_, String>(3)?,
11295 ))
11296 })
11297 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11298 let mut doomed = HashSet::new();
11299 for row in rows {
11300 let (id, table, intent, row_id) =
11301 row.map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11302 let values =
11303 serde_json::from_str(&intent).map_err(|_| "sync.local_corrupt".to_owned())?;
11304 let base_matches = self
11305 .schema
11306 .table(&table)
11307 .map(|schema| self.stored_row(schema, &row_id, true))
11308 .transpose()?
11309 .flatten()
11310 .is_some_and(|(base, _)| {
11311 let schema = self.schema.table(&table).expect("acknowledged table");
11312 let values = schema
11313 .columns
11314 .iter()
11315 .zip(base.iter())
11316 .map(|(column, value)| (column.name.clone(), column_value_to_json(value)))
11317 .collect();
11318 matches(&table, &values)
11319 });
11320 if matches(&table, &values) || base_matches {
11321 doomed.insert(id);
11322 }
11323 }
11324 let mut dropped = Vec::new();
11325 for id in doomed {
11326 let mut statement = self
11327 .conn
11328 .prepare("SELECT op_json FROM _syncular_acked_rows WHERE commit_id=?1 ORDER BY idx")
11329 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11330 let operations = statement
11331 .query_map([&id], |row| row.get::<_, String>(0))
11332 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11333 let mut ops = Vec::new();
11334 for operation in operations {
11335 let json = operation
11336 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11337 let op: CommitOperation =
11338 serde_json::from_str(&json).map_err(|_| "sync.local_corrupt".to_owned())?;
11339 self.overlay_dirty.table(&op.table);
11340 ops.push(OutboxOp {
11341 table: op.table,
11342 row_id: op.row_id,
11343 upsert: op.op == "upsert",
11344 base_version: op.base_version,
11345 values: op.values,
11346 insert: op.insert,
11347 });
11348 }
11349 dropped.push(OutboxCommit {
11350 client_commit_id: id.clone(),
11351 ops,
11352 });
11353 self.conn
11354 .execute("DELETE FROM _syncular_acked_rows WHERE commit_id=?1", [id])
11355 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11356 }
11357 Ok(dropped)
11358 }
11359
11360 fn rebuild_overlay_if_dirty(&mut self) -> Result<(), String> {
11363 if self.overlay_dirty.get() {
11364 self.rebuild_overlay()?;
11365 }
11366 Ok(())
11367 }
11368
11369 fn rebuild_overlay(&mut self) -> Result<(), String> {
11373 #[cfg(feature = "bench-internals")]
11374 let _phase = self.benchmark_phases.start(Phase::OverlayRebuild);
11375 #[cfg(test)]
11376 self.overlay_rebuild_count
11377 .set(self.overlay_rebuild_count.get() + 1);
11378 let dirty = self.overlay_dirty.snapshot();
11379 let tables: Vec<_> = self
11380 .schema
11381 .tables
11382 .iter()
11383 .filter(|table| {
11384 dirty
11385 .as_ref()
11386 .is_none_or(|names| names.is_empty() || names.contains(&table.name))
11387 })
11388 .cloned()
11389 .collect();
11390 let names: BTreeSet<_> = tables.iter().map(|table| table.name.as_str()).collect();
11391 self.begin_observation("syncular_overlay")?;
11392 let rebuilt = (|| {
11393 for table in &tables {
11394 for index in &table.fts_indexes {
11395 self.drop_fts_triggers(index)?;
11396 }
11397 let visible = visible_table(&table.name);
11398 let base = base_table(&table.name);
11399 self.conn
11400 .execute_batch(&format!(
11401 "DELETE FROM {visible}; INSERT INTO {visible} SELECT * FROM {base}"
11402 ))
11403 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11404 }
11405 self.replay_overlay(&names, None)?;
11406 for table in &tables {
11407 for index in &table.fts_indexes {
11408 self.rebuild_fts_projection(table, index)?;
11409 self.create_fts_triggers(table, index)?;
11410 }
11411 }
11412 self.conn
11413 .execute_batch("RELEASE syncular_overlay")
11414 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11415 Ok(())
11416 })();
11417 if rebuilt.is_err() {
11418 self.conn
11419 .execute_batch("ROLLBACK TO syncular_overlay; RELEASE syncular_overlay")
11420 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11421 } else {
11422 self.overlay_dirty.set(false);
11423 }
11424 rebuilt
11425 }
11426
11427 fn replay_overlay(
11431 &self,
11432 tables: &BTreeSet<&str>,
11433 rows: Option<&HashSet<(&str, &str)>>,
11434 ) -> Result<(), String> {
11435 let replays = |op: &&OutboxOp| {
11436 tables.contains(op.table.as_str())
11437 && rows.is_none_or(|rows| rows.contains(&(op.table.as_str(), op.row_id.as_str())))
11438 };
11439 self.apply_outbox_ops(&self.acknowledged_operations(rows, Some(tables))?)?;
11440 for (commit, initial) in self.failed_commits.iter() {
11441 for operation in commit.ops.iter().filter(replays) {
11442 let Some(table) = self.schema.table(&operation.table) else {
11443 continue;
11444 };
11445 if operation.upsert
11446 && self.visible_row(table, &operation.row_id)?.is_none()
11447 && operation.presented_values(table).is_some_and(|values| {
11448 table
11449 .columns
11450 .iter()
11451 .all(|column| values.contains_key(&column.name))
11452 })
11453 {
11454 let key = serde_json::to_string(&(&operation.table, &operation.row_id))
11455 .expect("row key serialization");
11456 if let Some(Value::Object(values)) = initial.get(&key) {
11457 self.apply_outbox_ops(&[OutboxOp {
11458 values: Some(values.clone()),
11459 ..operation.clone()
11460 }])?;
11461 }
11462 }
11463 }
11464 self.apply_outbox_ops(commit.ops.iter().filter(replays))?;
11465 }
11466 self.apply_outbox_ops(
11467 self.outbox
11468 .iter()
11469 .flat_map(|commit| &commit.ops)
11470 .filter(replays),
11471 )
11472 }
11473
11474 fn apply_outbox_ops<'a>(
11479 &self,
11480 ops: impl IntoIterator<Item = &'a OutboxOp>,
11481 ) -> Result<(), String> {
11482 #[cfg(feature = "bench-internals")]
11483 let mut phase = self.benchmark_phases.start(Phase::PendingReplay);
11484 for op in ops {
11485 #[cfg(feature = "bench-internals")]
11486 if let Some(phase) = &mut phase {
11487 phase.unit();
11488 }
11489 if op.upsert {
11490 if let Some((table, values)) = self
11491 .schema
11492 .table(&op.table)
11493 .filter(|table| table.indexes.iter().any(|index| index.unique))
11494 .and_then(|table| Some((table, op.presented_values(table)?)))
11495 {
11496 let mut intended: Map<String, Value> = self
11497 .visible_row(table, &op.row_id)?
11498 .map(|(row, _)| {
11499 table
11500 .columns
11501 .iter()
11502 .zip(&row)
11503 .map(|(column, value)| {
11504 (column.name.clone(), column_value_to_json(value))
11505 })
11506 .collect()
11507 })
11508 .unwrap_or_default();
11509 intended.extend(values.into_owned());
11510 if !self.unique_conflicts(table, &intended, false)?.is_empty() {
11511 continue;
11512 }
11513 }
11514 }
11515 self.apply_outbox_op(op)?;
11516 }
11517 Ok(())
11518 }
11519
11520 fn apply_outbox_op(&self, op: &OutboxOp) -> Result<(), String> {
11521 let Some(table) = self.schema.table(&op.table) else {
11522 return Ok(());
11523 };
11524 let visible = visible_table(&table.name);
11525 if !op.upsert {
11526 let sql = format!("DELETE FROM {visible} WHERE {}", row_id_predicate(table));
11527 self.conn
11528 .prepare_cached(&sql)
11529 .and_then(|mut statement| statement.execute(rusqlite::params![op.row_id]))
11530 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11531 return Ok(());
11532 }
11533 let Some(values) = op.presented_values(table) else {
11537 return Ok(());
11538 };
11539 let full = table
11545 .columns
11546 .iter()
11547 .all(|column| values.contains_key(&column.name));
11548 let local = self.visible_row(table, &op.row_id)?;
11549 if local.is_none() && !full {
11550 return Ok(());
11551 }
11552 let mut row: Row = Vec::with_capacity(table.columns.len());
11553 for (index, column) in table.columns.iter().enumerate() {
11554 let incoming = match values.get(&column.name) {
11555 Some(raw) => json_to_column_value(
11556 if raw.get("$bytes").is_some()
11557 && table
11558 .encrypted_columns
11559 .iter()
11560 .any(|encrypted| encrypted.index == index)
11561 {
11562 &table.wire_columns[index]
11563 } else {
11564 column
11565 },
11566 Some(raw),
11567 ),
11568 None => Ok(local
11569 .as_ref()
11570 .and_then(|(values, _)| values.get(index).cloned())
11571 .flatten()),
11572 };
11573 row.push(incoming.map_err(|_| {
11574 "sync.local_corrupt: persisted optimistic row value is invalid".to_owned()
11575 })?);
11576 }
11577 let version = local.as_ref().map_or(-1, |(_, version)| *version);
11578 self.write_row(&visible, &table.name, &row, version)
11579 .map(|_| ())
11580 .map_err(|error| {
11581 if error.starts_with("UNIQUE constraint failed:") {
11582 "sync.constraint_violation: local write violates a unique constraint".to_owned()
11583 } else {
11584 error
11585 }
11586 })
11587 }
11588
11589 fn visible_row_exists(&self, table: &TableSchema, row_id: &str) -> Result<bool, String> {
11593 self.conn
11594 .query_row(
11595 &format!(
11596 "SELECT EXISTS(SELECT 1 FROM {} WHERE {})",
11597 visible_table(&table.name),
11598 row_id_predicate(table)
11599 ),
11600 rusqlite::params![row_id],
11601 |row| row.get(0),
11602 )
11603 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))
11604 }
11605
11606 fn visible_row(&self, table: &TableSchema, row_id: &str) -> Result<Option<(Row, i64)>, String> {
11609 self.stored_row(table, row_id, false)
11610 }
11611
11612 fn stored_row(
11613 &self,
11614 table: &TableSchema,
11615 row_id: &str,
11616 base: bool,
11617 ) -> Result<Option<(Row, i64)>, String> {
11618 let sql = format!(
11619 "SELECT * FROM {} WHERE {}",
11620 if base {
11621 base_table(&table.name)
11622 } else {
11623 visible_table(&table.name)
11624 },
11625 row_id_predicate(table)
11626 );
11627 let mut stmt = self
11628 .conn
11629 .prepare_cached(&sql)
11630 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11631 let mut rows = stmt
11632 .query(rusqlite::params![row_id])
11633 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11634 let Some(row) = rows
11635 .next()
11636 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?
11637 else {
11638 return Ok(None);
11639 };
11640 let mut values = Vec::with_capacity(table.columns.len());
11641 for (index, column) in table.columns.iter().enumerate() {
11642 let raw = row
11643 .get_ref(index)
11644 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11645 if let rusqlite::types::ValueRef::Blob(bytes) = raw {
11646 if table
11647 .encrypted_columns
11648 .iter()
11649 .any(|encrypted| encrypted.index == index)
11650 {
11651 values.push(Some(ColumnValue::Bytes(bytes.to_vec())));
11652 continue;
11653 }
11654 }
11655 let json = sql_ref_to_json(column, raw);
11656 values.push(json_to_column_value(column, Some(&json)).map_err(|_| {
11657 "sync.local_corrupt: persisted local row value is invalid".to_owned()
11658 })?);
11659 }
11660 let version: i64 = row
11661 .get(table.columns.len())
11662 .map_err(|error| Self::sqlite_failure(&self.storage_failure, error))?;
11663 Ok(Some((values, version)))
11664 }
11665
11666 #[must_use]
11670 pub fn realtime_policy(&self) -> RealtimePolicy {
11671 self.realtime_policy
11672 }
11673
11674 #[must_use]
11676 pub fn realtime_state(&self) -> RealtimeState {
11677 self.realtime_state
11678 }
11679
11680 pub fn set_realtime_policy(&mut self, policy: RealtimePolicy) -> Result<(), String> {
11683 if self.realtime_state == RealtimeState::Connected && policy != self.realtime_policy {
11684 return Err(
11685 "sync.invalid_request: disconnect realtime before changing the realtime policy"
11686 .to_owned(),
11687 );
11688 }
11689 self.realtime_policy = policy;
11690 match policy {
11691 RealtimePolicy::Off => self.set_realtime_state(RealtimeState::Disabled, None),
11692 RealtimePolicy::Required | RealtimePolicy::Optional
11693 if self.realtime_state == RealtimeState::Disabled =>
11694 {
11695 self.set_realtime_state(RealtimeState::Disconnected, None);
11696 }
11697 RealtimePolicy::Required | RealtimePolicy::Optional => {}
11698 }
11699 Ok(())
11700 }
11701
11702 fn set_realtime_state(&mut self, state: RealtimeState, reason_code: Option<&str>) {
11705 self.realtime_state = state;
11706 self.realtime_reason_code = reason_code.map(Self::diagnostic_code);
11707 if matches!(state, RealtimeState::Connected | RealtimeState::Disabled) {
11708 self.realtime_reason_code = None;
11709 self.realtime_retry_delay_ms = None;
11710 }
11711 }
11712
11713 #[must_use]
11714 fn realtime_connected(&self) -> bool {
11715 self.realtime_state == RealtimeState::Connected
11716 }
11717
11718 pub fn connect_realtime(&mut self, transport: &mut dyn Transport) -> Result<(), String> {
11719 if !self.transport_enabled {
11720 return Err("sync.offline: transport gate is closed".into());
11721 }
11722 if self.realtime_policy == RealtimePolicy::Off {
11723 return Err(
11724 "sync.invalid_request: realtime is off; realtime connect is refused".to_owned(),
11725 );
11726 }
11727 if self.realtime_connected() {
11728 return Ok(());
11729 }
11730 self.realtime_state = RealtimeState::Connecting;
11731 match transport.realtime_connect_for_client(&self.client_id) {
11732 Ok(()) => {
11733 self.set_realtime_state(RealtimeState::Connected, None);
11734 Ok(())
11735 }
11736 Err(error) => {
11737 self.set_realtime_state(RealtimeState::Refused, Some(&error.code));
11738 Err(format!("{}: {}", error.code, error.message))
11739 }
11740 }
11741 }
11742
11743 pub fn disconnect_realtime(&mut self, transport: &mut dyn Transport) {
11744 self.cancel_sync_round();
11745 if self.realtime_state == RealtimeState::Disconnected
11748 || self.realtime_state == RealtimeState::Disabled
11749 {
11750 return;
11751 }
11752 let _ = transport.realtime_close();
11753 self.set_realtime_state(
11754 if self.realtime_policy == RealtimePolicy::Off {
11755 RealtimeState::Disabled
11756 } else {
11757 RealtimeState::Disconnected
11758 },
11759 None,
11760 );
11761 self.presence.clear(); }
11763
11764 pub fn realtime_lost(&mut self, transport: &mut dyn Transport, error: &TransportError) {
11770 if self.realtime_state != RealtimeState::Connected {
11771 return;
11772 }
11773 self.cancel_sync_round();
11774 let _ = transport.realtime_close();
11775 self.set_realtime_state(RealtimeState::Lost, Some(&error.code));
11776 self.realtime_retry_delay_ms = Some(self.schedule_background_retry());
11777 self.presence.clear();
11778 }
11779
11780 pub fn set_presence(
11786 &mut self,
11787 transport: &mut dyn Transport,
11788 scope_key: &str,
11789 doc: Option<&Value>,
11790 ) -> Result<(), String> {
11791 if !self.transport_enabled {
11792 return Err("sync.offline: transport gate is closed".into());
11793 }
11794 if !self.realtime_connected() {
11795 return Err("setPresence requires a connected realtime socket (§8.6)".to_string());
11796 }
11797 let text = encode_presence_publish(scope_key, doc);
11798 transport
11799 .realtime_send(&text)
11800 .map_err(|e| format!("{}: {}", e.code, e.message))
11801 }
11802
11803 pub fn presence(&self, scope_key: &str) -> Vec<PresencePeer> {
11805 self.presence
11806 .get(scope_key)
11807 .map(|peers| peers.values().cloned().collect())
11808 .unwrap_or_default()
11809 }
11810
11811 fn apply_presence(
11813 &mut self,
11814 scope_key: String,
11815 kind: Option<PresenceKind>,
11816 actor_id: Option<String>,
11817 client_id: Option<String>,
11818 doc: Option<Value>,
11819 error: Option<String>,
11820 ) {
11821 if error.is_some() {
11824 return;
11825 }
11826 let (Some(kind), Some(actor_id), Some(client_id)) = (kind, actor_id, client_id) else {
11827 return;
11828 };
11829 let peer_key = format!("{actor_id} {client_id}");
11830 match kind {
11831 PresenceKind::Leave => {
11832 if let Some(peers) = self.presence.get_mut(&scope_key) {
11833 peers.remove(&peer_key);
11834 if peers.is_empty() {
11835 self.presence.remove(&scope_key);
11836 }
11837 }
11838 }
11839 _ => {
11840 let doc = match doc {
11841 Some(Value::Object(_)) => doc.unwrap(),
11842 _ => return,
11843 };
11844 self.presence.entry(scope_key).or_default().insert(
11845 peer_key,
11846 PresencePeer {
11847 actor_id,
11848 client_id,
11849 doc,
11850 },
11851 );
11852 }
11853 }
11854 }
11855
11856 pub fn on_realtime_text(&mut self, text: &str) {
11858 if self.stopped || !self.transport_enabled || self.security_preflight {
11859 return;
11860 }
11861 match parse_control(text) {
11862 Ok(ControlMessage::Hello { requires_sync, .. }) => {
11863 if requires_sync {
11864 self.set_sync_needed(true, true);
11866 }
11867 }
11868 Ok(ControlMessage::Presence {
11869 scope_key,
11870 kind,
11871 actor_id,
11872 client_id,
11873 doc,
11874 error,
11875 ..
11876 }) => {
11877 self.apply_presence(scope_key, kind, actor_id, client_id, doc, error);
11878 }
11879 Ok(ControlMessage::Wake { .. }) => {
11880 self.set_sync_needed(true, true);
11882 }
11883 _ => {}
11884 }
11885 }
11886
11887 pub fn on_realtime_binary_queued(&mut self, bytes: &[u8]) -> Vec<String> {
11891 let mut transport = crate::round::DownloadResults::default();
11892 self.on_realtime_binary(&mut transport, bytes);
11893 transport.controls
11894 }
11895
11896 pub fn on_realtime_binary(&mut self, transport: &mut dyn Transport, bytes: &[u8]) {
11902 if self.stopped || !self.transport_enabled || self.security_preflight {
11903 return;
11904 }
11905 #[cfg(feature = "bench-internals")]
11906 let decode_phase = self.benchmark_phases.start(Phase::ResponseDecode);
11907 let message = match decode_message(bytes) {
11908 Ok(m) if m.msg_kind == MsgKind::Response => m,
11909 _ => {
11910 self.set_sync_needed(true, true);
11911 return;
11912 }
11913 };
11914 #[cfg(feature = "bench-internals")]
11915 drop(decode_phase);
11916 if let Some(Frame::RespHeader {
11919 required_schema_version: Some(required),
11920 latest_schema_version,
11921 ..
11922 }) = message.frames.first()
11923 {
11924 let floor = SchemaFloor {
11925 required_schema_version: Some(*required),
11926 latest_schema_version: *latest_schema_version,
11927 };
11928 if self.set_schema_floor(Some(floor)).is_err() {
11929 self.set_sync_needed(true, true);
11930 }
11931 return;
11932 }
11933 let through = message.frames.iter().find_map(|frame| match frame {
11934 Frame::LiveEnd { through_seq, .. } => Some(*through_seq),
11935 _ => None,
11936 });
11937 let epoch = self.get_meta(LOG_EPOCH_KEY).ok().flatten();
11938 let mut dropped = !matches!(message.frames.first(), Some(Frame::RespHeader {
11939 reset_required: false,
11940 log_epoch,
11941 ..
11942 }) if Some(log_epoch) == epoch.as_ref());
11943 let mut applied: Option<i64> = None;
11944 dropped |= self.begin_coalesced_apply().is_err();
11945 let mut receivers: Option<Vec<UnitRecord>> = None;
11946 for frame in message.frames {
11947 if dropped {
11948 break;
11949 }
11950 match frame {
11951 Frame::LiveStart { status, .. } => {
11952 let Ok(cursor) = self.stored(replica_cursor(&self.conn)) else {
11953 dropped = true;
11954 break;
11955 };
11956 if status != LiveStatus::Active || cursor < 0 {
11957 dropped = true;
11958 } else if !through.is_some_and(|through| through <= cursor) {
11959 receivers = self.stored(load_units(&self.conn)).ok().map(|units| {
11961 units
11962 .into_iter()
11963 .filter(|unit| {
11964 unit.status == UnitStatus::Active
11965 && unit.echo.is_some()
11966 && unit.cursor.is_none()
11967 })
11968 .collect()
11969 });
11970 dropped = receivers.is_none();
11971 }
11972 }
11973 Frame::Commit {
11974 commit_seq,
11975 tables,
11976 changes,
11977 ..
11978 } => {
11979 let Some(receivers) = receivers.clone() else {
11980 continue;
11981 };
11982 dropped = self
11983 .apply_live_commit(
11984 &receivers,
11985 &tables,
11986 changes,
11987 commit_seq,
11988 AppliedServerCommitSource::Delta,
11989 )
11990 .is_err();
11991 }
11992 Frame::LiveEnd { through_seq, .. } if receivers.is_some() => {
11993 dropped = self
11994 .set_meta(REPLICA_CURSOR_KEY, &through_seq.to_string())
11995 .is_err();
11996 applied = Some(through_seq);
11997 }
11998 Frame::Error { .. } => dropped = true,
11999 _ => {}
12000 }
12001 }
12002 if self.finish_coalesced_apply().is_err() || dropped {
12005 applied = None;
12006 dropped = true;
12007 }
12008 if let Some(cursor) = applied {
12009 let ack = format!("{{\"type\":\"ack\",\"cursor\":{cursor}}}");
12011 let _ = transport.realtime_send(&ack);
12012 }
12013 if dropped {
12014 self.set_sync_needed(true, true);
12015 }
12016 }
12017
12018 fn ack_after_pull(&mut self, transport: &mut dyn Transport) {
12021 if !self.realtime_connected() {
12022 return;
12023 }
12024 if let Ok(cursor @ 0..) = self.stored(replica_cursor(&self.conn)) {
12025 let ack = format!("{{\"type\":\"ack\",\"cursor\":{cursor}}}");
12026 let _ = transport.realtime_send(&ack);
12027 }
12028 }
12029}
12030
12031#[cfg(test)]
12032mod previous_version_wiring_tests;
12033
12034#[cfg(test)]
12035mod identity_tests;
12036
12037#[cfg(test)]
12038mod insert_intent_tests;
12039
12040#[cfg(test)]
12041mod robustness_tests;