From 3f71b90c1f18d110458f4136eb008dd6741b39fa Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 14 Jul 2026 10:59:55 +0200 Subject: [PATCH] Add get_prune_table_queries centralized logic (#2268) --- .../src/utils/VaultMergeService.ts | 19 +++-------- .../app/rustcore/VaultMergeService.kt | 18 +++-------- .../Services/VaultMergeService.swift | 14 ++------ .../Services/Database/DbService.cs | 17 +++------- .../JsInterop/RustCore/PruneTableQuery.cs | 28 ++++++++++++++++ .../JsInterop/RustCore/RustCoreService.cs | 22 +++++++++++++ .../wwwroot/js/rustCoreInterop.js | 18 +++++++++++ core/rust/src/ffi.rs | 11 +++++++ core/rust/src/uniffi_api.rs | 11 ++++++- core/rust/src/vault_pruner/mod.rs | 32 +++++++++++++++++++ core/rust/src/wasm.rs | 10 ++++++ 11 files changed, 148 insertions(+), 52 deletions(-) create mode 100644 apps/server/AliasVault.Client/Services/JsInterop/RustCore/PruneTableQuery.cs diff --git a/apps/browser-extension/src/utils/VaultMergeService.ts b/apps/browser-extension/src/utils/VaultMergeService.ts index eb37a20fa..cb2ac9404 100644 --- a/apps/browser-extension/src/utils/VaultMergeService.ts +++ b/apps/browser-extension/src/utils/VaultMergeService.ts @@ -4,7 +4,7 @@ import { browser } from 'wxt/browser'; import { TRASH_RETENTION_DAYS } from '@/utils/constants/vault'; import { devLog } from '@/utils/DevLogger'; -import init, { getSyncableTableNames, mergeVaults, pruneVault } from './dist/core/rust/aliasvault_core.js'; +import init, { getPruneTableQueries, getSyncableTableNames, mergeVaults, pruneVault } from './dist/core/rust/aliasvault_core.js'; /** * Record type for JSON data passed to/from Rust. @@ -268,22 +268,11 @@ export class VaultMergeService { const db = this.loadDatabase(SQL, vaultBase64); try { - /* - * Read only the columns the Rust pruner inspects. Blob columns are reduced - * to a 1-byte presence marker to avoid serializing megabytes of binary data - * to JSON on every sync. - */ - const tableQueries: Record = { - Items: 'SELECT Id, IsDeleted, DeletedAt, LogoId FROM Items', - FieldValues: 'SELECT ItemId, IsDeleted FROM FieldValues', - Attachments: 'SELECT Id, ItemId, IsDeleted, substr(Blob, 1, 1) AS Blob FROM Attachments', - TotpCodes: 'SELECT ItemId, IsDeleted FROM TotpCodes', - Passkeys: 'SELECT ItemId, IsDeleted FROM Passkeys', - Logos: 'SELECT Id, IsDeleted, substr(FileData, 1, 1) AS FileData FROM Logos', - }; + // Get the per-table SELECT queries clients should run to build `PruneInput`. + const tableQueries = getPruneTableQueries() as { name: string; query: string }[]; // Read tables as JSON - const tables: TableData[] = Object.entries(tableQueries).map(([name, query]) => ({ + const tables: TableData[] = tableQueries.map(({ name, query }) => ({ name, records: this.readQueryAsJson(db, query), })); diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/rustcore/VaultMergeService.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/rustcore/VaultMergeService.kt index 25c63a5de..e3e568179 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/rustcore/VaultMergeService.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/rustcore/VaultMergeService.kt @@ -220,23 +220,15 @@ object VaultMergeService { val db = SQLiteDatabase.openDatabase(dbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE) try { - // Read only the columns the Rust pruner inspects. Blob columns are reduced - // to a 1-byte presence marker to avoid serializing large binary data to JSON. - val pruneTableQueries = listOf( - "Items" to "SELECT Id, IsDeleted, DeletedAt, LogoId FROM Items", - "FieldValues" to "SELECT ItemId, IsDeleted FROM FieldValues", - "Attachments" to "SELECT Id, ItemId, IsDeleted, substr(Blob, 1, 1) AS Blob FROM Attachments", - "TotpCodes" to "SELECT ItemId, IsDeleted FROM TotpCodes", - "Passkeys" to "SELECT ItemId, IsDeleted FROM Passkeys", - "Logos" to "SELECT Id, IsDeleted, substr(FileData, 1, 1) AS FileData FROM Logos", - ) + // Get the per-table SELECT queries clients should run to build `PruneInput`. + val pruneTableQueries = uniffi.aliasvault_core.getPruneTableQueries() val tables = JSONArray() - for ((tableName, query) in pruneTableQueries) { + for (tableQuery in pruneTableQueries) { tables.put( JSONObject().apply { - put("name", tableName) - put("records", readQuery(db, tableName, query)) + put("name", tableQuery.name) + put("records", readQuery(db, tableQuery.name, tableQuery.query)) }, ) } diff --git a/apps/mobile-app/ios/VaultStoreKit/Services/VaultMergeService.swift b/apps/mobile-app/ios/VaultStoreKit/Services/VaultMergeService.swift index 08cd5caa1..e65b0723d 100644 --- a/apps/mobile-app/ios/VaultStoreKit/Services/VaultMergeService.swift +++ b/apps/mobile-app/ios/VaultStoreKit/Services/VaultMergeService.swift @@ -154,22 +154,14 @@ public class VaultMergeService { let db = try openDatabase(from: decrypted) defer { sqlite3_close(db) } - // Read only the columns the Rust pruner inspects. Blob columns are reduced - // to a 1-byte presence marker to avoid serializing large binary data to JSON. - let pruneTableQueries: [(name: String, sql: String)] = [ - ("Items", "SELECT Id, IsDeleted, DeletedAt, LogoId FROM Items"), - ("FieldValues", "SELECT ItemId, IsDeleted FROM FieldValues"), - ("Attachments", "SELECT Id, ItemId, IsDeleted, substr(Blob, 1, 1) AS Blob FROM Attachments"), - ("TotpCodes", "SELECT ItemId, IsDeleted FROM TotpCodes"), - ("Passkeys", "SELECT ItemId, IsDeleted FROM Passkeys"), - ("Logos", "SELECT Id, IsDeleted, substr(FileData, 1, 1) AS FileData FROM Logos") - ] + // Get the per-table SELECT queries clients should run to build `PruneInput`. + let pruneTableQueries = getPruneTableQueries() var tables: [[String: Any]] = [] for entry in pruneTableQueries { tables.append([ "name": entry.name, - "records": try readQuery(from: db, tableName: entry.name, sql: entry.sql) + "records": try readQuery(from: db, tableName: entry.name, sql: entry.query) ]) } diff --git a/apps/server/AliasVault.Client/Services/Database/DbService.cs b/apps/server/AliasVault.Client/Services/Database/DbService.cs index 539c6299b..6ab0d5d6f 100644 --- a/apps/server/AliasVault.Client/Services/Database/DbService.cs +++ b/apps/server/AliasVault.Client/Services/Database/DbService.cs @@ -1055,27 +1055,20 @@ public sealed class DbService : IDisposable } /// - /// Reads only the columns the Rust pruner inspects. Blob columns are reduced to a - /// 1-byte presence marker to avoid serializing large binary data to JSON. + /// Get the per-table SELECT queries clients should run to build `PruneInput`. /// /// The SQLite connection to read from. /// List of TableData objects containing the trimmed table records. private async Task> ReadPruneTablesAsJsonAsync(SqliteConnection connection) { - var tableQueries = new (string Name, string Query)[] - { - ("Items", "SELECT Id, IsDeleted, DeletedAt, LogoId FROM Items"), - ("FieldValues", "SELECT ItemId, IsDeleted FROM FieldValues"), - ("Attachments", "SELECT Id, ItemId, IsDeleted, substr(Blob, 1, 1) AS Blob FROM Attachments"), - ("TotpCodes", "SELECT ItemId, IsDeleted FROM TotpCodes"), - ("Passkeys", "SELECT ItemId, IsDeleted FROM Passkeys"), - ("Logos", "SELECT Id, IsDeleted, substr(FileData, 1, 1) AS FileData FROM Logos"), - }; + var tableQueries = await _rustCore.GetPruneTableQueriesAsync(); var tables = new List(); - foreach (var (tableName, query) in tableQueries) + foreach (var tableQuery in tableQueries) { + var tableName = tableQuery.Name; + var query = tableQuery.Query; var tableData = new TableData { Name = tableName }; // Check if table exists in the database. diff --git a/apps/server/AliasVault.Client/Services/JsInterop/RustCore/PruneTableQuery.cs b/apps/server/AliasVault.Client/Services/JsInterop/RustCore/PruneTableQuery.cs new file mode 100644 index 000000000..b22186c09 --- /dev/null +++ b/apps/server/AliasVault.Client/Services/JsInterop/RustCore/PruneTableQuery.cs @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) aliasvault. All rights reserved. +// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.Client.Services.JsInterop.RustCore; + +using System.Text.Json.Serialization; + +/// +/// A per-table SELECT query for building prune input, defined in the Rust core. +/// +public class PruneTableQuery +{ + /// + /// Gets or sets the table name. + /// + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the SELECT query reading only the columns the pruner inspects. + /// + [JsonPropertyName("query")] + public string Query { get; set; } = string.Empty; +} diff --git a/apps/server/AliasVault.Client/Services/JsInterop/RustCore/RustCoreService.cs b/apps/server/AliasVault.Client/Services/JsInterop/RustCore/RustCoreService.cs index 5b3eea5d7..708b7b188 100644 --- a/apps/server/AliasVault.Client/Services/JsInterop/RustCore/RustCoreService.cs +++ b/apps/server/AliasVault.Client/Services/JsInterop/RustCore/RustCoreService.cs @@ -124,6 +124,28 @@ public class RustCoreService : IAsyncDisposable return result; } + /// + /// Get the per-table SELECT queries used to build prune input. + /// + /// List of table queries. + /// Thrown if WASM module is unavailable. + public async Task> GetPruneTableQueriesAsync() + { + // Wait for WASM to be available with retries, as it may still be loading. + if (!await WaitForAvailabilityAsync()) + { + throw new InvalidOperationException("Rust WASM module is not available."); + } + + var result = await jsRuntime.InvokeAsync>("rustCoreGetPruneTableQueries"); + if (result == null || result.Count == 0) + { + throw new InvalidOperationException("Failed to get prune table queries from Rust WASM."); + } + + return result; + } + /// /// Prune expired items from trash. /// Items that have been in trash (DeletedAt set) for longer than retentionDays diff --git a/apps/server/AliasVault.Client/wwwroot/js/rustCoreInterop.js b/apps/server/AliasVault.Client/wwwroot/js/rustCoreInterop.js index 3f19f2af2..7f511d918 100644 --- a/apps/server/AliasVault.Client/wwwroot/js/rustCoreInterop.js +++ b/apps/server/AliasVault.Client/wwwroot/js/rustCoreInterop.js @@ -169,6 +169,24 @@ window.rustCoreGetSyncableTableNames = async function() { } }; +/** + * Get the per-table SELECT queries used to build prune input. + * Blob columns are reduced to a 1-byte presence marker. + * @returns {Promise>} Array of table queries. + */ +window.rustCoreGetPruneTableQueries = async function() { + if (!await initRustCore()) { + return []; + } + + try { + return wasmModule.getPruneTableQueries(); + } catch (error) { + console.error('[RustCore] Get prune table queries failed:', error); + return []; + } +}; + /** * Extract domain from URL. * @param {string} url - The URL to extract domain from. diff --git a/core/rust/src/ffi.rs b/core/rust/src/ffi.rs index 174816375..657428a1f 100644 --- a/core/rust/src/ffi.rs +++ b/core/rust/src/ffi.rs @@ -151,6 +151,17 @@ pub extern "C" fn get_syncable_table_names_ffi() -> *mut c_char { } } +/// Get the per-table SELECT queries used to build prune input, as a JSON array +/// of `{ "name": ..., "query": ... }` objects. Blob columns are reduced to a +/// 1-byte presence marker to avoid serializing large binary data to JSON. +#[no_mangle] +pub extern "C" fn get_prune_table_queries_ffi() -> *mut c_char { + match serde_json::to_string(&crate::vault_pruner::get_prune_table_queries()) { + Ok(json) => string_to_c_char(json), + Err(_) => ptr::null_mut(), + } +} + /// Free a string that was allocated by Rust. /// /// # Safety diff --git a/core/rust/src/uniffi_api.rs b/core/rust/src/uniffi_api.rs index 645dbdab2..c52e2fc67 100644 --- a/core/rust/src/uniffi_api.rs +++ b/core/rust/src/uniffi_api.rs @@ -72,6 +72,14 @@ pub fn prune_vault_json(input_json: String) -> Result { crate::vault_pruner::prune_vault_json(&input_json) } +/// Get the per-table SELECT queries used to build prune input. +/// Blob columns are reduced to a 1-byte presence marker to avoid +/// serializing large binary data to JSON. +#[uniffi::export] +pub fn get_prune_table_queries() -> Vec { + crate::vault_pruner::get_prune_table_queries() +} + /// Filter credentials for autofill based on current URL/app and page title. /// /// # Arguments @@ -304,7 +312,8 @@ mod tests { fn test_prune_vault_json() { let input = r#"{ "tables": [{"name": "Items", "records": []}], - "retention_days": 30 + "retention_days": 30, + "current_time": "2024-01-15T10:30:00.000Z" }"#; let result = prune_vault_json(input.to_string()); diff --git a/core/rust/src/vault_pruner/mod.rs b/core/rust/src/vault_pruner/mod.rs index c2cc764be..428e56746 100644 --- a/core/rust/src/vault_pruner/mod.rs +++ b/core/rust/src/vault_pruner/mod.rs @@ -88,6 +88,38 @@ pub struct PruneOutput { pub stats: PruneStats, } +/// A per-table SELECT query for building `PruneInput`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct PruneTableQuery { + /// Table name + pub name: String, + /// SELECT query reading only the columns the pruner inspects + pub query: String, +} + +/// Get the per-table SELECT queries clients should run to build `PruneInput`. +/// +/// Only the columns the pruner inspects are selected; blob columns are reduced +/// to a 1-byte presence marker (via `substr`) to avoid serializing large binary +/// data to JSON on every prune. +pub fn get_prune_table_queries() -> Vec { + [ + ("Items", "SELECT Id, IsDeleted, DeletedAt, LogoId FROM Items"), + ("FieldValues", "SELECT ItemId, IsDeleted FROM FieldValues"), + ("Attachments", "SELECT Id, ItemId, IsDeleted, substr(Blob, 1, 1) AS Blob FROM Attachments"), + ("TotpCodes", "SELECT ItemId, IsDeleted FROM TotpCodes"), + ("Passkeys", "SELECT ItemId, IsDeleted FROM Passkeys"), + ("Logos", "SELECT Id, IsDeleted, substr(FileData, 1, 1) AS FileData FROM Logos"), + ] + .iter() + .map(|(name, query)| PruneTableQuery { + name: (*name).to_string(), + query: (*query).to_string(), + }) + .collect() +} + /// Main entry point: prune expired items from trash. /// /// This function finds all Items with DeletedAt set that are older than diff --git a/core/rust/src/wasm.rs b/core/rust/src/wasm.rs index 9f81d2687..a1f572aec 100644 --- a/core/rust/src/wasm.rs +++ b/core/rust/src/wasm.rs @@ -99,6 +99,16 @@ pub fn prune_vault_json_js(input_json: &str) -> Result { .map_err(|e| JsValue::from_str(&format!("Prune failed: {}", e))) } +/// Get the per-table SELECT queries used to build prune input. +/// +/// Returns an array of `{ name, query }` objects. Blob columns are reduced to a +/// 1-byte presence marker to avoid serializing large binary data to JSON. +#[wasm_bindgen(js_name = getPruneTableQueries)] +pub fn get_prune_table_queries_js() -> Result { + serde_wasm_bindgen::to_value(&crate::vault_pruner::get_prune_table_queries()) + .map_err(|e| JsValue::from_str(&format!("Failed to serialize output: {}", e))) +} + // ═══════════════════════════════════════════════════════════════════════════════ // Credential Matcher WASM Bindings // ═══════════════════════════════════════════════════════════════════════════════