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