refactor(sql): move builders to Rust

This commit is contained in:
t8y2
2026-05-23 01:28:52 +08:00
parent efc68c4244
commit 7a3ff3bff9
76 changed files with 7635 additions and 4487 deletions
+39 -55
View File
@@ -73,12 +73,7 @@ import ImagePreviewDialog from "@/components/grid/ImagePreviewDialog.vue";
import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue";
import type { QueryResult, ColumnInfo, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo } from "@/types/database";
import * as api from "@/lib/api";
import {
buildTableSelectSql,
qualifiedTableName,
normalizeWhereInput,
quoteTableIdentifier,
} from "@/lib/tableSelectSql";
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
import { uuid } from "@/lib/utils";
import {
canEditExistingTableRows,
@@ -87,7 +82,12 @@ import {
isTdengineExistingRowReadonlyColumn,
usesSyntheticRowIdKey,
} from "@/lib/tableEditing";
import { formatGridSqlLiteral } from "@/lib/dataGridSql";
import {
buildDataGridContextFilterCondition,
buildDataGridCountSql,
buildHiveTablePropertiesSql,
type DataGridContextFilterMode,
} from "@/lib/dataGridSql";
import {
buildVisibleTransposeRows,
nextAppendedTransposeState,
@@ -718,7 +718,7 @@ async function applyTypedLocalFilterValue() {
if (!draft) return;
const columnName = props.result.columns[draft.columnIndex];
if (!columnName) return;
const condition = buildColumnValueFilterCondition({
const condition = await buildColumnValueFilterCondition({
databaseType: props.databaseType,
columnName,
columnInfo: props.tableMeta?.columns.find((column) => column.name === columnName),
@@ -1306,18 +1306,13 @@ watch(
hiveTableTransactional.value = undefined;
return;
}
const table = qualifiedTableName({
databaseType: "hive",
schema: props.tableMeta.schema,
tableName: props.tableMeta.tableName,
});
try {
const result = await api.executeQuery(
props.connectionId,
props.database,
`SHOW TBLPROPERTIES ${table} ('transactional')`,
props.tableMeta.schema,
);
const sql = await buildHiveTablePropertiesSql({
schema: props.tableMeta.schema,
tableName: props.tableMeta.tableName,
propertyName: "transactional",
});
const result = await api.executeQuery(props.connectionId, props.database, sql, props.tableMeta.schema);
hiveTableTransactional.value = hiveTablePropertiesIndicateTransactional(result);
} catch {
hiveTableTransactional.value = false;
@@ -1385,14 +1380,12 @@ async function lastPage() {
let sql = props.countSql;
let schema = props.schema;
if (props.tableMeta) {
const table = qualifiedTableName({
sql = await buildDataGridCountSql({
databaseType: props.databaseType,
schema: props.tableMeta.schema,
tableName: props.tableMeta.tableName,
whereInput: currentWhereInput(),
});
const predicate = normalizeWhereInput(currentWhereInput());
const where = predicate ? ` WHERE (${predicate})` : "";
sql = `SELECT COUNT(*) AS cnt FROM ${table}${where}`;
schema = props.tableMeta.schema;
}
if (!sql) return;
@@ -2122,34 +2115,24 @@ function applyContextSort(direction: "asc" | "desc" | null) {
emit("sort", column, columnIndex, direction, currentWhereInput());
}
type FilterMode =
| "equals"
| "not-equals"
| "is-null"
| "is-not-null"
| "like"
| "not-like"
| "less-than"
| "greater-than";
type FilterMode = DataGridContextFilterMode;
function contextFilterCondition(mode: FilterMode): string | null {
async function contextFilterCondition(mode: FilterMode): Promise<string | null> {
if (!contextColumn.value) return null;
const column = queryColumnRef(contextColumn.value);
const value = contextCellValue.value;
if (mode === "is-null") return `${column} IS NULL`;
if (mode === "is-not-null") return `${column} IS NOT NULL`;
if (value === null) return mode === "equals" ? `${column} IS NULL` : `${column} IS NOT NULL`;
if (mode === "like") return `${column} LIKE ${escapeVal(`%${value}%`)}`;
if (mode === "not-like") return `${column} NOT LIKE ${escapeVal(`%${value}%`)}`;
if (mode === "less-than") return `${column} < ${escapeVal(value)}`;
if (mode === "greater-than") return `${column} > ${escapeVal(value)}`;
return mode === "equals" ? `${column} = ${escapeVal(value)}` : `${column} <> ${escapeVal(value)}`;
return (
(await buildDataGridContextFilterCondition({
databaseType: props.databaseType,
columnName: contextColumn.value,
columnInfo: props.tableMeta?.columns.find((column) => column.name === contextColumn.value),
mode,
value: contextCellValue.value,
})) ?? null
);
}
async function applyContextFilter(mode: FilterMode) {
if (!canUseWhereSearch.value) return;
const condition = contextFilterCondition(mode);
const condition = await contextFilterCondition(mode);
if (!condition) return;
const existing = whereFilterInput.value.trim();
whereFilterInput.value = existing ? `(${existing}) AND (${condition})` : condition;
@@ -2172,7 +2155,7 @@ async function applyOrderBySearch() {
sortColIndex.value = null;
sortDir.value = "asc";
try {
const sql = buildTableSelectSql({
const sql = await buildTableSelectSql({
databaseType: props.databaseType,
schema: props.tableMeta.schema,
tableName: props.tableMeta.tableName,
@@ -2197,7 +2180,7 @@ async function applyWhereFilter() {
saveError.value = "";
currentPage.value = 1;
try {
const sql = buildTableSelectSql({
const sql = await buildTableSelectSql({
databaseType: props.databaseType,
schema: props.tableMeta.schema,
tableName: props.tableMeta.tableName,
@@ -2235,10 +2218,6 @@ function queryColumnRef(name: string): string {
return props.databaseType === "neo4j" ? `n.${quoted}` : quoted;
}
function escapeVal(value: CellValue): string {
return formatGridSqlLiteral(value);
}
function isNull(value: unknown): boolean {
return value === null;
}
@@ -2520,12 +2499,17 @@ function copyDetailColumnName() {
copyText(activeCellDetail.value.column);
}
function copyDetailSqlCondition() {
async function copyDetailSqlCondition() {
const detail = activeCellDetail.value;
if (!detail) return;
const column = quoteIdent(detail.column);
const condition = detail.value === null ? `${column} IS NULL` : `${column} = ${escapeVal(detail.value)}`;
copyText(condition);
const condition = await buildDataGridContextFilterCondition({
databaseType: props.databaseType,
columnName: detail.column,
columnInfo: props.tableMeta?.columns.find((column) => column.name === detail.column),
mode: "equals",
value: detail.value,
});
if (condition) copyText(condition);
}
const TRANSPOSE_RECORD_DEFAULT_WIDTH = 168;
@@ -51,10 +51,15 @@ import {
supportsTableImport,
supportsTableStructureEditing,
supportsTableTruncate,
usesPostgresLikeStructureCopy,
usesFetchFirst,
} from "@/lib/databaseFeatureSupport";
import { buildTableSelectSql, qualifiedTableName, quoteTableIdentifier } from "@/lib/tableSelectSql";
import { buildTableSelectSql } from "@/lib/tableSelectSql";
import {
buildDropObjectSql,
buildDuplicateTableStructureSql,
buildEmptyTableSql,
buildTruncateTableSql,
type TableAdminSqlOptions,
} from "@/lib/dbAdminSql";
import { useToast } from "@/composables/useToast";
import {
buildExecutableObjectSourceStatements,
@@ -128,10 +133,13 @@ const showRenameDialog = ref(false);
const renameTarget = ref<ObjectBrowserRow | null>(null);
const renameInput = ref("");
const renameError = ref("");
const renamePreviewSqlText = ref("");
const showTruncateConfirm = ref(false);
const truncateTarget = ref<ObjectBrowserRow | null>(null);
const truncatePreviewSql = ref("");
const showEmptyConfirm = ref(false);
const emptyTarget = ref<ObjectBrowserRow | null>(null);
const emptyPreviewSql = ref("");
const showDuplicateDialog = ref(false);
const duplicateTarget = ref<ObjectBrowserRow | null>(null);
const duplicateTableName = ref("");
@@ -295,7 +303,7 @@ async function openViewDdl(row: ObjectBrowserRow) {
row.name,
"VIEW",
);
const ddl = buildViewDdl({
const ddl = await buildViewDdl({
databaseType: props.connection.db_type,
schema: row.schema || selectedSchema.value || props.database,
name: row.name,
@@ -308,11 +316,11 @@ async function openViewDdl(row: ObjectBrowserRow) {
}
}
function openNewQuery(row: ObjectBrowserRow) {
async function openNewQuery(row: ObjectBrowserRow) {
const tabId = queryStore.createTab(props.connection.id, props.database, row.name);
queryStore.updateSql(
tabId,
buildTableSelectSql({
await buildTableSelectSql({
databaseType: props.connection.db_type,
schema: row.schema || selectedSchema.value,
tableName: row.name,
@@ -321,14 +329,6 @@ function openNewQuery(row: ObjectBrowserRow) {
);
}
function qualifiedName(row: ObjectBrowserRow): string {
return qualifiedTableName({
databaseType: props.connection.db_type,
schema: row.schema || selectedSchema.value,
tableName: row.name,
});
}
function requestDrop(row: ObjectBrowserRow) {
dropTarget.value = row;
showDropConfirm.value = true;
@@ -338,29 +338,42 @@ function requestRename(row: ObjectBrowserRow) {
renameTarget.value = row;
renameInput.value = row.name;
renameError.value = "";
renamePreviewSqlText.value = "";
showRenameDialog.value = true;
}
function renamePreviewSql() {
let renamePreviewRequestId = 0;
async function refreshRenamePreviewSql() {
const requestId = ++renamePreviewRequestId;
const row = renameTarget.value;
const newName = renameInput.value.trim();
if (!row || !newName || newName === row.name) return "";
if (!showRenameDialog.value || !row || !newName || newName === row.name) {
renamePreviewSqlText.value = "";
return;
}
if (supportsSourceBackedRoutineRename(props.connection.db_type, row.type as ObjectSourceKind)) {
return `-- Recreate ${row.type} from source, then drop the original object.`;
renamePreviewSqlText.value = `-- Recreate ${row.type} from source, then drop the original object.`;
return;
}
try {
return buildRenameObjectSql({
const sql = await buildRenameObjectSql({
databaseType: props.connection.db_type,
objectType: row.type,
schema: row.schema || selectedSchema.value,
oldName: row.name,
newName,
});
if (requestId === renamePreviewRequestId) renamePreviewSqlText.value = sql;
} catch {
return "";
if (requestId === renamePreviewRequestId) renamePreviewSqlText.value = "";
}
}
watch([showRenameDialog, renameTarget, renameInput, selectedSchema], () => {
void refreshRenamePreviewSql();
});
async function confirmRename() {
const row = renameTarget.value;
const newName = renameInput.value.trim();
@@ -376,7 +389,7 @@ async function confirmRename() {
row.name,
row.type as ObjectSourceKind,
);
const statements = buildRoutineRenameObjectSourceStatements({
const statements = await buildRoutineRenameObjectSourceStatements({
databaseType: props.connection.db_type,
objectType: row.type as ObjectSourceKind,
schema,
@@ -388,7 +401,7 @@ async function confirmRename() {
await api.executeQuery(props.connection.id, props.database, sql, schema);
}
} else {
const sql = buildRenameObjectSql({
const sql = await buildRenameObjectSql({
databaseType: props.connection.db_type,
objectType: row.type,
schema,
@@ -414,17 +427,14 @@ async function confirmRename() {
async function confirmDrop() {
if (!dropTarget.value) return;
const row = dropTarget.value;
const typeSql =
row.type === "VIEW"
? "VIEW"
: row.type === "PROCEDURE"
? "PROCEDURE"
: row.type === "FUNCTION"
? "FUNCTION"
: "TABLE";
const name = qualifiedName(row);
try {
await api.executeQuery(props.connection.id, props.database, `DROP ${typeSql} ${name}`);
const sql = await buildDropObjectSql({
databaseType: props.connection.db_type,
objectType: row.type,
schema: row.schema || selectedSchema.value,
name: row.name,
});
await api.executeQuery(props.connection.id, props.database, sql);
const successKey =
row.type === "VIEW"
? "contextMenu.dropViewSuccess"
@@ -474,10 +484,6 @@ function closeSource() {
sourceSaveError.value = "";
}
function quoteIdent(name: string): string {
return quoteTableIdentifier(props.connection.db_type, name);
}
async function saveFileContent(content: string, defaultFileName: string, filterName: string, filterExt: string) {
if (isTauriRuntime()) {
const { save } = await import("@tauri-apps/plugin-dialog");
@@ -562,10 +568,6 @@ async function exportStructure(row: ObjectBrowserRow) {
async function exportData(row: ObjectBrowserRow, format: "csv" | "json" | "sql") {
try {
const schema = row.schema || selectedSchema.value;
const qualName =
isSchemaAware(props.connection.db_type) && schema
? `${quoteIdent(schema)}.${quoteIdent(row.name)}`
: quoteIdent(row.name);
const queryColumns =
props.connection.db_type === "neo4j"
? (await api.getColumns(props.connection.id, props.database, schema || props.database, row.name)).map(
@@ -612,7 +614,13 @@ async function exportData(row: ObjectBrowserRow, format: "csv" | "json" | "sql")
return;
}
const content = formatSqlInsert(qualName, result.columns, result.rows, quoteIdent);
const content = await formatSqlInsert({
databaseType: props.connection.db_type,
schema,
tableName: row.name,
columns: result.columns,
rows: result.rows,
});
await saveFileContent(content, `${row.name}.sql`, "SQL", "sql");
toast(t("grid.exported"));
} catch (e: any) {
@@ -667,25 +675,12 @@ async function confirmDuplicateStructure() {
showDuplicateDialog.value = false;
try {
const schema = row.schema || selectedSchema.value;
const source = qualifiedName(row);
const target = qualifiedTableName({
const sql = await buildDuplicateTableStructureSql({
databaseType: props.connection.db_type,
schema,
tableName: newName,
sourceName: row.name,
targetName: newName,
});
let sql: string;
const dbType = props.connection.db_type;
if (dbType === "mysql") {
sql = `CREATE TABLE ${target} LIKE ${source};`;
} else if (usesPostgresLikeStructureCopy(dbType)) {
sql = `CREATE TABLE ${target} (LIKE ${source} INCLUDING ALL);`;
} else if (dbType === "sqlserver") {
sql = `SELECT TOP 0 * INTO ${target} FROM ${source};`;
} else if (usesFetchFirst(dbType)) {
sql = `CREATE TABLE ${target} AS SELECT * FROM ${source} WHERE 1=0`;
} else {
sql = `CREATE TABLE ${target} AS SELECT * FROM ${source} WHERE 0;`;
}
await api.executeQuery(props.connection.id, props.database, sql, schema);
toast(t("contextMenu.duplicateStructureSuccess", { name: newName }));
await reload();
@@ -695,15 +690,22 @@ async function confirmDuplicateStructure() {
}
}
function buildTruncateTableSql(row: ObjectBrowserRow): string {
const name = qualifiedName(row);
const dbType = props.connection.db_type;
if (dbType === "sqlite" || dbType === "duckdb") return `DELETE FROM ${name};`;
return `TRUNCATE TABLE ${name};`;
function tableAdminSqlOptions(row: ObjectBrowserRow): TableAdminSqlOptions {
return {
databaseType: props.connection.db_type,
schema: row.schema || selectedSchema.value,
tableName: row.name,
};
}
async function refreshTruncatePreviewSql(row: ObjectBrowserRow) {
truncatePreviewSql.value = "";
truncatePreviewSql.value = await buildTruncateTableSql(tableAdminSqlOptions(row)).catch(() => "");
}
function requestTruncateTable(row: ObjectBrowserRow) {
truncateTarget.value = row;
void refreshTruncatePreviewSql(row);
showTruncateConfirm.value = true;
}
@@ -711,7 +713,8 @@ async function confirmTruncateTable() {
const row = truncateTarget.value;
if (!row) return;
try {
await api.executeQuery(props.connection.id, props.database, buildTruncateTableSql(row));
const sql = truncatePreviewSql.value || (await buildTruncateTableSql(tableAdminSqlOptions(row)));
await api.executeQuery(props.connection.id, props.database, sql);
toast(t("contextMenu.truncateTableSuccess", { name: row.name }));
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
@@ -719,12 +722,14 @@ async function confirmTruncateTable() {
truncateTarget.value = null;
}
function buildEmptyTableSql(row: ObjectBrowserRow): string {
return `DELETE FROM ${qualifiedName(row)};`;
async function refreshEmptyPreviewSql(row: ObjectBrowserRow) {
emptyPreviewSql.value = "";
emptyPreviewSql.value = await buildEmptyTableSql(tableAdminSqlOptions(row)).catch(() => "");
}
function requestEmptyTable(row: ObjectBrowserRow) {
emptyTarget.value = row;
void refreshEmptyPreviewSql(row);
showEmptyConfirm.value = true;
}
@@ -732,7 +737,8 @@ async function confirmEmptyTable() {
const row = emptyTarget.value;
if (!row) return;
try {
await api.executeQuery(props.connection.id, props.database, buildEmptyTableSql(row));
const sql = emptyPreviewSql.value || (await buildEmptyTableSql(tableAdminSqlOptions(row)));
await api.executeQuery(props.connection.id, props.database, sql);
toast(t("contextMenu.emptyTableSuccess", { name: row.name }));
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
@@ -771,7 +777,7 @@ async function saveSource() {
sourceSaving.value = true;
sourceSaveError.value = "";
try {
const statements = buildExecutableObjectSourceStatements({
const statements = await buildExecutableObjectSourceStatements({
databaseType: props.connection.db_type,
objectType: row.type as ObjectSourceKind,
schema,
@@ -1300,9 +1306,9 @@ watch(
@keydown.enter.prevent="confirmRename"
/>
<pre
v-if="renamePreviewSql()"
v-if="renamePreviewSqlText"
class="max-h-32 overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap"
>{{ renamePreviewSql() }}</pre
>{{ renamePreviewSqlText }}</pre
>
<p v-if="renameError" class="text-sm text-destructive">{{ renameError }}</p>
</div>
@@ -1319,7 +1325,7 @@ watch(
v-model:open="showTruncateConfirm"
:title="t('contextMenu.confirmTruncateTableTitle')"
:message="t('contextMenu.confirmTruncateTableMessage', { name: truncateTarget?.name ?? '' })"
:sql="truncateTarget ? buildTruncateTableSql(truncateTarget) : ''"
:sql="truncatePreviewSql"
:confirm-label="t('contextMenu.truncateTable')"
@confirm="confirmTruncateTable"
/>
@@ -1328,7 +1334,7 @@ watch(
v-model:open="showEmptyConfirm"
:title="t('contextMenu.confirmEmptyTableTitle')"
:message="t('contextMenu.confirmEmptyTableMessage', { name: emptyTarget?.name ?? '' })"
:sql="emptyTarget ? buildEmptyTableSql(emptyTarget) : ''"
:sql="emptyPreviewSql"
:confirm-label="t('contextMenu.emptyTable')"
@confirm="confirmEmptyTable"
/>
@@ -216,7 +216,7 @@ async function searchTable(task: SearchTableTask, databaseType: DatabaseType, cu
try {
const schema = task.schema || props.prefillDatabase;
const columns = await api.getColumns(props.prefillConnectionId, props.prefillDatabase, schema, task.table.name);
const query = buildDatabaseSearchSql({
const query = await buildDatabaseSearchSql({
databaseType,
schema: task.schema,
tableName: task.table.name,
@@ -228,13 +228,19 @@ async function searchTable(task: SearchTableTask, databaseType: DatabaseType, cu
const executionId = makeExecutionId();
currentExecutionId.value = executionId;
const result = await api.executeQuery(props.prefillConnectionId, props.prefillDatabase, query.sql, executionId);
const result = await api.executeQuery(
props.prefillConnectionId,
props.prefillDatabase,
query.sql,
undefined,
executionId,
);
if (currentExecutionId.value === executionId) currentExecutionId.value = "";
if (currentRun !== runId || cancelled.value) return;
result.rows.forEach((row, rowIndex) => {
for (const [rowIndex, row] of result.rows.entries()) {
const matchedColumns = findMatchedSearchColumns(result.columns, row, columns, keyword.value);
const whereInput = buildSearchResultWhere({
const whereInput = await buildSearchResultWhere({
databaseType,
columns,
resultColumns: result.columns,
@@ -249,7 +255,7 @@ async function searchTable(task: SearchTableTask, databaseType: DatabaseType, cu
preview: rowPreview(result.columns, row, matchedColumns),
whereInput,
});
});
}
} catch (error: any) {
if (!cancelled.value) {
tableErrors.value.push({ tableName: tableLabel, message: error?.message || String(error) });
+154 -88
View File
@@ -65,14 +65,9 @@ import * as api from "@/lib/api";
import { uuid } from "@/lib/utils";
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
import { canTreeNodeShowExpander, treeItemPaddingLeft } from "@/lib/sidebarTreeItemLayout";
import {
buildTableSelectSql,
qualifiedTableName as buildQualifiedTableName,
quoteTableIdentifier,
} from "@/lib/tableSelectSql";
import { buildTableSelectSql } from "@/lib/tableSelectSql";
import { editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/tableEditing";
import {
isSchemaAware,
supportsDatabaseCreation,
supportsDatabaseSearch,
supportsFieldLineage,
@@ -82,9 +77,7 @@ import {
supportsTableImport,
supportsTableTruncate,
supportsTableStructureEditing,
usesPostgresLikeStructureCopy,
usesTreeSchemaMode,
usesFetchFirst,
} from "@/lib/databaseCapabilities";
import {
objectSourceKindForTreeNode,
@@ -101,6 +94,18 @@ import {
supportsCreateDatabaseCharset,
uniqueDuckDbAttachedDatabaseName,
} from "@/lib/createDatabaseSql";
import {
buildCreateSchemaSql,
buildDropDatabaseSql,
buildDropObjectSql,
buildDropSchemaSql,
buildDropTableSql,
buildDuplicateTableStructureSql,
buildEmptyTableSql,
buildTruncateTableSql,
type DropObjectSqlOptions,
type TableAdminSqlOptions,
} from "@/lib/dbAdminSql";
import { buildRenameObjectSql, supportsObjectRename, type RenameableObjectType } from "@/lib/objectRenameSql";
import { buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/objectSourceEditor";
import { buildViewDdl } from "@/lib/viewDdl";
@@ -151,18 +156,6 @@ function currentDatabaseType(): DatabaseType | undefined {
return props.node.connectionId ? connectionStore.getConfig(props.node.connectionId)?.db_type : undefined;
}
function quoteIdent(name: string): string {
return quoteTableIdentifier(currentDatabaseType(), name);
}
function qualifiedTableName(tableName: string, schema?: string): string {
return buildQualifiedTableName({
databaseType: currentDatabaseType(),
schema,
tableName,
});
}
function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
switch (node.type) {
case "connection":
@@ -527,7 +520,7 @@ async function openData() {
});
const pks = editablePrimaryKeys(config.db_type, columns);
const limit = settingsStore.editorSettings.pageSize;
const sql = buildTableSelectSql({
const sql = await buildTableSelectSql({
databaseType: config.db_type,
schema: node.schema,
tableName: node.label,
@@ -660,6 +653,13 @@ const showTruncateTableConfirm = ref(false);
const showRenameObjectDialog = ref(false);
const renameObjectName = ref("");
const renameObjectError = ref("");
const renameObjectPreviewSql = ref("");
const dropTablePreviewSql = ref("");
const emptyTablePreviewSql = ref("");
const truncateTablePreviewSql = ref("");
const dropObjectPreviewSql = ref("");
const dropDatabasePreviewSql = ref("");
const dropSchemaPreviewSql = ref("");
const showDuplicateDialog = ref(false);
const duplicateTableName = ref("");
@@ -676,11 +676,21 @@ const showDropSchemaConfirm = ref(false);
// --- Procedure / Function Management ---
const showDropObjectConfirm = ref(false);
function buildDropObjectSql(): string {
function dropObjectSqlOptions(): DropObjectSqlOptions | null {
const node = props.node;
const keyword = node.type === "procedure" ? "PROCEDURE" : "FUNCTION";
const name = qualifiedTableName(node.label, node.schema);
return `DROP ${keyword} ${name};`;
if (node.type !== "procedure" && node.type !== "function") return null;
return {
databaseType: currentDatabaseType(),
objectType: node.type === "procedure" ? "PROCEDURE" : "FUNCTION",
schema: node.schema,
name: node.label,
};
}
async function refreshDropObjectPreviewSql() {
const options = dropObjectSqlOptions();
dropObjectPreviewSql.value = "";
dropObjectPreviewSql.value = options ? await buildDropObjectSql(options).catch(() => "") : "";
}
function viewObjectSource() {
@@ -695,7 +705,7 @@ function viewObjectSource() {
connectionStore.activeConnectionId = node.connectionId!;
return api.getObjectSource(node.connectionId!, node.database!, schema, node.label, objectType as any);
})
.then((result) => {
.then(async (result) => {
const tabId = queryStore.createTab(node.connectionId!, node.database!, node.label);
queryStore.updateSql(tabId, result.source);
queryStore.setObjectSource(tabId, {
@@ -719,9 +729,9 @@ function viewObjectDdl() {
connectionStore.activeConnectionId = node.connectionId!;
return api.getObjectSource(node.connectionId!, node.database!, schema, node.label, "VIEW");
})
.then((result) => {
.then(async (result) => {
const connection = connectionStore.getConfig(node.connectionId!);
const ddl = buildViewDdl({
const ddl = await buildViewDdl({
databaseType: connection?.db_type,
schema,
name: node.label,
@@ -736,6 +746,7 @@ function viewObjectDdl() {
}
function requestDropObject() {
void refreshDropObjectPreviewSql();
showDropObjectConfirm.value = true;
}
@@ -759,29 +770,52 @@ const canRenameObject = computed(() => {
function openRenameObjectDialog() {
renameObjectName.value = props.node.label;
renameObjectError.value = "";
renameObjectPreviewSql.value = "";
showRenameObjectDialog.value = true;
}
function buildRenameObjectPreviewSql(): string {
let renameObjectPreviewRequestId = 0;
async function refreshRenameObjectPreviewSql() {
const requestId = ++renameObjectPreviewRequestId;
const objectType = nodeRenameObjectType();
const newName = renameObjectName.value.trim();
if (!objectType || !newName || newName === props.node.label) return "";
if (!showRenameObjectDialog.value || !objectType || !newName || newName === props.node.label) {
renameObjectPreviewSql.value = "";
return;
}
if (supportsSourceBackedRoutineRename(currentDatabaseType(), objectType as any)) {
return `-- Recreate ${objectType} from source, then drop the original object.`;
renameObjectPreviewSql.value = `-- Recreate ${objectType} from source, then drop the original object.`;
return;
}
try {
return buildRenameObjectSql({
const sql = await buildRenameObjectSql({
databaseType: currentDatabaseType(),
objectType,
schema: props.node.schema,
oldName: props.node.label,
newName,
});
if (requestId === renameObjectPreviewRequestId) renameObjectPreviewSql.value = sql;
} catch {
return "";
if (requestId === renameObjectPreviewRequestId) renameObjectPreviewSql.value = "";
}
}
watch(
[
showRenameObjectDialog,
renameObjectName,
() => props.node.label,
() => props.node.schema,
() => props.node.type,
() => currentDatabaseType(),
],
() => {
void refreshRenameObjectPreviewSql();
},
);
async function confirmRenameObject() {
const node = props.node;
const objectType = nodeRenameObjectType();
@@ -794,7 +828,7 @@ async function confirmRenameObject() {
if (supportsSourceBackedRoutineRename(dbType, objectType as any)) {
const schema = node.schema || node.database;
const source = await api.getObjectSource(node.connectionId, node.database, schema, node.label, objectType as any);
const statements = buildRoutineRenameObjectSourceStatements({
const statements = await buildRoutineRenameObjectSourceStatements({
databaseType: dbType!,
objectType: objectType as any,
schema,
@@ -806,7 +840,7 @@ async function confirmRenameObject() {
await api.executeQuery(node.connectionId, node.database, sql, schema);
}
} else {
const sql = buildRenameObjectSql({
const sql = await buildRenameObjectSql({
databaseType: dbType,
objectType,
schema: node.schema,
@@ -826,9 +860,12 @@ async function confirmRenameObject() {
async function confirmDropObject() {
const node = props.node;
if (!node.connectionId || !node.database) return;
const options = dropObjectSqlOptions();
if (!options) return;
try {
await connectionStore.ensureConnected(node.connectionId);
await api.executeQuery(node.connectionId, node.database, buildDropObjectSql(), node.schema);
const sql = dropObjectPreviewSql.value || (await buildDropObjectSql(options));
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
const msgKey = node.type === "procedure" ? "contextMenu.dropProcedureSuccess" : "contextMenu.dropFunctionSuccess";
toast(t(msgKey, { name: node.label }), 3000);
await refreshTableList(node);
@@ -884,22 +921,31 @@ const canDropSchema = computed(() => {
return props.node.type === "schema" && usesTreeSchemaMode(config?.db_type);
});
function buildDropTableSql(): string {
return `DROP TABLE ${qualifiedTableName(props.node.label, props.node.schema)};`;
function tableAdminSqlOptions(): TableAdminSqlOptions {
return {
databaseType: currentDatabaseType(),
schema: props.node.schema,
tableName: props.node.label,
};
}
function buildEmptyTableSql(): string {
return `DELETE FROM ${qualifiedTableName(props.node.label, props.node.schema)};`;
async function refreshDropTablePreviewSql() {
dropTablePreviewSql.value = "";
dropTablePreviewSql.value = await buildDropTableSql(tableAdminSqlOptions()).catch(() => "");
}
function buildTruncateTableSql(): string {
const dbType = currentDatabaseType();
const name = qualifiedTableName(props.node.label, props.node.schema);
if (dbType === "sqlite" || dbType === "duckdb") return `DELETE FROM ${name};`;
return `TRUNCATE TABLE ${name};`;
async function refreshEmptyTablePreviewSql() {
emptyTablePreviewSql.value = "";
emptyTablePreviewSql.value = await buildEmptyTableSql(tableAdminSqlOptions()).catch(() => "");
}
async function refreshTruncateTablePreviewSql() {
truncateTablePreviewSql.value = "";
truncateTablePreviewSql.value = await buildTruncateTableSql(tableAdminSqlOptions()).catch(() => "");
}
function dropTable() {
void refreshDropTablePreviewSql();
showDropTableConfirm.value = true;
}
@@ -913,7 +959,8 @@ async function confirmDropTable() {
if (!node.connectionId || !node.database) return;
try {
await connectionStore.ensureConnected(node.connectionId);
await api.executeQuery(node.connectionId, node.database, buildDropTableSql(), node.schema);
const sql = dropTablePreviewSql.value || (await buildDropTableSql(tableAdminSqlOptions()));
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
toast(t("contextMenu.dropTableSuccess", { name: node.label }), 3000);
connectionStore.removeTreeNode(node.id);
} catch (e: any) {
@@ -922,6 +969,7 @@ async function confirmDropTable() {
}
function emptyTable() {
void refreshEmptyTablePreviewSql();
showEmptyTableConfirm.value = true;
}
@@ -930,7 +978,8 @@ async function confirmEmptyTable() {
if (!node.connectionId || !node.database) return;
try {
await connectionStore.ensureConnected(node.connectionId);
await api.executeQuery(node.connectionId, node.database, buildEmptyTableSql(), node.schema);
const sql = emptyTablePreviewSql.value || (await buildEmptyTableSql(tableAdminSqlOptions()));
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
toast(t("contextMenu.emptyTableSuccess", { name: node.label }), 3000);
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
@@ -938,6 +987,7 @@ async function confirmEmptyTable() {
}
function truncateTable() {
void refreshTruncateTablePreviewSql();
showTruncateTableConfirm.value = true;
}
@@ -946,22 +996,28 @@ async function confirmTruncateTable() {
if (!node.connectionId || !node.database) return;
try {
await connectionStore.ensureConnected(node.connectionId);
await api.executeQuery(node.connectionId, node.database, buildTruncateTableSql(), node.schema);
const sql = truncateTablePreviewSql.value || (await buildTruncateTableSql(tableAdminSqlOptions()));
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
toast(t("contextMenu.truncateTableSuccess", { name: node.label }), 3000);
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
}
function buildDropDatabaseSql(): string {
return `DROP DATABASE ${quoteIdent(props.node.label)};`;
async function refreshDropDatabasePreviewSql() {
dropDatabasePreviewSql.value = "";
dropDatabasePreviewSql.value = await buildDropDatabaseSql({
databaseType: currentDatabaseType(),
name: props.node.label,
}).catch(() => "");
}
function buildDropSchemaSql(): string {
const dbType = currentDatabaseType();
const name = quoteIdent(props.node.label);
if (dbType === "postgres" || dbType === "gaussdb") return `DROP SCHEMA ${name} CASCADE;`;
return `DROP SCHEMA ${name};`;
async function refreshDropSchemaPreviewSql() {
dropSchemaPreviewSql.value = "";
dropSchemaPreviewSql.value = await buildDropSchemaSql({
databaseType: currentDatabaseType(),
name: props.node.label,
}).catch(() => "");
}
async function openCreateDatabase() {
@@ -1006,7 +1062,7 @@ async function createDuckDbAttachedDatabaseFile() {
duckDbAttachedDatabaseNameFromPath(path),
existingDatabases.map((database) => database.name),
);
await api.executeQuery(node.connectionId, "", buildDuckDbAttachDatabaseSql(path, name));
await api.executeQuery(node.connectionId, "", await buildDuckDbAttachDatabaseSql(path, name));
const config = connectionStore.getConfig(node.connectionId);
if (config) {
@@ -1031,7 +1087,7 @@ async function confirmCreateDatabase() {
try {
await connectionStore.ensureConnected(node.connectionId);
const config = connectionStore.getConfig(node.connectionId);
const sql = buildCreateDatabaseSql({
const sql = await buildCreateDatabaseSql({
databaseType: config?.db_type,
driverProfile: config?.driver_profile,
name,
@@ -1047,6 +1103,7 @@ async function confirmCreateDatabase() {
}
function dropDatabase() {
void refreshDropDatabasePreviewSql();
showDropDatabaseConfirm.value = true;
}
@@ -1077,7 +1134,13 @@ async function confirmDropDatabase() {
if (!node.connectionId) return;
try {
await connectionStore.ensureConnected(node.connectionId);
await api.executeQuery(node.connectionId, "", buildDropDatabaseSql());
const sql =
dropDatabasePreviewSql.value ||
(await buildDropDatabaseSql({
databaseType: currentDatabaseType(),
name: node.label,
}));
await api.executeQuery(node.connectionId, "", sql);
toast(t("contextMenu.dropDatabaseSuccess", { name: node.label }), 3000);
await connectionStore.loadDatabases(node.connectionId, { force: true });
} catch (e: any) {
@@ -1097,7 +1160,10 @@ async function confirmCreateSchema() {
showCreateSchemaDialog.value = false;
try {
await connectionStore.ensureConnected(node.connectionId);
const sql = `CREATE SCHEMA ${quoteIdent(name)};`;
const sql = await buildCreateSchemaSql({
databaseType: currentDatabaseType(),
name,
});
await api.executeQuery(node.connectionId, node.database, sql);
toast(t("contextMenu.createSchemaSuccess", { name }), 3000);
const config = connectionStore.getConfig(node.connectionId);
@@ -1112,6 +1178,7 @@ async function confirmCreateSchema() {
}
function dropSchema() {
void refreshDropSchemaPreviewSql();
showDropSchemaConfirm.value = true;
}
@@ -1120,7 +1187,13 @@ async function confirmDropSchema() {
if (!node.connectionId || !node.database) return;
try {
await connectionStore.ensureConnected(node.connectionId);
await api.executeQuery(node.connectionId, node.database, buildDropSchemaSql());
const sql =
dropSchemaPreviewSql.value ||
(await buildDropSchemaSql({
databaseType: currentDatabaseType(),
name: node.label,
}));
await api.executeQuery(node.connectionId, node.database, sql);
toast(t("contextMenu.dropSchemaSuccess", { name: node.label }), 3000);
const config = connectionStore.getConfig(node.connectionId);
if (config?.db_type === "sqlserver") {
@@ -1145,21 +1218,12 @@ async function confirmDuplicateStructure() {
showDuplicateDialog.value = false;
try {
await connectionStore.ensureConnected(node.connectionId);
const dbType = currentDatabaseType();
const source = qualifiedTableName(node.label, node.schema);
const target = qualifiedTableName(newName, node.schema);
let sql: string;
if (dbType === "mysql") {
sql = `CREATE TABLE ${target} LIKE ${source};`;
} else if (usesPostgresLikeStructureCopy(dbType)) {
sql = `CREATE TABLE ${target} (LIKE ${source} INCLUDING ALL);`;
} else if (dbType === "sqlserver") {
sql = `SELECT TOP 0 * INTO ${target} FROM ${source};`;
} else if (usesFetchFirst(dbType)) {
sql = `CREATE TABLE ${target} AS SELECT * FROM ${source} WHERE 1=0`;
} else {
sql = `CREATE TABLE ${target} AS SELECT * FROM ${source} WHERE 0;`;
}
const sql = await buildDuplicateTableStructureSql({
databaseType: currentDatabaseType(),
schema: node.schema,
sourceName: node.label,
targetName: newName,
});
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
toast(t("contextMenu.duplicateStructureSuccess", { name: newName }), 3000);
await refreshTableList(node);
@@ -1221,10 +1285,6 @@ async function exportData(format: "csv" | "json" | "sql") {
try {
await connectionStore.ensureConnected(connectionId);
const qualifiedName =
isSchemaAware(config.db_type) && node.schema
? `${quoteIdent(node.schema)}.${quoteIdent(node.label)}`
: quoteIdent(node.label);
const queryColumns =
config.db_type === "neo4j"
? (await api.getColumns(connectionId, database, node.schema || database, node.label)).map(
@@ -1271,7 +1331,13 @@ async function exportData(format: "csv" | "json" | "sql") {
return;
}
const content = formatSqlInsert(qualifiedName, result.columns, result.rows, quoteIdent);
const content = await formatSqlInsert({
databaseType: config.db_type,
schema: node.schema,
tableName: node.label,
columns: result.columns,
rows: result.rows,
});
await saveFileContent(content, `${node.label}.sql`, "SQL", "sql");
toast(t("grid.exported"));
} catch (e: any) {
@@ -2236,9 +2302,9 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
@keydown.enter.prevent="confirmRenameObject"
/>
<pre
v-if="buildRenameObjectPreviewSql()"
v-if="renameObjectPreviewSql"
class="max-h-32 overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap"
>{{ buildRenameObjectPreviewSql() }}</pre
>{{ renameObjectPreviewSql }}</pre
>
<p v-if="renameObjectError" class="text-sm text-destructive">{{ renameObjectError }}</p>
</div>
@@ -2290,7 +2356,7 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
v-model:open="showDropTableConfirm"
:title="t('contextMenu.confirmDropTableTitle')"
:message="t('contextMenu.confirmDropTableMessage', { name: node.label })"
:sql="buildDropTableSql()"
:sql="dropTablePreviewSql"
:confirm-label="t('contextMenu.dropTable')"
@confirm="confirmDropTable"
/>
@@ -2299,7 +2365,7 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
v-model:open="showEmptyTableConfirm"
:title="t('contextMenu.confirmEmptyTableTitle')"
:message="t('contextMenu.confirmEmptyTableMessage', { name: node.label })"
:sql="buildEmptyTableSql()"
:sql="emptyTablePreviewSql"
:confirm-label="t('contextMenu.emptyTable')"
@confirm="confirmEmptyTable"
/>
@@ -2308,7 +2374,7 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
v-model:open="showTruncateTableConfirm"
:title="t('contextMenu.confirmTruncateTableTitle')"
:message="t('contextMenu.confirmTruncateTableMessage', { name: node.label })"
:sql="buildTruncateTableSql()"
:sql="truncateTablePreviewSql"
:confirm-label="t('contextMenu.truncateTable')"
@confirm="confirmTruncateTable"
/>
@@ -2323,7 +2389,7 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
? t('contextMenu.confirmDropProcedureMessage', { name: node.label })
: t('contextMenu.confirmDropFunctionMessage', { name: node.label })
"
:sql="buildDropObjectSql()"
:sql="dropObjectPreviewSql"
:confirm-label="node.type === 'procedure' ? t('contextMenu.dropProcedure') : t('contextMenu.dropFunction')"
@confirm="confirmDropObject"
/>
@@ -2390,7 +2456,7 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
v-model:open="showDropDatabaseConfirm"
:title="t('contextMenu.confirmDropDatabaseTitle')"
:message="t('contextMenu.confirmDropDatabaseMessage', { name: node.label })"
:sql="buildDropDatabaseSql()"
:sql="dropDatabasePreviewSql"
:confirm-label="t('contextMenu.dropDatabase')"
@confirm="confirmDropDatabase"
/>
@@ -2427,7 +2493,7 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
v-model:open="showDropSchemaConfirm"
:title="t('contextMenu.confirmDropSchemaTitle')"
:message="t('contextMenu.confirmDropSchemaMessage', { name: node.label })"
:sql="buildDropSchemaSql()"
:sql="dropSchemaPreviewSql"
:confirm-label="t('contextMenu.dropSchema')"
@confirm="confirmDropSchema"
/>
@@ -33,12 +33,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import {
buildTableStructureChangeSql,
buildCreateTableSql,
type EditableStructureColumn,
type EditableStructureIndex,
} from "@/lib/tableStructureEditorSql";
import { type EditableStructureColumn, type EditableStructureIndex } from "@/lib/tableStructureEditorSql";
import { getTableStructureCapabilities } from "@/lib/tableStructureCapabilities";
import {
buildStructureTargetLabel,
@@ -68,9 +63,12 @@ const emit = defineEmits<{
const activeTab = ref("columns");
const loading = ref(false);
const saving = ref(false);
const sqlPreviewLoading = ref(false);
const errorMessage = ref("");
const columns = ref<EditableStructureColumn[]>([]);
const indexes = ref<EditableStructureIndex[]>([]);
const pendingStatements = ref<string[]>([]);
const warnings = ref<string[]>([]);
const foreignKeys = ref<ForeignKeyInfo[]>([]);
const triggers = ref<TriggerInfo[]>([]);
@@ -131,30 +129,44 @@ const targetLabel = computed(() =>
),
);
const changeSql = computed(() => {
if (isCreateMode.value) {
return buildCreateTableSql({
databaseType: databaseType.value,
schema: props.prefillSchema,
tableName: newTableName.value,
columns: columns.value,
indexes: indexes.value,
});
let sqlPreviewRequestId = 0;
async function refreshSqlPreview() {
const requestId = ++sqlPreviewRequestId;
if (!open.value) {
pendingStatements.value = [];
warnings.value = [];
return;
}
return buildTableStructureChangeSql({
sqlPreviewLoading.value = true;
const options = {
databaseType: databaseType.value,
schema: props.prefillSchema,
tableName: props.prefillTable || "",
tableName: isCreateMode.value ? newTableName.value : props.prefillTable || "",
columns: columns.value,
indexes: indexes.value,
});
});
const pendingStatements = computed(() => changeSql.value.statements);
const warnings = computed(() => changeSql.value.warnings);
};
try {
const result = isCreateMode.value
? await api.buildCreateTableSql(options)
: await api.buildTableStructureChangeSql(options);
if (requestId !== sqlPreviewRequestId) return;
pendingStatements.value = result.statements;
warnings.value = result.warnings;
} catch (e: any) {
if (requestId !== sqlPreviewRequestId) return;
pendingStatements.value = [];
warnings.value = [e?.message || String(e)];
} finally {
if (requestId === sqlPreviewRequestId) sqlPreviewLoading.value = false;
}
}
const canApply = computed(
() =>
!loading.value &&
!saving.value &&
!sqlPreviewLoading.value &&
pendingStatements.value.length > 0 &&
warnings.value.length === 0 &&
!!props.prefillConnectionId &&
@@ -165,9 +177,12 @@ function resetState() {
activeTab.value = "columns";
loading.value = false;
saving.value = false;
sqlPreviewLoading.value = false;
errorMessage.value = "";
columns.value = [];
indexes.value = [];
pendingStatements.value = [];
warnings.value = [];
foreignKeys.value = [];
triggers.value = [];
newTableName.value = "";
@@ -375,6 +390,23 @@ watch(
},
{ immediate: true },
);
watch(
[
open,
isCreateMode,
databaseType,
() => props.prefillSchema,
() => props.prefillTable,
newTableName,
columns,
indexes,
],
() => {
void refreshSqlPreview();
},
{ deep: true, immediate: true },
);
</script>
<template>
@@ -843,7 +875,10 @@ watch(
<div class="flex min-w-0 flex-col rounded-md border">
<div class="flex items-center justify-between border-b px-2 py-1.5 text-[11px] font-medium">
<span>{{ t("structureEditor.sqlPreview") }}</span>
<Badge variant="secondary">{{ pendingStatements.length }}</Badge>
<Badge variant="secondary">
<Loader2 v-if="sqlPreviewLoading" class="h-3 w-3 animate-spin" />
<span v-else>{{ pendingStatements.length }}</span>
</Badge>
</div>
<div class="min-h-0 flex-1 overflow-auto p-2">
<div v-if="warnings.length" class="mb-2 space-y-1">