feat(schema): 增加数据库对象列表缓存

This commit is contained in:
t8y2
2026-05-09 17:36:20 +08:00
parent 1fd1cb10eb
commit 5c52560368
12 changed files with 261 additions and 50 deletions
+48
View File
@@ -53,6 +53,11 @@ const SCHEMA_STATEMENTS: &[&str] = &[
id INTEGER PRIMARY KEY CHECK (id = 1),
settings_json TEXT NOT NULL
)",
"CREATE TABLE IF NOT EXISTS schema_cache (
cache_key TEXT PRIMARY KEY,
payload_json TEXT NOT NULL,
updated_at TEXT NOT NULL
)",
];
// ---------------------------------------------------------------------------
@@ -444,6 +449,49 @@ impl Storage {
}
}
// ---------------------------------------------------------------------------
// Schema cache
// ---------------------------------------------------------------------------
impl Storage {
pub async fn save_schema_cache(&self, cache_key: &str, payload: &serde_json::Value) -> Result<(), String> {
let json = serde_json::to_string(payload).map_err(|e| e.to_string())?;
sqlx::query(
"INSERT OR REPLACE INTO schema_cache (cache_key, payload_json, updated_at) \
VALUES (?, ?, datetime('now'))",
)
.bind(cache_key)
.bind(&json)
.execute(&self.db)
.await
.map_err(|e| e.to_string())?;
Ok(())
}
pub async fn load_schema_cache(&self, cache_key: &str) -> Result<Option<serde_json::Value>, String> {
let row: Option<(String,)> = sqlx::query_as("SELECT payload_json FROM schema_cache WHERE cache_key = ?")
.bind(cache_key)
.fetch_optional(&self.db)
.await
.map_err(|e| e.to_string())?;
match row {
Some((json,)) => serde_json::from_str(&json).map(Some).map_err(|e| e.to_string()),
None => Ok(None),
}
}
pub async fn delete_schema_cache_prefix(&self, prefix: &str) -> Result<(), String> {
sqlx::query("DELETE FROM schema_cache WHERE cache_key = ? OR substr(cache_key, 1, ?) = ?")
.bind(prefix)
.bind(prefix.len() as i64)
.bind(prefix)
.execute(&self.db)
.await
.map_err(|e| e.to_string())?;
Ok(())
}
}
// ---------------------------------------------------------------------------
// JSON migration
// ---------------------------------------------------------------------------
+1
View File
@@ -9,6 +9,7 @@ pub mod query;
pub mod query_cancel;
pub mod redis_cmd;
pub mod schema;
pub mod schema_cache;
pub mod sql_file;
pub mod table_import;
pub mod transfer;
+26
View File
@@ -0,0 +1,26 @@
use std::sync::Arc;
use tauri::State;
use dbx_core::connection::AppState;
#[tauri::command]
pub async fn save_schema_cache(
state: State<'_, Arc<AppState>>,
cache_key: String,
payload: serde_json::Value,
) -> Result<(), String> {
state.storage.save_schema_cache(&cache_key, &payload).await
}
#[tauri::command]
pub async fn load_schema_cache(
state: State<'_, Arc<AppState>>,
cache_key: String,
) -> Result<Option<serde_json::Value>, String> {
state.storage.load_schema_cache(&cache_key).await
}
#[tauri::command]
pub async fn delete_schema_cache_prefix(state: State<'_, Arc<AppState>>, prefix: String) -> Result<(), String> {
state.storage.delete_schema_cache_prefix(&prefix).await
}
+3
View File
@@ -81,6 +81,9 @@ pub fn run() {
commands::schema::list_foreign_keys,
commands::schema::list_triggers,
commands::schema::get_table_ddl,
commands::schema_cache::save_schema_cache,
commands::schema_cache::load_schema_cache,
commands::schema_cache::delete_schema_cache_prefix,
commands::query::execute_query,
commands::query::execute_multi,
commands::query::cancel_query,
+5
View File
@@ -89,6 +89,11 @@ async fn main() {
.route("/schema/foreign-keys", get(routes::schema::list_foreign_keys))
.route("/schema/triggers", get(routes::schema::list_triggers))
.route("/schema/ddl", get(routes::schema::get_ddl))
.route(
"/schema/cache",
post(routes::schema_cache::save_schema_cache).get(routes::schema_cache::load_schema_cache),
)
.route("/schema/cache-prefix", delete(routes::schema_cache::delete_schema_cache_prefix))
// Query
.route("/query/execute", post(routes::query::execute_query))
.route("/query/execute-multi", post(routes::query::execute_multi))
+1
View File
@@ -6,6 +6,7 @@ pub mod mongo;
pub mod query;
pub mod redis;
pub mod schema;
pub mod schema_cache;
pub mod sql_file;
pub mod table_import;
pub mod transfer;
+49
View File
@@ -0,0 +1,49 @@
use std::sync::Arc;
use axum::extract::{Query, State};
use axum::Json;
use serde::Deserialize;
use crate::error::AppError;
use crate::state::WebState;
#[derive(Deserialize)]
pub struct SchemaCacheKeyQuery {
pub cache_key: String,
}
#[derive(Deserialize)]
pub struct SchemaCachePrefixQuery {
pub prefix: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaveSchemaCacheRequest {
pub cache_key: String,
pub payload: serde_json::Value,
}
pub async fn save_schema_cache(
State(state): State<Arc<WebState>>,
Json(body): Json<SaveSchemaCacheRequest>,
) -> Result<Json<()>, AppError> {
state.app.storage.save_schema_cache(&body.cache_key, &body.payload).await.map_err(AppError)?;
Ok(Json(()))
}
pub async fn load_schema_cache(
State(state): State<Arc<WebState>>,
Query(query): Query<SchemaCacheKeyQuery>,
) -> Result<Json<serde_json::Value>, AppError> {
let payload = state.app.storage.load_schema_cache(&query.cache_key).await.map_err(AppError)?;
Ok(Json(payload.unwrap_or(serde_json::json!(null))))
}
pub async fn delete_schema_cache_prefix(
State(state): State<Arc<WebState>>,
Query(query): Query<SchemaCachePrefixQuery>,
) -> Result<Json<()>, AppError> {
state.app.storage.delete_schema_cache_prefix(&query.prefix).await.map_err(AppError)?;
Ok(Json(()))
}
+8 -8
View File
@@ -451,9 +451,9 @@ async function confirmDropTable() {
await api.executeQuery(node.connectionId, node.database, buildDropTableSql(), node.schema);
toast(t("contextMenu.dropTableSuccess", { name: node.label }), 3000);
if (node.schema) {
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
await connectionStore.loadTables(node.connectionId, node.database, node.schema, { force: true });
} else {
await connectionStore.loadTables(node.connectionId, node.database);
await connectionStore.loadTables(node.connectionId, node.database, undefined, { force: true });
}
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
@@ -518,7 +518,7 @@ async function confirmCreateDatabase() {
const sql = `CREATE DATABASE ${quoteIdent(name)};`;
await api.executeQuery(node.connectionId, "", sql);
toast(t("contextMenu.createDatabaseSuccess", { name }), 3000);
await connectionStore.loadDatabases(node.connectionId);
await connectionStore.loadDatabases(node.connectionId, { force: true });
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
@@ -535,7 +535,7 @@ async function confirmDropDatabase() {
await connectionStore.ensureConnected(node.connectionId);
await api.executeQuery(node.connectionId, "", buildDropDatabaseSql());
toast(t("contextMenu.dropDatabaseSuccess", { name: node.label }), 3000);
await connectionStore.loadDatabases(node.connectionId);
await connectionStore.loadDatabases(node.connectionId, { force: true });
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
@@ -556,7 +556,7 @@ async function confirmCreateSchema() {
const sql = `CREATE SCHEMA ${quoteIdent(name)};`;
await api.executeQuery(node.connectionId, node.database, sql);
toast(t("contextMenu.createSchemaSuccess", { name }), 3000);
await connectionStore.loadSchemas(node.connectionId, node.database);
await connectionStore.loadSchemas(node.connectionId, node.database, { force: true });
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
@@ -573,7 +573,7 @@ async function confirmDropSchema() {
await connectionStore.ensureConnected(node.connectionId);
await api.executeQuery(node.connectionId, node.database, buildDropSchemaSql());
toast(t("contextMenu.dropSchemaSuccess", { name: node.label }), 3000);
await connectionStore.loadSchemas(node.connectionId, node.database);
await connectionStore.loadSchemas(node.connectionId, node.database, { force: true });
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
@@ -609,9 +609,9 @@ async function confirmDuplicateStructure() {
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
toast(t("contextMenu.duplicateStructureSuccess", { name: newName }), 3000);
if (node.schema) {
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
await connectionStore.loadTables(node.connectionId, node.database, node.schema, { force: true });
} else {
await connectionStore.loadTables(node.connectionId, node.database);
await connectionStore.loadTables(node.connectionId, node.database, undefined, { force: true });
}
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
+3
View File
@@ -39,6 +39,9 @@ export const loadConnections = forward("loadConnections");
// Schema
export const listDatabases = forward("listDatabases");
export const saveSchemaCache = forward("saveSchemaCache");
export const loadSchemaCache = forward("loadSchemaCache");
export const deleteSchemaCachePrefix = forward("deleteSchemaCachePrefix");
export const listSchemas = forward("listSchemas");
export const listTables = forward("listTables");
export const getColumns = forward("getColumns");
+12
View File
@@ -96,6 +96,18 @@ export async function listDatabases(connectionId: string): Promise<DatabaseInfo[
return get(`/api/schema/databases?${qs({ connection_id: connectionId })}`);
}
export async function saveSchemaCache(cacheKey: string, payload: unknown): Promise<void> {
return post("/api/schema/cache", { cacheKey, payload });
}
export async function loadSchemaCache<T = unknown>(cacheKey: string): Promise<T | null> {
return get(`/api/schema/cache?${qs({ cache_key: cacheKey })}`);
}
export async function deleteSchemaCachePrefix(prefix: string): Promise<void> {
return del(`/api/schema/cache-prefix?${qs({ prefix })}`);
}
export async function listSchemas(connectionId: string, database: string): Promise<string[]> {
return get(`/api/schema/schemas?${qs({ connection_id: connectionId, database })}`);
}
+12
View File
@@ -117,6 +117,18 @@ export async function listDatabases(connectionId: string): Promise<DatabaseInfo[
return invoke("list_databases", { connectionId });
}
export async function saveSchemaCache(cacheKey: string, payload: unknown): Promise<void> {
return invoke("save_schema_cache", { cacheKey, payload });
}
export async function loadSchemaCache<T = unknown>(cacheKey: string): Promise<T | null> {
return invoke("load_schema_cache", { cacheKey });
}
export async function deleteSchemaCachePrefix(prefix: string): Promise<void> {
return invoke("delete_schema_cache_prefix", { prefix });
}
export async function listTables(connectionId: string, database: string, schema: string): Promise<TableInfo[]> {
return invoke("list_tables", { connectionId, database, schema });
}
+93 -42
View File
@@ -25,6 +25,10 @@ import { buildDatabaseTreeNodes } from "@/lib/databaseTree";
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
interface LoadTreeOptions {
force?: boolean;
}
export const useConnectionStore = defineStore("connection", () => {
const connections = ref<ConnectionConfig[]>([]);
const isDesktop = isTauriRuntime();
@@ -38,6 +42,7 @@ export const useConnectionStore = defineStore("connection", () => {
const treeNodes = ref<TreeNode[]>([]);
const pinnedTreeNodeIds = ref<Set<string>>(loadPinnedTreeNodeIds());
const connectedIds = ref<Set<string>>(new Set());
const loadedTreeNodeChildrenIds = ref<Set<string>>(new Set());
const connectionErrors = ref<Record<string, string>>({});
const editingConnectionId = ref<string | null>(null);
const newConnectionGroupId = ref<string | null>(null);
@@ -188,6 +193,38 @@ export const useConnectionStore = defineStore("connection", () => {
function setChildren(parent: TreeNode, children: TreeNode[]) {
parent.children = orderPinnedFirst(children.map(pinTreeNode), (node) => !!node.pinned);
loadedTreeNodeChildrenIds.value.add(parent.id);
}
function schemaCacheKey(...parts: string[]): string {
return parts.map((part) => encodeURIComponent(part)).join(":");
}
async function loadPersistedTreeChildren(node: TreeNode, cacheKey: string): Promise<boolean> {
const children = await api.loadSchemaCache<TreeNode[]>(cacheKey).catch(() => null);
if (!children) return false;
setChildren(node, children);
node.isExpanded = true;
return true;
}
async function savePersistedTreeChildren(cacheKey: string, children: TreeNode[]) {
await api.saveSchemaCache(cacheKey, children).catch(() => undefined);
}
function useCachedChildren(node: TreeNode, options?: LoadTreeOptions): boolean {
if (options?.force || !loadedTreeNodeChildrenIds.value.has(node.id)) return false;
node.isExpanded = true;
return true;
}
function clearLoadedChildrenCache(prefix: string) {
for (const id of loadedTreeNodeChildrenIds.value) {
if (id === prefix || id.startsWith(`${prefix}:`)) {
loadedTreeNodeChildrenIds.value.delete(id);
}
}
api.deleteSchemaCachePrefix(`${schemaCacheKey(prefix)}:`).catch(() => undefined);
}
function findParentNode(nodes: TreeNode[], id: string, parent: TreeNode | null = null): TreeNode | null {
@@ -263,6 +300,7 @@ export const useConnectionStore = defineStore("connection", () => {
activeConnectionId.value = null;
}
invalidateCompletionCache(id);
clearLoadedChildrenCache(id);
}
async function updateConnection(config: ConnectionConfig) {
@@ -276,6 +314,7 @@ export const useConnectionStore = defineStore("connection", () => {
rebuildTreeNodes();
connectedIds.value.delete(config.id);
invalidateCompletionCache(config.id);
clearLoadedChildrenCache(config.id);
}
async function setDefaultDatabase(connectionId: string, database: string) {
@@ -343,6 +382,7 @@ export const useConnectionStore = defineStore("connection", () => {
node.isExpanded = false;
node.children = [];
}
clearLoadedChildrenCache(connectionId);
if (activeConnectionId.value === connectionId) {
activeConnectionId.value = null;
}
@@ -368,15 +408,20 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function loadDatabases(connectionId: string) {
async function loadDatabases(connectionId: string, options?: LoadTreeOptions) {
const node = findNode(treeNodes.value, connectionId);
if (!node) return;
if (useCachedChildren(node, options)) return;
const cacheKey = schemaCacheKey(connectionId, "databases");
if (!options?.force && (await loadPersistedTreeChildren(node, cacheKey))) return;
node.isLoading = true;
try {
await ensureConnected(connectionId);
const databases = await api.listDatabases(connectionId);
setChildren(node, buildDatabaseTreeNodes(connectionId, databases));
const children = buildDatabaseTreeNodes(connectionId, databases);
setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children);
node.isExpanded = true;
} finally {
node.isLoading = false;
@@ -460,57 +505,63 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function loadSchemas(connectionId: string, database: string) {
async function loadSchemas(connectionId: string, database: string, options?: LoadTreeOptions) {
const nodeId = `${connectionId}:${database}`;
const node = findNode(treeNodes.value, nodeId);
if (!node) return;
if (useCachedChildren(node, options)) return;
const cacheKey = schemaCacheKey(connectionId, database, "schemas");
if (!options?.force && (await loadPersistedTreeChildren(node, cacheKey))) return;
node.isLoading = true;
try {
await ensureConnected(connectionId);
const schemas = await api.listSchemas(connectionId, database);
setChildren(
node,
schemas.map((s) => ({
id: `${connectionId}:${database}:${s}`,
label: s,
type: "schema" as const,
connectionId,
database,
schema: s,
isExpanded: false,
children: [],
})),
);
const children = schemas.map((s) => ({
id: `${connectionId}:${database}:${s}`,
label: s,
type: "schema" as const,
connectionId,
database,
schema: s,
isExpanded: false,
children: [],
}));
setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children);
node.isExpanded = true;
} finally {
node.isLoading = false;
}
}
async function loadTables(connectionId: string, database: string, schema?: string) {
async function loadTables(connectionId: string, database: string, schema?: string, options?: LoadTreeOptions) {
const nodeId = schema ? `${connectionId}:${database}:${schema}` : `${connectionId}:${database}`;
const node = findNode(treeNodes.value, nodeId);
if (!node) return;
if (useCachedChildren(node, options)) return;
const cacheKey = schemaCacheKey(connectionId, database, schema || "", "tables");
if (!options?.force && (await loadPersistedTreeChildren(node, cacheKey))) return;
node.isLoading = true;
try {
await ensureConnected(connectionId);
const querySchema = schema || database;
const tables = await api.listTables(connectionId, database, querySchema);
const config = getConfig(connectionId);
const effectiveSchema = schema || (config?.db_type && isSchemaAware(config.db_type) ? database : undefined);
setChildren(
node,
tables.map((t) => ({
id: `${nodeId}:${t.name}`,
label: t.name,
type: (t.table_type === "VIEW" ? "view" : "table") as "view" | "table",
connectionId,
database,
schema: effectiveSchema,
isExpanded: false,
children: [],
})),
);
const children = tables.map((t) => ({
id: `${nodeId}:${t.name}`,
label: t.name,
type: (t.table_type === "VIEW" ? "view" : "table") as "view" | "table",
connectionId,
database,
schema: effectiveSchema,
isExpanded: false,
children: [],
}));
setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children);
node.isExpanded = true;
} finally {
node.isLoading = false;
@@ -696,7 +747,7 @@ export const useConnectionStore = defineStore("connection", () => {
return ids;
}
async function loadTreeNodeChildren(node: TreeNode) {
async function loadTreeNodeChildren(node: TreeNode, options?: LoadTreeOptions) {
if (node.type === "connection" && node.connectionId) {
const config = getConfig(node.connectionId);
if (config?.db_type === "redis") {
@@ -704,19 +755,19 @@ export const useConnectionStore = defineStore("connection", () => {
} else if (config?.db_type === "mongodb" || config?.db_type === "elasticsearch") {
await loadMongoDatabases(node.connectionId);
} else {
await loadDatabases(node.connectionId);
await loadDatabases(node.connectionId, options);
}
} else if (node.type === "mongo-db" && node.connectionId && node.database) {
await loadMongoCollections(node.connectionId, node.database);
} else if (node.type === "database" && node.connectionId && node.database) {
const config = getConfig(node.connectionId);
if (config?.db_type && TREE_SCHEMA_TYPES.has(config.db_type)) {
await loadSchemas(node.connectionId, node.database);
await loadSchemas(node.connectionId, node.database, options);
} else {
await loadTables(node.connectionId, node.database);
await loadTables(node.connectionId, node.database, undefined, options);
}
} else if (node.type === "schema" && node.connectionId && node.database && node.schema) {
await loadTables(node.connectionId, node.database, node.schema);
await loadTables(node.connectionId, node.database, node.schema, options);
} else if ((node.type === "table" || node.type === "view") && node.connectionId && node.database) {
await loadTableGroups(node.connectionId, node.database, node.label, node.schema);
} else if (node.type === "group-columns" && node.connectionId && node.database && node.tableName) {
@@ -730,12 +781,12 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function restoreExpandedChildren(node: TreeNode, expandedIds: Set<string>) {
async function restoreExpandedChildren(node: TreeNode, expandedIds: Set<string>, options?: LoadTreeOptions) {
if (!node.children) return;
for (const child of node.children) {
if (!expandedIds.has(child.id)) continue;
await loadTreeNodeChildren(child);
await restoreExpandedChildren(child, expandedIds);
await loadTreeNodeChildren(child, options);
await restoreExpandedChildren(child, expandedIds, options);
}
}
@@ -745,8 +796,8 @@ export const useConnectionStore = defineStore("connection", () => {
if (node.type !== "connection-group") {
node.children = [];
}
await loadTreeNodeChildren(node);
await restoreExpandedChildren(node, expandedIds);
await loadTreeNodeChildren(node, { force: true });
await restoreExpandedChildren(node, expandedIds, { force: true });
}
function isSchemaAwareDatabase(connectionId: string): boolean {
@@ -874,8 +925,8 @@ export const useConnectionStore = defineStore("connection", () => {
}
if (!expandedIds.has(node.id)) continue;
node.children = [];
await loadTreeNodeChildren(node);
await restoreExpandedChildren(node, expandedIds);
await loadTreeNodeChildren(node, { force: true });
await restoreExpandedChildren(node, expandedIds, { force: true });
}
};
await refreshExpandedNodes(treeNodes.value);