diff --git a/apps/desktop/src/lib/__tests__/savedSql/savedSqlDatabaseTree.spec.ts b/apps/desktop/src/lib/__tests__/savedSql/savedSqlDatabaseTree.spec.ts index 9a71f62987..d999a7edc6 100644 --- a/apps/desktop/src/lib/__tests__/savedSql/savedSqlDatabaseTree.spec.ts +++ b/apps/desktop/src/lib/__tests__/savedSql/savedSqlDatabaseTree.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { buildDatabaseSavedSqlRootNode, decorateDatabaseSavedSqlTreeNodes, indexSavedSqlFilesByDatabase, savedSqlFilesForDatabase, stripDatabaseSavedSqlTreeNodes } from "@/lib/savedSql/savedSqlDatabaseTree"; +import { buildDatabaseSavedSqlRootNode, decorateDatabaseSavedSqlTreeNodes, indexSavedSqlFilesByDatabase, savedSqlFilesForDatabase, stripDatabaseSavedSqlTreeNodes, withDatabaseSavedSqlRoot } from "@/lib/savedSql/savedSqlDatabaseTree"; import type { SavedSqlFile, TreeNode } from "@/types/database"; function file(input: Partial & Pick): SavedSqlFile { @@ -87,6 +87,16 @@ describe("database saved SQL tree", () => { expect(decorated[0].children?.at(-1)).toMatchObject({ isExpanded: false, children: [{ savedSqlId: "orders-2" }, { savedSqlId: "orders-10" }] }); }); + it("reuses unchanged database children when the saved SQL index changes elsewhere", () => { + const existingChildren = [{ id: "conn-1:app:tables", label: "tree.tables", type: "group-tables" as const }]; + const existing = { ...database, children: [...existingChildren, buildDatabaseSavedSqlRootNode(database, files)!] }; + const next = withDatabaseSavedSqlRoot({ ...database, children: existing.children }, existing.children, indexSavedSqlFilesByDatabase([...files, file({ id: "analytics-2", name: "analytics-2.sql", connectionId: "conn-1", database: "analytics" })])); + + expect(next).toEqual(existing.children); + expect(next[0]).toBe(existingChildren[0]); + expect(next[1]).toBe(existing.children[1]); + }); + it("removes runtime saved SQL nodes before metadata is cached", () => { const decorated = decorateDatabaseSavedSqlTreeNodes([database], files); expect(stripDatabaseSavedSqlTreeNodes(decorated)).toEqual([{ ...database, children: [] }]); diff --git a/apps/desktop/src/lib/savedSql/savedSqlDatabaseTree.ts b/apps/desktop/src/lib/savedSql/savedSqlDatabaseTree.ts index 1cbe74beb1..9ef74f7bb3 100644 --- a/apps/desktop/src/lib/savedSql/savedSqlDatabaseTree.ts +++ b/apps/desktop/src/lib/savedSql/savedSqlDatabaseTree.ts @@ -42,8 +42,8 @@ export function indexSavedSqlFilesByDatabase(files: readonly SavedSqlFile[]): Sa return index; } -export function savedSqlFilesForDatabase(source: SavedSqlDatabaseSource, scope: SavedSqlDatabaseScope): SavedSqlFile[] { - if (!Array.isArray(source)) return [...((source as SavedSqlDatabaseIndex).get(savedSqlDatabaseScopeKey(scope)) ?? [])]; +export function savedSqlFilesForDatabase(source: SavedSqlDatabaseSource, scope: SavedSqlDatabaseScope): readonly SavedSqlFile[] { + if (!Array.isArray(source)) return (source as SavedSqlDatabaseIndex).get(savedSqlDatabaseScopeKey(scope)) ?? []; return source.filter((file) => savedSqlDatabaseScopeKey(file) === savedSqlDatabaseScopeKey(scope)).sort(compareSavedSqlFiles); } @@ -84,8 +84,27 @@ export function buildDatabaseSavedSqlRootNode(databaseNode: Pick, children: readonly TreeNode[], source: SavedSqlDatabaseSource): TreeNode[] { const existingRoot = databaseNode.children?.find((child) => child.type === "saved-sql-root"); - const root = buildDatabaseSavedSqlRootNode(databaseNode, source, existingRoot); + const existingMetadataChildren = databaseNode.children?.filter((child) => child.type !== "saved-sql-root") ?? []; const metadataChildren = children.filter((child) => child.type !== "saved-sql-root"); + const files = + databaseNode.connectionId && databaseNode.database !== undefined + ? savedSqlFilesForDatabase(source, { + connectionId: databaseNode.connectionId, + catalog: databaseNode.catalog, + database: databaseNode.database, + }) + : []; + const sameMetadata = metadataChildren.length === existingMetadataChildren.length && metadataChildren.every((node, index) => node === existingMetadataChildren[index]); + const sameFiles = + !!existingRoot && + existingRoot.children?.length === files.length && + files.every((file, index) => { + const node = existingRoot.children?.[index]; + return node?.type === "saved-sql-file" && node.savedSqlId === file.id && node.label === file.name && node.connectionId === file.connectionId && node.catalog === file.catalog && node.database === file.database && node.schema === file.schema; + }); + if (existingRoot && sameMetadata && sameFiles) return databaseNode.children ? [...databaseNode.children] : metadataChildren; + + const root = buildDatabaseSavedSqlRootNode(databaseNode, source, existingRoot); return root ? [...metadataChildren, root] : metadataChildren; } diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index f5c0e30c78..a21d76fd86 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -1813,7 +1813,7 @@ export const useConnectionStore = defineStore("connection", () => { savedSqlFilesByDatabase = indexSavedSqlFilesByDatabase(savedSqlStore.allFiles); refreshDatabaseSavedSqlTrees(); }, - { flush: "sync" }, + { flush: "post" }, ); function schemaCacheKey(...parts: string[]): string { diff --git a/crates/dbx-core/src/table_export.rs b/crates/dbx-core/src/table_export.rs index 3725ac7e20..8dde1cd0ee 100644 --- a/crates/dbx-core/src/table_export.rs +++ b/crates/dbx-core/src/table_export.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::borrow::Cow; +use std::collections::HashMap; use std::io::{BufWriter, Write}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -198,6 +199,7 @@ fn resolve_requested_export_column_types( requested_column_types: &[Option], table_columns: &[crate::db::ColumnInfo], ) -> Vec> { + let table_columns_by_name = table_columns_by_name(table_columns); requested_columns .iter() .enumerate() @@ -208,26 +210,29 @@ fn resolve_requested_export_column_types( .flatten() .filter(|column_type| !column_type.trim().is_empty()) .or_else(|| { - table_columns - .iter() - .find(|column| column.name.eq_ignore_ascii_case(requested)) - .map(|column| column.data_type.clone()) + table_columns_by_name.get(&requested.to_ascii_lowercase()).map(|column| column.data_type.clone()) }) }) .collect() } +fn table_columns_by_name(table_columns: &[crate::db::ColumnInfo]) -> HashMap { + let mut by_name = HashMap::with_capacity(table_columns.len()); + for column in table_columns { + by_name.entry(column.name.to_ascii_lowercase()).or_insert(column); + } + by_name +} + fn resolve_requested_export_column_extras( requested_columns: &[String], table_columns: &[crate::db::ColumnInfo], ) -> Vec> { + let table_columns_by_name = table_columns_by_name(table_columns); requested_columns .iter() .map(|requested| { - table_columns - .iter() - .find(|column| column.name.eq_ignore_ascii_case(requested)) - .and_then(|column| column.extra.clone()) + table_columns_by_name.get(&requested.to_ascii_lowercase()).and_then(|column| column.extra.clone()) }) .collect() }