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