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,