klukai_types/
pubsub.rs

1#![allow(clippy::result_large_err)]
2
3use std::{
4    cmp,
5    collections::{BTreeMap, HashMap, HashSet},
6    sync::Arc,
7    time::{Duration, Instant},
8};
9
10use crate::api::{ChangeId, ColumnName, ColumnType, RowId, SqliteValue, SqliteValueRef, TableName};
11use crate::spawn::spawn_counted;
12use crate::sqlite_pool::{self, RusqlitePool};
13use crate::tripwire::{Outcome, PreemptibleFutureExt, Tripwire};
14use bytes::{Buf, BufMut};
15use camino::{Utf8Path, Utf8PathBuf};
16use compact_str::{ToCompactString, format_compact};
17use enquote::unquote;
18use fallible_iterator::FallibleIterator;
19use indexmap::{IndexMap, IndexSet};
20use metrics::{counter, histogram};
21use parking_lot::{Condvar, Mutex, RwLock};
22use rusqlite::{
23    Connection, OptionalExtension, params_from_iter,
24    types::{FromSqlError, ValueRef},
25};
26use sqlite3_parser::{
27    ast::{
28        As, Cmd, Expr, FromClause, JoinConstraint, JoinOperator, JoinType, JoinedSelectTable, Name,
29        OneSelect, Operator, QualifiedName, ResultColumn, Select, SelectTable, Stmt,
30    },
31    lexer::sql::Parser,
32};
33use tokio::{
34    sync::{AcquireError, mpsc, watch},
35    task::block_in_place,
36};
37use tokio_util::sync::{CancellationToken, DropGuard, WaitForCancellationFuture};
38use tracing::{debug, error, info, trace, warn};
39use uuid::Uuid;
40
41use crate::{
42    agent::SplitPool,
43    api::QueryEvent,
44    change::Change,
45    schema::{Schema, Table},
46    sqlite::CrConn,
47    updates::HandleMetrics,
48};
49
50pub use crate::api::sqlite::ChangeType;
51use crate::updates::{Handle, Manager};
52
53#[derive(Debug, Default, Clone)]
54pub struct SubsManager(Arc<RwLock<InnerSubsManager>>);
55
56#[derive(Debug, Default)]
57struct InnerSubsManager {
58    handles: BTreeMap<Uuid, MatcherHandle>,
59    queries: HashMap<String, Uuid>,
60}
61
62// tools to bootstrap a new subscriber or notifier
63pub struct MatcherCreated {
64    pub evt_rx: mpsc::Receiver<QueryEvent>,
65}
66
67const SUB_EVENT_CHANNEL_CAP: usize = 512;
68
69impl Manager<MatcherHandle> for SubsManager {
70    fn trait_type(&self) -> String {
71        "subs".to_string()
72    }
73
74    fn get(&self, id: &Uuid) -> Option<MatcherHandle> {
75        self.0.read().get(id)
76    }
77
78    fn remove(&self, id: &Uuid) -> Option<MatcherHandle> {
79        let mut inner = self.0.write();
80        inner.remove(id)
81    }
82
83    fn get_handles(&self) -> BTreeMap<Uuid, MatcherHandle> {
84        self.0.read().handles.clone()
85    }
86}
87
88impl SubsManager {
89    pub fn get(&self, id: &Uuid) -> Option<MatcherHandle> {
90        self.0.read().get(id)
91    }
92
93    pub fn get_by_query(&self, sql: &str) -> Option<MatcherHandle> {
94        self.0.read().get_by_query(sql)
95    }
96
97    pub fn get_by_hash(&self, hash: &str) -> Option<MatcherHandle> {
98        self.0.read().get_by_hash(hash)
99    }
100
101    pub fn get_handles(&self) -> BTreeMap<Uuid, MatcherHandle> {
102        self.0.read().handles.clone()
103    }
104
105    pub async fn drop_handles(&self) {
106        let handles = {
107            let mut inner = self.0.write();
108            std::mem::take(&mut inner.handles)
109        };
110        for (_, handle) in handles.iter() {
111            handle.cleanup().await;
112        }
113    }
114
115    pub fn get_or_insert(
116        &self,
117        sql: &str,
118        subs_path: &Utf8Path,
119        schema: &Schema,
120        pool: &SplitPool,
121        tripwire: Tripwire,
122    ) -> Result<(MatcherHandle, Option<MatcherCreated>), MatcherError> {
123        if let Some(handle) = self.get_by_query(sql) {
124            return Ok((handle, None));
125        }
126
127        let mut inner = self.0.write();
128        if let Some(handle) = inner.get_by_query(sql) {
129            return Ok((handle, None));
130        }
131
132        let id = Uuid::new_v4();
133        let (evt_tx, evt_rx) = mpsc::channel(SUB_EVENT_CHANNEL_CAP);
134
135        let handle_res = Matcher::create(
136            id,
137            subs_path.to_path_buf(),
138            schema,
139            pool.client_dedicated()?,
140            evt_tx,
141            sql,
142            tripwire,
143        );
144
145        let handle = match handle_res {
146            Ok(handle) => handle,
147            Err(e) => {
148                error!(sub_id = %id, "could not create subscription: {e}");
149                if let Err(e) = Matcher::cleanup(id, Matcher::sub_path(subs_path, id)) {
150                    error!("could not cleanup subscription: {e}");
151                }
152
153                return Err(e);
154            }
155        };
156
157        inner.handles.insert(id, handle.clone());
158        inner.queries.insert(sql.to_owned(), id);
159
160        Ok((handle, Some(MatcherCreated { evt_rx })))
161    }
162
163    #[allow(clippy::too_many_arguments)]
164    pub fn restore(
165        &self,
166        id: Uuid,
167        subs_path: &Utf8Path,
168        schema: &Schema,
169        pool: &SplitPool,
170        tripwire: Tripwire,
171    ) -> Result<(MatcherHandle, MatcherCreated), MatcherError> {
172        let mut inner = self.0.write();
173
174        if inner.handles.contains_key(&id) {
175            return Err(MatcherError::CannotRestoreExisting);
176        }
177
178        let (evt_tx, evt_rx) = mpsc::channel(SUB_EVENT_CHANNEL_CAP);
179
180        let handle = Matcher::restore(
181            id,
182            subs_path.to_path_buf(),
183            schema,
184            pool.client_dedicated()?,
185            evt_tx,
186            tripwire,
187        )?;
188
189        inner.handles.insert(id, handle.clone());
190        inner.queries.insert(handle.inner.sql.clone(), id);
191
192        Ok((handle, MatcherCreated { evt_rx }))
193    }
194
195    pub fn remove(&self, id: &Uuid) -> Option<MatcherHandle> {
196        let mut inner = self.0.write();
197        inner.remove(id)
198    }
199}
200
201#[derive(Debug)]
202pub struct MatchableChange<'a> {
203    pub table: &'a TableName,
204    pub pk: &'a [u8],
205    pub column: &'a ColumnName,
206    pub cl: i64,
207}
208
209impl<'a> From<&'a Change> for MatchableChange<'a> {
210    fn from(value: &'a Change) -> Self {
211        MatchableChange {
212            table: &value.table,
213            pk: &value.pk,
214            column: &value.cid,
215            cl: value.cl,
216        }
217    }
218}
219
220impl InnerSubsManager {
221    fn get(&self, id: &Uuid) -> Option<MatcherHandle> {
222        self.handles.get(id).cloned()
223    }
224
225    fn get_by_query(&self, sql: &str) -> Option<MatcherHandle> {
226        self.queries
227            .get(sql)
228            .and_then(|id| self.handles.get(id).cloned())
229    }
230
231    pub fn get_by_hash(&self, hash: &str) -> Option<MatcherHandle> {
232        self.handles
233            .values()
234            .find(|x| x.inner.hash == hash)
235            .cloned()
236    }
237
238    fn remove(&mut self, id: &Uuid) -> Option<MatcherHandle> {
239        let handle = self.handles.remove(id)?;
240        self.queries.remove(&handle.inner.sql);
241        Some(handle)
242    }
243}
244
245#[derive(Debug, Clone, Copy)]
246pub enum MatcherState {
247    Created,
248    Running,
249}
250
251impl MatcherState {
252    fn is_running(&self) -> bool {
253        matches!(self, MatcherState::Running)
254    }
255}
256
257#[derive(Clone, Debug)]
258pub struct MatcherHandle {
259    inner: Arc<InnerMatcherHandle>,
260    state: StateLock,
261}
262
263#[derive(Debug)]
264struct InnerMatcherHandle {
265    id: Uuid,
266    sql: String,
267    hash: String,
268    pool: sqlite_pool::RusqlitePool,
269    parsed: ParsedSelect,
270    col_names: Vec<ColumnName>,
271    cancel: CancellationToken,
272    changes_tx: mpsc::Sender<MatchCandidates>,
273    last_change_rx: watch::Receiver<ChangeId>,
274    // some state from the matcher so we can take a look later
275    subs_path: String,
276    cached_statements: HashMap<String, MatcherStmt>,
277    metrics: HashMap<String, HandleMetrics>,
278}
279
280pub type MatchCandidates = IndexMap<TableName, IndexMap<Vec<u8>, i64>>;
281
282impl Handle for MatcherHandle {
283    fn id(&self) -> Uuid {
284        self.inner.id
285    }
286
287    fn cancelled(&self) -> WaitForCancellationFuture<'_> {
288        self.inner.cancel.cancelled()
289    }
290
291    fn changes_tx(&self) -> mpsc::Sender<MatchCandidates> {
292        self.inner.changes_tx.clone()
293    }
294
295    fn cleanup(&self) -> impl std::future::Future<Output = ()> + Send {
296        let cancel = self.inner.cancel.clone();
297        let id = self.inner.id;
298
299        async move {
300            cancel.cancel();
301            info!(sub_id = %id, "Canceled subscription");
302        }
303    }
304
305    fn filter_matchable_change(
306        &self,
307        candidates: &mut MatchCandidates,
308        change: MatchableChange,
309    ) -> bool {
310        trace!("filtering change {change:?}");
311        // don't double process the same pk
312        if candidates
313            .get(change.table)
314            .map(|pks| pks.contains_key(change.pk))
315            .unwrap_or_default()
316        {
317            trace!("already contained key");
318            return false;
319        }
320
321        // don't consider changes that don't have both the table + col in the matcher query
322        if !self
323            .inner
324            .parsed
325            .table_columns
326            .get(change.table.as_str())
327            .map(|cols| change.column.is_crsql_sentinel() || cols.contains(change.column.as_str()))
328            .unwrap_or_default()
329        {
330            trace!("could not match against parsed query table and columns");
331            return false;
332        }
333
334        if let Some(v) = candidates.get_mut(change.table) {
335            v.insert(change.pk.to_vec(), change.cl).is_none()
336        } else {
337            candidates.insert(
338                change.table.clone(),
339                [(change.pk.to_vec(), change.cl)].into(),
340            );
341            true
342        }
343    }
344
345    fn get_counter(&self, table: &str) -> &HandleMetrics {
346        self.inner.metrics.get(table).unwrap_or_else(|| {
347            panic!(
348                "metrics counter for table '{}' missing. subs hash {}!",
349                self.inner.hash, table
350            )
351        })
352    }
353}
354
355impl MatcherHandle {
356    pub fn sql(&self) -> &String {
357        &self.inner.sql
358    }
359
360    pub fn hash(&self) -> &str {
361        &self.inner.hash
362    }
363
364    pub fn parsed_columns(&self) -> &[ResultColumn] {
365        &self.inner.parsed.columns
366    }
367
368    pub fn col_names(&self) -> &[ColumnName] {
369        &self.inner.col_names
370    }
371
372    pub fn subs_path(&self) -> &String {
373        &self.inner.subs_path
374    }
375
376    pub fn cached_stmts(&self) -> &HashMap<String, MatcherStmt> {
377        &self.inner.cached_statements
378    }
379
380    pub fn pool(&self) -> &RusqlitePool {
381        &self.inner.pool
382    }
383
384    fn wait_for_running_state(&self) {
385        let (lock, cvar) = &*self.state;
386        let mut state = lock.lock();
387        while !state.is_running() {
388            cvar.wait(&mut state);
389        }
390    }
391
392    pub fn max_change_id(&self, conn: &Connection) -> rusqlite::Result<ChangeId> {
393        self.wait_for_running_state();
394        let mut prepped = conn.prepare_cached("SELECT COALESCE(MAX(id), 0) FROM changes")?;
395        prepped.query_row([], |row| row.get(0))
396    }
397
398    pub fn last_change_id_sent(&self) -> ChangeId {
399        *self.inner.last_change_rx.borrow()
400    }
401
402    pub fn max_row_id(&self, conn: &Connection) -> rusqlite::Result<RowId> {
403        self.wait_for_running_state();
404        let mut prepped =
405            conn.prepare_cached("SELECT COALESCE(MAX(__corro_rowid), 0) FROM query")?;
406        prepped.query_row([], |row| row.get(0))
407    }
408
409    pub fn changes_since(
410        &self,
411        since: ChangeId,
412        conn: &Connection,
413        tx: mpsc::Sender<QueryEvent>,
414    ) -> rusqlite::Result<ChangeId> {
415        self.wait_for_running_state();
416
417        let mut prepped = conn.prepare_cached("SELECT MIN(id) FROM changes")?;
418        let min_change_id: u64 = prepped.query_row([], |row| row.get(0))?;
419
420        if since.0 < min_change_id {
421            return Err(rusqlite::Error::ModuleError(format!(
422                "subscription already deleted older changes, min change id: {min_change_id}",
423            )));
424        }
425
426        let mut query_cols = vec![];
427        for i in 0..(self.parsed_columns().len()) {
428            query_cols.push(format!("col_{i}"));
429        }
430        let mut prepped = conn.prepare_cached(&format!(
431            "SELECT id, type, __corro_rowid, {} FROM changes WHERE id > ? ORDER BY id ASC",
432            query_cols.join(",")
433        ))?;
434
435        let col_count = prepped.column_count();
436
437        let mut max_change_id = since;
438
439        let mut rows = prepped.query([since])?;
440
441        loop {
442            let row = match rows.next()? {
443                Some(row) => row,
444                None => break,
445            };
446
447            let change_id: ChangeId = row.get(0)?;
448            if change_id.0 > max_change_id.0 {
449                max_change_id = change_id;
450            }
451
452            if let Err(e) = tx.blocking_send(QueryEvent::Change(
453                row.get(1)?,
454                row.get(2)?,
455                (3..col_count)
456                    .map(|i| row.get::<_, SqliteValue>(i))
457                    .collect::<rusqlite::Result<Vec<_>>>()?,
458                change_id,
459            )) {
460                error!("could not send change to channel: {e}");
461                break;
462            }
463        }
464
465        Ok(max_change_id)
466    }
467
468    pub fn all_rows(
469        &self,
470        conn: &Connection,
471        tx: mpsc::Sender<QueryEvent>,
472    ) -> Result<ChangeId, MatcherError> {
473        self.wait_for_running_state();
474        let mut query_cols = vec![];
475        for i in 0..(self.parsed_columns().len()) {
476            query_cols.push(format!("col_{i}"));
477        }
478        let mut prepped = conn.prepare_cached(&format!(
479            "SELECT __corro_rowid, {} FROM query",
480            query_cols.join(",")
481        ))?;
482
483        let col_count = prepped.column_count();
484
485        tx.blocking_send(QueryEvent::Columns(self.col_names().to_vec()))
486            .map_err(|_| MatcherError::EventReceiverClosed)?;
487
488        let start = Instant::now();
489        let mut rows = prepped.query([])?;
490        let elapsed = start.elapsed();
491
492        let mut count = 0;
493
494        loop {
495            let row = match rows.next()? {
496                Some(row) => row,
497                None => break,
498            };
499
500            tx.blocking_send(QueryEvent::Row(
501                row.get(0)?,
502                (1..col_count)
503                    .map(|i| row.get::<_, SqliteValue>(i))
504                    .collect::<rusqlite::Result<Vec<_>>>()?,
505            ))
506            .map_err(|_| MatcherError::EventReceiverClosed)?;
507            count += 1;
508        }
509
510        trace!("sent {count} rows");
511
512        let max_change_id = conn
513            .prepare("SELECT COALESCE(MAX(id),0) FROM changes")?
514            .query_row([], |row| row.get(0))?;
515
516        tx.blocking_send(QueryEvent::EndOfQuery {
517            time: elapsed.as_secs_f64(),
518            change_id: Some(max_change_id),
519        })
520        .map_err(|_| MatcherError::EventReceiverClosed)?;
521
522        Ok(max_change_id)
523    }
524}
525
526type StateLock = Arc<(Mutex<MatcherState>, Condvar)>;
527
528pub struct Matcher {
529    pub id: Uuid,
530    pub hash: String,
531    pub query: Stmt,
532    pub cached_statements: HashMap<String, MatcherStmt>,
533    pub pks: IndexMap<String, Vec<String>>,
534    pub parsed: ParsedSelect,
535    pub evt_tx: mpsc::Sender<QueryEvent>,
536    pub col_names: Vec<ColumnName>,
537    pub last_rowid: u64,
538    conn: Connection,
539    base_path: Utf8PathBuf,
540    cancel: CancellationToken,
541    state: StateLock,
542    last_change_tx: watch::Sender<ChangeId>,
543    changes_rx: mpsc::Receiver<MatchCandidates>,
544}
545
546#[derive(Debug, Clone)]
547pub struct MatcherStmt {
548    new_query: String,
549    temp_query: String,
550}
551
552impl MatcherStmt {
553    pub fn new_query(&self) -> &String {
554        &self.new_query
555    }
556}
557
558const CHANGE_ID_COL: &str = "id";
559const CHANGE_TYPE_COL: &str = "type";
560
561pub const QUERY_TABLE_NAME: &str = "query";
562
563pub const SUB_DB_PATH: &str = "sub.sqlite";
564
565impl Matcher {
566    fn new(
567        id: Uuid,
568        subs_path: Utf8PathBuf,
569        schema: &Schema,
570        state_conn: &Connection,
571        evt_tx: mpsc::Sender<QueryEvent>,
572        sql: &str,
573    ) -> Result<(Matcher, MatcherHandle), MatcherError> {
574        let sub_path = Self::sub_path(subs_path.as_path(), id);
575        let sql_hash = hex::encode(seahash::hash(sql.as_bytes()).to_be_bytes());
576
577        info!(%sql_hash, sub_id = %id, "Initializing subscription at {sub_path}");
578
579        std::fs::create_dir_all(&sub_path)?;
580
581        let sub_db_path = sub_path.join(SUB_DB_PATH);
582
583        let col_names: Vec<ColumnName> = {
584            state_conn
585                .prepare(sql)?
586                .column_names()
587                .into_iter()
588                .map(|s| ColumnName(s.to_compact_string()))
589                .collect()
590        };
591
592        let conn = Connection::open(&sub_db_path)?;
593        conn.execute_batch(
594            r#"
595                PRAGMA journal_mode = WAL;
596                PRAGMA synchronous = NORMAL;
597                PRAGMA temp_store = memory;
598                PRAGMA cache_size = -32000; -- 32MB
599                PRAGMA mmap_size = 536870912; -- 512MB
600            "#,
601        )?;
602
603        let mut parser = Parser::new(sql.as_bytes());
604
605        let (mut stmt, parsed) = match parser.next()?.ok_or(MatcherError::StatementRequired)? {
606            Cmd::Stmt(stmt) => {
607                let parsed = match stmt {
608                    Stmt::Select(ref select) => extract_select_columns(select, schema)?,
609                    _ => return Err(MatcherError::UnsupportedStatement),
610                };
611
612                (stmt, parsed)
613            }
614            _ => return Err(MatcherError::StatementRequired),
615        };
616
617        if parsed.table_columns.is_empty() {
618            return Err(MatcherError::TableRequired);
619        }
620
621        let mut statements = HashMap::new();
622
623        let mut pks = IndexMap::default();
624
625        match &mut stmt {
626            Stmt::Select(select) => match &mut select.body.select {
627                OneSelect::Select { columns, .. } => {
628                    let mut new_cols = parsed
629                        .table_columns
630                        .iter()
631                        .filter_map(|(tbl_name, _cols)| {
632                            schema.tables.get(tbl_name).map(|table| {
633                                let tbl_name = parsed
634                                    .aliases
635                                    .iter()
636                                    .find_map(|(alias, actual)| {
637                                        (actual == tbl_name).then_some(alias)
638                                    })
639                                    .unwrap_or(tbl_name);
640                                table
641                                    .pk
642                                    .iter()
643                                    .map(|pk| {
644                                        let alias = format!("__corro_pk_{tbl_name}_{pk}");
645                                        let entry: &mut Vec<String> =
646                                            pks.entry(table.name.clone()).or_default();
647                                        entry.push(alias.clone());
648
649                                        ResultColumn::Expr(
650                                            Expr::Qualified(
651                                                Name(tbl_name.clone().into()),
652                                                Name(pk.clone().into()),
653                                            ),
654                                            Some(As::As(Name(alias.into()))),
655                                        )
656                                    })
657                                    .collect::<Vec<_>>()
658                            })
659                        })
660                        .flatten()
661                        .collect::<Vec<_>>();
662
663                    new_cols.append(&mut parsed.columns.clone());
664                    *columns = new_cols;
665                }
666                _ => unreachable!(),
667            },
668            _ => unreachable!(),
669        }
670
671        for (idx, (tbl_name, _cols)) in parsed.table_columns.iter().enumerate() {
672            let expr = table_to_expr(
673                &parsed.aliases,
674                schema
675                    .tables
676                    .get(tbl_name)
677                    .expect("this should not happen, missing table in schema"),
678                tbl_name,
679            )?;
680
681            let mut stmt = stmt.clone();
682
683            if let Stmt::Select(select) = &mut stmt
684                && let OneSelect::Select {
685                    where_clause, from, ..
686                } = &mut select.body.select
687            {
688                *where_clause = if let Some(prev) = where_clause.take() {
689                    Some(Box::new(Expr::Binary(
690                        Box::new(expr),
691                        Operator::And,
692                        Box::new(Expr::parenthesized(*prev)),
693                    )))
694                } else {
695                    Some(Box::new(expr))
696                };
697
698                match from {
699                    Some(FromClause {
700                        joins: Some(joins), ..
701                    }) if idx > 0 => {
702                        // Replace LEFT JOIN with INNER join if the target is the joined table
703                        if let Some(joined_table) = joins.get_mut(idx - 1)
704                            && let JoinOperator::TypedJoin(Some(join_type)) = &joined_table.operator
705                            && join_type.contains(JoinType::LEFT)
706                        {
707                            joined_table.operator = JoinOperator::TypedJoin(Some(JoinType::INNER));
708                        };
709
710                        // Remove all custom INDEXED BY clauses for the table as the most efficient
711                        // way is to query it by the primary keys
712                        if let Some(JoinedSelectTable {
713                            table: SelectTable::Table(_, _, indexed @ Some(_)),
714                            ..
715                        }) = joins.get_mut(idx - 1)
716                        {
717                            *indexed = None
718                        };
719                    }
720                    _ => (),
721                };
722            }
723
724            let mut new_query = Cmd::Stmt(stmt).to_string();
725            new_query.pop();
726
727            let mut all_cols = pks.values().flatten().cloned().collect::<Vec<String>>();
728            for i in 0..(parsed.columns.len()) {
729                all_cols.push(format!("col_{i}"));
730            }
731
732            let temp_query = format!(
733                "SELECT {} FROM query WHERE ({}) IN temp_{tbl_name}",
734                all_cols.join(","),
735                pks.get(tbl_name)
736                    .cloned()
737                    .ok_or(MatcherError::MissingPrimaryKeys)?
738                    .into_iter()
739                    .map(|pk| format!("coalesce({pk}, \"\")"))
740                    .collect::<Vec<_>>()
741                    .join(","),
742            );
743
744            info!(%sql_hash, sub_id = %id, "modified query for table '{tbl_name}': {new_query}");
745
746            statements.insert(
747                tbl_name.clone(),
748                MatcherStmt {
749                    new_query,
750                    temp_query,
751                },
752            );
753        }
754
755        let cancel = CancellationToken::new();
756
757        let state = Arc::new((Mutex::new(MatcherState::Created), Condvar::new()));
758
759        let (last_change_tx, last_change_rx) = watch::channel(ChangeId(0));
760
761        // big channel to not miss anything
762        let (changes_tx, changes_rx) = mpsc::channel(20480);
763
764        // metrics counters
765        let mut counter_map = HashMap::new();
766        for table in parsed.table_columns.keys() {
767            counter_map.insert(table.clone(), HandleMetrics{
768                matched_count: counter!("corro.subs.changes.matched.count", "sql_hash" => sql_hash.clone(), "table" => table.to_string()),
769            });
770        }
771
772        let handle = MatcherHandle {
773            inner: Arc::new(InnerMatcherHandle {
774                id,
775                sql: sql.to_owned(),
776                hash: sql_hash.clone(),
777                pool: sqlite_pool::Config::new(sub_db_path.into_std_path_buf())
778                    .max_size(5)
779                    .read_only()
780                    .create_pool()
781                    .expect("could not build pool, this can't fail because we specified a runtime"),
782                parsed: parsed.clone(),
783                col_names: col_names.clone(),
784                cancel: cancel.clone(),
785                last_change_rx,
786                changes_tx,
787                cached_statements: statements.clone(),
788                subs_path: sub_path.to_string(),
789                metrics: counter_map,
790            }),
791            state: state.clone(),
792        };
793
794        let matcher = Self {
795            id,
796            hash: sql_hash,
797            query: stmt,
798            cached_statements: statements,
799            pks,
800            parsed,
801            evt_tx,
802            col_names,
803            last_rowid: 0,
804            conn,
805            base_path: sub_path,
806            cancel,
807            state,
808            last_change_tx,
809            changes_rx,
810        };
811
812        Ok((matcher, handle))
813    }
814
815    pub fn cleanup(id: Uuid, sub_path: Utf8PathBuf) -> rusqlite::Result<()> {
816        info!(sub_id = %id, "Attempting to cleanup... {}", sub_path);
817
818        block_in_place(|| {
819            if let Err(e) = std::fs::remove_dir_all(&sub_path) {
820                error!(sub_id = %id, "could not delete subscription base path {} due to: {e}", sub_path);
821            }
822
823            Ok(())
824        })
825    }
826
827    pub fn sub_path(subs_path: &Utf8Path, id: Uuid) -> Utf8PathBuf {
828        subs_path.join(id.as_simple().to_string())
829    }
830
831    pub fn sub_db_path(subs_path: &Utf8Path, id: Uuid) -> Utf8PathBuf {
832        Self::sub_path(subs_path, id).join(SUB_DB_PATH)
833    }
834
835    #[allow(clippy::too_many_arguments)]
836    pub fn restore(
837        id: Uuid,
838        subs_path: Utf8PathBuf,
839        schema: &Schema,
840        state_conn: CrConn,
841        evt_tx: mpsc::Sender<QueryEvent>,
842        tripwire: Tripwire,
843    ) -> Result<MatcherHandle, MatcherError> {
844        let sql: String = block_in_place(|| {
845            let conn = Connection::open(Matcher::sub_db_path(&subs_path, id))?;
846            let state: Option<String> = conn
847                .query_row("SELECT value FROM meta WHERE key = 'state'", [], |row| {
848                    row.get(0)
849                })
850                .optional()?;
851            if !matches!(state.as_deref(), Some("completed")) {
852                return Err(MatcherError::NotRunning);
853            }
854
855            let sql: Option<String> = conn
856                .query_row("SELECT value FROM meta WHERE key = 'sql'", [], |row| {
857                    row.get(0)
858                })
859                .optional()?;
860
861            match sql {
862                Some(sql) => Ok(sql),
863                None => Err(MatcherError::MissingSql),
864            }
865        })?;
866
867        let (matcher, handle) = Self::new(id, subs_path, schema, &state_conn, evt_tx, &sql)?;
868
869        spawn_counted(matcher.run_restore(state_conn, tripwire));
870
871        Ok(handle)
872    }
873
874    #[allow(clippy::too_many_arguments)]
875    pub fn create(
876        id: Uuid,
877        subs_path: Utf8PathBuf,
878        schema: &Schema,
879        state_conn: CrConn,
880        evt_tx: mpsc::Sender<QueryEvent>,
881        sql: &str,
882        tripwire: Tripwire,
883    ) -> Result<MatcherHandle, MatcherError> {
884        let (mut matcher, handle) = Self::new(id, subs_path, schema, &state_conn, evt_tx, sql)?;
885
886        let pk_cols = matcher
887            .pks
888            .values()
889            .flatten()
890            .cloned()
891            .collect::<Vec<String>>();
892
893        let mut all_cols = pk_cols.clone();
894
895        let mut query_cols = vec![];
896
897        for i in 0..(matcher.parsed.columns.len()) {
898            let col_name = format!("col_{i}");
899            all_cols.push(col_name.clone());
900            query_cols.push(col_name);
901        }
902
903        block_in_place(|| {
904            let tx = matcher.conn.transaction()?;
905
906            info!(sub_id = %id, "Creating subscription database schema");
907            let create_temp_table = format!(
908                r#"
909                CREATE TABLE query (__corro_rowid INTEGER PRIMARY KEY AUTOINCREMENT, {columns});
910
911                CREATE UNIQUE INDEX index_{id}_pk ON query ({pks_coalesced});
912
913                CREATE TABLE changes (
914                    {CHANGE_ID_COL} INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
915                    __corro_rowid INTEGER NOT NULL,
916                    {CHANGE_TYPE_COL} INTEGER NOT NULL,
917                    {actual_columns}
918                );
919
920                CREATE TABLE meta (
921                    key TEXT PRIMARY KEY NOT NULL,
922                    value
923                ) WITHOUT ROWID;
924
925                CREATE TABLE columns (
926                    "table" TEXT NOT NULL,
927                    cid TEXT,
928
929                    PRIMARY KEY ("table", cid)
930                );
931            "#,
932                columns = all_cols.join(","),
933                id = id.as_simple(),
934                pks_coalesced = pk_cols
935                    .iter()
936                    .map(|pk| format!("coalesce({pk}, \"\")"))
937                    .collect::<Vec<_>>()
938                    .join(","),
939                actual_columns = query_cols.join(","),
940            );
941
942            tx.execute_batch(&create_temp_table)?;
943            trace!("created sub tables");
944
945            for (table, pks) in matcher.pks.iter() {
946                tx.execute(
947                    &format!(
948                        "CREATE INDEX index_{id}_{table}_pk ON query ({pks})",
949                        id = id.as_simple(),
950                        table = table,
951                        pks = pks.to_vec().join(","),
952                    ),
953                    [],
954                )?;
955            }
956            trace!("created query indexes");
957
958            for (table, columns) in matcher.parsed.table_columns.iter() {
959                tx.execute(
960                    r#"INSERT INTO columns ("table", cid) VALUES (?, '-1')"#,
961                    [table.as_str()],
962                )?;
963                for column in columns.iter() {
964                    trace!("inserting sub column {} => {}", table, column);
965                    tx.execute(
966                        r#"INSERT INTO columns ("table", cid) VALUES (?, ?)"#,
967                        [table.as_str(), column.as_str()],
968                    )?;
969                }
970            }
971            trace!("inserted sub columns");
972
973            tx.execute("INSERT INTO meta (key, value) VALUES ('sql', ?)", [sql])?;
974            tx.execute(
975                "INSERT INTO meta (key, value) VALUES ('state', 'created')",
976                [],
977            )?;
978
979            tx.commit()?;
980            trace!("committed subscription");
981
982            Ok::<_, MatcherError>(())
983        })?;
984
985        spawn_counted(matcher.run(state_conn, tripwire));
986
987        Ok(handle)
988    }
989
990    async fn run_restore(mut self, mut state_conn: CrConn, tripwire: Tripwire) {
991        info!(sub_id = %self.id, "Restoring subscription");
992        let init_res = block_in_place(|| {
993            self.last_rowid = self
994                .conn
995                .query_row(
996                    "SELECT COALESCE(MAX(__corro_rowid), 0) FROM query",
997                    (),
998                    |row| row.get(0),
999                )
1000                .optional()?
1001                .unwrap_or_default();
1002
1003            let max_change_id = self
1004                .conn
1005                .prepare_cached("SELECT COALESCE(MAX(id), 0) FROM changes")?
1006                .query_row([], |row| row.get(0))?;
1007
1008            _ = self.last_change_tx.send(max_change_id);
1009
1010            Ok::<_, MatcherError>(())
1011        });
1012
1013        if let Err(e) = init_res {
1014            error!(sub_id = %self.id, "could not re-initialize subscription: {e}");
1015            return;
1016        }
1017
1018        if let Err(e) = block_in_place(|| self.setup(&mut state_conn)) {
1019            error!(sub_id = %self.id, "could not setup connection: {e}");
1020            return;
1021        }
1022
1023        if let Err(e) = self.set_status("running") {
1024            error!(sub_id = %self.id, "could not set status: {e}");
1025            return;
1026        }
1027
1028        self.cmd_loop(state_conn, tripwire).await
1029    }
1030
1031    fn set_status(&self, status: &str) -> Result<(), MatcherError> {
1032        self.conn.execute(
1033            "INSERT OR REPLACE INTO meta (key, value) VALUES ('state', ?)",
1034            [status],
1035        )?;
1036        Ok(())
1037    }
1038
1039    fn setup(&self, state_conn: &mut Connection) -> Result<(), MatcherError> {
1040        info!(sub_id = %self.id, "Attaching __corro_sub to state db");
1041        if let Err(e) = state_conn.execute_batch(&format!(
1042            "ATTACH DATABASE {} AS __corro_sub",
1043            enquote::enquote('\'', self.base_path.join(SUB_DB_PATH).as_str()),
1044        )) {
1045            error!(sub_id = %self.id, "could not ATTACH sub db as __corro_sub on state db: {e}");
1046            _ = self.evt_tx.try_send(QueryEvent::Error(format_compact!(
1047                "could not ATTACH subscription db: {e}"
1048            )));
1049            return Err(e.into());
1050        }
1051
1052        info!(sub_id = %self.id, "Attached __corro_sub to state db");
1053
1054        for (tbl_name, pks) in &self.pks {
1055            if let Ok(plan) = dump_query_plan(
1056                state_conn,
1057                &self
1058                    .cached_statements
1059                    .get(tbl_name)
1060                    .ok_or(MatcherError::StatementRequired)?
1061                    .new_query,
1062                tbl_name,
1063                pks,
1064            ) {
1065                info!(sub_id = %self.id, sql_hash = %self.hash, "query plan for table '{tbl_name}':\n{plan}");
1066            }
1067        }
1068
1069        Ok(())
1070    }
1071
1072    async fn cmd_loop(mut self, mut state_conn: CrConn, mut tripwire: Tripwire) {
1073        const PROCESS_CHANGES_THRESHOLD: usize = 1000;
1074        const PROCESSING_WARN_THRESHOLD: Duration = Duration::from_secs(5);
1075        const PROCESS_BUFFER_DEADLINE: Duration = Duration::from_millis(600);
1076
1077        info!(sub_id = %self.id, "Starting loop to run the subscription");
1078        {
1079            let (lock, cvar) = &*self.state;
1080            let mut state = lock.lock();
1081            *state = MatcherState::Running;
1082            cvar.notify_all();
1083            info!(sub_id = %self.id, "Notified condvar that the subscription is 'running'");
1084        }
1085        trace!("set state!");
1086
1087        let mut buf = MatchCandidates::new();
1088        let mut buf_count = 0;
1089
1090        let mut purge_changes_interval = tokio::time::interval(Duration::from_secs(300));
1091
1092        // max duration of aggregating candidates
1093        let process_changes_deadline = tokio::time::sleep(PROCESS_BUFFER_DEADLINE);
1094        tokio::pin!(process_changes_deadline);
1095
1096        loop {
1097            enum Branch {
1098                NewCandidates(MatchCandidates),
1099                PurgeOldChanges,
1100            }
1101
1102            // trace!("looping...");
1103
1104            let branch = tokio::select! {
1105                biased;
1106                _ = self.cancel.cancelled() => {
1107                    info!(sub_id = %self.id, "Acknowledged subscription cancellation, breaking loop.");
1108                    if let Err(e) = self.set_status("cancelled") {
1109                        error!(sub_id = %self.id, "could not set status during cancellation: {e}");
1110                    }
1111                    break;
1112                }
1113                Some(candidates) = self.changes_rx.recv() => {
1114                    for (table, pks) in  candidates {
1115                        let buffed = buf.entry(table).or_default();
1116                        for (pk, cl) in pks {
1117                            if buffed.insert(pk, cl).is_none() {
1118                                buf_count += 1;
1119                            }
1120                        }
1121                    }
1122
1123                    if buf_count >= PROCESS_CHANGES_THRESHOLD {
1124                        buf_count = 0;
1125                        Branch::NewCandidates(std::mem::take(&mut buf))
1126                    } else {
1127                        continue;
1128                    }
1129                },
1130                _ = process_changes_deadline.as_mut() => {
1131                    process_changes_deadline
1132                        .as_mut()
1133                        .reset((Instant::now() + PROCESS_BUFFER_DEADLINE).into());
1134                    if buf_count == 0 {
1135                        continue;
1136                    }
1137                    Branch::NewCandidates(std::mem::take(&mut buf))
1138                },
1139                _ = &mut tripwire => {
1140                    info!(sub_id = %self.id, "tripped cmd_loop, returning");
1141                    // just return!
1142                    break;
1143                }
1144                _ = purge_changes_interval.tick() => Branch::PurgeOldChanges,
1145                else => {
1146                    return;
1147                }
1148            };
1149
1150            match branch {
1151                Branch::NewCandidates(candidates) => {
1152                    let start = Instant::now();
1153                    if let Err(e) = block_in_place(|| {
1154                        self.handle_candidates(&mut state_conn, candidates, false)
1155                    }) {
1156                        if !matches!(e, MatcherError::EventReceiverClosed) {
1157                            error!(sub_id = %self.id, "could not handle change: {e}");
1158                        }
1159                        info!(sub_id = %self.id, "attempting to cleanup");
1160                        if let Err(e) = Self::cleanup(self.id, self.base_path.clone()) {
1161                            error!("could not handle cleanup: {e}");
1162                        }
1163                        return;
1164                    }
1165                    let elapsed = start.elapsed();
1166
1167                    histogram!("corro.subs.changes.processing.duration.seconds", "sql_hash" => self.hash.clone()).record(elapsed);
1168
1169                    if elapsed >= PROCESSING_WARN_THRESHOLD {
1170                        warn!(sub_id = %self.id, "processed {buf_count} changes (very slowly) for subscription in {elapsed:?}");
1171                    } else {
1172                        debug!(sub_id = %self.id, "processed {buf_count} changes for subscription in {elapsed:?}");
1173                    }
1174                    buf_count = 0;
1175
1176                    // reset the deadline
1177                    process_changes_deadline
1178                        .as_mut()
1179                        .reset((Instant::now() + PROCESS_BUFFER_DEADLINE).into());
1180                }
1181                Branch::PurgeOldChanges => {
1182                    let start = Instant::now();
1183                    let res = block_in_place(|| {
1184                        let tx = self.conn.transaction()?;
1185
1186                        let deleted = tx
1187                            .prepare_cached(
1188                                "DELETE FROM changes WHERE id < (SELECT COALESCE(MAX(id),0) - 500 FROM changes)"
1189                            )?
1190                            .execute([])?;
1191
1192                        tx.commit().map(|_| deleted)
1193                    });
1194
1195                    match res {
1196                        Ok(deleted) => {
1197                            info!(sub_id = %self.id, "Deleted {deleted} old changes row in {:?}", start.elapsed());
1198                        }
1199                        Err(e) => {
1200                            error!(sub_id = %self.id, "could not delete old changes: {e}");
1201                        }
1202                    }
1203                }
1204            }
1205        }
1206
1207        info!(sub_id = %self.id, "draining changes channel");
1208        while let Some(candidates) = self.changes_rx.recv().await {
1209            for (table, pks) in candidates {
1210                let buffed = buf.entry(table).or_default();
1211                for (pk, cl) in pks {
1212                    buffed.insert(pk, cl);
1213                }
1214            }
1215        }
1216
1217        if !buf.is_empty() {
1218            info!(sub_id = %self.id, "handling buffered candidates");
1219            let start = Instant::now();
1220            if let Err(e) = block_in_place(|| self.handle_candidates(&mut state_conn, buf, true)) {
1221                error!(sub_id = %self.id, "could not handle final buffered candidates: {e}");
1222                if let Err(e) = Self::cleanup(self.id, self.base_path.clone()) {
1223                    error!(sub_id = %self.id, "could not handle cleanup: {e}");
1224                }
1225                return;
1226            }
1227            let elapsed = start.elapsed();
1228            info!(sub_id = %self.id, "handled final buffered candidates in {elapsed:?}");
1229        }
1230
1231        if let Err(e) = self.set_status("completed") {
1232            error!(sub_id = %self.id, "could not set status: {e}");
1233        };
1234
1235        info!(sub_id = %self.id, "matcher loop is done");
1236    }
1237
1238    async fn run(mut self, mut state_conn: CrConn, tripwire: Tripwire) {
1239        info!(sub_id = %self.id, "Running initial query");
1240        if let Err(e) = self
1241            .evt_tx
1242            .send(QueryEvent::Columns(self.col_names.clone()))
1243            .await
1244        {
1245            error!(sub_id = %self.id, "could not send back columns, probably means no receivers! {e}");
1246            return;
1247        }
1248
1249        let mut query_cols = vec![];
1250        for i in 0..(self.parsed.columns.len()) {
1251            query_cols.push(format!("col_{i}"));
1252        }
1253
1254        let res = block_in_place(|| {
1255            let tx = self.conn.transaction()?;
1256
1257            let mut stmt_str = Cmd::Stmt(self.query.clone()).to_string();
1258            stmt_str.pop(); // remove trailing `;`
1259
1260            let mut all_cols = self
1261                .pks
1262                .values()
1263                .flatten()
1264                .cloned()
1265                .collect::<Vec<String>>();
1266
1267            for i in 0..(self.parsed.columns.len()) {
1268                let col_name = format!("col_{i}");
1269                all_cols.push(col_name.clone());
1270            }
1271
1272            let mut last_rowid = 0;
1273
1274            // ensure drop and recreate
1275            tx.execute_batch(&format!(
1276                "DROP TABLE IF EXISTS state_rows;
1277                    CREATE TEMP TABLE state_rows ({})",
1278                all_cols.join(",")
1279            ))?;
1280
1281            info!(sub_id = %self.id, "Starting state conn read transaction for initial query");
1282            // this is read transaction up until the end!
1283            let state_tx = state_conn.transaction()?;
1284
1285            let elapsed = {
1286                debug!("select stmt: {stmt_str:?}");
1287
1288                let mut select = state_tx.prepare(&stmt_str)?;
1289                let start = Instant::now();
1290                let mut select_rows = {
1291                    let _guard = interrupt_deadline_guard(&state_tx, Duration::from_secs(15));
1292                    select.query(())?
1293                };
1294                let elapsed = start.elapsed();
1295                info!(sub_id = %self.id, "Initial query done in {elapsed:?}");
1296
1297                let insert_into = format!(
1298                    "INSERT INTO query ({}) VALUES ({}) RETURNING __corro_rowid,{}",
1299                    all_cols.join(","),
1300                    all_cols
1301                        .iter()
1302                        .map(|_| "?".to_string())
1303                        .collect::<Vec<_>>()
1304                        .join(","),
1305                    query_cols.join(","),
1306                );
1307                trace!("insert stmt: {insert_into:?}");
1308
1309                {
1310                    let mut insert = tx.prepare(&insert_into)?;
1311
1312                    loop {
1313                        match select_rows.next() {
1314                            Ok(Some(row)) => {
1315                                for i in 0..all_cols.len() {
1316                                    insert.raw_bind_parameter(
1317                                        i + 1,
1318                                        SqliteValueRef::from(row.get_ref(i)?),
1319                                    )?;
1320                                }
1321
1322                                let mut rows = insert.raw_query();
1323
1324                                let row = match rows.next()? {
1325                                    Some(row) => row,
1326                                    None => continue,
1327                                };
1328
1329                                let rowid = row.get(0)?;
1330                                let cells = (1..=query_cols.len())
1331                                    .map(|i| row.get::<_, SqliteValue>(i))
1332                                    .collect::<rusqlite::Result<Vec<_>>>()?;
1333
1334                                if let Err(e) = self
1335                                    .evt_tx
1336                                    .blocking_send(QueryEvent::Row(RowId(rowid), cells))
1337                                {
1338                                    error!(sub_id = %self.id, "could not send back row: {e}");
1339                                    return Err(MatcherError::EventReceiverClosed);
1340                                }
1341
1342                                last_rowid = cmp::max(rowid, last_rowid);
1343                            }
1344                            Ok(None) => {
1345                                info!(sub_id = %self.id, "Done iterating through rows for initial query");
1346                                // done!
1347                                break;
1348                            }
1349                            Err(e) => {
1350                                return Err(e.into());
1351                            }
1352                        }
1353                    }
1354                }
1355
1356                tx.execute_batch("DROP TABLE IF EXISTS state_rows;")?;
1357
1358                tx.execute(
1359                    "INSERT OR REPLACE INTO meta (key, value) VALUES ('state', 'running')",
1360                    [],
1361                )?;
1362
1363                tx.commit()?;
1364
1365                elapsed
1366            };
1367
1368            self.last_rowid = last_rowid;
1369
1370            Ok::<_, MatcherError>(elapsed)
1371        });
1372
1373        match res {
1374            Ok(elapsed) => {
1375                trace!(
1376                    "done w/ block_in_place for initial query, elapsed: {:?}",
1377                    elapsed
1378                );
1379                if let Err(e) = self
1380                    .evt_tx
1381                    .send(QueryEvent::EndOfQuery {
1382                        time: elapsed.as_secs_f64(),
1383                        change_id: Some(ChangeId(0)),
1384                    })
1385                    .await
1386                {
1387                    error!(sub_id = %self.id, "could not return end of query event: {e}");
1388                    return;
1389                }
1390                // db_version
1391            }
1392            Err(e) => {
1393                warn!(sub_id = %self.id, "could not complete initial query: {e}");
1394                _ = self
1395                    .evt_tx
1396                    .send(QueryEvent::Error(e.to_compact_string()))
1397                    .await;
1398
1399                return;
1400            }
1401        };
1402
1403        if let Err(e) = block_in_place(|| self.setup(&mut state_conn)) {
1404            error!(sub_id = %self.id, "could not setup connection: {e}");
1405            return;
1406        }
1407
1408        self.cmd_loop(state_conn, tripwire).await
1409    }
1410
1411    fn handle_candidates(
1412        &mut self,
1413        state_conn: &mut Connection,
1414        candidates: MatchCandidates,
1415        skip_send: bool,
1416    ) -> Result<(), MatcherError> {
1417        let mut tables = IndexSet::new();
1418
1419        if candidates.is_empty() {
1420            return Ok(());
1421        }
1422
1423        trace!(
1424            "got some candidates! {:?}",
1425            candidates.keys().collect::<Vec<_>>()
1426        );
1427
1428        let tx = self.conn.transaction()?;
1429        for (table, pks) in candidates {
1430            let pks = pks
1431                .iter()
1432                .map(|(pk, _)| unpack_columns(pk))
1433                .collect::<Result<Vec<Vec<SqliteValueRef>>, _>>()?;
1434
1435            let tmp_table_name = format!("temp_{table}");
1436            if tables.insert(table.clone()) {
1437                // create a temporary table to mix and match the data
1438                tx.prepare_cached(
1439                    // TODO: cache the statement's string somewhere, it's always the same!
1440                    // NOTE: this can't be an actual CREATE TEMP TABLE or it won't be visible
1441                    //       from the state db
1442                    &format!(
1443                        "CREATE TABLE IF NOT EXISTS {tmp_table_name} ({})",
1444                        self.pks
1445                            .get(table.as_str())
1446                            .ok_or_else(|| MatcherError::MissingPrimaryKeys)?
1447                            .to_vec()
1448                            .join(",")
1449                    ),
1450                )?
1451                .execute(())?;
1452            }
1453
1454            for pks in pks {
1455                tx.prepare_cached(&format!(
1456                    "INSERT INTO {tmp_table_name} VALUES ({})",
1457                    (0..pks.len())
1458                        .map(|_i| "coalesce(?, \"\")")
1459                        .collect::<Vec<_>>()
1460                        .join(",")
1461                ))?
1462                .execute(params_from_iter(pks))?;
1463            }
1464        }
1465        // commit so it is visible to the other db!
1466        tx.commit()?;
1467        trace!("committed temp pk tables");
1468
1469        let mut query_cols = vec![];
1470
1471        let pk_cols = self
1472            .pks
1473            .values()
1474            .flatten()
1475            .cloned()
1476            .collect::<Vec<String>>();
1477
1478        let mut all_cols = pk_cols.clone();
1479        for i in 0..(self.parsed.columns.len()) {
1480            let col_name = format!("col_{i}");
1481            all_cols.push(col_name.clone());
1482            query_cols.push(col_name);
1483        }
1484
1485        // start a new tx
1486        let tx = self.conn.transaction()?;
1487
1488        // ensure drop and recreate
1489        tx.execute_batch(&format!(
1490            "CREATE TEMP TABLE IF NOT EXISTS state_results ({})",
1491            all_cols.join(",")
1492        ))?;
1493
1494        let mut new_last_rowid = self.last_rowid;
1495
1496        {
1497            // read-only!
1498            let state_tx = state_conn.transaction()?;
1499            let mut tmp_insert_prepped = tx.prepare_cached(&format!(
1500                "INSERT INTO state_results VALUES ({})",
1501                (0..all_cols.len())
1502                    .map(|_| "?")
1503                    .collect::<Vec<_>>()
1504                    .join(",")
1505            ))?;
1506
1507            for table in tables.iter() {
1508                let start = Instant::now();
1509                let stmt = match self.cached_statements.get(table.as_str()) {
1510                    Some(stmt) => stmt,
1511                    None => {
1512                        warn!(sub_id = %self.id, "no statements pre-computed for table {table}");
1513                        continue;
1514                    }
1515                };
1516
1517                trace!("SELECT SQL: {}", stmt.new_query);
1518
1519                let mut prepped = state_tx.prepare_cached(&stmt.new_query)?;
1520
1521                let col_count = prepped.column_count();
1522
1523                let mut rows = prepped.raw_query();
1524
1525                // insert each row in the other DB...
1526                while let Some(row) = rows.next()? {
1527                    for i in 0..col_count {
1528                        tmp_insert_prepped
1529                            .raw_bind_parameter(i + 1, SqliteValueRef::from(row.get_ref(i)?))?;
1530                    }
1531                    tmp_insert_prepped.raw_execute()?;
1532                }
1533
1534                let coalesced_pks = pk_cols
1535                    .iter()
1536                    .map(|pk| format!("coalesce({pk},\"\")"))
1537                    .collect::<Vec<_>>()
1538                    .join(",");
1539                let sql = format!(
1540                    "INSERT INTO query ({insert_cols})
1541                        SELECT * FROM (
1542                            SELECT * FROM state_results
1543                            EXCEPT
1544                            {query_query}
1545                        ) WHERE 1
1546                        ON CONFLICT({conflict_clause})
1547                            DO UPDATE SET
1548                                {excluded}
1549                            WHERE {excluded_not_same}
1550                        RETURNING __corro_rowid,{return_cols}",
1551                    // insert into
1552                    insert_cols = all_cols.join(","),
1553                    query_query = stmt.temp_query,
1554                    conflict_clause = coalesced_pks,
1555                    excluded = (0..(self.parsed.columns.len()))
1556                        .map(|i| format!("col_{i} = excluded.col_{i}"))
1557                        .collect::<Vec<_>>()
1558                        .join(","),
1559                    excluded_not_same = (0..(self.parsed.columns.len()))
1560                        .map(|i| format!("col_{i} IS NOT excluded.col_{i}"))
1561                        .collect::<Vec<_>>()
1562                        .join(" OR "),
1563                    return_cols = query_cols.join(",")
1564                );
1565
1566                trace!("INSERT SQL: {sql}");
1567
1568                let insert_prepped = tx.prepare_cached(&sql)?;
1569
1570                let sql = format!(
1571                    "
1572                    DELETE FROM query WHERE ({pks}) IN (SELECT {select_pks} FROM (
1573                        {query_query}
1574                        EXCEPT
1575                        SELECT * FROM state_results
1576                    )) RETURNING __corro_rowid,{return_cols}
1577                ",
1578                    // delete from
1579                    pks = coalesced_pks,
1580                    select_pks = coalesced_pks,
1581                    query_query = stmt.temp_query,
1582                    return_cols = query_cols.join(",")
1583                );
1584
1585                trace!("DELETE SQL: {sql}");
1586
1587                let delete_prepped = tx.prepare_cached(&sql)?;
1588
1589                let mut change_insert_stmt = tx.prepare_cached(&format!(
1590                    "INSERT INTO changes (__corro_rowid, {CHANGE_TYPE_COL}, {}) VALUES (?, ?, {}) RETURNING {CHANGE_ID_COL}",
1591                    query_cols.join(","),
1592                    (0..query_cols.len())
1593                        .map(|_i| "?")
1594                        .collect::<Vec<_>>()
1595                        .join(",")
1596                ))?;
1597
1598                for (change_type, mut prepped) in [
1599                    (None, insert_prepped),
1600                    (Some(ChangeType::Delete), delete_prepped),
1601                ] {
1602                    let col_count = prepped.column_count();
1603
1604                    let mut rows = prepped.raw_query();
1605
1606                    while let Ok(Some(row)) = rows.next() {
1607                        let rowid: RowId = row.get(0)?;
1608
1609                        let change_type = change_type.unwrap_or({
1610                            if rowid.0 > self.last_rowid {
1611                                ChangeType::Insert
1612                            } else {
1613                                ChangeType::Update
1614                            }
1615                        });
1616
1617                        new_last_rowid = cmp::max(new_last_rowid, rowid.0);
1618
1619                        match (1..col_count)
1620                            .map(|i| row.get::<_, SqliteValue>(i))
1621                            .collect::<rusqlite::Result<Vec<_>>>()
1622                        {
1623                            Ok(cells) => {
1624                                change_insert_stmt.raw_bind_parameter(1, rowid)?;
1625                                change_insert_stmt.raw_bind_parameter(2, change_type)?;
1626                                for (i, cell) in cells.iter().enumerate() {
1627                                    // increment index by 3 because that's where we're starting...
1628                                    change_insert_stmt.raw_bind_parameter(i + 3, cell)?;
1629                                }
1630
1631                                let mut change_rows = change_insert_stmt.raw_query();
1632
1633                                let change_id: ChangeId = change_rows
1634                                    .next()?
1635                                    .ok_or(MatcherError::NoChangeInserted)?
1636                                    .get(0)?;
1637
1638                                trace!("got change id: {change_id}");
1639
1640                                if !skip_send
1641                                    && let Err(e) = self.evt_tx.blocking_send(QueryEvent::Change(
1642                                        change_type,
1643                                        rowid,
1644                                        cells,
1645                                        change_id,
1646                                    ))
1647                                {
1648                                    warn!("could not send back row to matcher sub sender: {e}");
1649                                    return Err(MatcherError::EventReceiverClosed);
1650                                }
1651                                _ = self.last_change_tx.send(change_id);
1652                            }
1653                            Err(e) => {
1654                                error!("could not deserialize row's cells: {e}");
1655                                return Ok(());
1656                            }
1657                        }
1658                    }
1659                }
1660                // clean that up
1661                tx.execute_batch("DELETE FROM state_results")?;
1662
1663                let elapsed = start.elapsed();
1664                histogram!("corro.subs.changes.processing.table.duration.seconds", "sql_hash" => self.hash.clone(), "table" => table.0.to_string()).record(elapsed);
1665            }
1666
1667            // clean up temporary tables immediately
1668            for table in tables {
1669                // TODO: reduce mistakes by computing this table name once
1670                tx.prepare_cached(&format!("DELETE FROM temp_{table}",))?
1671                    .execute(())?;
1672                trace!("cleaned up temp_{table}");
1673            }
1674        }
1675
1676        tx.commit()?;
1677
1678        trace!("committed!");
1679
1680        self.last_rowid = new_last_rowid;
1681
1682        Ok(())
1683    }
1684}
1685
1686fn dump_query_plan(
1687    conn: &mut Connection,
1688    query: &str,
1689    table_name: &str,
1690    pks: &[String],
1691) -> Result<String, MatcherError> {
1692    let tx = conn.transaction()?;
1693
1694    tx.prepare_cached(&format!(
1695        "CREATE TABLE IF NOT EXISTS __corro_sub.temp_{table_name} ({})",
1696        pks.join(",")
1697    ))?
1698    .execute(())?;
1699
1700    let mut prepped = tx.prepare(&format!("EXPLAIN QUERY PLAN {query}"))?;
1701    let mut rows = prepped.query(())?;
1702
1703    let mut output = String::new();
1704    while let Some(row) = rows.next()? {
1705        output.push_str(&row.get::<_, String>("detail")?);
1706        output.push('\n');
1707    }
1708
1709    Ok(output)
1710}
1711
1712fn interrupt_deadline_guard(conn: &Connection, dur: Duration) -> DropGuard {
1713    let int_handle = conn.get_interrupt_handle();
1714    let cancel = CancellationToken::new();
1715    tokio::spawn({
1716        let cancel = cancel.clone();
1717        async move {
1718            match tokio::time::sleep(dur)
1719                .preemptible(cancel.cancelled())
1720                .await
1721            {
1722                Outcome::Completed(_) => {
1723                    warn!("subscription query deadline reached, interrupting!");
1724                    int_handle.interrupt();
1725                    // no need to send any query event, it should bubble up properly and if not
1726                    // then it means the conn was not interrupted in time which is also fine
1727                }
1728                Outcome::Preempted(_) => {
1729                    debug!("deadline was canceled, not interrupting query");
1730                }
1731            }
1732        }
1733    });
1734    cancel.drop_guard()
1735}
1736
1737#[derive(Debug, Default, Clone)]
1738pub struct ParsedSelect {
1739    table_columns: IndexMap<String, HashSet<String>>,
1740    aliases: HashMap<String, String>,
1741    pub columns: Vec<ResultColumn>,
1742    children: Vec<ParsedSelect>,
1743}
1744
1745fn extract_select_columns(select: &Select, schema: &Schema) -> Result<ParsedSelect, MatcherError> {
1746    let mut parsed = ParsedSelect::default();
1747
1748    if let OneSelect::Select {
1749        ref from,
1750        ref columns,
1751        ref where_clause,
1752        ..
1753    } = select.body.select
1754    {
1755        let from_table = match from {
1756            Some(from) => {
1757                let from_table = match &from.select {
1758                    Some(table) => match table.as_ref() {
1759                        SelectTable::Table(name, alias, _) => {
1760                            if schema.tables.contains_key(name.name.0.as_ref()) {
1761                                if let Some(As::As(alias) | As::Elided(alias)) = alias {
1762                                    parsed
1763                                        .aliases
1764                                        .insert(alias.0.to_string(), name.name.0.to_string());
1765                                } else if let Some(ref alias) = name.alias {
1766                                    parsed
1767                                        .aliases
1768                                        .insert(alias.0.to_string(), name.name.0.to_string());
1769                                }
1770                                parsed
1771                                    .table_columns
1772                                    .entry(name.name.0.to_string())
1773                                    .or_default();
1774                                Some(&name.name)
1775                            } else {
1776                                return Err(MatcherError::TableNotFound(name.name.0.to_string()));
1777                            }
1778                        }
1779                        // TODO: add support for:
1780                        // TableCall(QualifiedName, Option<Vec<Expr>>, Option<As>),
1781                        // Select(Select, Option<As>),
1782                        // Sub(FromClause, Option<As>),
1783                        t => {
1784                            warn!("ignoring {t:?}");
1785                            None
1786                        }
1787                    },
1788                    _ => {
1789                        // according to the sqlite3-parser docs, this can't really happen
1790                        // ignore!
1791                        unreachable!()
1792                    }
1793                };
1794                if let Some(ref joins) = from.joins {
1795                    for join in joins.iter() {
1796                        // let mut tbl_name = None;
1797                        let tbl_name = match &join.table {
1798                            SelectTable::Table(name, alias, _) => {
1799                                if let Some(As::As(alias) | As::Elided(alias)) = alias {
1800                                    parsed
1801                                        .aliases
1802                                        .insert(alias.0.to_string(), name.name.0.to_string());
1803                                } else if let Some(ref alias) = name.alias {
1804                                    parsed
1805                                        .aliases
1806                                        .insert(alias.0.to_string(), name.name.0.to_string());
1807                                }
1808                                parsed
1809                                    .table_columns
1810                                    .entry(name.name.0.to_string())
1811                                    .or_default();
1812                                &name.name
1813                            }
1814                            // TODO: add support for:
1815                            // TableCall(QualifiedName, Option<Vec<Expr>>, Option<As>),
1816                            // Select(Select, Option<As>),
1817                            // Sub(FromClause, Option<As>),
1818                            t => {
1819                                warn!("ignoring JOIN's non-SelectTable::Table:  {t:?}");
1820                                continue;
1821                            }
1822                        };
1823                        // ON or USING
1824                        if let Some(constraint) = &join.constraint {
1825                            match constraint {
1826                                JoinConstraint::On(expr) => {
1827                                    extract_expr_columns(expr, schema, &mut parsed)?;
1828                                }
1829                                JoinConstraint::Using(names) => {
1830                                    let entry = parsed
1831                                        .table_columns
1832                                        .entry(tbl_name.0.to_string())
1833                                        .or_default();
1834                                    for name in names.iter() {
1835                                        insert_col(entry, schema, &tbl_name.0, &name.0);
1836                                    }
1837                                }
1838                            }
1839                        }
1840                    }
1841                }
1842                if let Some(expr) = where_clause {
1843                    extract_expr_columns(expr, schema, &mut parsed)?;
1844                }
1845                from_table
1846            }
1847            _ => None,
1848        };
1849
1850        extract_columns(columns.as_slice(), from_table, schema, &mut parsed)?;
1851    }
1852
1853    Ok(parsed)
1854}
1855
1856fn insert_col(set: &mut HashSet<String>, schema: &Schema, tbl_name: &str, name: &str) {
1857    let table = schema.tables.get(tbl_name);
1858    if let Some(generated) =
1859        table.and_then(|tbl| tbl.columns.get(name).and_then(|col| col.generated.as_ref()))
1860    {
1861        // recursively check for generated columns
1862        for name in generated.from.iter() {
1863            insert_col(set, schema, tbl_name, name);
1864        }
1865    } else {
1866        set.insert(name.to_owned());
1867    }
1868}
1869
1870fn extract_expr_columns(
1871    expr: &Expr,
1872    schema: &Schema,
1873    parsed: &mut ParsedSelect,
1874) -> Result<(), MatcherError> {
1875    match expr {
1876        // simplest case
1877        Expr::Qualified(tblname, colname) => {
1878            let resolved_name = parsed
1879                .aliases
1880                .get(tblname.0.as_ref())
1881                .map(|s| s.as_str())
1882                .unwrap_or(tblname.0.as_ref());
1883            // println!("adding column: {resolved_name} => {colname:?}");
1884            insert_col(
1885                parsed
1886                    .table_columns
1887                    .entry(resolved_name.to_string())
1888                    .or_default(),
1889                schema,
1890                resolved_name,
1891                &colname.0,
1892            );
1893        }
1894        // simplest case but also mentioning the schema
1895        Expr::DoublyQualified(schema_name, tblname, colname)
1896            if schema_name.0.as_ref() == "main" =>
1897        {
1898            let resolved_name = parsed
1899                .aliases
1900                .get(tblname.0.as_ref())
1901                .map(|s| s.as_str())
1902                .unwrap_or(tblname.0.as_ref());
1903            // println!("adding column: {resolved_name} => {colname:?}");
1904            insert_col(
1905                parsed
1906                    .table_columns
1907                    .entry(resolved_name.to_string())
1908                    .or_default(),
1909                schema,
1910                resolved_name,
1911                &colname.0,
1912            );
1913        }
1914
1915        Expr::Name(colname) => {
1916            let check_col_name = unquote(&colname.0).ok().unwrap_or(colname.0.to_string());
1917
1918            let mut found = None;
1919            for tbl in parsed.table_columns.keys() {
1920                if let Some(tbl) = schema.tables.get(tbl)
1921                    && tbl.columns.contains_key(&check_col_name)
1922                {
1923                    if found.is_some() {
1924                        return Err(MatcherError::QualificationRequired {
1925                            col_name: check_col_name,
1926                        });
1927                    }
1928                    found = Some(tbl.name.as_str());
1929                }
1930            }
1931
1932            if let Some(found) = found {
1933                insert_col(
1934                    parsed.table_columns.entry(found.to_owned()).or_default(),
1935                    schema,
1936                    found,
1937                    &check_col_name,
1938                );
1939            } else {
1940                return Err(MatcherError::TableForColumnNotFound {
1941                    col_name: check_col_name,
1942                });
1943            }
1944        }
1945
1946        Expr::Id(colname) => {
1947            let check_col_name = unquote(&colname.0).ok().unwrap_or(colname.0.to_string());
1948
1949            let mut found = None;
1950            for tbl in parsed.table_columns.keys() {
1951                if let Some(tbl) = schema.tables.get(tbl)
1952                    && tbl.columns.contains_key(&check_col_name)
1953                {
1954                    if found.is_some() {
1955                        return Err(MatcherError::QualificationRequired {
1956                            col_name: check_col_name,
1957                        });
1958                    }
1959                    found = Some(tbl.name.as_str());
1960                }
1961            }
1962
1963            if let Some(found) = found {
1964                insert_col(
1965                    parsed.table_columns.entry(found.to_owned()).or_default(),
1966                    schema,
1967                    found,
1968                    &colname.0,
1969                );
1970            } else {
1971                if colname.0.starts_with('"') {
1972                    return Ok(());
1973                }
1974                return Err(MatcherError::TableForColumnNotFound {
1975                    col_name: colname.0.to_string(),
1976                });
1977            }
1978        }
1979
1980        Expr::Between { lhs, .. } => extract_expr_columns(lhs, schema, parsed)?,
1981        Expr::Binary(lhs, _, rhs) => {
1982            extract_expr_columns(lhs, schema, parsed)?;
1983            extract_expr_columns(rhs, schema, parsed)?;
1984        }
1985        Expr::Case {
1986            base,
1987            when_then_pairs,
1988            else_expr,
1989        } => {
1990            if let Some(expr) = base {
1991                extract_expr_columns(expr, schema, parsed)?;
1992            }
1993            for (when_expr, _then_expr) in when_then_pairs.iter() {
1994                // NOTE: should we also parse the then expr?
1995                extract_expr_columns(when_expr, schema, parsed)?;
1996            }
1997            if let Some(expr) = else_expr {
1998                extract_expr_columns(expr, schema, parsed)?;
1999            }
2000        }
2001        Expr::Cast { expr, .. } => extract_expr_columns(expr, schema, parsed)?,
2002        Expr::Collate(expr, _) => extract_expr_columns(expr, schema, parsed)?,
2003        Expr::Exists(select) => {
2004            parsed
2005                .children
2006                .push(extract_select_columns(select, schema)?);
2007        }
2008        Expr::FunctionCall { args, .. } => {
2009            if let Some(args) = args {
2010                for expr in args.iter() {
2011                    extract_expr_columns(expr, schema, parsed)?;
2012                }
2013            }
2014        }
2015        Expr::InList { lhs, rhs, .. } => {
2016            extract_expr_columns(lhs, schema, parsed)?;
2017            if let Some(rhs) = rhs {
2018                for expr in rhs.iter() {
2019                    extract_expr_columns(expr, schema, parsed)?;
2020                }
2021            }
2022        }
2023        Expr::InSelect { lhs, rhs, .. } => {
2024            extract_expr_columns(lhs, schema, parsed)?;
2025            parsed.children.push(extract_select_columns(rhs, schema)?);
2026        }
2027        expr @ Expr::InTable { .. } => {
2028            return Err(MatcherError::UnsupportedExpr { expr: expr.clone() });
2029        }
2030        Expr::IsNull(expr) => {
2031            extract_expr_columns(expr, schema, parsed)?;
2032        }
2033        Expr::Like { lhs, rhs, .. } => {
2034            extract_expr_columns(lhs, schema, parsed)?;
2035            extract_expr_columns(rhs, schema, parsed)?;
2036        }
2037
2038        Expr::NotNull(expr) => {
2039            extract_expr_columns(expr, schema, parsed)?;
2040        }
2041        Expr::Parenthesized(parens) => {
2042            for expr in parens.iter() {
2043                extract_expr_columns(expr, schema, parsed)?;
2044            }
2045        }
2046        Expr::Subquery(select) => {
2047            parsed
2048                .children
2049                .push(extract_select_columns(select, schema)?);
2050        }
2051        Expr::Unary(_, expr) => {
2052            extract_expr_columns(expr, schema, parsed)?;
2053        }
2054
2055        // no column names in there...
2056        // Expr::FunctionCallStar { name, filter_over } => todo!(),
2057        // Expr::Id(_) => todo!(),
2058        // Expr::Literal(_) => todo!(),
2059        // Expr::Raise(_, _) => todo!(),
2060        // Expr::Variable(_) => todo!(),
2061        _ => {}
2062    }
2063
2064    Ok(())
2065}
2066
2067fn extract_columns(
2068    columns: &[ResultColumn],
2069    from: Option<&Name>,
2070    schema: &Schema,
2071    parsed: &mut ParsedSelect,
2072) -> Result<(), MatcherError> {
2073    let mut i = 0;
2074    for col in columns.iter() {
2075        match col {
2076            ResultColumn::Expr(expr, _) => {
2077                // println!("extracting col: {expr:?} (as: {maybe_as:?})");
2078                extract_expr_columns(expr, schema, parsed)?;
2079                parsed.columns.push(ResultColumn::Expr(
2080                    expr.clone(),
2081                    Some(As::As(Name(format!("col_{i}").into()))),
2082                ));
2083                i += 1;
2084            }
2085            ResultColumn::Star => {
2086                if let Some(tbl_name) = from {
2087                    if let Some(table) = schema.tables.get(tbl_name.0.as_ref()) {
2088                        let entry = parsed.table_columns.entry(table.name.clone()).or_default();
2089                        for col in table.columns.keys() {
2090                            entry.insert(col.clone());
2091                            parsed.columns.push(ResultColumn::Expr(
2092                                Expr::Name(Name(col.clone().into())),
2093                                Some(As::As(Name(format!("col_{i}").into()))),
2094                            ));
2095                            i += 1;
2096                        }
2097                    } else {
2098                        return Err(MatcherError::TableStarNotFound {
2099                            tbl_name: tbl_name.0.to_string(),
2100                        });
2101                    }
2102                } else {
2103                    unreachable!()
2104                }
2105            }
2106            ResultColumn::TableStar(tbl_name) => {
2107                let name = parsed
2108                    .aliases
2109                    .get(tbl_name.0.as_ref())
2110                    .map(|s| s.as_str())
2111                    .unwrap_or(tbl_name.0.as_ref());
2112                if let Some(table) = schema.tables.get(name) {
2113                    let entry = parsed.table_columns.entry(table.name.clone()).or_default();
2114                    for col in table.columns.keys() {
2115                        entry.insert(col.clone());
2116                        parsed.columns.push(ResultColumn::Expr(
2117                            Expr::Qualified(tbl_name.clone(), Name(col.clone().into())),
2118                            Some(As::As(Name(format!("col_{i}").into()))),
2119                        ));
2120                        i += 1;
2121                    }
2122                } else {
2123                    return Err(MatcherError::TableStarNotFound {
2124                        tbl_name: name.to_string(),
2125                    });
2126                }
2127            }
2128        }
2129    }
2130    Ok(())
2131}
2132
2133fn table_to_expr(
2134    aliases: &HashMap<String, String>,
2135    tbl: &Table,
2136    table: &str,
2137) -> Result<Expr, MatcherError> {
2138    let tbl_name = aliases
2139        .iter()
2140        .find_map(|(alias, actual)| (actual == table).then_some(alias))
2141        .cloned()
2142        .unwrap_or_else(|| table.to_owned());
2143
2144    let expr = Expr::in_table(
2145        Expr::Parenthesized(
2146            tbl.pk
2147                .iter()
2148                .map(|pk| {
2149                    Expr::Qualified(Name(tbl_name.clone().into()), Name(pk.to_owned().into()))
2150                })
2151                .collect(),
2152        ),
2153        false,
2154        QualifiedName::fullname(
2155            Name("__corro_sub".into()),
2156            Name(format!("temp_{table}").into()),
2157        ),
2158        None,
2159    );
2160
2161    Ok(expr)
2162}
2163
2164#[derive(Debug, thiserror::Error)]
2165pub enum MatcherError {
2166    #[error(transparent)]
2167    Lexer(#[from] sqlite3_parser::lexer::sql::Error),
2168    #[error("one statement is required for matching")]
2169    StatementRequired,
2170    #[error("unsupported statement")]
2171    UnsupportedStatement,
2172    #[error("at least 1 table is required in FROM / JOIN clause")]
2173    TableRequired,
2174    #[error(transparent)]
2175    Sqlite(#[from] rusqlite::Error),
2176    #[error("table not found in schema: {0}")]
2177    TableNotFound(String),
2178    #[error("no primary key for table: {0}")]
2179    NoPrimaryKey(String),
2180    #[error("aggregate missing primary key {0}.{1}")]
2181    AggPrimaryKeyMissing(String, String),
2182    #[error("JOIN .. ON expression is not supported for join on table '{table}': {expr:?}")]
2183    JoinOnExprUnsupported { table: String, expr: Box<Expr> },
2184    #[error("expression is not supported: {expr:?}")]
2185    UnsupportedExpr { expr: Expr },
2186    #[error("could not find table for {tbl_name}.* in corrosion's schema")]
2187    TableStarNotFound { tbl_name: String },
2188    #[error("<tbl>.{col_name} qualification required for ambiguous column name")]
2189    QualificationRequired { col_name: String },
2190    #[error("could not find table for column {col_name}")]
2191    TableForColumnNotFound { col_name: String },
2192    #[error("missing primary keys, this shouldn't happen")]
2193    MissingPrimaryKeys,
2194    #[error("change queue has been closed or is full")]
2195    ChangeQueueClosedOrFull,
2196    #[error("no change was inserted, this is not supposed to happen")]
2197    NoChangeInserted,
2198    #[error("change receiver is closed")]
2199    EventReceiverClosed,
2200    #[error(transparent)]
2201    Unpack(#[from] UnpackError),
2202    #[error("did not insert subscription")]
2203    InsertSub,
2204    #[error(transparent)]
2205    FromSql(#[from] FromSqlError),
2206    #[error(transparent)]
2207    Io(#[from] std::io::Error),
2208    #[error("could not encode changeset for queue: {0}")]
2209    ChangesetEncode(#[from] speedy::Error),
2210    #[error("queue is full")]
2211    QueueFull,
2212    #[error("cannot restore existing subscription")]
2213    CannotRestoreExisting,
2214    #[error("could not acquire write permit")]
2215    WritePermitAcquire(#[from] AcquireError),
2216    #[error("subscription is not running")]
2217    NotRunning,
2218    #[error("subscription restore is missing SQL query")]
2219    MissingSql,
2220}
2221
2222impl MatcherError {
2223    pub fn is_event_recv_closed(&self) -> bool {
2224        matches!(self, MatcherError::EventReceiverClosed)
2225    }
2226}
2227
2228#[derive(Debug, thiserror::Error)]
2229#[allow(clippy::large_enum_variant)]
2230pub enum NormalizeStatementError {
2231    #[error(transparent)]
2232    Parse(#[from] sqlite3_parser::lexer::sql::Error),
2233    #[error("unexpected statement: {0}")]
2234    UnexpectedStatement(Box<Cmd>),
2235    #[error("only 1 statement is supported")]
2236    Multiple,
2237    #[error("at least 1 statement is required")]
2238    NoStatement,
2239}
2240
2241pub fn normalize_sql(sql: &str) -> Result<String, NormalizeStatementError> {
2242    let mut parser = Parser::new(sql.as_bytes());
2243
2244    let stmt = match parser.next()? {
2245        Some(Cmd::Stmt(stmt)) => stmt,
2246        Some(cmd) => {
2247            return Err(NormalizeStatementError::UnexpectedStatement(Box::new(cmd)));
2248        }
2249        None => {
2250            return Err(NormalizeStatementError::NoStatement);
2251        }
2252    };
2253
2254    if parser.next()?.is_some() {
2255        return Err(NormalizeStatementError::Multiple);
2256    }
2257
2258    Ok(Cmd::Stmt(stmt).to_string())
2259}
2260
2261#[derive(Debug, thiserror::Error)]
2262pub enum PackError {
2263    #[error("abort")]
2264    Abort,
2265}
2266
2267pub fn pack_columns(args: &[SqliteValue]) -> Result<Vec<u8>, PackError> {
2268    let mut buf = vec![];
2269    /*
2270     * Format:
2271     * [num_columns:u8,...[(type(0-3),num_bytes?(3-7)):u8, length?:i32, ...bytes:u8[]]]
2272     *
2273     * The byte used for column type also encodes the number of bytes used for the integer.
2274     * e.g.: (type(0-3),num_bytes?(3-7)):u8
2275     * first 3 bits are type
2276     * last 5 encode how long the following integer, if there is a following integer, is. 1, 2, 3, ... 8 bytes.
2277     *
2278     * Not packing an integer into the minimal number of bytes required is rather wasteful.
2279     * E.g., the number `0` would take 8 bytes rather than 1 byte.
2280     */
2281    let len_result: Result<u8, _> = args.len().try_into();
2282    if let Ok(len) = len_result {
2283        buf.put_u8(len);
2284        for value in args {
2285            match value {
2286                SqliteValue::Null => {
2287                    buf.put_u8(ColumnType::Null as u8);
2288                }
2289                SqliteValue::Integer(val) => {
2290                    let num_bytes_for_int = num_bytes_needed_i64(*val);
2291                    let type_byte = num_bytes_for_int << 3 | (ColumnType::Integer as u8);
2292                    buf.put_u8(type_byte);
2293                    buf.put_int(*val, num_bytes_for_int as usize);
2294                }
2295                SqliteValue::Real(v) => {
2296                    buf.put_u8(ColumnType::Float as u8);
2297                    buf.put_f64(v.0);
2298                }
2299                SqliteValue::Text(value) => {
2300                    let len = value.len() as i32;
2301                    let num_bytes_for_len = num_bytes_needed_i32(len);
2302                    let type_byte = num_bytes_for_len << 3 | (ColumnType::Text as u8);
2303                    buf.put_u8(type_byte);
2304                    buf.put_int(len as i64, num_bytes_for_len as usize);
2305                    buf.put_slice(value.as_bytes());
2306                }
2307                SqliteValue::Blob(value) => {
2308                    let len = value.len() as i32;
2309                    let num_bytes_for_len = num_bytes_needed_i32(len);
2310                    let type_byte = num_bytes_for_len << 3 | (ColumnType::Blob as u8);
2311                    buf.put_u8(type_byte);
2312                    buf.put_int(len as i64, num_bytes_for_len as usize);
2313                    buf.put_slice(value);
2314                }
2315            }
2316        }
2317        Ok(buf)
2318    } else {
2319        Err(PackError::Abort)
2320    }
2321}
2322
2323fn num_bytes_needed_i64(val: i64) -> u8 {
2324    if val & 0xFF00000000000000u64 as i64 != 0 {
2325        8
2326    } else if val & 0x00FF000000000000 != 0 {
2327        7
2328    } else if val & 0x0000FF0000000000 != 0 {
2329        6
2330    } else if val & 0x000000FF00000000 != 0 {
2331        5
2332    } else {
2333        num_bytes_needed_i32(val as i32)
2334    }
2335}
2336
2337fn num_bytes_needed_i32(val: i32) -> u8 {
2338    if val & 0xFF000000u32 as i32 != 0 {
2339        4
2340    } else if val & 0x00FF0000 != 0 {
2341        3
2342    } else if val & 0x0000FF00 != 0 {
2343        2
2344    } else if val * 0x000000FF != 0 {
2345        1
2346    } else {
2347        0
2348    }
2349}
2350
2351#[derive(Debug, thiserror::Error)]
2352pub enum UnpackError {
2353    #[error("abort")]
2354    Abort,
2355    #[error("misuse")]
2356    Misuse,
2357}
2358
2359pub fn unpack_columns(mut buf: &[u8]) -> Result<Vec<SqliteValueRef<'_>>, UnpackError> {
2360    let mut ret = vec![];
2361    let num_columns = buf.get_u8();
2362
2363    for _i in 0..num_columns {
2364        if !buf.has_remaining() {
2365            return Err(UnpackError::Abort);
2366        }
2367        let column_type_and_maybe_intlen = buf.get_u8();
2368        let column_type = ColumnType::from_u8(column_type_and_maybe_intlen & 0x07);
2369        let intlen = (column_type_and_maybe_intlen >> 3) as usize;
2370
2371        match column_type {
2372            Some(ColumnType::Blob) => {
2373                if buf.remaining() < intlen {
2374                    return Err(UnpackError::Abort);
2375                }
2376                let len = buf.get_int(intlen) as usize;
2377                if buf.remaining() < len {
2378                    return Err(UnpackError::Abort);
2379                }
2380                ret.push(SqliteValueRef(ValueRef::Blob(&buf[0..len])));
2381                buf.advance(len);
2382            }
2383            Some(ColumnType::Float) => {
2384                if buf.remaining() < 8 {
2385                    return Err(UnpackError::Abort);
2386                }
2387                ret.push(SqliteValueRef(ValueRef::Real(buf.get_f64())));
2388            }
2389            Some(ColumnType::Integer) => {
2390                if buf.remaining() < intlen {
2391                    return Err(UnpackError::Abort);
2392                }
2393                ret.push(SqliteValueRef(ValueRef::Integer(buf.get_int(intlen))));
2394            }
2395            Some(ColumnType::Null) => {
2396                ret.push(SqliteValueRef(ValueRef::Null));
2397            }
2398            Some(ColumnType::Text) => {
2399                if buf.remaining() < intlen {
2400                    return Err(UnpackError::Abort);
2401                }
2402                let len = buf.get_int(intlen) as usize;
2403                if buf.remaining() < len {
2404                    return Err(UnpackError::Abort);
2405                }
2406                ret.push(SqliteValueRef(ValueRef::Text(&buf[0..len])));
2407                buf.advance(len);
2408            }
2409            None => return Err(UnpackError::Misuse),
2410        }
2411    }
2412
2413    Ok(ret)
2414}
2415
2416#[cfg(test)]
2417mod tests {
2418    use std::net::Ipv4Addr;
2419
2420    use crate::spawn::wait_for_all_pending_handles;
2421    use camino::Utf8PathBuf;
2422    use rusqlite::params;
2423    use tokio::sync::Semaphore;
2424
2425    use crate::{
2426        actor::ActorId,
2427        agent::migrate,
2428        base::CrsqlDbVersion,
2429        change::row_to_change,
2430        schema::{apply_schema, parse_sql},
2431        sqlite::{CrConn, setup_conn},
2432    };
2433    use klukai_tests::tempdir::TempDir;
2434
2435    use super::*;
2436
2437    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2438    async fn test_matcher() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2439        _ = tracing_subscriber::fmt::try_init();
2440        let (tripwire, tripwire_worker, tripwire_tx) = Tripwire::new_simple();
2441        let schema_sql = "CREATE TABLE sw (pk TEXT NOT NULL PRIMARY KEY, sandwich TEXT);";
2442        let mut schema = parse_sql(schema_sql)?;
2443
2444        let sql = "SELECT sandwich FROM sw WHERE pk=\"mad\"";
2445
2446        let subs = SubsManager::default();
2447
2448        let tmpdir = TempDir::new(tempfile::tempdir()?);
2449        let db_path = tmpdir.path().join("test.db");
2450        println!("db_path: {}", db_path.display());
2451        let subscriptions_path: Utf8PathBuf =
2452            tmpdir.path().join("subs").display().to_string().into();
2453
2454        let pool = SplitPool::create(db_path, Arc::new(Semaphore::new(1))).await?;
2455        let clock = Arc::new(uhlc::HLC::default());
2456
2457        {
2458            let mut conn = pool.write_priority().await?;
2459            setup_conn(&conn)?;
2460            migrate(clock, &mut conn)?;
2461            let tx = conn.transaction()?;
2462            apply_schema(&tx, &Schema::default(), &mut schema)?;
2463            tx.commit()?;
2464        }
2465
2466        {
2467            let (handle, maybe_created) = subs.get_or_insert(
2468                sql,
2469                subscriptions_path.as_path(),
2470                &schema,
2471                &pool,
2472                tripwire.clone(),
2473            )?;
2474
2475            assert!(maybe_created.is_some());
2476
2477            handle.cleanup().await;
2478            subs.remove(&handle.id());
2479        }
2480
2481        tripwire_tx.send(()).await.ok();
2482        tripwire_worker.await;
2483        wait_for_all_pending_handles().await;
2484
2485        Ok(())
2486    }
2487
2488    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2489    async fn test_diff() {
2490        _ = tracing_subscriber::fmt::try_init();
2491
2492        let (tripwire, tripwire_worker, tripwire_tx) = Tripwire::new_simple();
2493
2494        let sql = "SELECT json_object(
2495            'targets', json_array(cs.address||':'||cs.port),
2496            'labels',  json_object(
2497              '__metrics_path__', JSON_EXTRACT(cs.meta, '$.path'),
2498              'app',            cs.app_name,
2499              'vm_account_id',  cs.organization_id,
2500              'instance',       cs.instance_id
2501            )
2502          )
2503          FROM consul_services cs
2504            LEFT JOIN machines m                   ON m.id = cs.instance_id
2505            LEFT JOIN machine_versions mv          ON m.id = mv.machine_id  AND m.machine_version_id = mv.id
2506            LEFT JOIN machine_version_statuses mvs ON m.id = mvs.machine_id AND m.machine_version_id = mvs.id
2507          WHERE cs.node = 'test-hostname'
2508            AND (mvs.status IS NULL OR mvs.status = 'started')
2509            AND cs.name == 'app-prometheus'";
2510
2511        let schema_sql = "
2512          CREATE TABLE consul_services (
2513              node TEXT NOT NULL,
2514              id TEXT NOT NULL,
2515              name TEXT NOT NULL DEFAULT '',
2516              tags TEXT NOT NULL DEFAULT '[]',
2517              meta TEXT NOT NULL DEFAULT '{}',
2518              port INTEGER NOT NULL DEFAULT 0,
2519              address TEXT NOT NULL DEFAULT '',
2520              updated_at INTEGER NOT NULL DEFAULT 0,
2521              app_id INTEGER AS (CAST(JSON_EXTRACT(meta, '$.app_id') AS INTEGER)), network_id INTEGER AS (
2522                  CAST(JSON_EXTRACT(meta, '$.network_id') AS INTEGER)
2523              ), app_name TEXT AS (JSON_EXTRACT(meta, '$.app_name')), instance_id TEXT AS (
2524                  COALESCE(
2525                      JSON_EXTRACT(meta, '$.machine_id'),
2526                      SUBSTR(JSON_EXTRACT(meta, '$.alloc_id'), 1, 8),
2527                      CASE
2528                          WHEN INSTR(id, '_nomad-task-') = 1 THEN SUBSTR(id, 13, 8)
2529                          ELSE NULL
2530                      END
2531                  )
2532              ), organization_id INTEGER AS (
2533                  CAST(
2534                      JSON_EXTRACT(meta, '$.organization_id') AS INTEGER
2535                  )
2536              ), protocol TEXT
2537          AS (JSON_EXTRACT(meta, '$.protocol')),
2538              PRIMARY KEY (node, id)
2539          );
2540
2541          CREATE TABLE machines (
2542              id TEXT NOT NULL PRIMARY KEY,
2543              node TEXT NOT NULL DEFAULT '',
2544              name TEXT NOT NULL DEFAULT '',
2545              machine_version_id TEXT NOT NULL DEFAULT '',
2546              app_id INTEGER NOT NULL DEFAULT 0,
2547              organization_id INTEGER NOT NULL DEFAULT 0,
2548              network_id INTEGER NOT NULL DEFAULT 0,
2549              updated_at INTEGER NOT NULL DEFAULT 0
2550          );
2551
2552          CREATE TABLE machine_versions (
2553              machine_id TEXT NOT NULL,
2554              id TEXT NOT NULL DEFAULT '',
2555              config TEXT NOT NULL DEFAULT '{}',
2556              updated_at INTEGER NOT NULL DEFAULT 0,
2557              PRIMARY KEY (machine_id, id)
2558          );
2559
2560          CREATE TABLE machine_version_statuses (
2561              machine_id TEXT NOT NULL,
2562              id TEXT NOT NULL,
2563              status TEXT NOT NULL DEFAULT '',
2564              updated_at INTEGER NOT NULL DEFAULT 0,
2565              PRIMARY KEY (machine_id, id)
2566          );
2567          ";
2568
2569        let mut schema = parse_sql(schema_sql).unwrap();
2570        let subs = SubsManager::default();
2571
2572        let tmpdir = TempDir::new(tempfile::tempdir().unwrap());
2573        let db_path = tmpdir.path().join("test.db");
2574        let subscriptions_path: Utf8PathBuf =
2575            tmpdir.path().join("subs").display().to_string().into();
2576
2577        let pool = SplitPool::create(&db_path, Arc::new(Semaphore::new(1)))
2578            .await
2579            .unwrap();
2580        let mut conn = pool.write_priority().await.unwrap();
2581        let clock = Arc::new(uhlc::HLC::default());
2582        {
2583            setup_conn(&conn).unwrap();
2584            migrate(clock.clone(), &mut conn).unwrap();
2585            let tx = conn.transaction().unwrap();
2586            apply_schema(&tx, &Schema::default(), &mut schema).unwrap();
2587            tx.commit().unwrap();
2588        }
2589
2590        // let's seed some data in there
2591        {
2592            let tx = conn.transaction().unwrap();
2593            tx.execute_batch(r#"
2594                INSERT INTO consul_services (node, id, name, address, port, meta) VALUES ('test-hostname', 'service-1', 'app-prometheus', '127.0.0.1', 1, '{"path": "/1", "machine_id": "m-1"}');
2595
2596                INSERT INTO machines (id, machine_version_id) VALUES ('m-1', 'mv-1');
2597
2598                INSERT INTO machine_versions (machine_id, id) VALUES ('m-1', 'mv-1');
2599
2600                INSERT INTO machine_version_statuses (machine_id, id, status) VALUES ('m-1', 'mv-1', 'started');
2601
2602                INSERT INTO consul_services (node, id, name, address, port, meta) VALUES ('test-hostname', 'service-2', 'not-app-prometheus', '127.0.0.1', 1, '{"path": "/1", "machine_id": "m-2"}');
2603
2604                INSERT INTO machines (id, machine_version_id) VALUES ('m-2', 'mv-2');
2605
2606                INSERT INTO machine_versions (machine_id, id) VALUES ('m-2', 'mv-2');
2607
2608                INSERT INTO machine_version_statuses (machine_id, id, status) VALUES ('m-2', 'mv-2', 'started');
2609                    "#).unwrap();
2610            tx.commit().unwrap();
2611        }
2612
2613        {
2614            let mut conn2 = CrConn::init(
2615                rusqlite::Connection::open(tmpdir.path().join("test2.db"))
2616                    .expect("could not open conn"),
2617            )
2618            .expect("could not init crsql");
2619
2620            setup_conn(&conn2).unwrap();
2621            migrate(clock.clone(), &mut conn2).unwrap();
2622
2623            {
2624                let tx = conn2.transaction().unwrap();
2625                apply_schema(&tx, &Schema::default(), &mut schema).unwrap();
2626                tx.commit().unwrap();
2627            }
2628
2629            let changes = {
2630                let mut prepped = conn.prepare_cached(r#"SELECT "table", pk, cid, val, col_version, db_version, seq, site_id, cl FROM crsql_changes WHERE db_version = ? ORDER BY seq ASC"#).unwrap();
2631                let rows = prepped.query_map([1], row_to_change).unwrap();
2632
2633                let mut changes = vec![];
2634
2635                for row in rows {
2636                    changes.push(row.unwrap());
2637                }
2638                changes
2639            };
2640
2641            let tx = conn2.transaction().unwrap();
2642
2643            for change in changes {
2644                debug!("change: {change:?}");
2645                tx.prepare_cached(
2646                    r#"
2647                    INSERT INTO crsql_changes
2648                        ("table", pk, cid, val, col_version, db_version, site_id, cl, seq)
2649                    VALUES
2650                        (?,       ?,  ?,   ?,   ?,           ?,          ?,       ?,  ?)
2651                "#,
2652                )
2653                .unwrap()
2654                .execute(params![
2655                    change.table.as_str(),
2656                    change.pk,
2657                    change.cid.as_str(),
2658                    &change.val,
2659                    change.col_version,
2660                    change.db_version,
2661                    &change.site_id,
2662                    change.cl,
2663                    change.seq
2664                ])
2665                .unwrap();
2666            }
2667        }
2668
2669        let matcher_conn =
2670            CrConn::init(rusqlite::Connection::open(&db_path).expect("could not open conn"))
2671                .expect("could not init crconn");
2672
2673        setup_conn(&matcher_conn).unwrap();
2674
2675        let mut last_change_id = None;
2676
2677        let id = {
2678            // let (db_v_tx, db_v_rx) = watch::channel(0);
2679
2680            let (matcher, maybe_created) = subs
2681                .get_or_insert(
2682                    sql,
2683                    subscriptions_path.as_path(),
2684                    &schema,
2685                    &pool,
2686                    tripwire.clone(),
2687                )
2688                .unwrap();
2689
2690            let mut rx = maybe_created.unwrap().evt_rx;
2691
2692            println!("matcher created w/ id: {}", matcher.id().as_simple());
2693            println!("parsed: {:?}", matcher.inner.parsed);
2694
2695            assert!(matches!(rx.recv().await.unwrap(), QueryEvent::Columns(_)));
2696
2697            let cells = vec![SqliteValue::Text("{\"targets\":[\"127.0.0.1:1\"],\"labels\":{\"__metrics_path__\":\"/1\",\"app\":null,\"vm_account_id\":null,\"instance\":\"m-1\"}}".into())];
2698
2699            assert_eq!(rx.recv().await.unwrap(), QueryEvent::Row(RowId(1), cells));
2700            println!("received a row");
2701            assert!(matches!(
2702                rx.recv().await.unwrap(),
2703                QueryEvent::EndOfQuery { .. }
2704            ));
2705            println!("received end of query");
2706
2707            // insert the second row
2708            {
2709                let tx = conn.transaction().unwrap();
2710                tx.execute_batch(r#"
2711                INSERT INTO consul_services (node, id, name, address, port, meta) VALUES ('test-hostname', 'service-3', 'app-prometheus', '127.0.0.1', 1, '{"path": "/1", "machine_id": "m-3"}');
2712
2713                INSERT INTO machines (id, machine_version_id) VALUES ('m-3', 'mv-3');
2714
2715                INSERT INTO machine_versions (machine_id, id) VALUES ('m-3', 'mv-3');
2716
2717                INSERT INTO machine_version_statuses (machine_id, id, status) VALUES ('m-3', 'mv-3', 'started');
2718            "#).unwrap();
2719                tx.commit().unwrap();
2720            }
2721
2722            println!("processing change...");
2723            filter_changes_from_db(&matcher, &conn, None, CrsqlDbVersion(2)).unwrap();
2724            println!("processed changes");
2725
2726            let cells = vec![SqliteValue::Text("{\"targets\":[\"127.0.0.1:1\"],\"labels\":{\"__metrics_path__\":\"/1\",\"app\":null,\"vm_account_id\":null,\"instance\":\"m-3\"}}".into())];
2727
2728            assert_eq!(
2729                rx.recv().await.unwrap(),
2730                QueryEvent::Change(ChangeType::Insert, RowId(2), cells, ChangeId(1))
2731            );
2732
2733            println!("received change");
2734
2735            // delete the first row
2736            {
2737                let tx = conn.transaction().unwrap();
2738                tx.execute_batch(r#"
2739                        DELETE FROM consul_services where node = 'test-hostname' AND id = 'service-1';
2740                    "#).unwrap();
2741                tx.commit().unwrap();
2742            }
2743
2744            filter_changes_from_db(&matcher, &conn, None, CrsqlDbVersion(3)).unwrap();
2745
2746            let cells = vec![SqliteValue::Text("{\"targets\":[\"127.0.0.1:1\"],\"labels\":{\"__metrics_path__\":\"/1\",\"app\":null,\"vm_account_id\":null,\"instance\":\"m-1\"}}".into())];
2747
2748            println!("waiting for a change (A)");
2749
2750            assert_eq!(
2751                rx.recv().await.unwrap(),
2752                QueryEvent::Change(ChangeType::Delete, RowId(1), cells, ChangeId(2))
2753            );
2754
2755            println!("got change (A)");
2756
2757            // update the second row
2758            {
2759                let tx = conn.transaction().unwrap();
2760                let n = tx.execute(r#"
2761                        UPDATE consul_services SET address = '127.0.0.2' WHERE node = 'test-hostname' AND id = 'service-3';
2762                    "#, ()).unwrap();
2763                assert_eq!(n, 1);
2764                tx.commit().unwrap();
2765            }
2766
2767            filter_changes_from_db(&matcher, &conn, None, CrsqlDbVersion(4)).unwrap();
2768
2769            let cells = vec![SqliteValue::Text("{\"targets\":[\"127.0.0.2:1\"],\"labels\":{\"__metrics_path__\":\"/1\",\"app\":null,\"vm_account_id\":null,\"instance\":\"m-3\"}}".into())];
2770
2771            println!("waiting for a change (B)");
2772
2773            assert_eq!(
2774                rx.recv().await.unwrap(),
2775                QueryEvent::Change(ChangeType::Update, RowId(2), cells, ChangeId(3))
2776            );
2777
2778            println!("got change (B)");
2779
2780            // process the same and check that it doesn't produce a change again!
2781            // db_v_tx.send(db_version).unwrap();
2782
2783            assert_eq!(rx.try_recv(), Err(mpsc::error::TryRecvError::Empty));
2784
2785            // lots of operations
2786
2787            let range = 4u32..1000u32;
2788
2789            {
2790                let tx = conn.transaction().unwrap();
2791
2792                for n in range.clone() {
2793                    let svc_id = format!("service-{n}");
2794                    let ip = Ipv4Addr::from(n).to_string();
2795                    let port = n;
2796                    let machine_id = format!("m-{n}");
2797                    let mv = format!("mv-{n}");
2798                    let meta =
2799                        serde_json::json!({"path": format!("/path-{n}"), "machine_id": machine_id});
2800                    tx.execute("
2801                                    INSERT INTO consul_services (node, id, name, address, port, meta) VALUES ('test-hostname', ?, 'app-prometheus', ?, ?, ?);
2802                                    ", params![svc_id, ip, port, meta]).unwrap();
2803                    tx.execute(
2804                        "
2805                        INSERT INTO machines (id, machine_version_id) VALUES (?, ?);
2806                        ",
2807                        params![machine_id, mv],
2808                    )
2809                    .unwrap();
2810
2811                    tx.execute(
2812                        "
2813                        INSERT INTO machine_versions (machine_id, id) VALUES (?, ?);
2814                        ",
2815                        params![machine_id, mv],
2816                    )
2817                    .unwrap();
2818
2819                    tx.execute("
2820                        INSERT INTO machine_version_statuses (machine_id, id, status) VALUES (?, ?, 'started');", params![machine_id, mv]).unwrap();
2821                }
2822
2823                tx.commit().unwrap();
2824            }
2825
2826            filter_changes_from_db(&matcher, &conn, None, CrsqlDbVersion(5)).unwrap();
2827
2828            let start = Instant::now();
2829            for _ in range {
2830                if let QueryEvent::Change(change_type, _, _, change_id) = rx.recv().await.unwrap() {
2831                    assert_eq!(change_type, ChangeType::Insert);
2832                    last_change_id = Some(change_id);
2833                }
2834            }
2835
2836            assert_eq!(last_change_id, Some(ChangeId(999)));
2837
2838            println!("took: {:?}", start.elapsed());
2839            {
2840                let conn = rusqlite::Connection::open(
2841                    subscriptions_path
2842                        .join(matcher.id().as_simple().to_string())
2843                        .join("sub.sqlite"),
2844                )
2845                .unwrap();
2846                let mut prepped = conn.prepare("SELECT * FROM changes").unwrap();
2847                let cols = prepped
2848                    .column_names()
2849                    .iter()
2850                    .map(|s| s.to_string())
2851                    .collect::<Vec<_>>();
2852                let mut rows = prepped.query([]).unwrap();
2853                loop {
2854                    let row = rows.next().unwrap();
2855                    let row = match row {
2856                        None => break,
2857                        Some(row) => row,
2858                    };
2859
2860                    let s = cols
2861                        .iter()
2862                        .enumerate()
2863                        .map(|(idx, col)| {
2864                            format!("{col} = {}", row.get::<_, SqliteValue>(idx).unwrap())
2865                        })
2866                        .collect::<Vec<_>>();
2867                    println!("{}", s.join(", "));
2868                }
2869            }
2870            if let Some(handle) = subs.remove(&matcher.id()) {
2871                handle.cleanup().await;
2872            }
2873            matcher.id()
2874        };
2875
2876        tokio::time::sleep(Duration::from_secs(1)).await;
2877
2878        // restore subscription
2879        let matcher_id = {
2880            let (matcher, created) = subs
2881                .restore(id, &subscriptions_path, &schema, &pool, tripwire.clone())
2882                .unwrap();
2883            let mut rx = created.evt_rx;
2884
2885            let matcher_id = matcher.id().as_simple().to_string();
2886            println!("matcher restored w/ id: {matcher_id}");
2887
2888            let (catch_up_tx, mut catch_up_rx) = mpsc::channel(1);
2889
2890            tokio::spawn({
2891                let matcher = matcher.clone();
2892                async move {
2893                    let mut conn = matcher.pool().get().await.unwrap();
2894                    block_in_place(|| {
2895                        let conn_tx = conn.transaction()?;
2896
2897                        println!(
2898                            "max change id: {}",
2899                            matcher.max_change_id(&conn_tx).unwrap()
2900                        );
2901                        matcher.all_rows(&conn_tx, catch_up_tx)?;
2902                        Ok::<_, MatcherError>(())
2903                    })
2904                }
2905            });
2906
2907            assert!(matches!(
2908                catch_up_rx.recv().await.unwrap(),
2909                QueryEvent::Columns(_)
2910            ));
2911
2912            let mut rows_count = 0;
2913            let mut eoq_change_id = None;
2914
2915            while eoq_change_id.is_none() {
2916                match catch_up_rx.recv().await.unwrap() {
2917                    QueryEvent::EndOfQuery { change_id, .. } => eoq_change_id = Some(change_id),
2918                    QueryEvent::Row(_, _) => rows_count += 1,
2919                    QueryEvent::Change(_, _, _, _) => {
2920                        panic!("received a change intertwined w/ rows");
2921                    }
2922                    _ => {}
2923                }
2924            }
2925
2926            assert_eq!(rows_count, 997);
2927            assert_eq!(eoq_change_id, Some(Some(ChangeId(999))));
2928
2929            {
2930                let tx = conn.transaction().unwrap();
2931                tx.execute_batch(
2932                    r#"
2933                        DELETE FROM consul_services where node = 'test-hostname' AND id = 'service-3';
2934                    "#,
2935                )
2936                .unwrap();
2937                tx.commit().unwrap();
2938            }
2939            filter_changes_from_db(&matcher, &conn, None, CrsqlDbVersion(6)).unwrap();
2940            println!("waiting for a change (A)");
2941
2942            let cells = vec![SqliteValue::Text("{\"targets\":[\"127.0.0.2:1\"],\"labels\":{\"__metrics_path__\":\"/1\",\"app\":null,\"vm_account_id\":null,\"instance\":\"m-3\"}}".into())];
2943
2944            assert_eq!(
2945                rx.recv().await.unwrap(),
2946                QueryEvent::Change(ChangeType::Delete, RowId(2), cells, ChangeId(1000))
2947            );
2948
2949            assert!(rx.try_recv().is_err());
2950
2951            {
2952                let tx = conn.transaction().unwrap();
2953                for n in 1001..=1005 {
2954                    tx.execute_batch(format!(r#"
2955                        INSERT INTO consul_services (node, id, name, address, port, meta) VALUES ('test-hostname', 'service-{n}', 'app-prometheus', '127.0.0.{n}', 1, '{{"path": "/1", "machine_id": "m-{n}"}}');
2956
2957                        INSERT INTO machines (id, machine_version_id) VALUES ('m-{n}', 'mv-{n}');
2958
2959                        INSERT INTO machine_versions (machine_id, id) VALUES ('m-{n}', 'mv-{n}');
2960
2961                        INSERT INTO machine_version_statuses (machine_id, id, status) VALUES ('m-{n}', 'mv-{n}', 'started');
2962                    "#).as_str()).unwrap();
2963                }
2964                tx.commit().unwrap();
2965            }
2966            filter_changes_from_db(&matcher, &conn, None, CrsqlDbVersion(7)).unwrap();
2967
2968            // initiate shutdown
2969            tripwire_tx.send(()).await.ok();
2970            tripwire_worker.await;
2971
2972            {
2973                let tx = conn.transaction().unwrap();
2974                for n in 1006..=1010 {
2975                    tx.execute_batch(format!(r#"
2976                        INSERT INTO consul_services (node, id, name, address, port, meta) VALUES ('test-hostname', 'service-{n}', 'app-prometheus', '127.0.0.{n}', 1, '{{"path": "/1", "machine_id": "m-{n}"}}');
2977
2978                        INSERT INTO machines (id, machine_version_id) VALUES ('m-{n}', 'mv-{n}');
2979
2980                        INSERT INTO machine_versions (machine_id, id) VALUES ('m-{n}', 'mv-{n}');
2981
2982                        INSERT INTO machine_version_statuses (machine_id, id, status) VALUES ('m-{n}', 'mv-{n}', 'started');
2983                    "#).as_str()).unwrap();
2984                }
2985                tx.commit().unwrap();
2986            }
2987            filter_changes_from_db(&matcher, &conn, None, CrsqlDbVersion(8)).unwrap();
2988
2989            matcher_id
2990        };
2991
2992        subs.drop_handles().await;
2993        wait_for_all_pending_handles().await;
2994        info!("tripwire worker finished");
2995
2996        // check that changes sent after tripping the wire were persisted to db
2997
2998        {
2999            let conn = rusqlite::Connection::open(
3000                subscriptions_path
3001                    .join(matcher_id.clone())
3002                    .join("sub.sqlite"),
3003            )
3004            .unwrap();
3005
3006            let mut prepped = conn.prepare("SELECT max(id) FROM changes").unwrap();
3007            let max_id = prepped.query_row([], |row| row.get::<_, i64>(0)).unwrap();
3008            assert_eq!(max_id, 1010);
3009
3010            for n in 1001..=1010 {
3011                let mut prepped = conn
3012                    .prepare(
3013                        format!("SELECT 1 from query where __corro_pk_cs_id = 'service-{n}'")
3014                            .as_str(),
3015                    )
3016                    .unwrap();
3017                let max_db_version = prepped.query_row([], |row| row.get::<_, i64>(0)).unwrap();
3018                assert_eq!(max_db_version, 1);
3019            }
3020        }
3021
3022        // a restore should start ok if we shutdown properly
3023        {
3024            let res = subs.restore(id, &subscriptions_path, &schema, &pool, tripwire.clone());
3025            assert!(res.is_ok());
3026        }
3027
3028        // restore should fail if we don't shutdown properly
3029        {
3030            let conn =
3031                rusqlite::Connection::open(subscriptions_path.join(matcher_id).join("sub.sqlite"))
3032                    .unwrap();
3033            conn.execute(
3034                "INSERT OR REPLACE INTO meta (key, value) VALUES ('state', 'running')",
3035                (),
3036            )
3037            .unwrap();
3038
3039            let res = subs.restore(id, &subscriptions_path, &schema, &pool, tripwire.clone());
3040            assert!(res.is_err());
3041        }
3042    }
3043
3044    fn filter_changes_from_db(
3045        matcher: &MatcherHandle,
3046        state_conn: &Connection,
3047        actor_id: Option<ActorId>,
3048        db_version: CrsqlDbVersion,
3049    ) -> rusqlite::Result<()> {
3050        let mut candidates = MatchCandidates::new();
3051
3052        let mut prepped = state_conn.prepare_cached(r#"SELECT "table", pk, cid, val, col_version, db_version, seq, site_id, cl  FROM crsql_changes WHERE site_id = COALESCE(?, crsql_site_id()) AND db_version = ? ORDER BY seq ASC"#)?;
3053        let rows = prepped
3054            .query_map(params![actor_id, db_version], row_to_change)
3055            .unwrap();
3056
3057        for row in rows {
3058            let change = row?;
3059            matcher.filter_matchable_change(&mut candidates, (&change).into());
3060        }
3061
3062        if let Err(e) = matcher.inner.changes_tx.try_send(candidates) {
3063            error!(sub_id = %matcher.inner.id, "could not send candidates to matcher: {e}");
3064        }
3065        Ok(())
3066    }
3067}