From 7e24e409e6bfababded9f72065c9544f8ebb9698 Mon Sep 17 00:00:00 2001
From: Paul Morris <10599524+pmorris-dev@users.noreply.github.com>
Date: Wed, 18 Feb 2026 16:39:02 -0500
Subject: [PATCH] feat: add fetch_page Tauri command for keyset pagination
Bridge the toolkit's keyset pagination to the Tauri plugin layer:
- New `fetch_page` command with `after`/`before` cursor params and
`attach` support for cross-database queries
- Conflicting cursors (both after and before) rejected at command level
- Command registered in plugin builder and permission system
- `allow-fetch-page` added to default permission set
---
Cargo.toml | 2 +-
build.rs | 1 +
.../autogenerated/commands/fetch_page.toml | 13 ++++++
permissions/autogenerated/reference.md | 27 ++++++++++++
permissions/default.toml | 1 +
permissions/schemas/schema.json | 16 ++++++-
src/commands.rs | 44 +++++++++++++++++++
src/lib.rs | 1 +
8 files changed, 102 insertions(+), 3 deletions(-)
create mode 100644 permissions/autogenerated/commands/fetch_page.toml
diff --git a/Cargo.toml b/Cargo.toml
index d085825..eb63037 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,7 @@ members = [
"crates/sqlx-sqlite-observer",
"crates/sqlx-sqlite-toolkit",
]
-exclude = ["examples/observer-demo/src-tauri"]
+exclude = ["examples/observer-demo/src-tauri", "examples/pagination-demo/src-tauri"]
[package]
name = "tauri-plugin-sqlite"
diff --git a/build.rs b/build.rs
index 9fd7bf2..8fa635a 100644
--- a/build.rs
+++ b/build.rs
@@ -8,6 +8,7 @@ fn main() {
"transaction_read",
"fetch_all",
"fetch_one",
+ "fetch_page",
"close",
"close_all",
"remove",
diff --git a/permissions/autogenerated/commands/fetch_page.toml b/permissions/autogenerated/commands/fetch_page.toml
new file mode 100644
index 0000000..54dbb63
--- /dev/null
+++ b/permissions/autogenerated/commands/fetch_page.toml
@@ -0,0 +1,13 @@
+# Automatically generated - DO NOT EDIT!
+
+"$schema" = "../../schemas/schema.json"
+
+[[permission]]
+identifier = "allow-fetch-page"
+description = "Enables the fetch_page command without any pre-configured scope."
+commands.allow = ["fetch_page"]
+
+[[permission]]
+identifier = "deny-fetch-page"
+description = "Denies the fetch_page command without any pre-configured scope."
+commands.deny = ["fetch_page"]
diff --git a/permissions/autogenerated/reference.md b/permissions/autogenerated/reference.md
index 87d0cf1..6c04d45 100644
--- a/permissions/autogenerated/reference.md
+++ b/permissions/autogenerated/reference.md
@@ -12,6 +12,7 @@ Default permissions for the sqlite plugin - allows all database operations
- `allow-transaction-read`
- `allow-fetch-all`
- `allow-fetch-one`
+- `allow-fetch-page`
- `allow-close`
- `allow-close-all`
- `allow-remove`
@@ -215,6 +216,32 @@ Denies the fetch_one command without any pre-configured scope.
|
+`sqlite:allow-fetch-page`
+
+ |
+
+
+Enables the fetch_page command without any pre-configured scope.
+
+ |
+
+
+
+|
+
+`sqlite:deny-fetch-page`
+
+ |
+
+
+Denies the fetch_page command without any pre-configured scope.
+
+ |
+
+
+
+|
+
`sqlite:allow-get-migration-events`
|
diff --git a/permissions/default.toml b/permissions/default.toml
index 3ca5e44..a2494c7 100644
--- a/permissions/default.toml
+++ b/permissions/default.toml
@@ -15,6 +15,7 @@ permissions = [
"allow-transaction-read",
"allow-fetch-all",
"allow-fetch-one",
+ "allow-fetch-page",
"allow-close",
"allow-close-all",
"allow-remove",
diff --git a/permissions/schemas/schema.json b/permissions/schemas/schema.json
index a2f9459..ff6dbd9 100644
--- a/permissions/schemas/schema.json
+++ b/permissions/schemas/schema.json
@@ -378,6 +378,18 @@
"const": "deny-fetch-one",
"markdownDescription": "Denies the fetch_one command without any pre-configured scope."
},
+ {
+ "description": "Enables the fetch_page command without any pre-configured scope.",
+ "type": "string",
+ "const": "allow-fetch-page",
+ "markdownDescription": "Enables the fetch_page command without any pre-configured scope."
+ },
+ {
+ "description": "Denies the fetch_page command without any pre-configured scope.",
+ "type": "string",
+ "const": "deny-fetch-page",
+ "markdownDescription": "Denies the fetch_page command without any pre-configured scope."
+ },
{
"description": "Enables the get_migration_events command without any pre-configured scope.",
"type": "string",
@@ -499,10 +511,10 @@
"markdownDescription": "Denies the unsubscribe command without any pre-configured scope."
},
{
- "description": "Default permissions for the sqlite plugin - allows all database operations\n#### This default permission set includes:\n\n- `allow-load`\n- `allow-execute`\n- `allow-execute-transaction`\n- `allow-begin-interruptible-transaction`\n- `allow-transaction-continue`\n- `allow-transaction-read`\n- `allow-fetch-all`\n- `allow-fetch-one`\n- `allow-close`\n- `allow-close-all`\n- `allow-remove`\n- `allow-get-migration-events`\n- `allow-observe`\n- `allow-subscribe`\n- `allow-unsubscribe`\n- `allow-unobserve`",
+ "description": "Default permissions for the sqlite plugin - allows all database operations\n#### This default permission set includes:\n\n- `allow-load`\n- `allow-execute`\n- `allow-execute-transaction`\n- `allow-begin-interruptible-transaction`\n- `allow-transaction-continue`\n- `allow-transaction-read`\n- `allow-fetch-all`\n- `allow-fetch-one`\n- `allow-fetch-page`\n- `allow-close`\n- `allow-close-all`\n- `allow-remove`\n- `allow-get-migration-events`\n- `allow-observe`\n- `allow-subscribe`\n- `allow-unsubscribe`\n- `allow-unobserve`",
"type": "string",
"const": "default",
- "markdownDescription": "Default permissions for the sqlite plugin - allows all database operations\n#### This default permission set includes:\n\n- `allow-load`\n- `allow-execute`\n- `allow-execute-transaction`\n- `allow-begin-interruptible-transaction`\n- `allow-transaction-continue`\n- `allow-transaction-read`\n- `allow-fetch-all`\n- `allow-fetch-one`\n- `allow-close`\n- `allow-close-all`\n- `allow-remove`\n- `allow-get-migration-events`\n- `allow-observe`\n- `allow-subscribe`\n- `allow-unsubscribe`\n- `allow-unobserve`"
+ "markdownDescription": "Default permissions for the sqlite plugin - allows all database operations\n#### This default permission set includes:\n\n- `allow-load`\n- `allow-execute`\n- `allow-execute-transaction`\n- `allow-begin-interruptible-transaction`\n- `allow-transaction-continue`\n- `allow-transaction-read`\n- `allow-fetch-all`\n- `allow-fetch-one`\n- `allow-fetch-page`\n- `allow-close`\n- `allow-close-all`\n- `allow-remove`\n- `allow-get-migration-events`\n- `allow-observe`\n- `allow-subscribe`\n- `allow-unsubscribe`\n- `allow-unobserve`"
}
]
}
diff --git a/src/commands.rs b/src/commands.rs
index 3980210..68adc67 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -337,6 +337,50 @@ pub async fn fetch_one(
Ok(result)
}
+/// Execute a paginated SELECT query using keyset (cursor-based) pagination
+#[allow(clippy::too_many_arguments)]
+#[tauri::command]
+pub async fn fetch_page(
+ db_instances: State<'_, DbInstances>,
+ db: String,
+ query: String,
+ values: Vec,
+ keyset: Vec,
+ page_size: usize,
+ after: Option>,
+ before: Option>,
+ attached: Option>,
+) -> Result {
+ if after.is_some() && before.is_some() {
+ return Err(Error::Toolkit(
+ sqlx_sqlite_toolkit::Error::ConflictingCursors,
+ ));
+ }
+
+ let instances = db_instances.0.read().await;
+
+ let wrapper = instances
+ .get(&db)
+ .ok_or_else(|| Error::DatabaseNotLoaded(db.clone()))?;
+
+ let mut builder = wrapper.fetch_page(query, values, keyset, page_size);
+
+ if let Some(cursor_values) = after {
+ builder = builder.after(cursor_values);
+ } else if let Some(cursor_values) = before {
+ builder = builder.before(cursor_values);
+ }
+
+ if let Some(specs) = attached {
+ let resolved_specs = resolve_attached_specs(specs, &instances)?;
+ builder = builder.attach(resolved_specs);
+ }
+
+ let result = builder.execute().await?;
+
+ Ok(result)
+}
+
/// Close a specific database connection
///
/// Returns `true` if the database was loaded and successfully closed.
diff --git a/src/lib.rs b/src/lib.rs
index 663b87d..e40cee2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -184,6 +184,7 @@ impl Builder {
commands::transaction_read,
commands::fetch_all,
commands::fetch_one,
+ commands::fetch_page,
commands::close,
commands::close_all,
commands::remove,