feat(mysql): native SQL execution — fix #41 type decoding, in-process restore, .sql open-with (#42)

Fixes #41.

MySQL columns now decode by their declared type instead of a blind try_get chain,
so every non-string type (integers, DECIMAL, BIT, YEAR, DATE/TIME/DATETIME/TIMESTAMP)
renders its value instead of NULL. MySQL restore runs in-process over the sqlx driver
(no bundled MariaDB client, so MySQL 8 caching_sha2_password works) and the Clean toggle
is implemented. .sql files can be opened with Gridline from the OS file manager.

Verified: cargo test --lib 442 passed / 0 failed; bun run test 133 files / 1151 tests
exit 0; tsc clean; both live MySQL 8 integration tests pass with the bundled CLI hidden.
This commit is contained in:
2026-09-29 08:52:47 +08:00
committed by GitHub
parent ce4a73b0bb
commit 88e7e45818
36 changed files with 2265 additions and 141 deletions
+43
View File
@@ -638,3 +638,46 @@ describe("roles slice", () => {
expect(useDbViewerStore.getState().tabs.filter((x) => x.form?.kind === "table").length).toBe(1);
});
});
describe("openQueryTab with an initial script", () => {
it("seeds the tab text and label from the payload", () => {
useDbViewerStore.setState({ tabs: [], activeTabId: null });
useDbViewerStore.getState().openQueryTab({
sql: "SELECT 1;",
label: "student_db.sql",
filePath: "/Users/me/student_db.sql",
});
const tab = useDbViewerStore.getState().tabs.slice(-1)[0]!;
expect(tab.tabType).toBe("query");
expect(tab.query).toBe("SELECT 1;");
expect(tab.table).toBe("student_db.sql");
expect(tab.filePath).toBe("/Users/me/student_db.sql");
});
it("focuses an existing tab for the same file instead of duplicating it", () => {
useDbViewerStore.setState({ tabs: [], activeTabId: null });
const payload = {
sql: "SELECT 1;",
label: "student_db.sql",
filePath: "/Users/me/student_db.sql",
};
useDbViewerStore.getState().openQueryTab(payload);
useDbViewerStore.getState().openQueryTab({ ...payload, sql: "SELECT 2;" });
const queryTabs = useDbViewerStore.getState().tabs.filter((t) => t.tabType === "query");
expect(queryTabs).toHaveLength(1);
// The already-open tab keeps its text: re-opening must not clobber edits.
expect(queryTabs[0].query).toBe("SELECT 1;");
});
it("still opens a plain empty tab when called with no payload", () => {
useDbViewerStore.setState({ tabs: [], activeTabId: null });
useDbViewerStore.getState().openQueryTab();
const tab = useDbViewerStore.getState().tabs.slice(-1)[0]!;
expect(tab.tabType).toBe("query");
expect(tab.query).toBe("");
expect(tab.filePath).toBeUndefined();
});
});