Skip to main content

drasi_source_sqlite/
lib.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![allow(unexpected_cfgs)]
16
17//! SQLite source plugin for Drasi.
18//!
19//! This source owns an embedded SQLite connection running in a dedicated thread.
20//! Changes are captured through SQLite hooks:
21//! - `preupdate_hook` captures row-level INSERT/UPDATE/DELETE events
22//! - `commit_hook` flushes buffered changes
23//! - `rollback_hook` discards buffered changes
24//!
25//! The source supports:
26//! - file-backed and in-memory SQLite databases
27//! - optional table filtering
28//! - optional REST CRUD + transactional batch endpoints
29//! - bootstrap via pluggable bootstrap providers
30
31mod config;
32mod convert;
33pub mod descriptor;
34mod rest_api;
35mod thread;
36
37pub use config::{
38    RestApiConfig, SqliteSourceBuilder, SqliteSourceConfig, StartFrom, TableKeyConfig,
39};
40pub use thread::SqliteParam;
41
42use anyhow::{anyhow, Result};
43use async_trait::async_trait;
44use drasi_lib::channels::{ComponentStatus, SourceEvent, SourceEventWrapper, SubscriptionResponse};
45use drasi_lib::sources::base::SourceBase;
46use drasi_lib::Source;
47use std::collections::HashMap;
48use std::future::Future;
49use std::sync::Arc;
50use tokio::sync::{mpsc, oneshot, RwLock};
51use tracing::Instrument;
52
53use crate::thread::SqliteCommand;
54
55/// Cloneable handle for issuing SQL statements against the source-owned connection.
56#[derive(Clone)]
57pub struct SqliteSourceHandle {
58    command_tx: Arc<RwLock<Option<mpsc::UnboundedSender<SqliteCommand>>>>,
59    source_id: Arc<str>,
60}
61
62impl SqliteSourceHandle {
63    async fn sender(&self) -> Result<mpsc::UnboundedSender<SqliteCommand>> {
64        self.command_tx
65            .read()
66            .await
67            .clone()
68            .ok_or_else(|| anyhow!("source '{}' is not running", self.source_id))
69    }
70
71    /// Source ID this handle is associated with.
72    pub fn source_id(&self) -> &str {
73        &self.source_id
74    }
75
76    /// Execute a SQL statement.
77    pub async fn execute(&self, sql: impl Into<String>) -> Result<usize> {
78        let (response_tx, response_rx) = oneshot::channel();
79        self.sender().await?.send(SqliteCommand::Execute {
80            sql: sql.into(),
81            response_tx,
82        })?;
83        response_rx
84            .await
85            .map_err(|_| anyhow!("sqlite thread closed response channel"))?
86    }
87
88    /// Execute a parameterized SQL statement with bind parameters.
89    pub async fn execute_parameterized(
90        &self,
91        sql: impl Into<String>,
92        params: Vec<SqliteParam>,
93    ) -> Result<usize> {
94        let (response_tx, response_rx) = oneshot::channel();
95        self.sender()
96            .await?
97            .send(SqliteCommand::ExecuteParameterized {
98                sql: sql.into(),
99                params,
100                response_tx,
101            })?;
102        response_rx
103            .await
104            .map_err(|_| anyhow!("sqlite thread closed response channel"))?
105    }
106
107    /// Execute a SQL script batch.
108    pub async fn execute_batch(&self, sql: impl Into<String>) -> Result<()> {
109        let (response_tx, response_rx) = oneshot::channel();
110        self.sender().await?.send(SqliteCommand::ExecuteBatch {
111            sql: sql.into(),
112            response_tx,
113        })?;
114        response_rx
115            .await
116            .map_err(|_| anyhow!("sqlite thread closed response channel"))?
117    }
118
119    /// Execute a query and return rows as JSON objects.
120    pub async fn query(
121        &self,
122        sql: impl Into<String>,
123    ) -> Result<Vec<serde_json::Map<String, serde_json::Value>>> {
124        let (response_tx, response_rx) = oneshot::channel();
125        self.sender().await?.send(SqliteCommand::QueryRows {
126            sql: sql.into(),
127            response_tx,
128        })?;
129        response_rx
130            .await
131            .map_err(|_| anyhow!("sqlite thread closed response channel"))?
132    }
133
134    /// Execute a parameterized query and return rows as JSON objects.
135    pub async fn query_parameterized(
136        &self,
137        sql: impl Into<String>,
138        params: Vec<SqliteParam>,
139    ) -> Result<Vec<serde_json::Map<String, serde_json::Value>>> {
140        let (response_tx, response_rx) = oneshot::channel();
141        self.sender()
142            .await?
143            .send(SqliteCommand::QueryRowsParameterized {
144                sql: sql.into(),
145                params,
146                response_tx,
147            })?;
148        response_rx
149            .await
150            .map_err(|_| anyhow!("sqlite thread closed response channel"))?
151    }
152
153    async fn begin_transaction(&self) -> Result<()> {
154        let (response_tx, response_rx) = oneshot::channel();
155        self.sender()
156            .await?
157            .send(SqliteCommand::BeginTransaction { response_tx })?;
158        response_rx
159            .await
160            .map_err(|_| anyhow!("sqlite thread closed response channel"))?
161    }
162
163    async fn commit_transaction(&self) -> Result<()> {
164        let (response_tx, response_rx) = oneshot::channel();
165        self.sender()
166            .await?
167            .send(SqliteCommand::CommitTransaction { response_tx })?;
168        response_rx
169            .await
170            .map_err(|_| anyhow!("sqlite thread closed response channel"))?
171    }
172
173    async fn rollback_transaction(&self) -> Result<()> {
174        let (response_tx, response_rx) = oneshot::channel();
175        self.sender()
176            .await?
177            .send(SqliteCommand::RollbackTransaction { response_tx })?;
178        response_rx
179            .await
180            .map_err(|_| anyhow!("sqlite thread closed response channel"))?
181    }
182
183    /// Execute a sequence of SQL statements atomically.
184    pub async fn execute_statements_in_transaction(&self, statements: Vec<String>) -> Result<()> {
185        self.begin_transaction().await?;
186
187        for statement in statements {
188            if let Err(err) = self.execute(statement).await {
189                let _ = self.rollback_transaction().await;
190                return Err(err);
191            }
192        }
193
194        self.commit_transaction().await
195    }
196
197    /// Execute multiple parameterized statements atomically in a transaction.
198    pub async fn execute_parameterized_in_transaction(
199        &self,
200        statements: Vec<(String, Vec<SqliteParam>)>,
201    ) -> Result<()> {
202        self.begin_transaction().await?;
203
204        for (sql, params) in statements {
205            if let Err(err) = self.execute_parameterized(sql, params).await {
206                let _ = self.rollback_transaction().await;
207                return Err(err);
208            }
209        }
210
211        self.commit_transaction().await
212    }
213
214    /// Execute user logic in a transaction scope.
215    pub async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
216    where
217        F: FnOnce(SqliteTxHandle) -> Fut,
218        Fut: Future<Output = Result<T>>,
219    {
220        self.begin_transaction().await?;
221        let tx_handle = SqliteTxHandle {
222            handle: self.clone(),
223        };
224
225        match f(tx_handle).await {
226            Ok(value) => {
227                self.commit_transaction().await?;
228                Ok(value)
229            }
230            Err(err) => {
231                let _ = self.rollback_transaction().await;
232                Err(err)
233            }
234        }
235    }
236}
237
238/// Transaction-scoped SQL handle.
239#[derive(Clone)]
240pub struct SqliteTxHandle {
241    handle: SqliteSourceHandle,
242}
243
244impl SqliteTxHandle {
245    pub async fn execute(&self, sql: impl Into<String>) -> Result<usize> {
246        self.handle.execute(sql).await
247    }
248
249    pub async fn execute_batch(&self, sql: impl Into<String>) -> Result<()> {
250        self.handle.execute_batch(sql).await
251    }
252}
253
254/// SQLite source implementation.
255pub struct SqliteSource {
256    base: SourceBase,
257    config: SqliteSourceConfig,
258    command_tx: Arc<RwLock<Option<mpsc::UnboundedSender<SqliteCommand>>>>,
259    thread_handle: Arc<RwLock<Option<std::thread::JoinHandle<()>>>>,
260    rest_shutdown_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
261}
262
263impl SqliteSource {
264    pub fn builder(id: impl Into<String>) -> SqliteSourceBuilder {
265        SqliteSourceBuilder::new(id)
266    }
267
268    pub(crate) fn from_parts(base: SourceBase, config: SqliteSourceConfig) -> Result<Self> {
269        Ok(Self {
270            base,
271            config,
272            command_tx: Arc::new(RwLock::new(None)),
273            thread_handle: Arc::new(RwLock::new(None)),
274            rest_shutdown_tx: Arc::new(RwLock::new(None)),
275        })
276    }
277
278    /// Get a handle for issuing SQL operations against this source.
279    pub fn handle(&self) -> SqliteSourceHandle {
280        SqliteSourceHandle {
281            command_tx: self.command_tx.clone(),
282            source_id: Arc::from(self.base.id.as_str()),
283        }
284    }
285}
286
287#[async_trait]
288impl Source for SqliteSource {
289    fn id(&self) -> &str {
290        &self.base.id
291    }
292
293    fn type_name(&self) -> &str {
294        "sqlite"
295    }
296
297    fn properties(&self) -> HashMap<String, serde_json::Value> {
298        use crate::descriptor::SqliteSourceConfigDto;
299
300        self.base
301            .properties_or_serialize(&SqliteSourceConfigDto::from(&self.config))
302    }
303
304    fn auto_start(&self) -> bool {
305        self.base.get_auto_start()
306    }
307
308    async fn start(&self) -> Result<()> {
309        if self.base.get_status().await == ComponentStatus::Running {
310            return Ok(());
311        }
312
313        self.base
314            .set_status(
315                ComponentStatus::Starting,
316                Some("Starting SQLite source".to_string()),
317            )
318            .await;
319
320        let (command_tx, command_rx) = mpsc::unbounded_channel::<SqliteCommand>();
321        *self.command_tx.write().await = Some(command_tx.clone());
322        let (event_tx, mut event_rx) = mpsc::unbounded_channel::<thread::ChangeEvent>();
323
324        let thread_config = thread::SqliteThreadConfig {
325            path: self.config.path.clone(),
326            tables: self.config.tables.clone(),
327        };
328        let source_id_for_thread = self.base.id.clone();
329        let handle = std::thread::spawn(move || {
330            if let Err(err) = thread::run_sqlite_thread(thread_config, command_rx, event_tx) {
331                log::error!("sqlite source thread '{source_id_for_thread}' failed: {err}");
332            }
333        });
334        *self.thread_handle.write().await = Some(handle);
335
336        let configured_keys = self
337            .config
338            .table_keys
339            .iter()
340            .map(|item| (item.table.clone(), item.key_columns.clone()))
341            .collect::<HashMap<_, _>>();
342
343        let source_id = self.base.id.clone();
344        let dispatchers = self.base.dispatchers.clone();
345        let instance_id = self
346            .base
347            .context()
348            .await
349            .map(|ctx| ctx.instance_id)
350            .unwrap_or_default();
351
352        let span = tracing::info_span!(
353            "sqlite_source_dispatcher",
354            instance_id = %instance_id,
355            component_id = %source_id,
356            component_type = "source"
357        );
358        let task = tokio::spawn(
359            async move {
360                while let Some(event) = event_rx.recv().await {
361                    let configured = configured_keys.get(&event.table).map(|v| v.as_slice());
362                    let change =
363                        convert::change_event_to_source_change(event, &source_id, configured);
364                    let mut profiling = drasi_lib::profiling::ProfilingMetadata::new();
365                    profiling.source_send_ns = Some(drasi_lib::profiling::timestamp_ns());
366
367                    let wrapper = SourceEventWrapper::with_profiling(
368                        source_id.clone(),
369                        SourceEvent::Change(change),
370                        chrono::Utc::now(),
371                        profiling,
372                    );
373                    if let Err(err) =
374                        SourceBase::dispatch_from_task(dispatchers.clone(), wrapper, &source_id)
375                            .await
376                    {
377                        log::debug!("failed dispatching sqlite change for '{source_id}': {err}");
378                    }
379                }
380            }
381            .instrument(span),
382        );
383        *self.base.task_handle.write().await = Some(task);
384
385        if let Some(rest_config) = &self.config.rest_api {
386            let (shutdown_tx, shutdown_rx) = oneshot::channel();
387            rest_api::start_rest_api(
388                rest_config.clone(),
389                self.handle(),
390                self.config.tables.clone(),
391                self.config.table_keys.clone(),
392                shutdown_rx,
393            )
394            .await?;
395            *self.rest_shutdown_tx.write().await = Some(shutdown_tx);
396        }
397
398        self.base
399            .set_status(
400                ComponentStatus::Running,
401                Some("SQLite source running".to_string()),
402            )
403            .await;
404
405        Ok(())
406    }
407
408    async fn stop(&self) -> Result<()> {
409        if self.base.get_status().await != ComponentStatus::Running {
410            return Ok(());
411        }
412
413        self.base
414            .set_status(
415                ComponentStatus::Stopping,
416                Some("Stopping SQLite source".to_string()),
417            )
418            .await;
419
420        if let Some(rest_shutdown_tx) = self.rest_shutdown_tx.write().await.take() {
421            let _ = rest_shutdown_tx.send(());
422        }
423
424        if let Some(sender) = self.command_tx.write().await.take() {
425            let _ = sender.send(SqliteCommand::Shutdown);
426        }
427
428        if let Some(handle) = self.thread_handle.write().await.take() {
429            let _ = tokio::task::spawn_blocking(move || handle.join()).await;
430        }
431
432        if let Some(task) = self.base.task_handle.write().await.take() {
433            task.abort();
434        }
435
436        self.base
437            .set_status(
438                ComponentStatus::Stopped,
439                Some("SQLite source stopped".to_string()),
440            )
441            .await;
442
443        Ok(())
444    }
445
446    async fn status(&self) -> ComponentStatus {
447        self.base.get_status().await
448    }
449
450    async fn subscribe(
451        &self,
452        settings: drasi_lib::config::SourceSubscriptionSettings,
453    ) -> Result<SubscriptionResponse> {
454        self.base
455            .subscribe_with_bootstrap(&settings, "SQLite")
456            .await
457    }
458
459    fn as_any(&self) -> &dyn std::any::Any {
460        self
461    }
462
463    async fn initialize(&self, context: drasi_lib::context::SourceRuntimeContext) {
464        self.base.initialize(context).await;
465    }
466
467    async fn set_bootstrap_provider(
468        &self,
469        provider: Box<dyn drasi_lib::bootstrap::BootstrapProvider + 'static>,
470    ) {
471        self.base.set_bootstrap_provider(provider).await;
472    }
473}
474
475/// Dynamic plugin entry point.
476#[cfg(feature = "dynamic-plugin")]
477drasi_plugin_sdk::export_plugin!(
478    plugin_id = "sqlite-source",
479    core_version = env!("CARGO_PKG_VERSION"),
480    lib_version = env!("CARGO_PKG_VERSION"),
481    plugin_version = env!("CARGO_PKG_VERSION"),
482    source_descriptors = [descriptor::SqliteSourceDescriptor],
483    reaction_descriptors = [],
484    bootstrap_descriptors = [],
485);