1use std::ffi::c_int;
75
76use rusqlite::{Connection, OptionalExtension, Params};
77
78use crate::quote_ident;
79
80mod audience;
81mod ffi;
82mod model;
83mod outbound;
84
85pub(crate) use audience::{
86 active_circle_control, align_inbound_scoped_root_audiences, audience_moves,
87 capture_routing_changes, filter_inbound_circle_changeset, filter_inbound_store_rows,
88 filter_snapshot_circle_changeset, live_row_audience, normalize_inbound_store_changeset,
89 partition_outbound, prune_ineligible_scoped_rows, prune_private_routes_without_rows,
90 recorded_host_changeset, retain_projection_rows, retain_snapshot_audience_rows,
91 validate_accepted_foreign_key_closure, validate_scoped_foreign_key_audiences,
92 validate_snapshot_routing_state, PartitionedAudienceWrite,
93};
94pub use audience::{
95 is_routing_table, store_audience_transitions, AudienceMove, AudiencePartition,
96 CirclePartitionControl, CirclePartitionControlError, RoutingChanges, StoreAudienceTransitions,
97};
98pub(crate) use ffi::{for_each_change, update_values, Changegroup};
99pub use model::Gates;
100#[cfg(any(test, feature = "test-utils"))]
101pub use model::{from_tables_call_count, reset_from_tables_call_count};
102pub(crate) use outbound::attach_empty_clone;
103pub(crate) use outbound::query_truth;
104
105fn gate_table_columns(conn: &Connection, table: &str) -> Result<Vec<String>, GateError> {
108 crate::table_columns(conn, table)
109 .map_err(|e| GateError::Sql(format!("read columns of {table}"), e))
110}
111
112fn all_row_ids(conn: &Connection, table: &str) -> Result<Vec<String>, GateError> {
115 let sql = format!(
116 "SELECT {id} FROM {table} ORDER BY {id}",
117 id = quote_ident("id"),
118 table = quote_ident(table),
119 );
120 query_mapped_rows(conn, &sql, [], |row| row.get::<_, String>(0))
121}
122
123fn execute_batch(conn: &Connection, sql: &str) -> Result<(), GateError> {
124 conn.execute_batch(sql)
125 .map_err(|e| GateError::Sql(format!("execute batch: {sql}"), e))
126}
127
128fn query_mapped_rows<T, P, F>(
131 conn: &Connection,
132 sql: &str,
133 params: P,
134 mapper: F,
135) -> Result<Vec<T>, GateError>
136where
137 P: Params,
138 F: FnMut(&rusqlite::Row<'_>) -> rusqlite::Result<T>,
139{
140 crate::query_mapped_rows(conn, sql, params, mapper)
141 .map_err(|e| GateError::Sql(format!("query: {sql}"), e))
142}
143
144fn query_row_optional<T, P, F>(
145 conn: &Connection,
146 sql: &str,
147 params: P,
148 mapper: F,
149) -> Result<Option<T>, GateError>
150where
151 P: Params,
152 F: FnOnce(&rusqlite::Row<'_>) -> rusqlite::Result<T>,
153{
154 conn.query_row(sql, params, mapper)
155 .optional()
156 .map_err(|e| GateError::Sql(format!("query: {sql}"), e))
157}
158fn row_value_to_string(row: &rusqlite::Row<'_>, idx: usize) -> rusqlite::Result<Option<String>> {
163 Ok(crate::value_ref_to_string(row.get_ref(idx)?))
164}
165
166#[derive(Debug)]
167pub enum GateError {
168 Ffi(&'static str, c_int),
169 Session {
170 operation: String,
171 source: rusqlite::Error,
172 },
173 MissingGateColumn(String, String),
174 MissingFkColumn(String, String),
175 ForeignKeySchema(crate::ForeignKeySchemaError),
176 CompositeGateForeignKey {
177 table: String,
178 parent: String,
179 },
180 MissingAudienceParentDeclaration {
181 table: String,
182 },
183 InvalidAudienceParentDeclaration {
184 table: String,
185 column: String,
186 reason: String,
187 },
188 ScopedOutboundRequiresPartitioning {
189 table: String,
190 },
191 InvalidAudience {
192 table: String,
193 value: Option<String>,
194 reason: String,
195 },
196 InvalidAudienceEncoding {
197 table: String,
198 value: Option<String>,
199 source: coven_protocol::circle::CircleIdError,
200 },
201 InvalidInboundAudiencePackage(String),
202 InvalidInboundAudienceEncoding {
203 context: String,
204 source: coven_protocol::circle::CircleIdError,
205 },
206 InvalidInboundRowIdentity {
207 context: String,
208 source: coven_protocol::synced_schema::RowIdentityError,
209 },
210 InvalidMaterializedRouting(String),
211 InvalidMaterializedRoutingId {
212 context: String,
213 source: coven_protocol::circle::RowRoutingIdError,
214 },
215 InvalidMaterializedAudience {
216 context: String,
217 source: coven_protocol::circle::CircleIdError,
218 },
219 InvalidMaterializedRowIdentity {
220 context: String,
221 source: coven_protocol::synced_schema::RowIdentityError,
222 },
223 MissingChangesetPrimaryKey(String),
224 MissingAudienceRow {
225 table: String,
226 row_id: String,
227 },
228 MissingAudienceParent {
229 table: String,
230 row_id: Option<String>,
231 parent: String,
232 },
233 CircleAuthority {
234 circle_id: coven_protocol::circle::CircleId,
235 active_records: usize,
236 },
237 CircleDeleted {
240 circle_id: coven_protocol::circle::CircleId,
241 },
242 InvalidCircleControl {
243 circle_id: coven_protocol::circle::CircleId,
244 source: CircleControlFailure,
245 },
246 NoGatedDescendants(String),
251 FkCycle(Vec<String>),
253 UnsharedForeignKeyParent(Box<UnsharedForeignKeyParent>),
259 CreateTableSchema(crate::CreateTableSchemaError),
260 Sql(String, rusqlite::Error),
261 Cleanup {
262 operation: Box<GateError>,
263 cleanup: Box<GateError>,
264 },
265}
266
267impl std::fmt::Display for GateError {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 match self {
270 GateError::Ffi(func, rc) => write!(f, "{func} failed (rc={rc})"),
271 GateError::Session { operation, source } => {
272 write!(f, "session {operation} failed: {source}")
273 }
274 GateError::MissingGateColumn(tbl, col) => {
275 write!(f, "gated table {tbl} has no gate column {col}")
276 }
277 GateError::MissingFkColumn(tbl, col) => {
278 write!(f, "table {tbl} has no FK column {col}")
279 }
280 GateError::ForeignKeySchema(error) => write!(f, "foreign-key schema: {error}"),
281 GateError::CompositeGateForeignKey { table, parent } => write!(
282 f,
283 "table {table} inherits its gate through a composite foreign key to {parent}, but gate inheritance requires one child column"
284 ),
285 GateError::MissingAudienceParentDeclaration { table } => write!(
286 f,
287 "scoped descendant table {table} must declare its audience-parent foreign key"
288 ),
289 GateError::InvalidAudienceParentDeclaration {
290 table,
291 column,
292 reason,
293 } => write!(
294 f,
295 "table {table} cannot inherit its audience through {column}: {reason}"
296 ),
297 GateError::ScopedOutboundRequiresPartitioning { table } => write!(
298 f,
299 "scoped root {table} must use audience-partitioned outbound capture"
300 ),
301 GateError::InvalidAudience {
302 table,
303 value,
304 reason,
305 } => write!(
306 f,
307 "scoped table {table} has invalid audience {value:?}: {reason}"
308 ),
309 GateError::InvalidAudienceEncoding {
310 table,
311 value,
312 source,
313 } => write!(
314 f,
315 "scoped table {table} has invalid audience {value:?}: {source}"
316 ),
317 GateError::InvalidInboundAudiencePackage(reason) => {
318 write!(f, "invalid inbound audience package: {reason}")
319 }
320 GateError::InvalidInboundAudienceEncoding { context, source } => {
321 write!(f, "invalid inbound audience package: {context}: {source}")
322 }
323 GateError::InvalidInboundRowIdentity { context, source } => {
324 write!(f, "invalid inbound audience package: {context}: {source}")
325 }
326 GateError::InvalidMaterializedRouting(reason) => {
327 write!(f, "invalid materialized routing state: {reason}")
328 }
329 GateError::InvalidMaterializedRoutingId { context, source } => {
330 write!(f, "invalid materialized routing state: {context}: {source}")
331 }
332 GateError::InvalidMaterializedAudience { context, source } => {
333 write!(f, "invalid materialized routing state: {context}: {source}")
334 }
335 GateError::InvalidMaterializedRowIdentity { context, source } => {
336 write!(f, "invalid materialized routing state: {context}: {source}")
337 }
338 GateError::MissingChangesetPrimaryKey(table) => {
339 write!(f, "scoped changeset row in {table} has no primary key")
340 }
341 GateError::MissingAudienceRow { table, row_id } => {
342 write!(
343 f,
344 "scoped row {table}.{row_id} is absent while resolving its audience"
345 )
346 }
347 GateError::MissingAudienceParent {
348 table,
349 row_id,
350 parent,
351 } => write!(
352 f,
353 "scoped row {table}.{row_id:?} has no audience parent in {parent}"
354 ),
355 GateError::CircleAuthority {
356 circle_id,
357 active_records,
358 } => write!(
359 f,
360 "circle {circle_id} has {active_records} active local access records; expected exactly one"
361 ),
362 GateError::CircleDeleted { circle_id } => {
363 write!(f, "circle {circle_id} is deleted and accepts no writes")
364 }
365 GateError::InvalidCircleControl { circle_id, source } => {
366 write!(f, "circle {circle_id} has invalid active control: {source}")
367 }
368 GateError::NoGatedDescendants(tbl) => {
369 write!(
370 f,
371 "gated_by_descendants ancestor {tbl} has no inferred gated descendant: no \
372 synced table references it"
373 )
374 }
375 GateError::FkCycle(tables) => {
376 write!(f, "gated tables form an FK cycle: {}", tables.join(", "))
377 }
378 GateError::UnsharedForeignKeyParent(unshared) => match &unshared.parent_id {
379 Some(parent_id) => write!(
380 f,
381 "shared row {table}.{row_id} names {parent}.{parent_id} through {column}, \
382 which the gate does not share",
383 table = unshared.table,
384 row_id = unshared.row_id,
385 parent = unshared.parent,
386 column = unshared.column,
387 ),
388 None => write!(
389 f,
390 "shared row {table}.{row_id} names a {parent} row through {column} that the \
391 database does not hold",
392 table = unshared.table,
393 row_id = unshared.row_id,
394 parent = unshared.parent,
395 column = unshared.column,
396 ),
397 },
398 GateError::CreateTableSchema(error) => error.fmt(f),
399 GateError::Sql(op, err) => write!(f, "{op} failed: {err}"),
400 GateError::Cleanup { operation, cleanup } => {
401 write!(
402 f,
403 "{operation}; temporary gate cleanup also failed: {cleanup}"
404 )
405 }
406 }
407 }
408}
409
410impl std::error::Error for GateError {
411 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
412 match self {
413 Self::Session { source, .. } | Self::Sql(_, source) => Some(source),
414 Self::ForeignKeySchema(source) => Some(source),
415 Self::CreateTableSchema(source) => Some(source),
416 Self::InvalidCircleControl { source, .. } => Some(source),
417 Self::InvalidAudienceEncoding { source, .. } => Some(source),
418 Self::InvalidInboundAudienceEncoding { source, .. }
419 | Self::InvalidMaterializedAudience { source, .. } => Some(source),
420 Self::InvalidInboundRowIdentity { source, .. }
421 | Self::InvalidMaterializedRowIdentity { source, .. } => Some(source),
422 Self::InvalidMaterializedRoutingId { source, .. } => Some(source),
423 Self::Cleanup { operation, .. } => Some(operation.as_ref()),
424 _ => None,
425 }
426 }
427}
428
429#[derive(Debug)]
433pub struct UnsharedForeignKeyParent {
434 pub table: String,
436 pub row_id: String,
437 pub column: String,
439 pub parent: String,
440 pub parent_id: Option<String>,
444}
445
446#[derive(Debug, thiserror::Error)]
447pub enum CircleControlFailure {
448 #[error("parse current state: {0}")]
449 ParseCurrentState(serde_json::Error),
450 #[error("current state failed verification")]
451 Verification,
452 #[error("serialize current control coordinate: {0}")]
453 SerializeCoordinate(serde_json::Error),
454 #[error(transparent)]
455 PartitionControl(#[from] CirclePartitionControlError),
456}
457
458impl From<crate::CreateTableSchemaError> for GateError {
459 fn from(error: crate::CreateTableSchemaError) -> Self {
460 Self::CreateTableSchema(error)
461 }
462}
463
464#[cfg(test)]
465mod retraction_tests;
466#[cfg(test)]
467mod tests;