diff --git a/plugins/sql/permissions/autogenerated/commands/remove_update_hook.toml b/plugins/sql/permissions/autogenerated/commands/remove_update_hook.toml
new file mode 100644
index 0000000000..0178cbdcf5
--- /dev/null
+++ b/plugins/sql/permissions/autogenerated/commands/remove_update_hook.toml
@@ -0,0 +1,13 @@
+# Automatically generated - DO NOT EDIT!
+
+"$schema" = "../../schemas/schema.json"
+
+[[permission]]
+identifier = "allow-remove-update-hook"
+description = "Enables the remove_update_hook command without any pre-configured scope."
+commands.allow = ["remove_update_hook"]
+
+[[permission]]
+identifier = "deny-remove-update-hook"
+description = "Denies the remove_update_hook command without any pre-configured scope."
+commands.deny = ["remove_update_hook"]
diff --git a/plugins/sql/permissions/autogenerated/commands/setup_update_hook.toml b/plugins/sql/permissions/autogenerated/commands/setup_update_hook.toml
new file mode 100644
index 0000000000..b8e54470d0
--- /dev/null
+++ b/plugins/sql/permissions/autogenerated/commands/setup_update_hook.toml
@@ -0,0 +1,13 @@
+# Automatically generated - DO NOT EDIT!
+
+"$schema" = "../../schemas/schema.json"
+
+[[permission]]
+identifier = "allow-setup-update-hook"
+description = "Enables the setup_update_hook command without any pre-configured scope."
+commands.allow = ["setup_update_hook"]
+
+[[permission]]
+identifier = "deny-setup-update-hook"
+description = "Denies the setup_update_hook command without any pre-configured scope."
+commands.deny = ["setup_update_hook"]
diff --git a/plugins/sql/permissions/autogenerated/reference.md b/plugins/sql/permissions/autogenerated/reference.md
index c829b103e8..7a60eaf5ee 100644
--- a/plugins/sql/permissions/autogenerated/reference.md
+++ b/plugins/sql/permissions/autogenerated/reference.md
@@ -106,6 +106,32 @@ Denies the load command without any pre-configured scope.
|
+`sql:allow-remove-update-hook`
+
+ |
+
+
+Enables the remove_update_hook command without any pre-configured scope.
+
+ |
+
+
+
+|
+
+`sql:deny-remove-update-hook`
+
+ |
+
+
+Denies the remove_update_hook command without any pre-configured scope.
+
+ |
+
+
+
+|
+
`sql:allow-select`
|
@@ -126,6 +152,32 @@ Enables the select command without any pre-configured scope.
Denies the select command without any pre-configured scope.
+
+
+
+
+|
+
+`sql:allow-setup-update-hook`
+
+ |
+
+
+Enables the setup_update_hook command without any pre-configured scope.
+
+ |
+
+
+
+|
+
+`sql:deny-setup-update-hook`
+
+ |
+
+
+Denies the setup_update_hook command without any pre-configured scope.
+
|
diff --git a/plugins/sql/permissions/schemas/schema.json b/plugins/sql/permissions/schemas/schema.json
index 488a953c59..3c7093443c 100644
--- a/plugins/sql/permissions/schemas/schema.json
+++ b/plugins/sql/permissions/schemas/schema.json
@@ -330,6 +330,18 @@
"const": "deny-load",
"markdownDescription": "Denies the load command without any pre-configured scope."
},
+ {
+ "description": "Enables the remove_update_hook command without any pre-configured scope.",
+ "type": "string",
+ "const": "allow-remove-update-hook",
+ "markdownDescription": "Enables the remove_update_hook command without any pre-configured scope."
+ },
+ {
+ "description": "Denies the remove_update_hook command without any pre-configured scope.",
+ "type": "string",
+ "const": "deny-remove-update-hook",
+ "markdownDescription": "Denies the remove_update_hook command without any pre-configured scope."
+ },
{
"description": "Enables the select command without any pre-configured scope.",
"type": "string",
@@ -342,6 +354,18 @@
"const": "deny-select",
"markdownDescription": "Denies the select command without any pre-configured scope."
},
+ {
+ "description": "Enables the setup_update_hook command without any pre-configured scope.",
+ "type": "string",
+ "const": "allow-setup-update-hook",
+ "markdownDescription": "Enables the setup_update_hook command without any pre-configured scope."
+ },
+ {
+ "description": "Denies the setup_update_hook command without any pre-configured scope.",
+ "type": "string",
+ "const": "deny-setup-update-hook",
+ "markdownDescription": "Denies the setup_update_hook command without any pre-configured scope."
+ },
{
"description": "### Default Permissions\n\nThis permission set configures what kind of\ndatabase operations are available from the sql plugin.\n\n### Granted Permissions\n\nAll reading related operations are enabled.\nAlso allows to load or close a connection.\n\n\n#### This default permission set includes:\n\n- `allow-close`\n- `allow-load`\n- `allow-select`",
"type": "string",
diff --git a/plugins/sql/src/lib.rs b/plugins/sql/src/lib.rs
index 56b2a3a6b7..96b8b93139 100644
--- a/plugins/sql/src/lib.rs
+++ b/plugins/sql/src/lib.rs
@@ -12,6 +12,8 @@
mod commands;
mod decode;
mod error;
+#[cfg(feature = "sqlite")]
+mod update_hook;
mod wrapper;
pub use error::Error;
@@ -140,7 +142,11 @@ impl Builder {
commands::load,
commands::execute,
commands::select,
- commands::close
+ commands::close,
+ #[cfg(feature = "sqlite")]
+ update_hook::setup_update_hook,
+ #[cfg(feature = "sqlite")]
+ update_hook::remove_update_hook,
])
.setup(|app, api| {
let config = api.config().clone().unwrap_or_default();
diff --git a/plugins/sql/src/update_hook.rs b/plugins/sql/src/update_hook.rs
new file mode 100644
index 0000000000..710b91c0c4
--- /dev/null
+++ b/plugins/sql/src/update_hook.rs
@@ -0,0 +1,98 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use serde::Serialize;
+use tauri::{command, AppHandle, Emitter, Runtime, State};
+
+use crate::{DbInstances, DbPool, Error};
+
+#[derive(Clone, Serialize)]
+pub struct UpdateHookEvent {
+ pub operation: String,
+ pub database: String,
+ pub table: String,
+ pub rowid: i64,
+}
+
+#[command]
+pub(crate) async fn setup_update_hook(
+ app: AppHandle,
+ db_instances: State<'_, DbInstances>,
+ db: String,
+) -> Result<(), Error> {
+ #[cfg(feature = "sqlite")]
+ {
+ let instances = db_instances.0.read().await;
+ let db_pool = instances
+ .get(&db)
+ .ok_or_else(|| Error::DatabaseNotLoaded(db.clone()))?;
+
+ let sqlite_pool = match db_pool {
+ DbPool::Sqlite(pool) => pool,
+ _ => return Err(Error::InvalidDbUrl(
+ format!("Cannot setup update hook for {}: update hooks are only supported for SQLite databases", db)
+ )),
+ };
+
+ sqlite_pool
+ .rebuild_pool(Some(move |result: sqlx::sqlite::UpdateHookResult| {
+ let operation = match result.operation {
+ sqlx::sqlite::SqliteOperation::Insert => "INSERT",
+ sqlx::sqlite::SqliteOperation::Update => "UPDATE",
+ sqlx::sqlite::SqliteOperation::Delete => "DELETE",
+ sqlx::sqlite::SqliteOperation::Unknown(_) => "UNKNOWN",
+ };
+
+ let event = UpdateHookEvent {
+ operation: operation.to_string(),
+ database: result.database.to_string(),
+ table: result.table.to_string(),
+ rowid: result.rowid,
+ };
+
+ if let Err(e) = app.emit("sqlite-update-hook", &event) {
+ log::error!("[tauri-plugin-sql] Failed to emit update hook event: {}", e);
+ }
+ }))
+ .await
+ .map_err(Error::Sql)?;
+
+ Ok(())
+ }
+}
+
+#[command]
+pub(crate) async fn remove_update_hook(
+ db_instances: State<'_, DbInstances>,
+ db: String,
+) -> Result<(), Error> {
+ #[cfg(feature = "sqlite")]
+ {
+ let instances = db_instances.0.read().await;
+ let db_pool = instances
+ .get(&db)
+ .ok_or_else(|| Error::DatabaseNotLoaded(db.clone()))?;
+
+ let sqlite_pool = match db_pool {
+ DbPool::Sqlite(pool) => pool,
+ _ => return Err(Error::InvalidDbUrl(
+ format!("Cannot remove update hook for {}: update hooks are only supported for SQLite databases", db)
+ )),
+ };
+
+ sqlite_pool
+ .rebuild_pool(None::)
+ .await
+ .map_err(Error::Sql)?;
+
+ Ok(())
+ }
+
+ #[cfg(not(feature = "sqlite"))]
+ {
+ Err(Error::InvalidDbUrl(
+ "Update hooks are only supported for SQLite".to_string(),
+ ))
+ }
+}
diff --git a/plugins/sql/src/wrapper.rs b/plugins/sql/src/wrapper.rs
index d47b2d1cbe..2ba4ff2f58 100644
--- a/plugins/sql/src/wrapper.rs
+++ b/plugins/sql/src/wrapper.rs
@@ -18,13 +18,63 @@ use sqlx::MySql;
#[cfg(feature = "postgres")]
use sqlx::Postgres;
#[cfg(feature = "sqlite")]
-use sqlx::Sqlite;
+use sqlx::{pool::PoolOptions, sqlite::SqliteConnection, Sqlite};
+#[cfg(feature = "sqlite")]
+use std::sync::Arc;
+#[cfg(feature = "sqlite")]
+use tokio::sync::RwLock;
use crate::LastInsertId;
+#[cfg(feature = "sqlite")]
+pub struct SqlitePoolWithHook {
+ pool: Arc>>,
+ db_url: String,
+}
+
+#[cfg(feature = "sqlite")]
+impl SqlitePoolWithHook {
+ pub fn pool(&self) -> Arc>> {
+ Arc::clone(&self.pool)
+ }
+
+ pub fn db_url(&self) -> &str {
+ &self.db_url
+ }
+
+ pub async fn rebuild_pool(&self, hook_fn: Option) -> Result<(), sqlx::Error>
+ where
+ F: Fn(sqlx::sqlite::UpdateHookResult) + Send + Sync + 'static,
+ {
+ let new_pool = if let Some(hook_fn) = hook_fn {
+ let hook_fn = Arc::new(hook_fn);
+ PoolOptions::new()
+ .after_connect(move |conn: &mut SqliteConnection, _meta| {
+ let hook_fn = Arc::clone(&hook_fn);
+ Box::pin(async move {
+ conn.lock_handle().await?.set_update_hook(move |result| {
+ hook_fn(result);
+ });
+ Ok(())
+ })
+ })
+ .connect(&self.db_url)
+ .await?
+ } else {
+ Pool::connect(&self.db_url).await?
+ };
+
+ let mut pool_guard = self.pool.write().await;
+ pool_guard.close().await;
+ *pool_guard = new_pool;
+
+ Ok(())
+ }
+}
+
pub enum DbPool {
#[cfg(feature = "sqlite")]
- Sqlite(Pool),
+ Sqlite(SqlitePoolWithHook),
#[cfg(feature = "mysql")]
MySql(Pool),
#[cfg(feature = "postgres")]
@@ -88,7 +138,11 @@ impl DbPool {
if !Sqlite::database_exists(conn_url).await.unwrap_or(false) {
Sqlite::create_database(conn_url).await?;
}
- Ok(Self::Sqlite(Pool::connect(conn_url).await?))
+ let pool = Pool::connect(conn_url).await?;
+ Ok(Self::Sqlite(SqlitePoolWithHook {
+ pool: Arc::new(RwLock::new(pool)),
+ db_url: conn_url.to_string(),
+ }))
}
#[cfg(feature = "mysql")]
"mysql" => {
@@ -119,7 +173,10 @@ impl DbPool {
) -> Result<(), crate::Error> {
match self {
#[cfg(feature = "sqlite")]
- DbPool::Sqlite(pool) => _migrator.run(pool).await?,
+ DbPool::Sqlite(sqlite_pool) => {
+ let pool = sqlite_pool.pool.read().await;
+ _migrator.run(&*pool).await?
+ }
#[cfg(feature = "mysql")]
DbPool::MySql(pool) => _migrator.run(pool).await?,
#[cfg(feature = "postgres")]
@@ -133,7 +190,7 @@ impl DbPool {
pub(crate) async fn close(&self) {
match self {
#[cfg(feature = "sqlite")]
- DbPool::Sqlite(pool) => pool.close().await,
+ DbPool::Sqlite(sqlite_pool) => sqlite_pool.pool.read().await.close().await,
#[cfg(feature = "mysql")]
DbPool::MySql(pool) => pool.close().await,
#[cfg(feature = "postgres")]
@@ -150,7 +207,8 @@ impl DbPool {
) -> Result<(u64, LastInsertId), crate::Error> {
Ok(match self {
#[cfg(feature = "sqlite")]
- DbPool::Sqlite(pool) => {
+ DbPool::Sqlite(sqlite_pool) => {
+ let pool = sqlite_pool.pool.read().await;
let mut query = sqlx::query(&_query);
for value in _values {
if value.is_null() {
@@ -218,7 +276,8 @@ impl DbPool {
) -> Result>, crate::Error> {
Ok(match self {
#[cfg(feature = "sqlite")]
- DbPool::Sqlite(pool) => {
+ DbPool::Sqlite(sqlite_pool) => {
+ let pool = sqlite_pool.pool.read().await;
let mut query = sqlx::query(&_query);
for value in _values {
if value.is_null() {