fix(sql): preserve file associations and list selection
This commit is contained in:
25 files changed
+558
-186
No files matched your search
@@ -5,6 +5,7 @@ const savedTarget = {
|
||||
connectionId: "saved-connection",
|
||||
database: "saved_database",
|
||||
schema: "saved_schema",
|
||||
catalog: "saved_catalog",
|
||||
};
|
||||
|
||||
describe("saved SQL execution targets", () => {
|
||||
@@ -46,11 +47,13 @@ describe("saved SQL execution targets", () => {
|
||||
connectionId: "runtime-connection",
|
||||
database: "runtime_database",
|
||||
schema: "runtime_schema",
|
||||
catalog: "runtime_catalog",
|
||||
}),
|
||||
).toEqual({
|
||||
connectionId: "runtime-connection",
|
||||
database: "runtime_database",
|
||||
schema: "runtime_schema",
|
||||
catalog: "runtime_catalog",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { savedSqlImportTarget } from "@/lib/savedSql/savedSqlImportTarget";
|
||||
|
||||
describe("savedSqlImportTarget", () => {
|
||||
it("inherits an explicitly associated local file target at the library root", () => {
|
||||
expect(savedSqlImportTarget({ connectionId: "conn-1", database: "sales", catalog: "hive" })).toEqual({
|
||||
connectionId: "conn-1",
|
||||
database: "sales",
|
||||
catalog: "hive",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps an unassociated local file unassociated at the library root", () => {
|
||||
expect(savedSqlImportTarget({ connectionId: "", database: "" })).toEqual({ connectionId: "", database: "" });
|
||||
});
|
||||
|
||||
it("keeps the destination folder connection and inherits a matching database", () => {
|
||||
expect(savedSqlImportTarget({ connectionId: "conn-1", database: "sales", catalog: "iceberg" }, { connectionId: "conn-1" })).toEqual({
|
||||
connectionId: "conn-1",
|
||||
database: "sales",
|
||||
catalog: "iceberg",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not apply a database from a different connection to the destination folder", () => {
|
||||
expect(savedSqlImportTarget({ connectionId: "conn-2", database: "analytics" }, { connectionId: "conn-1" })).toEqual({
|
||||
connectionId: "conn-1",
|
||||
database: "",
|
||||
catalog: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the destination folder for an unassociated source", () => {
|
||||
expect(savedSqlImportTarget({ connectionId: "", database: "" }, { connectionId: "conn-1" })).toEqual({
|
||||
connectionId: "conn-1",
|
||||
database: "",
|
||||
catalog: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { orderedListRangeAnchorIndex, orderedListSelectionIntent } from "@/lib/selection/orderedListSelection";
|
||||
|
||||
describe("orderedListSelectionIntent", () => {
|
||||
it("uses Shift for range selection even when another modifier is also reported", () => {
|
||||
expect(orderedListSelectionIntent({ shiftKey: true, metaKey: false, ctrlKey: false })).toBe("range");
|
||||
expect(orderedListSelectionIntent({ shiftKey: true, metaKey: true, ctrlKey: false })).toBe("range");
|
||||
expect(orderedListSelectionIntent({ shiftKey: true, metaKey: false, ctrlKey: true })).toBe("range");
|
||||
});
|
||||
|
||||
it("uses Cmd or Ctrl for toggle selection", () => {
|
||||
expect(orderedListSelectionIntent({ shiftKey: false, metaKey: true, ctrlKey: false })).toBe("toggle");
|
||||
expect(orderedListSelectionIntent({ shiftKey: false, metaKey: false, ctrlKey: true })).toBe("toggle");
|
||||
});
|
||||
|
||||
it("uses an unmodified click for single selection", () => {
|
||||
expect(orderedListSelectionIntent({ shiftKey: false, metaKey: false, ctrlKey: false })).toBe("single");
|
||||
});
|
||||
});
|
||||
|
||||
describe("orderedListRangeAnchorIndex", () => {
|
||||
const items = [
|
||||
{ type: "file", id: "first" },
|
||||
{ type: "folder", id: "folder" },
|
||||
{ type: "file", id: "last" },
|
||||
];
|
||||
|
||||
it("keeps the explicit selection anchor", () => {
|
||||
expect(orderedListRangeAnchorIndex(items, 2, { type: "file", id: "first" })).toBe(2);
|
||||
});
|
||||
|
||||
it("uses the active item when no explicit anchor exists", () => {
|
||||
expect(orderedListRangeAnchorIndex(items, null, { type: "folder", id: "folder" })).toBe(1);
|
||||
});
|
||||
|
||||
it("does not fall back to the first item without an anchor", () => {
|
||||
expect(orderedListRangeAnchorIndex(items, null, null)).toBeNull();
|
||||
expect(orderedListRangeAnchorIndex(items, null, { type: "file", id: "missing" })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { EXTERNAL_SQL_FILE_TARGETS_STORAGE_KEY, MAX_EXTERNAL_SQL_FILE_TARGETS, rememberExternalSqlFileTarget, resolveExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget";
|
||||
import { EXTERNAL_SQL_FILE_TARGETS_STORAGE_KEY, MAX_EXTERNAL_SQL_FILE_TARGETS, rememberExternalSqlFileTarget, resolveExternalSqlFileTarget, unassociatedExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget";
|
||||
|
||||
describe("external SQL file targets", () => {
|
||||
beforeEach(() => {
|
||||
@@ -9,20 +9,48 @@ describe("external SQL file targets", () => {
|
||||
});
|
||||
|
||||
it("restores the saved data source across normalized file paths", () => {
|
||||
rememberExternalSqlFileTarget(" C:\\work\\report.sql ", { connectionId: "saved-connection", database: "analytics" });
|
||||
rememberExternalSqlFileTarget(" C:\\work\\report.sql ", { connectionId: "saved-connection", database: "sales", catalog: "hive" });
|
||||
|
||||
expect(resolveExternalSqlFileTarget("C:/work/report.sql", () => true, { connectionId: "fallback", database: "default" })).toEqual({
|
||||
connectionId: "saved-connection",
|
||||
database: "analytics",
|
||||
database: "sales",
|
||||
catalog: "hive",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps same-named databases in different catalogs distinct", () => {
|
||||
rememberExternalSqlFileTarget("/work/hive.sql", { connectionId: "saved-connection", database: "sales", catalog: "hive" });
|
||||
rememberExternalSqlFileTarget("/work/iceberg.sql", { connectionId: "saved-connection", database: "sales", catalog: "iceberg" });
|
||||
|
||||
expect(resolveExternalSqlFileTarget("/work/hive.sql", () => true, unassociatedExternalSqlFileTarget()).catalog).toBe("hive");
|
||||
expect(resolveExternalSqlFileTarget("/work/iceberg.sql", () => true, unassociatedExternalSqlFileTarget()).catalog).toBe("iceberg");
|
||||
});
|
||||
|
||||
it("treats historical records without catalog as the default catalog", () => {
|
||||
localStorage.setItem(EXTERNAL_SQL_FILE_TARGETS_STORAGE_KEY, JSON.stringify([{ path: "/work/legacy.sql", connectionId: "saved-connection", database: "sales", updatedAt: 1 }]));
|
||||
|
||||
expect(resolveExternalSqlFileTarget("/work/legacy.sql", () => true, unassociatedExternalSqlFileTarget())).toEqual({
|
||||
connectionId: "saved-connection",
|
||||
database: "sales",
|
||||
catalog: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a new file path unassociated", () => {
|
||||
expect(resolveExternalSqlFileTarget("/work/new.sql", () => true, unassociatedExternalSqlFileTarget())).toEqual({
|
||||
connectionId: "",
|
||||
database: "",
|
||||
catalog: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back when the saved connection no longer exists", () => {
|
||||
rememberExternalSqlFileTarget("/work/report.sql", { connectionId: "deleted-connection", database: "analytics" });
|
||||
|
||||
expect(resolveExternalSqlFileTarget("/work/report.sql", () => false, { connectionId: "fallback", database: "default" })).toEqual({
|
||||
connectionId: "fallback",
|
||||
database: "default",
|
||||
expect(resolveExternalSqlFileTarget("/work/report.sql", () => false, unassociatedExternalSqlFileTarget())).toEqual({
|
||||
connectionId: "",
|
||||
database: "",
|
||||
catalog: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +75,7 @@ describe("external SQL file targets", () => {
|
||||
expect(resolveExternalSqlFileTarget(`/work/report-${MAX_EXTERNAL_SQL_FILE_TARGETS}.sql`, () => true, { connectionId: "fallback", database: "default" })).toEqual({
|
||||
connectionId: `connection-${MAX_EXTERNAL_SQL_FILE_TARGETS}`,
|
||||
database: `database-${MAX_EXTERNAL_SQL_FILE_TARGETS}`,
|
||||
catalog: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ export interface SavedSqlExecutionTarget {
|
||||
catalog?: string;
|
||||
}
|
||||
|
||||
type SavedSqlFileTarget = Pick<SavedSqlFile, "connectionId" | "database" | "schema">;
|
||||
type SavedSqlFileTarget = Pick<SavedSqlFile, "connectionId" | "database" | "schema" | "catalog">;
|
||||
type QueryTabTarget = Pick<QueryTab, "connectionId" | "database" | "schema" | "catalog">;
|
||||
|
||||
export function savedSqlExecutionTargetFromFile(file: SavedSqlFileTarget): SavedSqlExecutionTarget {
|
||||
@@ -17,6 +17,7 @@ export function savedSqlExecutionTargetFromFile(file: SavedSqlFileTarget): Saved
|
||||
connectionId: file.connectionId,
|
||||
database: file.database,
|
||||
schema: file.schema,
|
||||
catalog: file.catalog,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,5 +41,6 @@ export function savedSqlDefaultTargetForWrite(currentTarget: SavedSqlExecutionTa
|
||||
connectionId: currentTarget.connectionId,
|
||||
database: currentTarget.database,
|
||||
schema: currentTarget.schema,
|
||||
catalog: currentTarget.catalog,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { SavedSqlFolder } from "@/types/database";
|
||||
import type { ExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget";
|
||||
|
||||
export function savedSqlImportTarget(sourceTarget: ExternalSqlFileTarget, folder?: Pick<SavedSqlFolder, "connectionId">): ExternalSqlFileTarget {
|
||||
if (!folder) return sourceTarget;
|
||||
return {
|
||||
connectionId: folder.connectionId,
|
||||
database: sourceTarget.connectionId === folder.connectionId ? sourceTarget.database : "",
|
||||
catalog: sourceTarget.connectionId === folder.connectionId ? sourceTarget.catalog : undefined,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export type OrderedListSelectionIntent = "range" | "toggle" | "single";
|
||||
|
||||
export interface OrderedListSelectionItem {
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function orderedListSelectionIntent(event: Pick<MouseEvent, "shiftKey" | "metaKey" | "ctrlKey">): OrderedListSelectionIntent {
|
||||
if (event.shiftKey) return "range";
|
||||
if (event.metaKey || event.ctrlKey) return "toggle";
|
||||
return "single";
|
||||
}
|
||||
|
||||
export function orderedListRangeAnchorIndex(items: OrderedListSelectionItem[], anchorIndex: number | null, activeItem: OrderedListSelectionItem | null): number | null {
|
||||
if (anchorIndex !== null && anchorIndex >= 0 && anchorIndex < items.length) return anchorIndex;
|
||||
if (!activeItem) return null;
|
||||
const index = items.findIndex((item) => item.type === activeItem.type && item.id === activeItem.id);
|
||||
return index >= 0 ? index : null;
|
||||
}
|
||||
@@ -6,6 +6,11 @@ export const MAX_EXTERNAL_SQL_FILE_TARGETS = 200;
|
||||
export interface ExternalSqlFileTarget {
|
||||
connectionId: string;
|
||||
database: string;
|
||||
catalog?: string;
|
||||
}
|
||||
|
||||
export function unassociatedExternalSqlFileTarget(): ExternalSqlFileTarget {
|
||||
return { connectionId: "", database: "", catalog: undefined };
|
||||
}
|
||||
|
||||
interface StoredExternalSqlFileTarget extends ExternalSqlFileTarget {
|
||||
@@ -17,7 +22,9 @@ function loadExternalSqlFileTargets(): StoredExternalSqlFileTarget[] {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(EXTERNAL_SQL_FILE_TARGETS_STORAGE_KEY) || "[]");
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter((item): item is StoredExternalSqlFileTarget => typeof item?.path === "string" && typeof item?.connectionId === "string" && typeof item?.database === "string" && typeof item?.updatedAt === "number");
|
||||
return parsed
|
||||
.filter((item) => typeof item?.path === "string" && typeof item?.connectionId === "string" && typeof item?.database === "string" && (item?.catalog === undefined || typeof item.catalog === "string") && typeof item?.updatedAt === "number")
|
||||
.map((item) => ({ ...item, catalog: item.catalog })) as StoredExternalSqlFileTarget[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -44,5 +51,5 @@ export function resolveExternalSqlFileTarget(path: string, connectionExists: (co
|
||||
const normalizedPath = normalizeExternalSqlPath(path);
|
||||
const saved = loadExternalSqlFileTargets().find((item) => item.path === normalizedPath);
|
||||
if (!saved || !connectionExists(saved.connectionId)) return fallback;
|
||||
return { connectionId: saved.connectionId, database: saved.database };
|
||||
return { connectionId: saved.connectionId, database: saved.database, catalog: saved.catalog };
|
||||
}
|
||||
Reference in new issue
Block a user