mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-02 00:29:30 +00:00
refactor(sql): move builders to Rust
This commit is contained in:
@@ -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) });
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user