fix(tabs): allow saving a saved sql tab after clearing content

Closes #9247
This commit is contained in:
t8y2 authored and GitHub committed 2026-09-16 23:24:10 +08:00
1 parent 57e98b6d86
commit b891bf80fd
4 files changed
+66 -5

No files matched your search

@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { canSaveSqlTab, type SqlTabSaveTarget } from "@/lib/tabs/sqlTabSaveTarget";
const PROCEDURE_SOURCE = { schema: "dbo", name: "p", objectType: "PROCEDURE" } as const;
function saveTarget(overrides: Partial<SqlTabSaveTarget>): SqlTabSaveTarget {
return { externalSqlPath: undefined, savedSqlId: undefined, sql: "", objectSource: undefined, ...overrides };
}
describe("canSaveSqlTab", () => {
it("keeps a saved SQL library tab saveable after its content is deleted", () => {
// Issue #9247: emptying a query file is a legitimate edit that must reach the file.
expect(canSaveSqlTab(saveTarget({ savedSqlId: "file-1", sql: "" }))).toBe(true);
});
it("treats whitespace-only content in a saved tab as the emptied state, not as content", () => {
expect(canSaveSqlTab(saveTarget({ savedSqlId: "file-1", sql: "\n\t " }))).toBe(true);
});
it("keeps an external .sql file tab saveable after its content is deleted", () => {
expect(canSaveSqlTab(saveTarget({ externalSqlPath: "/tmp/query.sql", sql: "" }))).toBe(true);
});
it("still allows a brand-new tab with content", () => {
expect(canSaveSqlTab(saveTarget({ sql: "SELECT 1;" }))).toBe(true);
});
it("still blocks a brand-new tab with nothing to persist", () => {
expect(canSaveSqlTab(saveTarget({ sql: " \n" }))).toBe(false);
});
it("still blocks an emptied object-source tab", () => {
// Saving an empty routine body would send a body-less CREATE to the server.
expect(canSaveSqlTab(saveTarget({ objectSource: PROCEDURE_SOURCE, sql: "" }))).toBe(false);
expect(canSaveSqlTab(saveTarget({ objectSource: PROCEDURE_SOURCE, savedSqlId: "file-1", sql: "" }))).toBe(false);
});
it("allows an object-source tab with content", () => {
expect(canSaveSqlTab(saveTarget({ objectSource: PROCEDURE_SOURCE, sql: "CREATE PROCEDURE dbo.p AS SELECT 1;" }))).toBe(true);
});
});
@@ -0,0 +1,22 @@
import type { QueryTab } from "@/types/database";
/** Tab fields that decide whether its SQL can be persisted. */
export type SqlTabSaveTarget = Pick<QueryTab, "externalSqlPath" | "savedSqlId" | "sql" | "objectSource">;
/**
* Whether the tab's SQL can be saved at all.
*
* A tab that already has a persistence target — an external `.sql` file or a
* saved-SQL library entry — stays saveable after its content is emptied.
* Deleting everything is a legitimate edit that must reach the target, and the
* emptied state is also the only state in which the user can observe the guard.
*
* A brand-new tab has nothing to persist, and an emptied object-source tab would
* push a body-less `CREATE` for a routine or view to the server, so both keep the
* content requirement.
*/
export function canSaveSqlTab(tab: SqlTabSaveTarget): boolean {
if (tab.externalSqlPath || tab.sql.trim()) return true;
if (tab.objectSource) return false;
return !!tab.savedSqlId;
}