Add get_prune_table_queries centralized logic (#2268)

This commit is contained in:
Leendert de Borst
2026-07-14 10:59:55 +02:00
committed by Leendert de Borst
parent e40b924f51
commit 3f71b90c1f
11 changed files with 148 additions and 52 deletions

View File

@@ -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<string, 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`.
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),
}));

View File

@@ -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))
},
)
}

View File

@@ -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)
])
}

View File

@@ -1055,27 +1055,20 @@ public sealed class DbService : IDisposable
}
/// <summary>
/// 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`.
/// </summary>
/// <param name="connection">The SQLite connection to read from.</param>
/// <returns>List of TableData objects containing the trimmed table records.</returns>
private async Task<List<TableData>> 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<TableData>();
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.

View File

@@ -0,0 +1,28 @@
//-----------------------------------------------------------------------
// <copyright file="PruneTableQuery.cs" company="aliasvault">
// Copyright (c) aliasvault. All rights reserved.
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Client.Services.JsInterop.RustCore;
using System.Text.Json.Serialization;
/// <summary>
/// A per-table SELECT query for building prune input, defined in the Rust core.
/// </summary>
public class PruneTableQuery
{
/// <summary>
/// Gets or sets the table name.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the SELECT query reading only the columns the pruner inspects.
/// </summary>
[JsonPropertyName("query")]
public string Query { get; set; } = string.Empty;
}

View File

@@ -124,6 +124,28 @@ public class RustCoreService : IAsyncDisposable
return result;
}
/// <summary>
/// Get the per-table SELECT queries used to build prune input.
/// </summary>
/// <returns>List of table queries.</returns>
/// <exception cref="InvalidOperationException">Thrown if WASM module is unavailable.</exception>
public async Task<List<PruneTableQuery>> 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<List<PruneTableQuery>>("rustCoreGetPruneTableQueries");
if (result == null || result.Count == 0)
{
throw new InvalidOperationException("Failed to get prune table queries from Rust WASM.");
}
return result;
}
/// <summary>
/// Prune expired items from trash.
/// Items that have been in trash (DeletedAt set) for longer than retentionDays

View File

@@ -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<{name: string, query: string}>>} 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.

View File

@@ -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

View File

@@ -72,6 +72,14 @@ pub fn prune_vault_json(input_json: String) -> Result<String, VaultError> {
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::PruneTableQuery> {
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());

View File

@@ -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<PruneTableQuery> {
[
("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

View File

@@ -99,6 +99,16 @@ pub fn prune_vault_json_js(input_json: &str) -> Result<String, JsValue> {
.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<JsValue, JsValue> {
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
// ═══════════════════════════════════════════════════════════════════════════════