feat(sql): persist SQL library in app database

This commit is contained in:
t8y2
2026-05-10 00:41:17 +08:00
parent cac383e6bf
commit b013a2ac29
17 changed files with 462 additions and 88 deletions
+9 -8
View File
@@ -161,12 +161,12 @@ function defaultSavedSqlName(title: string) {
return trimmed.endsWith(".sql") ? trimmed : `${trimmed}.sql`;
}
function openSaveSqlDialog() {
async function openSaveSqlDialog() {
const tab = activeTab.value;
if (!tab || !tab.sql.trim()) return;
const existing = tab.savedSqlId ? savedSqlStore.getFile(tab.savedSqlId) : undefined;
if (existing) {
const updated = savedSqlStore.saveFile({
const updated = await savedSqlStore.saveFile({
id: existing.id,
connectionId: tab.connectionId,
folderId: existing.folderId,
@@ -186,12 +186,12 @@ function openSaveSqlDialog() {
showSaveSqlDialog.value = true;
}
function confirmSaveSqlToLibrary() {
async function confirmSaveSqlToLibrary() {
const tab = activeTab.value;
const name = saveSqlName.value.trim();
if (!tab || !tab.sql.trim() || !name) return;
try {
const saved = savedSqlStore.saveFile({
const saved = await savedSqlStore.saveFile({
id: tab.savedSqlId,
connectionId: tab.connectionId,
folderId: saveSqlFolderId.value === ROOT_SAVED_SQL_FOLDER ? undefined : saveSqlFolderId.value,
@@ -378,7 +378,7 @@ function handleKeydown(e: KeyboardEvent) {
) {
e.preventDefault();
e.stopPropagation();
openSaveSqlDialog();
void openSaveSqlDialog();
return;
}
if (
@@ -402,8 +402,9 @@ function onLoginSuccess() {
}
function initApp() {
connectionStore
.initFromDisk()
savedSqlStore
.initFromStorage()
.then(() => connectionStore.initFromDisk())
.then(() => {
reconnectRestoredTabs();
})
@@ -531,7 +532,7 @@ onUnmounted(() => {
@cancel="cancelActiveExecution()"
@explain="tryExplain()"
@format-sql="formatActiveSql"
@save-sql="openSaveSqlDialog"
@save-sql="void openSaveSqlDialog()"
@open-sql="openSqlFile"
@change-connection="changeActiveConnection"
@change-database="changeActiveDatabase"
+8 -8
View File
@@ -1221,16 +1221,16 @@ function openRenameSavedSqlFile() {
showSavedSqlNameDialog.value = true;
}
function confirmSavedSqlName() {
async function confirmSavedSqlName() {
const name = savedSqlNameInput.value.trim();
if (!name || !props.node.connectionId || !savedSqlNameMode.value) return;
if (savedSqlNameMode.value === "folder-create") {
savedSqlStore.createFolder(props.node.connectionId, name);
await savedSqlStore.createFolder(props.node.connectionId, name);
} else if (savedSqlNameMode.value === "folder-rename" && props.node.savedSqlFolderId) {
savedSqlStore.renameFolder(props.node.savedSqlFolderId, name);
await savedSqlStore.renameFolder(props.node.savedSqlFolderId, name);
} else if (savedSqlNameMode.value === "file-rename" && props.node.savedSqlId) {
savedSqlStore.renameFile(props.node.savedSqlId, name.endsWith(".sql") ? name : `${name}.sql`);
await savedSqlStore.renameFile(props.node.savedSqlId, name.endsWith(".sql") ? name : `${name}.sql`);
}
connectionStore.refreshSavedSqlTree(props.node.connectionId);
@@ -1242,9 +1242,9 @@ function deleteSavedSqlFile() {
showDeleteSavedSqlFileConfirm.value = true;
}
function confirmDeleteSavedSqlFile() {
async function confirmDeleteSavedSqlFile() {
if (!props.node.savedSqlId) return;
savedSqlStore.deleteFile(props.node.savedSqlId);
await savedSqlStore.deleteFile(props.node.savedSqlId);
connectionStore.refreshSavedSqlTree(props.node.connectionId);
showDeleteSavedSqlFileConfirm.value = false;
}
@@ -1253,9 +1253,9 @@ function deleteSavedSqlFolder() {
showDeleteSavedSqlFolderConfirm.value = true;
}
function confirmDeleteSavedSqlFolder() {
async function confirmDeleteSavedSqlFolder() {
if (!props.node.savedSqlFolderId) return;
savedSqlStore.deleteFolder(props.node.savedSqlFolderId);
await savedSqlStore.deleteFolder(props.node.savedSqlFolderId);
connectionStore.refreshSavedSqlTree(props.node.connectionId);
showDeleteSavedSqlFolderConfirm.value = false;
}
+5
View File
@@ -36,6 +36,11 @@ export const connectDb = forward("connectDb");
export const disconnectDb = forward("disconnectDb");
export const saveConnections = forward("saveConnections");
export const loadConnections = forward("loadConnections");
export const loadSavedSqlLibrary = forward("loadSavedSqlLibrary");
export const saveSavedSqlFolder = forward("saveSavedSqlFolder");
export const deleteSavedSqlFolder = forward("deleteSavedSqlFolder");
export const saveSavedSqlFile = forward("saveSavedSqlFile");
export const deleteSavedSqlFile = forward("deleteSavedSqlFile");
// Schema
export const listDatabases = forward("listDatabases");
+23
View File
@@ -8,6 +8,9 @@ import type {
TriggerInfo,
QueryResult,
SidebarLayout,
SavedSqlFile,
SavedSqlFolder,
SavedSqlLibrary,
} from "@/types/database";
import type { AiConfig } from "@/stores/settingsStore";
import type {
@@ -88,6 +91,26 @@ export async function loadConnections(): Promise<ConnectionConfig[]> {
return get("/api/connection/list");
}
export async function loadSavedSqlLibrary(): Promise<SavedSqlLibrary> {
return get("/api/saved-sql");
}
export async function saveSavedSqlFolder(folder: SavedSqlFolder): Promise<SavedSqlFolder> {
return post("/api/saved-sql/folders", folder);
}
export async function deleteSavedSqlFolder(id: string): Promise<void> {
return del(`/api/saved-sql/folders/${encodeURIComponent(id)}`);
}
export async function saveSavedSqlFile(file: SavedSqlFile): Promise<SavedSqlFile> {
return post("/api/saved-sql", file);
}
export async function deleteSavedSqlFile(id: string): Promise<void> {
return del(`/api/saved-sql/${encodeURIComponent(id)}`);
}
// ---------------------------------------------------------------------------
// Schema
// ---------------------------------------------------------------------------
+23
View File
@@ -9,6 +9,9 @@ import type {
ForeignKeyInfo,
TriggerInfo,
QueryResult,
SavedSqlFile,
SavedSqlFolder,
SavedSqlLibrary,
} from "@/types/database";
import type { AiConfig } from "@/stores/settingsStore";
@@ -241,6 +244,26 @@ export async function loadConnections(): Promise<ConnectionConfig[]> {
return invoke("load_connections");
}
export async function loadSavedSqlLibrary(): Promise<SavedSqlLibrary> {
return invoke("load_saved_sql_library");
}
export async function saveSavedSqlFolder(folder: SavedSqlFolder): Promise<SavedSqlFolder> {
return invoke("save_saved_sql_folder", { folder });
}
export async function deleteSavedSqlFolder(id: string): Promise<void> {
return invoke("delete_saved_sql_folder", { id });
}
export async function saveSavedSqlFile(file: SavedSqlFile): Promise<SavedSqlFile> {
return invoke("save_saved_sql_file", { file });
}
export async function deleteSavedSqlFile(id: string): Promise<void> {
return invoke("delete_saved_sql_file", { id });
}
export async function saveSidebarLayout(layout: import("@/types/database").SidebarLayout): Promise<void> {
return invoke("save_sidebar_layout", { layout });
}
+1 -1
View File
@@ -11,7 +11,7 @@ import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersisten
import * as api from "@/lib/api";
import { useConnectionStore } from "@/stores/connectionStore";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import type { SavedSqlFile } from "@/stores/savedSqlStore";
import type { SavedSqlFile } from "@/types/database";
const STORAGE_KEY = "dbx-open-tabs";
const ACTIVE_TAB_KEY = "dbx-active-tab";
+68 -71
View File
@@ -1,28 +1,10 @@
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { uuid } from "@/lib/utils";
import * as api from "@/lib/api";
import type { SavedSqlFile, SavedSqlFolder, SavedSqlLibrary } from "@/types/database";
const STORAGE_KEY = "dbx-saved-sql-library";
export interface SavedSqlFolder {
id: string;
connectionId: string;
name: string;
createdAt: string;
updatedAt: string;
}
export interface SavedSqlFile {
id: string;
connectionId: string;
folderId?: string;
name: string;
database: string;
schema?: string;
sql: string;
createdAt: string;
updatedAt: string;
}
const LEGACY_STORAGE_KEY = "dbx-saved-sql-library";
interface SavedSqlState {
folders: SavedSqlFolder[];
@@ -33,9 +15,9 @@ function nowIso() {
return new Date().toISOString();
}
function loadState(): SavedSqlState {
function loadLegacyState(): SavedSqlState {
try {
const raw = localStorage.getItem(STORAGE_KEY);
const raw = localStorage.getItem(LEGACY_STORAGE_KEY);
if (!raw) return { folders: [], files: [] };
const parsed = JSON.parse(raw) as Partial<SavedSqlState>;
return {
@@ -48,16 +30,36 @@ function loadState(): SavedSqlState {
}
export const useSavedSqlStore = defineStore("savedSql", () => {
const initial = loadState();
const folders = ref<SavedSqlFolder[]>(initial.folders);
const files = ref<SavedSqlFile[]>(initial.files);
const folders = ref<SavedSqlFolder[]>([]);
const files = ref<SavedSqlFile[]>([]);
const isLoaded = ref(false);
const version = computed(
() => `${folders.value.length}:${files.value.length}:${files.value.map((f) => f.updatedAt).join("|")}`,
);
function persist() {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ folders: folders.value, files: files.value }));
function applyLibrary(library: SavedSqlLibrary) {
folders.value = library.folders;
files.value = library.files;
}
async function migrateLegacyLocalStorage() {
const legacy = loadLegacyState();
if (legacy.folders.length === 0 && legacy.files.length === 0) return;
for (const folder of legacy.folders) {
await api.saveSavedSqlFolder(folder);
}
for (const file of legacy.files) {
await api.saveSavedSqlFile(file);
}
localStorage.removeItem(LEGACY_STORAGE_KEY);
}
async function initFromStorage() {
await migrateLegacyLocalStorage();
applyLibrary(await api.loadSavedSqlLibrary());
isLoaded.value = true;
}
function listFolders(connectionId: string) {
@@ -76,7 +78,7 @@ export const useSavedSqlStore = defineStore("savedSql", () => {
return files.value.find((file) => file.id === id);
}
function createFolder(connectionId: string, name: string) {
async function createFolder(connectionId: string, name: string) {
const timestamp = nowIso();
const folder: SavedSqlFolder = {
id: uuid(),
@@ -85,26 +87,25 @@ export const useSavedSqlStore = defineStore("savedSql", () => {
createdAt: timestamp,
updatedAt: timestamp,
};
folders.value = [...folders.value, folder];
persist();
return folder;
const saved = await api.saveSavedSqlFolder(folder);
folders.value = [...folders.value.filter((item) => item.id !== saved.id), saved];
return saved;
}
function renameFolder(id: string, name: string) {
const timestamp = nowIso();
folders.value = folders.value.map((folder) =>
folder.id === id ? { ...folder, name, updatedAt: timestamp } : folder,
);
persist();
async function renameFolder(id: string, name: string) {
const existing = folders.value.find((folder) => folder.id === id);
if (!existing) return;
const saved = await api.saveSavedSqlFolder({ ...existing, name, updatedAt: nowIso() });
folders.value = folders.value.map((folder) => (folder.id === id ? saved : folder));
}
function deleteFolder(id: string) {
async function deleteFolder(id: string) {
await api.deleteSavedSqlFolder(id);
folders.value = folders.value.filter((folder) => folder.id !== id);
files.value = files.value.filter((file) => file.folderId !== id);
persist();
}
function saveFile(input: {
async function saveFile(input: {
id?: string;
connectionId: string;
folderId?: string;
@@ -114,10 +115,9 @@ export const useSavedSqlStore = defineStore("savedSql", () => {
sql: string;
}) {
const timestamp = nowIso();
if (input.id) {
const existing = getFile(input.id);
if (existing) {
const updated: SavedSqlFile = {
const existing = input.id ? getFile(input.id) : undefined;
const file: SavedSqlFile = existing
? {
...existing,
folderId: input.folderId || undefined,
name: input.name,
@@ -125,44 +125,41 @@ export const useSavedSqlStore = defineStore("savedSql", () => {
schema: input.schema,
sql: input.sql,
updatedAt: timestamp,
}
: {
id: uuid(),
connectionId: input.connectionId,
folderId: input.folderId || undefined,
name: input.name,
database: input.database,
schema: input.schema,
sql: input.sql,
createdAt: timestamp,
updatedAt: timestamp,
};
files.value = files.value.map((file) => (file.id === input.id ? updated : file));
persist();
return updated;
}
}
const file: SavedSqlFile = {
id: uuid(),
connectionId: input.connectionId,
folderId: input.folderId || undefined,
name: input.name,
database: input.database,
schema: input.schema,
sql: input.sql,
createdAt: timestamp,
updatedAt: timestamp,
};
files.value = [...files.value, file];
persist();
return file;
const saved = await api.saveSavedSqlFile(file);
files.value = [...files.value.filter((item) => item.id !== saved.id), saved];
return saved;
}
function renameFile(id: string, name: string) {
const timestamp = nowIso();
files.value = files.value.map((file) => (file.id === id ? { ...file, name, updatedAt: timestamp } : file));
persist();
async function renameFile(id: string, name: string) {
const existing = getFile(id);
if (!existing) return;
const saved = await api.saveSavedSqlFile({ ...existing, name, updatedAt: nowIso() });
files.value = files.value.map((file) => (file.id === id ? saved : file));
}
function deleteFile(id: string) {
async function deleteFile(id: string) {
await api.deleteSavedSqlFile(id);
files.value = files.value.filter((file) => file.id !== id);
persist();
}
return {
folders,
files,
isLoaded,
version,
initFromStorage,
listFolders,
listFiles,
getFile,
+25
View File
@@ -188,3 +188,28 @@ export interface QueryTab {
columns: string[];
};
}
export interface SavedSqlFolder {
id: string;
connectionId: string;
name: string;
createdAt: string;
updatedAt: string;
}
export interface SavedSqlFile {
id: string;
connectionId: string;
folderId?: string;
name: string;
database: string;
schema?: string;
sql: string;
createdAt: string;
updatedAt: string;
}
export interface SavedSqlLibrary {
folders: SavedSqlFolder[];
files: SavedSqlFile[];
}