fix: address lineage review feedback

This commit is contained in:
SuLea-IT committed 2026-05-03 06:02:39 +08:00
1 parent 5a37f584aa
commit 4d6cd5351c
26 files changed
+671 -139

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
import type { DatabaseType } from "@/types/database";
import { qualifiedTableName, quoteTableIdentifier } from "./tableSelectSql.ts";
export type GridCellValue = string | number | boolean | null;
export interface DataGridTableMeta {
schema?: string;
tableName: string;
primaryKeys: string[];
}
export interface DataGridSaveStatementOptions {
databaseType?: DatabaseType;
tableMeta: DataGridTableMeta;
columns: string[];
rows: GridCellValue[][];
dirtyRows: Array<[number, Array<[number, GridCellValue]>]>;
deletedRows: number[];
newRows: GridCellValue[][];
}
export function buildDataGridSaveStatements(options: DataGridSaveStatementOptions): string[] {
const table = qualifiedTableName({
databaseType: options.databaseType,
schema: options.tableMeta.schema,
tableName: options.tableMeta.tableName,
});
const statements: string[] = [];
for (const [rowIndex, changes] of options.dirtyRows) {
const row = options.rows[rowIndex];
if (!row) continue;
const sets = changes
.map(([columnIndex, value]) => `${quoteIdent(options.databaseType, options.columns[columnIndex])} = ${formatGridSqlLiteral(value)}`)
.join(", ");
const where = buildPrimaryKeyWhere(options.databaseType, options.tableMeta.primaryKeys, options.columns, row);
statements.push(`UPDATE ${table} SET ${sets} WHERE ${where};`);
}
for (const rowIndex of options.deletedRows) {
const row = options.rows[rowIndex];
if (!row) continue;
const where = buildPrimaryKeyWhere(options.databaseType, options.tableMeta.primaryKeys, options.columns, row);
statements.push(`DELETE FROM ${table} WHERE ${where};`);
}
for (const row of options.newRows) {
const columns = options.columns.map((column) => quoteIdent(options.databaseType, column)).join(", ");
const values = row.map(formatGridSqlLiteral).join(", ");
statements.push(`INSERT INTO ${table} (${columns}) VALUES (${values});`);
}
return statements;
}
export function formatGridSqlLiteral(value: GridCellValue): string {
if (value === null || value === undefined) return "NULL";
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
if (typeof value === "number" && Number.isFinite(value)) return String(value);
const text = String(value);
if (text === "") return "''";
return `'${text.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
}
function buildPrimaryKeyWhere(
databaseType: DatabaseType | undefined,
primaryKeys: string[],
columns: string[],
row: GridCellValue[],
): string {
return primaryKeys
.map((primaryKey) => {
const value = row[columns.indexOf(primaryKey)];
return `${quoteIdent(databaseType, primaryKey)} = ${formatGridSqlLiteral(value)}`;
})
.join(" AND ");
}
function quoteIdent(databaseType: DatabaseType | undefined, name: string): string {
return quoteTableIdentifier(databaseType, name);
}
+7
View File
@@ -0,0 +1,7 @@
export function shouldDeleteHistoryEntry(confirmDelete: () => boolean): boolean {
return confirmDelete();
}
export function shouldClearHistory(entryCount: number, confirmClear: () => boolean): boolean {
return entryCount > 0 && confirmClear();
}
+34
View File
@@ -0,0 +1,34 @@
import type { GridCellValue } from "./dataGridSql.ts";
export interface MarkdownTableData {
columns: string[];
rows: GridCellValue[][];
}
export function formatMarkdownTable(data: MarkdownTableData): string {
const normalizedColumns = data.columns.map(markdownCell);
const normalizedRows = data.rows.map((row) => row.map((cell) => markdownCell(displayCell(cell))));
const widths = normalizedColumns.map((column, index) =>
Math.max(column.length, ...normalizedRows.map((row) => row[index]?.length ?? 0), 3)
);
const header = `| ${normalizedColumns.map((column, index) => pad(column, widths[index])).join(" | ")} |`;
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
const body = normalizedRows
.map((row) => `| ${row.map((cell, index) => pad(cell, widths[index])).join(" | ")} |`)
.join("\n");
return `${[header, separator, body].filter(Boolean).join("\n")}\n`;
}
function displayCell(value: GridCellValue): string {
if (value === null) return "NULL";
if (typeof value === "boolean") return value ? "true" : "false";
return String(value);
}
function markdownCell(value: string): string {
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
}
function pad(value: string, width: number): string {
return value.padEnd(width);
}
+6 -1
View File
@@ -5,6 +5,7 @@ export interface BuildTableSelectSqlOptions {
schema?: string;
tableName: string;
primaryKeys?: string[];
fallbackOrderColumns?: string[];
orderBy?: string;
limit?: number;
offset?: number;
@@ -13,6 +14,7 @@ export interface BuildTableSelectSqlOptions {
export function quoteTableIdentifier(databaseType: DatabaseType | undefined, name: string): string {
if (databaseType === "mysql") return `\`${name.replace(/`/g, "``")}\``;
if (databaseType === "sqlserver") return `[${name.replace(/\]/g, "]]")}]`;
return `"${name.replace(/"/g, '""')}"`;
}
@@ -37,6 +39,8 @@ export function buildTableSelectSql(options: BuildTableSelectSqlOptions): string
const where = predicate ? ` WHERE (${predicate})` : "";
const defaultOrderBy = options.primaryKeys?.length
? options.primaryKeys.map((pk) => `${quoteTableIdentifier(databaseType, pk)} ASC`).join(", ")
: options.fallbackOrderColumns?.length
? options.fallbackOrderColumns.map((column) => `${quoteTableIdentifier(databaseType, column)} ASC`).join(", ")
: undefined;
const orderBy = options.orderBy ?? defaultOrderBy;
const order = orderBy ? ` ORDER BY ${orderBy}` : "";
@@ -47,7 +51,8 @@ export function buildTableSelectSql(options: BuildTableSelectSqlOptions): string
}
if (databaseType === "sqlserver") {
return `SELECT TOP ${limit} * FROM ${table}${where}${order}`;
const stableOrder = order || " ORDER BY (SELECT NULL)";
return `SELECT * FROM ${table}${where}${stableOrder} OFFSET ${options.offset ?? 0} ROWS FETCH NEXT ${limit} ROWS ONLY`;
}
const offset = options.offset ? ` OFFSET ${options.offset}` : "";
+5 -3
View File
@@ -146,6 +146,10 @@ export async function executeBatch(connectionId: string, database: string, state
return invoke("execute_batch", { connectionId, database, statements });
}
export async function executeScript(connectionId: string, database: string, sql: string): Promise<QueryResult> {
return invoke("execute_script", { connectionId, database, sql });
}
export async function listIndexes(connectionId: string, database: string, schema: string, table: string): Promise<IndexInfo[]> {
return invoke("list_indexes", { connectionId, database, schema, table });
}
@@ -394,9 +398,7 @@ export async function startTransfer(
if (event.payload.transferId === request.transferId) {
onProgress(event.payload);
if (event.payload.status === "done" || event.payload.status === "error" || event.payload.status === "cancelled") {
if (event.payload.tableIndex === event.payload.totalTables - 1 || event.payload.status !== "error") {
unlisten();
}
unlisten();
}
}
});
+3
View File
@@ -0,0 +1,3 @@
export function isTauriRuntime(globalObject: Record<string, unknown> = globalThis as Record<string, unknown>): boolean {
return Boolean(globalObject.__TAURI_INTERNALS__ || globalObject.__TAURI__);
}
+19
View File
@@ -0,0 +1,19 @@
export interface TransferTerminalState {
done: boolean;
cancelled: boolean;
error: boolean;
}
export interface TransferStatusLike {
status: "running" | "tableDone" | "done" | "error" | "cancelled";
}
export function nextTransferTerminalState(
state: TransferTerminalState,
progress: TransferStatusLike,
): TransferTerminalState {
if (progress.status === "done") return { ...state, done: true };
if (progress.status === "cancelled") return { ...state, cancelled: true };
if (progress.status === "error") return { ...state, error: true };
return state;
}