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
+19 -3
View File
@@ -31,7 +31,7 @@ import * as api from "@/lib/api";
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
import { findTreeNodeById, resolveNewQueryTarget } from "@/lib/newQueryContext";
import { buildExecutableObjectSourceStatements, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor";
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
import { resolveExecutableSql, resolveExecutableSqlWithBackend } from "@/lib/sqlExecutionTarget";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { sqlFileTitleFromPath } from "@/lib/sqlFileOpen";
import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink";
@@ -167,6 +167,16 @@ const executableSql = computed(() => {
: "";
});
async function resolveActiveExecutableSql() {
const tab = activeTab.value;
return tab
? await resolveExecutableSqlWithBackend(tab.sql, selectedSql.value, {
mode: settingsStore.editorSettings.executeMode,
cursorPos: cursorPos.value,
})
: "";
}
const {
dangerSql,
pendingDangerSql,
@@ -176,7 +186,13 @@ const {
cancelActiveExecution,
tryExplain,
onDangerConfirm,
} = useSqlExecution({ activeTab, activeConnection, executableSql, activeOutputView });
} = useSqlExecution({
activeTab,
activeConnection,
executableSql,
resolveExecutableSql: resolveActiveExecutableSql,
activeOutputView,
});
const dialogs = useDialogSources();
const { getDatabaseOptions } = useDatabaseOptions();
@@ -310,7 +326,7 @@ async function saveActiveObjectSource(tab: NonNullable<typeof activeTab.value>)
if (!connection || !source) return;
try {
const statements = buildExecutableObjectSourceStatements({
const statements = await buildExecutableObjectSourceStatements({
databaseType: connection.db_type,
objectType: source.objectType,
schema: source.schema || tab.schema || tab.database,
+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">
@@ -5,7 +5,7 @@ import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
import { editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/tableEditing";
import { buildSortedQuerySql } from "@/lib/queryResultSort";
import * as api from "@/lib/api";
import type { QueryTab } from "@/types/database";
import { useToast } from "@/composables/useToast";
@@ -24,7 +24,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
function buildTableSql(
tab: QueryTab,
options: { orderBy?: string; limit?: number; offset?: number; whereInput?: string } = {},
): string {
): Promise<string> {
const config = connectionStore.getConfig(tab.connectionId);
const primaryKeys = tab.tableMeta ? editablePrimaryKeys(config?.db_type, tab.tableMeta.columns) : [];
if (tab.tableMeta && primaryKeys.join("\0") !== tab.tableMeta.primaryKeys.join("\0")) {
@@ -67,7 +67,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
if (!tab) return;
if (tab.mode === "data" && tab.tableMeta) {
tab.whereInput = whereInput ?? "";
queryStore.updateSql(tab.id, buildTableSql(tab, { whereInput, orderBy, limit, offset }));
queryStore.updateSql(tab.id, await buildTableSql(tab, { whereInput, orderBy, limit, offset }));
await queryStore.executeCurrentTab();
return;
}
@@ -109,7 +109,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
if (!tab.tableMeta) return;
tab.whereInput = whereInput ?? "";
const sql = buildTableSql(tab, { limit, offset, whereInput, orderBy });
const sql = await buildTableSql(tab, { limit, offset, whereInput, orderBy });
queryStore.updateSql(tab.id, sql);
await queryStore.executeCurrentTab();
}
@@ -126,7 +126,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
const orderBy = direction
? `${config?.db_type === "neo4j" ? `n.${quotedColumn}` : quotedColumn} ${direction.toUpperCase()}`
: undefined;
const sql = buildTableSql(tab, { orderBy, whereInput });
const sql = await buildTableSql(tab, { orderBy, whereInput });
queryStore.updateSql(tab.id, sql);
await queryStore.executeCurrentTab();
return;
@@ -144,15 +144,15 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
}
const config = connectionStore.getConfig(tab.connectionId);
const built = buildSortedQuerySql(
baseSql,
config?.db_type,
tab.result?.columns ?? [],
const built = await api.buildSortedQuerySql({
originalSql: baseSql,
databaseType: config?.db_type,
resultColumns: tab.result?.columns ?? [],
columnIndex,
column,
direction,
);
if (!built.ok) {
});
if (!built.ok || !built.sql) {
toast(t("grid.sortUnsupported"), 5000);
return;
}
@@ -169,7 +169,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
}
async function copyRowAsInsertStatement(excludePrimaryKeys: boolean) {
const statement = buildDataGridCopyInsertStatement({
const statement = await buildDataGridCopyInsertStatement({
databaseType: databaseType.value,
tableMeta: tableMeta.value,
columns: columns.value,
@@ -191,7 +191,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
async function copyRowAsUpdate() {
if (!tableMeta.value?.primaryKeys.length) return;
const statements = buildDataGridCopyUpdateStatements({
const statements = await buildDataGridCopyUpdateStatements({
databaseType: databaseType.value,
tableMeta: tableMeta.value,
columns: columns.value,
@@ -206,29 +206,25 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
if (!tableMeta.value?.primaryKeys.length) return false;
const rows = updateEligibleRows();
if (!rows.length) return false;
return (
buildDataGridCopyUpdateStatements({
databaseType: databaseType.value,
tableMeta: tableMeta.value,
columns: columns.value,
sourceColumns: sourceColumns.value,
rows: [rows[0].data],
}).length > 0
);
if (databaseType.value === "neo4j" || databaseType.value === "tdengine") return false;
const saveColumns = effectiveColumns(sourceColumns.value, columns.value);
const primaryKeys = tableMeta.value.primaryKeys;
if (primaryKeys.some((primaryKey) => findColumnIndex(saveColumns, primaryKey) === -1)) return false;
const primaryKeySet = new Set(primaryKeys.map(normalizeColumnName));
return saveColumns.some((column) => column && !primaryKeySet.has(normalizeColumnName(column)));
});
const canCopyRowAsInsertWithoutPrimaryKeys = computed(() => {
if (!tableMeta.value?.primaryKeys.length) return false;
const rows = insertEligibleRows();
if (!rows.length) return false;
return !!buildDataGridCopyInsertStatement({
databaseType: databaseType.value,
tableMeta: tableMeta.value,
columns: columns.value,
sourceColumns: sourceColumns.value,
rows: [rows[0].data],
excludePrimaryKeys: true,
});
const saveColumns = effectiveColumns(sourceColumns.value, columns.value);
const primaryKeySet = new Set(tableMeta.value.primaryKeys.map(normalizeColumnName));
const insertableCount = saveColumns.filter(Boolean).length;
const insertColumnsCount = saveColumns.filter(
(column) => column && !primaryKeySet.has(normalizeColumnName(column)),
).length;
return insertColumnsCount > 0 && insertColumnsCount < insertableCount;
});
async function copyAll() {
@@ -372,3 +368,20 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
copySql,
};
}
function effectiveColumns(
sourceColumns: Array<string | undefined> | undefined,
columns: string[],
): Array<string | undefined> {
if (!sourceColumns || sourceColumns.length !== columns.length) return columns;
return sourceColumns;
}
function findColumnIndex(columns: Array<string | undefined>, target: string): number {
const normalizedTarget = normalizeColumnName(target);
return columns.findIndex((column) => (column ? normalizeColumnName(column) : "") === normalizedTarget);
}
function normalizeColumnName(name: string): string {
return name.toUpperCase();
}
+3 -9
View File
@@ -15,14 +15,8 @@ function getDbType(path: string): "sqlite" | "duckdb" | null {
return null;
}
function getDataFileQuery(path: string): string | null {
const lower = path.toLowerCase();
const escaped = path.replace(/'/g, "''");
if (lower.endsWith(".parquet")) return `SELECT * FROM read_parquet('${escaped}') LIMIT 1000`;
if (lower.endsWith(".csv")) return `SELECT * FROM read_csv('${escaped}') LIMIT 1000`;
if (lower.endsWith(".tsv")) return `SELECT * FROM read_csv('${escaped}', delim='\\t') LIMIT 1000`;
if (lower.endsWith(".json")) return `SELECT * FROM read_json('${escaped}') LIMIT 1000`;
return null;
function getDataFileQuery(path: string): Promise<string | undefined> {
return api.buildDroppedFilePreviewSql({ path });
}
export function useFileDrop() {
@@ -39,7 +33,7 @@ export function useFileDrop() {
for (const path of event.payload.paths) {
const name = path.split("/").pop()?.split("\\").pop() || path;
const dataQuery = getDataFileQuery(path);
const dataQuery = await getDataFileQuery(path);
if (dataQuery) {
const config: ConnectionConfig = {
id: uuid(),
@@ -33,7 +33,7 @@ async function openTableTarget(target: NavigationTarget) {
if (config.db_type === "neo4j") {
const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName);
const primaryKeys = editablePrimaryKeys(config.db_type, columns);
const sql = buildTableSelectSql({
const sql = await buildTableSelectSql({
databaseType: config.db_type,
schema: target.schema,
tableName: target.tableName,
@@ -52,7 +52,7 @@ async function openTableTarget(target: NavigationTarget) {
await queryStore.executeTabSql(tabId, sql);
return;
}
const sql = buildTableSelectSql({
const sql = await buildTableSelectSql({
databaseType: config.db_type,
schema: target.schema,
tableName: target.tableName,
@@ -80,7 +80,7 @@ async function openTableTarget(target: NavigationTarget) {
primaryKeys,
});
if (useRowId || config.db_type === "tdengine") {
const newSql = buildTableSelectSql({
const newSql = await buildTableSelectSql({
databaseType: config.db_type,
schema: target.schema,
tableName: target.tableName,
@@ -35,6 +35,7 @@ export function useSqlExecution(deps: {
activeTab: ComputedRef<QueryTab | undefined>;
activeConnection: ComputedRef<ConnectionConfig | undefined>;
executableSql: ComputedRef<string>;
resolveExecutableSql?: () => Promise<string>;
activeOutputView: Ref<"result" | "explain" | "chart">;
}) {
const { t } = useI18n();
@@ -47,9 +48,13 @@ export function useSqlExecution(deps: {
const pendingDangerSql = ref("");
const showDangerDialog = ref(false);
function tryExecute(sqlOverride?: string) {
async function resolvedExecutableSql(): Promise<string> {
return deps.resolveExecutableSql ? await deps.resolveExecutableSql() : deps.executableSql.value;
}
async function tryExecute(sqlOverride?: string) {
const tab = deps.activeTab.value;
const sql = sqlOverride ?? deps.executableSql.value;
const sql = sqlOverride ?? (await resolvedExecutableSql());
if (!tab || !sql.trim()) return;
if (isDangerousSql(sql)) {
dangerSql.value = sql;
@@ -60,7 +65,8 @@ export function useSqlExecution(deps: {
}
}
async function doExecute(sql = deps.executableSql.value) {
async function doExecute(sql?: string) {
sql ??= await resolvedExecutableSql();
const tab = deps.activeTab.value;
if (!tab || !sql.trim()) return;
deps.activeOutputView.value = "result";
@@ -106,7 +112,7 @@ export function useSqlExecution(deps: {
async function tryExplain(sqlOverride?: string) {
const tab = deps.activeTab.value;
const sql = sqlOverride ?? deps.executableSql.value;
const sql = sqlOverride ?? (await resolvedExecutableSql());
if (!tab || !sql.trim()) {
toast(t("explain.emptySql"));
return;
@@ -123,10 +129,10 @@ export function useSqlExecution(deps: {
if (current?.explainError) toast(current.explainError, 5000);
}
function onDangerConfirm() {
const sql = pendingDangerSql.value || deps.executableSql.value;
async function onDangerConfirm() {
const sql = pendingDangerSql.value || (await resolvedExecutableSql());
pendingDangerSql.value = "";
doExecute(sql);
await doExecute(sql);
}
return {
+34
View File
@@ -90,8 +90,42 @@ export const executeInTransaction = forward("executeInTransaction");
export const cancelQuery = forward("cancelQuery");
export const closeQuerySession = forward("closeQuerySession");
export const analyzeSqlReferences = forward("analyzeSqlReferences");
export const findStatementAtCursor = forward("findStatementAtCursor");
export const prepareQueryPaginationExecutionPlan = forward("prepareQueryPaginationExecutionPlan");
export const buildSortedQuerySql = forward("buildSortedQuerySql");
export const buildExplainSql = forward("buildExplainSql");
export const buildDroppedFilePreviewSql = forward("buildDroppedFilePreviewSql");
export const buildTableSelectSql = forward("buildTableSelectSql");
export const buildDatabaseSearchSql = forward("buildDatabaseSearchSql");
export const buildSearchResultWhere = forward("buildSearchResultWhere");
export const buildRenameObjectSql = forward("buildRenameObjectSql");
export const buildCreateDatabaseSql = forward("buildCreateDatabaseSql");
export const buildDuckDbAttachDatabaseSql = forward("buildDuckDbAttachDatabaseSql");
export const buildDropObjectSql = forward("buildDropObjectSql");
export const buildDropTableSql = forward("buildDropTableSql");
export const buildEmptyTableSql = forward("buildEmptyTableSql");
export const buildTruncateTableSql = forward("buildTruncateTableSql");
export const buildDropDatabaseSql = forward("buildDropDatabaseSql");
export const buildCreateSchemaSql = forward("buildCreateSchemaSql");
export const buildDropSchemaSql = forward("buildDropSchemaSql");
export const buildDuplicateTableStructureSql = forward("buildDuplicateTableStructureSql");
export const buildExecutableObjectSourceStatements = forward("buildExecutableObjectSourceStatements");
export const buildExecutableObjectSourceSql = forward("buildExecutableObjectSourceSql");
export const buildRoutineRenameObjectSourceStatements = forward("buildRoutineRenameObjectSourceStatements");
export const buildViewDdlSql = forward("buildViewDdlSql");
export const buildTableStructureChangeSql = forward("buildTableStructureChangeSql");
export const buildCreateTableSql = forward("buildCreateTableSql");
export const analyzeEditableQueryEditability = forward("analyzeEditableQueryEditability");
export const prepareDataGridSave = forward("prepareDataGridSave");
export const buildDataGridCopyUpdateStatements = forward("buildDataGridCopyUpdateStatements");
export const buildDataGridCopyInsertStatement = forward("buildDataGridCopyInsertStatement");
export const buildDataGridContextFilterCondition = forward("buildDataGridContextFilterCondition");
export const buildDataGridColumnValueFilterCondition = forward("buildDataGridColumnValueFilterCondition");
export const buildDataGridCountSql = forward("buildDataGridCountSql");
export const buildHiveTablePropertiesSql = forward("buildHiveTablePropertiesSql");
export const buildExportInsertStatements = forward("buildExportInsertStatements");
export const buildExportSqlInsert = forward("buildExportSqlInsert");
export const buildDatabaseSqlExport = forward("buildDatabaseSqlExport");
export const prepareDataCompare = forward("prepareDataCompare");
export const prepareDataCompareFromTables = forward("prepareDataCompareFromTables");
+5 -20
View File
@@ -1,5 +1,5 @@
import type { DatabaseType } from "@/types/database";
import { quoteTableIdentifier } from "./tableSelectSql";
import * as api from "@/lib/api";
const MYSQL_COMPATIBLE_PROFILES = new Set([
"mysql",
@@ -27,19 +27,12 @@ export function supportsCreateDatabaseCharset(databaseType?: DatabaseType, drive
);
}
export function buildCreateDatabaseSql(options: CreateDatabaseSqlOptions): string {
const name = quoteTableIdentifier(options.databaseType, options.name);
const charset = cleanSqlOption(options.charset);
const collation = cleanSqlOption(options.collation);
if (!supportsCreateDatabaseCharset(options.databaseType, options.driverProfile) || !charset) {
return `CREATE DATABASE ${name};`;
}
const collateClause = collation ? ` COLLATE ${collation}` : "";
return `CREATE DATABASE ${name} CHARACTER SET ${charset}${collateClause};`;
export function buildCreateDatabaseSql(options: CreateDatabaseSqlOptions): Promise<string> {
return api.buildCreateDatabaseSql(options);
}
export function buildDuckDbAttachDatabaseSql(path: string, name: string): string {
return `ATTACH ${quoteSqlString(path)} AS ${quoteTableIdentifier("duckdb", name)};`;
export function buildDuckDbAttachDatabaseSql(path: string, name: string): Promise<string> {
return api.buildDuckDbAttachDatabaseSql(path, name);
}
export function duckDbAttachedDatabaseNameFromPath(path: string): string {
@@ -61,11 +54,3 @@ export function uniqueDuckDbAttachedDatabaseName(baseName: string, existingNames
}
return `${baseName}_${Date.now()}`;
}
function cleanSqlOption(value: string | undefined): string {
return value?.trim().replace(/[;\s]+/g, "") ?? "";
}
function quoteSqlString(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
+15 -44
View File
@@ -1,19 +1,25 @@
import type { ColumnInfo, DatabaseType } from "@/types/database";
import { formatGridSqlLiteral } from "@/lib/dataGridSql";
import { quoteTableIdentifier, normalizeWhereInput } from "@/lib/tableSelectSql";
import { buildDataGridColumnValueFilterCondition } from "@/lib/dataGridSql";
import { normalizeWhereInput } from "@/lib/tableSelectSql";
export function buildColumnValueFilterCondition(options: {
databaseType?: DatabaseType;
columnName: string;
columnInfo?: Pick<ColumnInfo, "data_type">;
rawValue: string;
}): string | undefined {
const text = options.rawValue.trim();
if (!text) return undefined;
const column = columnFilterRef(options.databaseType, options.columnName);
if (/^null$/i.test(text)) return `${column} IS NULL`;
return `${column} = ${formatGridSqlLiteral(parseTypedFilterValue(text, options.columnInfo), options.databaseType, options.columnInfo)}`;
}): Promise<string | undefined> {
return buildDataGridColumnValueFilterCondition({
databaseType: options.databaseType,
columnName: options.columnName,
columnInfo: options.columnInfo
? {
name: options.columnName,
data_type: options.columnInfo.data_type,
is_nullable: true,
}
: undefined,
rawValue: options.rawValue,
});
}
export function appendColumnValueFilterCondition(
@@ -24,38 +30,3 @@ export function appendColumnValueFilterCondition(
const existing = normalizeWhereInput(whereInput);
return existing ? `(${existing}) AND (${condition})` : condition;
}
function columnFilterRef(databaseType: DatabaseType | undefined, columnName: string): string {
const quoted = quoteTableIdentifier(databaseType, columnName);
return databaseType === "neo4j" ? `n.${quoted}` : quoted;
}
function parseTypedFilterValue(text: string, columnInfo: Pick<ColumnInfo, "data_type"> | undefined) {
const unquoted = unwrapMatchingQuotes(text);
const dataType = columnInfo?.data_type.toLowerCase() ?? "";
if (isBooleanType(dataType) && /^(true|false)$/i.test(unquoted)) return /^true$/i.test(unquoted);
if ((isNumericType(dataType) || !dataType) && isNumericLiteral(unquoted)) return Number(unquoted);
return unquoted;
}
function unwrapMatchingQuotes(text: string): string {
if (text.length < 2) return text;
const first = text[0];
const last = text[text.length - 1];
if ((first === "'" && last === "'") || (first === '"' && last === '"')) return text.slice(1, -1);
return text;
}
function isNumericType(dataType: string): boolean {
return /\b(int|integer|bigint|smallint|tinyint|mediumint|serial|number|numeric|decimal|float|double|real|money)\b/i.test(
dataType,
);
}
function isBooleanType(dataType: string): boolean {
return /\b(bool|boolean|bit)\b/i.test(dataType);
}
function isNumericLiteral(text: string): boolean {
return /^[+-]?(?:\d+|\d*\.\d+)(?:e[+-]?\d+)?$/i.test(text) && Number.isFinite(Number(text));
}
+61 -211
View File
@@ -1,6 +1,5 @@
import type { DatabaseType } from "@/types/database";
import { DBX_ROWID_COLUMN } from "./tableEditing.ts";
import { qualifiedTableName, quoteTableIdentifier } from "./tableSelectSql.ts";
import * as api from "@/lib/api";
export type GridCellValue = string | number | boolean | null;
@@ -15,12 +14,11 @@ export interface DataGridColumnInfo {
name: string;
data_type: string;
is_nullable: boolean;
is_primary_key?: boolean;
column_default?: string | null;
extra?: string | null;
}
export type GridSqlLiteralColumnInfo = Pick<DataGridColumnInfo, "data_type">;
export interface DataGridSaveStatementOptions {
databaseType?: DatabaseType;
tableMeta: DataGridTableMeta;
@@ -49,108 +47,72 @@ export interface DataGridCopyInsertStatementOptions {
excludePrimaryKeys?: boolean;
}
export function buildDataGridCopyUpdateStatements(options: DataGridCopyUpdateStatementOptions): string[] {
if (options.databaseType === "neo4j" || options.databaseType === "tdengine") return [];
const primaryKeys = options.tableMeta.primaryKeys;
if (primaryKeys.length === 0) return [];
export type DataGridContextFilterMode =
| "equals"
| "not-equals"
| "is-null"
| "is-not-null"
| "like"
| "not-like"
| "less-than"
| "greater-than";
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
const columnInfo = columnInfoByName(options.tableMeta.columns);
const primaryKeyIndexes = primaryKeys.map((primaryKey) => findColumnIndex(saveColumns, primaryKey));
if (primaryKeyIndexes.some((index) => index === -1)) return [];
const primaryKeySet = new Set(primaryKeys.map((primaryKey) => normalizeColumnName(primaryKey)));
const writableIndexes = saveColumns
.map((column, index) => ({ column, index }))
.filter((entry): entry is { column: string; index: number } => !!entry.column)
.filter((entry) => !primaryKeySet.has(normalizeColumnName(entry.column)))
.filter((entry) => !isOracleRowId(options.databaseType, entry.column));
if (writableIndexes.length === 0) return [];
const table = qualifiedTableName({
databaseType: options.databaseType,
schema: options.tableMeta.schema,
tableName: options.tableMeta.tableName,
});
const statements: string[] = [];
for (const row of options.rows) {
if (primaryKeyIndexes.some((index) => row[index] === null || row[index] === undefined)) continue;
const sets = writableIndexes
.map(
({ column, index }) =>
`${quoteIdent(options.databaseType, column)} = ${formatGridSqlLiteral(
row[index],
options.databaseType,
columnInfo.get(normalizeColumnName(column)),
)}`,
)
.join(", ");
if (!sets) continue;
const where = primaryKeys
.map((primaryKey, index) =>
buildColumnPredicate(
options.databaseType,
primaryKey,
row[primaryKeyIndexes[index]],
columnInfo.get(normalizeColumnName(primaryKey)),
),
)
.join(" AND ");
statements.push(`UPDATE ${table} SET ${sets} WHERE ${where};`);
}
return statements;
export interface DataGridContextFilterConditionOptions {
databaseType?: DatabaseType;
columnName: string;
mode: DataGridContextFilterMode;
value: GridCellValue;
columnInfo?: DataGridColumnInfo;
}
export function buildDataGridCopyInsertStatement(options: DataGridCopyInsertStatementOptions): string | undefined {
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
const columnInfo = columnInfoByName(options.tableMeta?.columns);
const primaryKeySet = new Set(
(options.tableMeta?.primaryKeys ?? []).map((primaryKey) => normalizeColumnName(primaryKey)),
);
const insertableColumns = saveColumns
.map((column, index) => ({ column, index }))
.filter((entry): entry is { column: string; index: number } => !!entry.column)
.filter((entry) => !isOracleRowId(options.databaseType, entry.column));
const insertColumns = insertableColumns.filter(
(entry) => !options.excludePrimaryKeys || !primaryKeySet.has(normalizeColumnName(entry.column)),
);
if (options.excludePrimaryKeys && insertColumns.length === insertableColumns.length) return undefined;
if (insertColumns.length === 0 || options.rows.length === 0) return undefined;
const table = options.tableMeta
? qualifiedTableName({
databaseType: options.databaseType,
schema: options.tableMeta.schema,
tableName: options.tableMeta.tableName,
})
: "table_name";
const columns = insertColumns.map((entry) => quoteIdent(options.databaseType, entry.column)).join(", ");
const valueRows = options.rows.map(
(row) =>
`(${insertColumns
.map(({ column, index }) =>
formatGridSqlLiteral(row[index], options.databaseType, columnInfo.get(normalizeColumnName(column))),
)
.join(", ")})`,
);
return `INSERT INTO ${table} (${columns}) VALUES${valueRows.length === 1 ? " " : "\n"}${valueRows.join(",\n")};`;
export interface DataGridColumnValueFilterConditionOptions {
databaseType?: DatabaseType;
columnName: string;
columnInfo?: DataGridColumnInfo;
rawValue: string;
}
function effectiveColumns(
sourceColumns: Array<string | undefined> | undefined,
columns: string[],
): Array<string | undefined> {
if (!sourceColumns || sourceColumns.length !== columns.length) return columns;
return sourceColumns;
export interface DataGridCountSqlOptions {
databaseType?: DatabaseType;
schema?: string;
tableName: string;
whereInput?: string;
}
function columnInfoByName(columns: DataGridColumnInfo[] | undefined): Map<string, DataGridColumnInfo> {
return new Map((columns ?? []).map((column) => [normalizeColumnName(column.name), column]));
export interface HiveTablePropertiesSqlOptions {
schema?: string;
tableName: string;
propertyName: string;
}
export function buildDataGridCopyUpdateStatements(options: DataGridCopyUpdateStatementOptions): Promise<string[]> {
return api.buildDataGridCopyUpdateStatements(options);
}
export function buildDataGridCopyInsertStatement(
options: DataGridCopyInsertStatementOptions,
): Promise<string | undefined> {
return api.buildDataGridCopyInsertStatement(options);
}
export function buildDataGridContextFilterCondition(
options: DataGridContextFilterConditionOptions,
): Promise<string | undefined> {
return api.buildDataGridContextFilterCondition(options);
}
export function buildDataGridColumnValueFilterCondition(
options: DataGridColumnValueFilterConditionOptions,
): Promise<string | undefined> {
return api.buildDataGridColumnValueFilterCondition(options);
}
export function buildDataGridCountSql(options: DataGridCountSqlOptions): Promise<string> {
return api.buildDataGridCountSql(options);
}
export function buildHiveTablePropertiesSql(options: HiveTablePropertiesSqlOptions): Promise<string> {
return api.buildHiveTablePropertiesSql(options);
}
export function normalizeDataGridSaveError(databaseType: DatabaseType | undefined, error: unknown): string {
@@ -160,115 +122,3 @@ export function normalizeDataGridSaveError(databaseType: DatabaseType | undefine
}
return message;
}
export function formatGridSqlLiteral(
value: GridCellValue,
databaseType?: DatabaseType,
columnInfo?: GridSqlLiteralColumnInfo,
): string {
if (value === null || value === undefined) return "NULL";
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
if (typeof value === "number" && Number.isFinite(value)) return String(value);
const text = String(value);
if (text === "") return databaseType === "sqlserver" ? "N''" : "''";
const literalText =
databaseType === "tdengine"
? formatTdengineTimestampLiteralText(text)
: isMysqlDatetimeLiteralDatabase(databaseType) && (!columnInfo || isTemporalColumnType(columnInfo.data_type))
? formatMysqlTemporalLiteralText(text, columnInfo?.data_type)
: text;
const escaped = `'${literalText.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
return databaseType === "sqlserver" ? `N${escaped}` : escaped;
}
function isMysqlDatetimeLiteralDatabase(databaseType: DatabaseType | undefined): boolean {
return (
databaseType === "mysql" ||
databaseType === "doris" ||
databaseType === "starrocks" ||
databaseType === "goldendb" ||
databaseType === "sundb"
);
}
function formatMysqlTemporalLiteralText(text: string, dataType: string | undefined): string {
const match = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/i.exec(text);
if (!match) return text;
const kind = temporalColumnKind(dataType);
if (kind === "date") return match[1];
if (kind === "time") return `${match[2]}${normalizeMysqlFractionalSeconds(match[3])}`;
return `${match[1]} ${match[2]}${normalizeMysqlFractionalSeconds(match[3])}`;
}
function normalizeMysqlFractionalSeconds(fraction: string | undefined): string {
if (!fraction) return "";
return fraction.length > 7 ? fraction.slice(0, 7) : fraction;
}
function isTemporalColumnType(dataType: string | undefined): boolean {
return temporalColumnKind(dataType) !== undefined;
}
function temporalColumnKind(dataType: string | undefined): "date" | "time" | "datetime" | undefined {
const base = (dataType ?? "")
.trim()
.toLowerCase()
.split(/[(:\s]/)[0];
if (base === "date") return "date";
if (base === "time") return "time";
if (base === "datetime" || base === "timestamp") return "datetime";
return undefined;
}
function formatTdengineTimestampLiteralText(text: string): string {
const match = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})(\.\d{1,9})?$/.exec(text);
if (!match) return text;
return `${match[1]}T${match[2]}${normalizeFractionalSeconds(match[3])}${localTimezoneOffsetSuffix(text)}`;
}
function normalizeFractionalSeconds(fraction: string | undefined): string {
if (!fraction) return ".000";
return fraction.length >= 4 ? fraction.slice(0, 4) : fraction.padEnd(4, "0");
}
function localTimezoneOffsetSuffix(text: string): string {
const date = new Date(text.replace(" ", "T"));
const offsetMinutes = Number.isNaN(date.getTime()) ? new Date().getTimezoneOffset() : date.getTimezoneOffset();
const sign = offsetMinutes <= 0 ? "+" : "-";
const abs = Math.abs(offsetMinutes);
const hours = String(Math.floor(abs / 60)).padStart(2, "0");
const minutes = String(abs % 60).padStart(2, "0");
return `${sign}${hours}:${minutes}`;
}
function buildColumnPredicate(
databaseType: DatabaseType | undefined,
column: string,
value: GridCellValue,
columnInfo?: DataGridColumnInfo,
): string {
const ident = predicateIdent(databaseType, column);
if (value === null || value === undefined) return `${ident} IS NULL`;
return `${ident} = ${formatGridSqlLiteral(value, databaseType, columnInfo)}`;
}
function isOracleRowId(databaseType: DatabaseType | undefined, name: string | undefined): boolean {
return databaseType === "oracle" && name?.toUpperCase() === DBX_ROWID_COLUMN;
}
function findColumnIndex(columns: Array<string | undefined>, target: string): number {
const normalizedTarget = normalizeColumnName(target);
return columns.findIndex((column) => (column ? normalizeColumnName(column) : "") === normalizedTarget);
}
function normalizeColumnName(name: string): string {
return name.toUpperCase();
}
function predicateIdent(databaseType: DatabaseType | undefined, name: string): string {
return isOracleRowId(databaseType, name) ? "ROWIDTOCHAR(ROWID)" : quoteIdent(databaseType, name);
}
function quoteIdent(databaseType: DatabaseType | undefined, name: string): string {
return quoteTableIdentifier(databaseType, name);
}
+24 -72
View File
@@ -1,16 +1,18 @@
import type { DatabaseType, QueryResult } from "../types/database.ts";
import * as api from "./api.ts";
import { buildTableSelectSql } from "./tableSelectSql.ts";
import { uuid } from "./utils.ts";
type SqlValue = QueryResult["rows"][number][number];
export const DATABASE_EXPORT_ROW_LIMIT = 10_000;
export const DATABASE_EXPORT_PAGE_SIZE = 500;
export const DATABASE_EXPORT_INSERT_BATCH_SIZE = 100;
export interface ExportedTableSql {
displayName: string;
qualifiedTableName: string;
databaseType?: DatabaseType;
schema?: string;
tableName?: string;
qualifiedTableName?: string;
ddl?: string;
columns: string[];
rows: QueryResult["rows"];
@@ -19,13 +21,22 @@ export interface ExportedTableSql {
export interface BuildDatabaseSqlExportOptions {
databaseName: string;
exportedAt?: Date;
exportedAt?: Date | string;
tables: ExportedTableSql[];
quoteIdentifier: (name: string) => string;
rowLimitPerTable?: number;
insertBatchSize?: number;
}
export interface BuildExportInsertStatementsOptions {
databaseType?: DatabaseType;
schema?: string;
tableName?: string;
qualifiedTableName?: string;
columns: string[];
rows: QueryResult["rows"];
batchSize?: number;
}
export interface BuildExportPageSqlOptions {
databaseType?: DatabaseType;
schema?: string;
@@ -34,36 +45,11 @@ export interface BuildExportPageSqlOptions {
offset?: number;
}
export function formatSqlLiteral(value: SqlValue): string {
if (value === null) return "NULL";
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "NULL";
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
return `'${String(value).replace(/'/g, "''")}'`;
export function buildInsertStatements(options: BuildExportInsertStatementsOptions): Promise<string[]> {
return api.buildExportInsertStatements(options);
}
export function buildInsertStatements(
table: Pick<ExportedTableSql, "qualifiedTableName" | "columns" | "rows"> & {
quoteIdentifier: (name: string) => string;
batchSize?: number;
},
): string[] {
if (table.columns.length === 0 || table.rows.length === 0) return [];
const batchSize = Math.max(1, table.batchSize ?? DATABASE_EXPORT_INSERT_BATCH_SIZE);
const columns = table.columns.map((column) => table.quoteIdentifier(column)).join(", ");
const statements: string[] = [];
for (let start = 0; start < table.rows.length; start += batchSize) {
const values = table.rows
.slice(start, start + batchSize)
.map((row) => `(${row.map(formatSqlLiteral).join(", ")})`)
.join(", ");
statements.push(`INSERT INTO ${table.qualifiedTableName} (${columns}) VALUES ${values};`);
}
return statements;
}
export function buildExportPageSql(options: BuildExportPageSqlOptions): string {
export async function buildExportPageSql(options: BuildExportPageSqlOptions): Promise<string> {
return buildTableSelectSql({
databaseType: options.databaseType,
schema: options.schema,
@@ -77,43 +63,9 @@ export function generateDatabaseExportId(): string {
return uuid();
}
export function buildDatabaseSqlExport(options: BuildDatabaseSqlExportOptions): string {
const exportedAt = options.exportedAt ?? new Date();
const rowLimit = options.rowLimitPerTable ?? DATABASE_EXPORT_ROW_LIMIT;
const insertBatchSize = options.insertBatchSize ?? DATABASE_EXPORT_INSERT_BATCH_SIZE;
const lines: string[] = [
"-- DBX database export",
`-- Database: ${options.databaseName}`,
`-- Exported at: ${exportedAt.toISOString()}`,
`-- Row limit per table: ${rowLimit}`,
"",
];
for (const table of options.tables) {
if (table.ddl?.trim()) {
lines.push(`-- Structure for ${table.displayName}`);
lines.push(table.ddl.trim().replace(/;*$/, ";"));
lines.push("");
}
lines.push(`-- Data for ${table.displayName}`);
lines.push(
table.truncated
? `-- Exported rows: ${table.rows.length} (truncated at ${rowLimit})`
: `-- Exported rows: ${table.rows.length}`,
);
const inserts = buildInsertStatements({
...table,
quoteIdentifier: options.quoteIdentifier,
batchSize: insertBatchSize,
});
if (inserts.length > 0) {
lines.push(...inserts);
} else {
lines.push("-- No rows");
}
lines.push("");
}
return lines.join("\n");
export function buildDatabaseSqlExport(options: BuildDatabaseSqlExportOptions): Promise<string> {
return api.buildDatabaseSqlExport({
...options,
exportedAt: options.exportedAt instanceof Date ? options.exportedAt.toISOString() : options.exportedAt,
});
}
+5 -115
View File
@@ -1,6 +1,5 @@
import type { ColumnInfo, DatabaseType } from "../types/database.ts";
import { qualifiedTableName, quoteTableIdentifier } from "./tableSelectSql.ts";
import { usesFetchFirst } from "@/lib/databaseCapabilities";
import * as api from "./api.ts";
export interface DatabaseSearchSqlOptions {
databaseType?: DatabaseType;
@@ -50,101 +49,14 @@ function cleanTerm(term: string): string {
return term.trim();
}
function clampLimit(limit?: number): number {
if (!Number.isFinite(limit)) return 20;
return Math.min(100, Math.max(1, Math.trunc(limit ?? 20)));
}
function parseNumericTerm(term: string): string | null {
const trimmed = cleanTerm(term);
if (!/^[+-]?(?:\d+|\d+\.\d+|\.\d+)$/.test(trimmed)) return null;
return trimmed;
}
function sqlStringLiteral(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
function likePattern(term: string): string {
return `%${cleanTerm(term)
.toLowerCase()
.replace(/[~%_]/g, (match) => `~${match}`)}%`;
}
function textCastExpression(databaseType: DatabaseType | undefined, identifier: string): string {
if (databaseType === "mysql") return `LOWER(CAST(${identifier} AS CHAR))`;
if (databaseType === "sqlserver") return `LOWER(CAST(${identifier} AS NVARCHAR(MAX)))`;
if (databaseType === "oracle") return `LOWER(CAST(${identifier} AS VARCHAR2(4000)))`;
if (databaseType === "clickhouse") return `lower(toString(${identifier}))`;
return `LOWER(CAST(${identifier} AS TEXT))`;
}
function sqlValueLiteral(
databaseType: DatabaseType | undefined,
column: ColumnInfo | undefined,
value: unknown,
): string {
if (value === null || value === undefined) return "NULL";
if (typeof value === "number" && Number.isFinite(value)) return String(value);
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
const stringValue = String(value);
if (column && isNumericSearchColumn(column) && parseNumericTerm(stringValue)) {
return stringValue.trim();
}
if (databaseType === "sqlserver") return `N${sqlStringLiteral(stringValue)}`;
return sqlStringLiteral(stringValue);
}
export function buildDatabaseSearchSql(options: DatabaseSearchSqlOptions): DatabaseSearchSql | null {
const term = cleanTerm(options.term);
if (!term) return null;
const textColumns = options.columns.filter(isTextSearchColumn);
const numericTerm = parseNumericTerm(term);
const numericColumns = numericTerm ? options.columns.filter(isNumericSearchColumn) : [];
const conditions: string[] = [];
for (const column of textColumns) {
const identifier = quoteTableIdentifier(options.databaseType, column.name);
conditions.push(
`${textCastExpression(options.databaseType, identifier)} LIKE ${sqlStringLiteral(likePattern(term))} ESCAPE '~'`,
);
}
for (const column of numericColumns) {
const identifier = quoteTableIdentifier(options.databaseType, column.name);
conditions.push(`${identifier} = ${numericTerm}`);
}
if (!conditions.length) return null;
const table = qualifiedTableName({
databaseType: options.databaseType,
schema: options.schema,
tableName: options.tableName,
});
const where = conditions.join(" OR ");
const limit = clampLimit(options.limit);
const searchableColumns = [...textColumns, ...numericColumns].map((column) => column.name);
if (options.databaseType === "sqlserver") {
return {
sql: `SELECT TOP ${limit} * FROM ${table} WHERE (${where})`,
searchableColumns,
};
}
if (usesFetchFirst(options.databaseType)) {
return {
sql: `SELECT * FROM ${table} WHERE (${where}) FETCH FIRST ${limit} ROWS ONLY`,
searchableColumns,
};
}
return {
sql: `SELECT * FROM ${table} WHERE (${where}) LIMIT ${limit};`,
searchableColumns,
};
export async function buildDatabaseSearchSql(options: DatabaseSearchSqlOptions): Promise<DatabaseSearchSql | null> {
return api.buildDatabaseSearchSql(options);
}
function columnByName(columns: ColumnInfo[], name: string): ColumnInfo | undefined {
@@ -180,28 +92,6 @@ export function findMatchedSearchColumns(
return matches;
}
export function buildSearchResultWhere(options: SearchResultWhereOptions): string {
const valueByColumn = new Map<string, unknown>();
options.resultColumns.forEach((column, index) => {
valueByColumn.set(column.toLowerCase(), options.row[index]);
});
const primaryColumns = options.columns.filter(
(column) => column.is_primary_key && valueByColumn.get(column.name.toLowerCase()) !== undefined,
);
const fallbackColumns = options.columns.filter(
(column) =>
(options.matchedColumns ?? []).some((name) => name.toLowerCase() === column.name.toLowerCase()) &&
valueByColumn.get(column.name.toLowerCase()) !== undefined,
);
const selectedColumns = (primaryColumns.length ? primaryColumns : fallbackColumns).slice(0, 3);
return selectedColumns
.map((column) => {
const identifier = quoteTableIdentifier(options.databaseType, column.name);
const value = valueByColumn.get(column.name.toLowerCase());
if (value === null || value === undefined) return `${identifier} IS NULL`;
return `${identifier} = ${sqlValueLiteral(options.databaseType, column, value)}`;
})
.join(" AND ");
export async function buildSearchResultWhere(options: SearchResultWhereOptions): Promise<string> {
return api.buildSearchResultWhere(options);
}
+64
View File
@@ -0,0 +1,64 @@
import type { DatabaseObjectType, DatabaseType } from "@/types/database";
import * as api from "@/lib/api";
export interface DropObjectSqlOptions {
databaseType?: DatabaseType;
objectType: DatabaseObjectType;
schema?: string | null;
name: string;
}
export interface TableAdminSqlOptions {
databaseType?: DatabaseType;
schema?: string | null;
tableName: string;
}
export interface DatabaseNameSqlOptions {
databaseType?: DatabaseType;
name: string;
}
export interface SchemaNameSqlOptions {
databaseType?: DatabaseType;
name: string;
}
export interface DuplicateTableStructureSqlOptions {
databaseType?: DatabaseType;
schema?: string | null;
sourceName: string;
targetName: string;
}
export function buildDropObjectSql(options: DropObjectSqlOptions): Promise<string> {
return api.buildDropObjectSql(options);
}
export function buildDropTableSql(options: TableAdminSqlOptions): Promise<string> {
return api.buildDropTableSql(options);
}
export function buildEmptyTableSql(options: TableAdminSqlOptions): Promise<string> {
return api.buildEmptyTableSql(options);
}
export function buildTruncateTableSql(options: TableAdminSqlOptions): Promise<string> {
return api.buildTruncateTableSql(options);
}
export function buildDropDatabaseSql(options: DatabaseNameSqlOptions): Promise<string> {
return api.buildDropDatabaseSql(options);
}
export function buildCreateSchemaSql(options: SchemaNameSqlOptions): Promise<string> {
return api.buildCreateSchemaSql(options);
}
export function buildDropSchemaSql(options: SchemaNameSqlOptions): Promise<string> {
return api.buildDropSchemaSql(options);
}
export function buildDuplicateTableStructureSql(options: DuplicateTableStructureSqlOptions): Promise<string> {
return api.buildDuplicateTableStructureSql(options);
}
+3 -25
View File
@@ -1,4 +1,5 @@
import type { DatabaseType, QueryResult } from "@/types/database";
import * as api from "@/lib/api";
export interface ExplainPlanNode {
id: string;
@@ -24,24 +25,12 @@ export type BuildExplainSqlResult =
| { ok: false; reason: "unsupported" | "empty" | "unsafe" };
const SUPPORTED_EXPLAIN_TYPES = new Set<DatabaseType>(["mysql", "postgres"]);
const SAFE_EXPLAIN_RE = /^(select|with|table|values)\b/i;
export function supportsExplainPlan(databaseType?: DatabaseType): databaseType is "mysql" | "postgres" {
return !!databaseType && SUPPORTED_EXPLAIN_TYPES.has(databaseType);
}
export function buildExplainSql(databaseType: DatabaseType | undefined, sql: string): BuildExplainSqlResult {
if (!supportsExplainPlan(databaseType)) return { ok: false, reason: "unsupported" };
const source = stripTrailingSemicolons(sql.trim());
if (!source) return { ok: false, reason: "empty" };
if (!SAFE_EXPLAIN_RE.test(stripSqlComments(source).trim())) return { ok: false, reason: "unsafe" };
if (databaseType === "postgres") {
return { ok: true, sql: `EXPLAIN (FORMAT JSON) ${source}` };
}
return { ok: true, sql: `EXPLAIN FORMAT=JSON ${source}` };
export function buildExplainSql(databaseType: DatabaseType | undefined, sql: string): Promise<BuildExplainSqlResult> {
return api.buildExplainSql({ databaseType, sql }) as Promise<BuildExplainSqlResult>;
}
export function parseExplainResult(databaseType: "mysql" | "postgres", result: QueryResult): ParsedExplainPlan {
@@ -61,17 +50,6 @@ export function flattenExplainPlanNodes(nodes: ExplainPlanNode[]): ExplainPlanNo
return rows;
}
function stripTrailingSemicolons(sql: string): string {
return sql.replace(/;\s*$/g, "");
}
function stripSqlComments(sql: string): string {
return sql
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/--.*$/gm, " ")
.replace(/#.*$/gm, " ");
}
function parseExplainCell(value: unknown): unknown {
if (typeof value !== "string") return value;
try {
+16 -17
View File
@@ -1,3 +1,6 @@
import type { DatabaseType } from "@/types/database";
import * as api from "@/lib/api";
export type ExportCellValue = string | number | boolean | null;
export function formatCsv(columns: string[], rows: ExportCellValue[][]): string {
@@ -7,22 +10,18 @@ export function formatCsv(columns: string[], rows: ExportCellValue[][]): string
return `${header}\n${body}`;
}
export function formatSqlInsert(
qualifiedName: string,
columns: string[],
rows: ExportCellValue[][],
quoteIdent: (name: string) => string,
): string {
const cols = columns.map((c) => quoteIdent(c)).join(", ");
const lines = rows.map((row) => {
const vals = row
.map((v) => {
if (v === null) return "NULL";
if (typeof v === "number" || typeof v === "boolean") return String(v);
return `'${String(v).replace(/'/g, "''")}'`;
})
.join(", ");
return `INSERT INTO ${qualifiedName} (${cols}) VALUES (${vals});`;
export interface FormatSqlInsertOptions {
databaseType?: DatabaseType;
schema?: string;
tableName?: string;
qualifiedTableName?: string;
columns: string[];
rows: ExportCellValue[][];
}
export function formatSqlInsert(options: FormatSqlInsertOptions): Promise<string> {
return api.buildExportSqlInsert({
...options,
batchSize: 1,
});
return lines.join("\n");
}
+189 -1
View File
@@ -50,9 +50,39 @@ import type {
DatabaseExportRequest,
ExportProgress,
XlsxCellValue,
QueryPaginationExecutionPlanOptions,
QueryPaginationExecutionPlan,
SortedQuerySqlOptions,
QuerySqlBuildResult,
BuildExplainSqlOptions,
ExplainSqlBuildResult,
DroppedFilePreviewSqlOptions,
} from "./tauri";
import type { QueryEditability } from "@/lib/sqlAnalysis";
import type { DataGridSaveStatementOptions } from "@/lib/dataGridSql";
import type {
DataGridColumnValueFilterConditionOptions,
DataGridContextFilterConditionOptions,
DataGridCountSqlOptions,
DataGridCopyInsertStatementOptions,
DataGridCopyUpdateStatementOptions,
DataGridSaveStatementOptions,
HiveTablePropertiesSqlOptions,
} from "@/lib/dataGridSql";
import type { BuildTableStructureChangeSqlOptions, TableStructureChangeSql } from "@/lib/tableStructureEditorSql";
import type { BuildTableSelectSqlOptions } from "@/lib/tableSelectSql";
import type { DatabaseSearchSql, DatabaseSearchSqlOptions, SearchResultWhereOptions } from "@/lib/databaseSearch";
import type { BuildEditableObjectSourceSqlInput, BuildRoutineRenameObjectSourceInput } from "@/lib/objectSourceEditor";
import type { BuildViewDdlInput } from "@/lib/viewDdl";
import type { BuildRenameObjectSqlOptions } from "@/lib/objectRenameSql";
import type { CreateDatabaseSqlOptions } from "@/lib/createDatabaseSql";
import type {
DatabaseNameSqlOptions,
DropObjectSqlOptions,
DuplicateTableStructureSqlOptions,
SchemaNameSqlOptions,
TableAdminSqlOptions,
} from "@/lib/dbAdminSql";
import type { BuildDatabaseSqlExportOptions, BuildExportInsertStatementsOptions } from "@/lib/databaseExport";
import type {
DataCompareFromTablesOptions,
DataCompareFromTablesPreparation,
@@ -432,6 +462,117 @@ export async function analyzeSqlReferences(sql: string, dialect?: string): Promi
return post("/api/query/analyze-sql-references", { sql, dialect });
}
export async function findStatementAtCursor(sql: string, cursorPos: number): Promise<string> {
return post("/api/query/find-statement-at-cursor", { sql, cursorPos });
}
export async function prepareQueryPaginationExecutionPlan(
options: QueryPaginationExecutionPlanOptions,
): Promise<QueryPaginationExecutionPlan> {
return post("/api/query/prepare-pagination-plan", { options });
}
export async function buildSortedQuerySql(options: SortedQuerySqlOptions): Promise<QuerySqlBuildResult> {
return post("/api/query/build-sorted-sql", { options });
}
export async function buildExplainSql(options: BuildExplainSqlOptions): Promise<ExplainSqlBuildResult> {
return post("/api/query/build-explain-sql", { options });
}
export async function buildDroppedFilePreviewSql(options: DroppedFilePreviewSqlOptions): Promise<string | undefined> {
const result = await post<string | null>("/api/query/build-dropped-file-preview-sql", { options });
return result ?? undefined;
}
export async function buildTableSelectSql(options: BuildTableSelectSqlOptions): Promise<string> {
return post("/api/query/build-table-select-sql", { options });
}
export async function buildDatabaseSearchSql(options: DatabaseSearchSqlOptions): Promise<DatabaseSearchSql | null> {
return post("/api/query/build-database-search-sql", { options });
}
export async function buildSearchResultWhere(options: SearchResultWhereOptions): Promise<string> {
return post("/api/query/build-search-result-where", { options });
}
export async function buildRenameObjectSql(options: BuildRenameObjectSqlOptions): Promise<string> {
return post("/api/query/build-rename-object-sql", { options });
}
export async function buildCreateDatabaseSql(options: CreateDatabaseSqlOptions): Promise<string> {
return post("/api/query/build-create-database-sql", { options });
}
export async function buildDuckDbAttachDatabaseSql(path: string, name: string): Promise<string> {
return post("/api/query/build-duckdb-attach-database-sql", { options: { path, name } });
}
export async function buildDropObjectSql(options: DropObjectSqlOptions): Promise<string> {
return post("/api/query/build-drop-object-sql", { options });
}
export async function buildDropTableSql(options: TableAdminSqlOptions): Promise<string> {
return post("/api/query/build-drop-table-sql", { options });
}
export async function buildEmptyTableSql(options: TableAdminSqlOptions): Promise<string> {
return post("/api/query/build-empty-table-sql", { options });
}
export async function buildTruncateTableSql(options: TableAdminSqlOptions): Promise<string> {
return post("/api/query/build-truncate-table-sql", { options });
}
export async function buildDropDatabaseSql(options: DatabaseNameSqlOptions): Promise<string> {
return post("/api/query/build-drop-database-sql", { options });
}
export async function buildCreateSchemaSql(options: SchemaNameSqlOptions): Promise<string> {
return post("/api/query/build-create-schema-sql", { options });
}
export async function buildDropSchemaSql(options: SchemaNameSqlOptions): Promise<string> {
return post("/api/query/build-drop-schema-sql", { options });
}
export async function buildDuplicateTableStructureSql(options: DuplicateTableStructureSqlOptions): Promise<string> {
return post("/api/query/build-duplicate-table-structure-sql", { options });
}
export async function buildExecutableObjectSourceStatements(
input: BuildEditableObjectSourceSqlInput,
): Promise<string[]> {
return post("/api/query/build-executable-object-source-statements", { input });
}
export async function buildExecutableObjectSourceSql(input: BuildEditableObjectSourceSqlInput): Promise<string> {
return post("/api/query/build-executable-object-source-sql", { input });
}
export async function buildRoutineRenameObjectSourceStatements(
input: BuildRoutineRenameObjectSourceInput,
): Promise<string[]> {
return post("/api/query/build-routine-rename-object-source-statements", { input });
}
export async function buildViewDdlSql(input: BuildViewDdlInput): Promise<string> {
return post("/api/query/build-view-ddl-sql", { input });
}
export async function buildTableStructureChangeSql(
options: BuildTableStructureChangeSqlOptions,
): Promise<TableStructureChangeSql> {
return post("/api/query/build-table-structure-change-sql", { options });
}
export async function buildCreateTableSql(
options: BuildTableStructureChangeSqlOptions,
): Promise<TableStructureChangeSql> {
return post("/api/query/build-create-table-sql", { options });
}
export async function analyzeEditableQueryEditability(sql: string): Promise<QueryEditability> {
return post("/api/query/analyze-editability", { sql });
}
@@ -440,6 +581,53 @@ export async function prepareDataGridSave(options: DataGridSaveStatementOptions)
return post("/api/query/prepare-data-grid-save", { options });
}
export async function buildDataGridCopyUpdateStatements(
options: DataGridCopyUpdateStatementOptions,
): Promise<string[]> {
return post("/api/query/build-data-grid-copy-update-statements", { options });
}
export async function buildDataGridCopyInsertStatement(
options: DataGridCopyInsertStatementOptions,
): Promise<string | undefined> {
const result = await post<string | null>("/api/query/build-data-grid-copy-insert-statement", { options });
return result ?? undefined;
}
export async function buildDataGridContextFilterCondition(
options: DataGridContextFilterConditionOptions,
): Promise<string | undefined> {
const result = await post<string | null>("/api/query/build-data-grid-context-filter-condition", { options });
return result ?? undefined;
}
export async function buildDataGridColumnValueFilterCondition(
options: DataGridColumnValueFilterConditionOptions,
): Promise<string | undefined> {
const result = await post<string | null>("/api/query/build-data-grid-column-value-filter-condition", { options });
return result ?? undefined;
}
export async function buildDataGridCountSql(options: DataGridCountSqlOptions): Promise<string> {
return post("/api/query/build-data-grid-count-sql", { options });
}
export async function buildHiveTablePropertiesSql(options: HiveTablePropertiesSqlOptions): Promise<string> {
return post("/api/query/build-hive-table-properties-sql", { options });
}
export async function buildExportInsertStatements(options: BuildExportInsertStatementsOptions): Promise<string[]> {
return post("/api/query/build-export-insert-statements", { options });
}
export async function buildExportSqlInsert(options: BuildExportInsertStatementsOptions): Promise<string> {
return post("/api/query/build-export-sql-insert", { options });
}
export async function buildDatabaseSqlExport(options: BuildDatabaseSqlExportOptions): Promise<string> {
return post("/api/query/build-database-sql-export", { options });
}
export async function prepareDataCompare(options: DataComparePreparationOptions): Promise<DataComparePreparation> {
return post("/api/data-compare/prepare", options);
}
+3 -49
View File
@@ -1,6 +1,5 @@
import type { DatabaseObjectType, DatabaseType } from "@/types/database";
import { isSchemaAware } from "@/lib/databaseCapabilities";
import { quoteTableIdentifier } from "@/lib/tableSelectSql";
import * as api from "@/lib/api";
export type RenameableObjectType = DatabaseObjectType;
@@ -23,26 +22,6 @@ const postgresLikeRenameTypes = new Set<DatabaseType>([
const oracleLikeRenameTypes = new Set<DatabaseType>(["oracle", "dameng"]);
function sqlServerString(value: string): string {
return `N'${value.replaceAll("'", "''")}'`;
}
function quoteRenameIdentifier(databaseType: DatabaseType | undefined, name: string): string {
if (databaseType === "mysql" || databaseType === "goldendb") return `\`${name.replaceAll("`", "``")}\``;
return quoteTableIdentifier(databaseType, name);
}
function qualifiedName(databaseType: DatabaseType | undefined, schema: string | null | undefined, name: string) {
if (isSchemaAware(databaseType) && schema) {
return `${quoteRenameIdentifier(databaseType, schema)}.${quoteRenameIdentifier(databaseType, name)}`;
}
return quoteRenameIdentifier(databaseType, name);
}
function sqlServerObjectName(schema: string | null | undefined, name: string) {
return schema ? `${schema}.${name}` : name;
}
export function supportsObjectRename(
databaseType: DatabaseType | undefined,
objectType: RenameableObjectType,
@@ -59,31 +38,6 @@ export function supportsObjectRename(
return false;
}
export function buildRenameObjectSql(options: BuildRenameObjectSqlOptions): string {
const { databaseType, objectType, schema, oldName, newName } = options;
if (!supportsObjectRename(databaseType, objectType)) {
throw new Error(`Renaming ${objectType} is not supported for ${databaseType ?? "this database"}.`);
}
if (databaseType === "sqlserver") {
return `EXEC sp_rename ${sqlServerString(sqlServerObjectName(schema, oldName))}, ${sqlServerString(newName)}, N'OBJECT';`;
}
if (databaseType === "mysql" || databaseType === "goldendb") {
return `RENAME TABLE ${qualifiedName(databaseType, schema, oldName)} TO ${qualifiedName(databaseType, schema, newName)};`;
}
if (databaseType === "sqlite") {
return `ALTER TABLE ${qualifiedName(databaseType, schema, oldName)} RENAME TO ${quoteRenameIdentifier(databaseType, newName)};`;
}
if (
postgresLikeRenameTypes.has(databaseType as DatabaseType) ||
oracleLikeRenameTypes.has(databaseType as DatabaseType)
) {
const keyword = objectType;
return `ALTER ${keyword} ${qualifiedName(databaseType, schema, oldName)} RENAME TO ${quoteRenameIdentifier(databaseType, newName)};`;
}
throw new Error(`Renaming ${objectType} is not supported for ${databaseType ?? "this database"}.`);
export function buildRenameObjectSql(options: BuildRenameObjectSqlOptions): Promise<string> {
return api.buildRenameObjectSql(options);
}
+11 -173
View File
@@ -1,6 +1,7 @@
import type { DatabaseType, ObjectSourceKind } from "@/types/database";
import * as api from "@/lib/api";
type BuildEditableObjectSourceSqlInput = {
export type BuildEditableObjectSourceSqlInput = {
databaseType: DatabaseType;
objectType: ObjectSourceKind;
schema?: string | null;
@@ -8,7 +9,7 @@ type BuildEditableObjectSourceSqlInput = {
source: string;
};
type BuildRoutineRenameObjectSourceInput = BuildEditableObjectSourceSqlInput & {
export type BuildRoutineRenameObjectSourceInput = BuildEditableObjectSourceSqlInput & {
newName: string;
};
@@ -25,128 +26,6 @@ const postgresLikeRoutineRenameTypes = new Set<DatabaseType>([
const mysqlLikeRoutineRenameTypes = new Set<DatabaseType>(["mysql", "goldendb"]);
const oracleLikeRoutineRenameTypes = new Set<DatabaseType>(["oracle", "dameng"]);
function quotePostgresIdentifier(value: string) {
return `"${value.replaceAll('"', '""')}"`;
}
function quoteMysqlIdentifier(value: string) {
return `\`${value.replaceAll("`", "``")}\``;
}
function ensureSemicolon(sql: string) {
const trimmed = sql.trim();
return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
}
function postgresQualifiedName(schema: string | null | undefined, name: string) {
return [schema, name]
.filter(Boolean)
.map((part) => quotePostgresIdentifier(part as string))
.join(".");
}
function mysqlQualifiedName(schema: string | null | undefined, name: string) {
return [schema, name]
.filter(Boolean)
.map((part) => quoteMysqlIdentifier(part as string))
.join(".");
}
function unquotePostgresIdentifier(value: string) {
const trimmed = value.trim();
if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed.slice(1, -1).replaceAll('""', '"');
return trimmed;
}
function splitQualifiedRoutineName(value: string) {
const parts = value.match(/"(?:""|[^"])+"|[A-Za-z_][\w$]*/g) ?? [];
return parts.map(unquotePostgresIdentifier);
}
function unquoteMysqlIdentifier(value: string) {
const trimmed = value.trim();
if (trimmed.startsWith("`") && trimmed.endsWith("`")) return trimmed.slice(1, -1).replaceAll("``", "`");
return trimmed;
}
function splitMysqlQualifiedRoutineName(value: string) {
const parts = value.match(/`(?:``|[^`])+`|[A-Za-z_][\w$]*/g) ?? [];
return parts.map(unquoteMysqlIdentifier);
}
function routineDeclaration(source: string) {
const match = source.match(
/^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:NON)?EDITIONABLE\s+)?(FUNCTION|PROCEDURE)\s+((?:"(?:""|[^"])+"|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:"(?:""|[^"])+"|[A-Za-z_][\w$]*))?)\s*(\([^]*?\))?/i,
);
if (!match) return null;
const nameParts = splitQualifiedRoutineName(match[2]);
const name = nameParts[nameParts.length - 1];
if (!name) return null;
return {
kind: match[1].toUpperCase() as "FUNCTION" | "PROCEDURE",
name,
signature: match[3]?.trim() ?? "",
};
}
function replaceSqlRoutineDeclarationName(source: string, schema: string | null | undefined, newName: string) {
const match = source.match(
/^(\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:NON)?EDITIONABLE\s+)?(?:FUNCTION|PROCEDURE)\s+)((?:"(?:""|[^"])+"|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:"(?:""|[^"])+"|[A-Za-z_][\w$]*))?)/i,
);
if (!match) return null;
const existingParts = splitQualifiedRoutineName(match[2]);
const schemaName = schema || (existingParts.length > 1 ? existingParts[0] : null);
const replacement = schemaName
? `${quotePostgresIdentifier(schemaName)}.${quotePostgresIdentifier(newName)}`
: quotePostgresIdentifier(newName);
return `${source.slice(0, match.index)}${match[1]}${replacement}${source.slice((match.index ?? 0) + match[0].length)}`;
}
function mysqlRoutineDeclaration(source: string) {
const match = source.match(
/^\s*CREATE\s+(?:DEFINER\s*=\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s*@\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s+)?(FUNCTION|PROCEDURE)\s+((?:`(?:``|[^`])+`|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:`(?:``|[^`])+`|[A-Za-z_][\w$]*))?)/i,
);
if (!match) return null;
const nameParts = splitMysqlQualifiedRoutineName(match[2]);
const name = nameParts[nameParts.length - 1];
if (!name) return null;
return {
kind: match[1].toUpperCase() as "FUNCTION" | "PROCEDURE",
name,
};
}
function replaceMysqlRoutineDeclarationName(source: string, newName: string) {
const match = source.match(
/^(\s*CREATE\s+(?:DEFINER\s*=\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s*@\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s+)?(?:FUNCTION|PROCEDURE)\s+)((?:`(?:``|[^`])+`|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:`(?:``|[^`])+`|[A-Za-z_][\w$]*))?)/i,
);
if (!match) return null;
return `${source.slice(0, match.index)}${match[1]}${quoteMysqlIdentifier(newName)}${source.slice((match.index ?? 0) + match[0].length)}`;
}
function routineNameChanged(sourceName: string, savedName: string) {
return sourceName.toLowerCase() !== savedName.toLowerCase();
}
function buildRoutineRenameCleanup(input: BuildEditableObjectSourceSqlInput, source: string) {
if (input.objectType !== "FUNCTION" && input.objectType !== "PROCEDURE") return null;
if (mysqlLikeRoutineRenameTypes.has(input.databaseType)) {
const declaration = mysqlRoutineDeclaration(source);
if (!declaration || declaration.kind !== input.objectType) return null;
if (!routineNameChanged(declaration.name, input.name)) return null;
return `DROP ${input.objectType} IF EXISTS ${mysqlQualifiedName(input.schema, input.name)};`;
}
if (!postgresLikeRoutineRenameTypes.has(input.databaseType)) return null;
const declaration = routineDeclaration(source);
if (!declaration || declaration.kind !== input.objectType) return null;
if (!routineNameChanged(declaration.name, input.name)) return null;
return `DROP ${input.objectType} IF EXISTS ${postgresQualifiedName(input.schema, input.name)}${declaration.signature};`;
}
export function supportsSourceBackedRoutineRename(
databaseType: DatabaseType | undefined,
objectType: ObjectSourceKind,
@@ -160,59 +39,18 @@ export function supportsSourceBackedRoutineRename(
);
}
export function buildRoutineRenameObjectSourceStatements(input: BuildRoutineRenameObjectSourceInput) {
if (!supportsSourceBackedRoutineRename(input.databaseType, input.objectType)) {
throw new Error(`Renaming ${input.objectType} from source is not supported for ${input.databaseType}.`);
}
const source = input.source.trim();
const declaration = mysqlLikeRoutineRenameTypes.has(input.databaseType)
? mysqlRoutineDeclaration(source)
: routineDeclaration(source);
if (!declaration || declaration.kind !== input.objectType) {
throw new Error(`Cannot find a CREATE ${input.objectType} declaration in the object source.`);
}
const renamedSource = mysqlLikeRoutineRenameTypes.has(input.databaseType)
? replaceMysqlRoutineDeclarationName(source, input.newName)
: replaceSqlRoutineDeclarationName(source, input.schema, input.newName);
if (!renamedSource) {
throw new Error(`Cannot rewrite the ${input.objectType} name in the object source.`);
}
if (oracleLikeRoutineRenameTypes.has(input.databaseType)) {
return [
ensureSemicolon(renamedSource),
`DROP ${input.objectType} ${postgresQualifiedName(input.schema, input.name)};`,
];
}
return buildExecutableObjectSourceStatements({
databaseType: input.databaseType,
objectType: input.objectType,
schema: input.schema,
name: input.name,
source: renamedSource,
});
export function buildRoutineRenameObjectSourceStatements(
input: BuildRoutineRenameObjectSourceInput,
): Promise<string[]> {
return api.buildRoutineRenameObjectSourceStatements(input);
}
export function buildExecutableObjectSourceStatements(input: BuildEditableObjectSourceSqlInput) {
const source = input.source.trim();
if (input.databaseType === "sqlserver") {
return [source.replace(/^CREATE\s+(?:OR\s+ALTER\s+)?/i, "ALTER ")];
}
if ((input.databaseType === "postgres" || input.databaseType === "gaussdb") && input.objectType === "VIEW") {
return [`CREATE OR REPLACE VIEW ${postgresQualifiedName(input.schema, input.name)} AS\n${ensureSemicolon(source)}`];
}
const createStatement = ensureSemicolon(source);
const cleanup = buildRoutineRenameCleanup(input, source);
return cleanup ? [createStatement, cleanup] : [createStatement];
export function buildExecutableObjectSourceStatements(input: BuildEditableObjectSourceSqlInput): Promise<string[]> {
return api.buildExecutableObjectSourceStatements(input);
}
export function buildExecutableObjectSourceSql(input: BuildEditableObjectSourceSqlInput) {
return buildExecutableObjectSourceStatements(input).join("\n");
export async function buildExecutableObjectSourceSql(input: BuildEditableObjectSourceSqlInput): Promise<string> {
return api.buildExecutableObjectSourceSql(input);
}
export function objectSourceSaveExecutionMode(_databaseType: DatabaseType): ObjectSourceSaveExecutionMode {
@@ -1,255 +0,0 @@
import type { DatabaseType } from "../types/database.ts";
import { usesFetchFirst } from "./databaseCapabilities.ts";
import { quoteTableIdentifier } from "./tableSelectSql.ts";
import { findStatementAtCursor } from "./sqlStatementSplit.ts";
import { sqlServerStatementForDerivedTable } from "./sqlServerQueryWrapping.ts";
export interface PaginatedQuerySqlResult {
ok: true;
sql: string;
}
export interface PaginatedQuerySqlError {
ok: false;
reason: "empty" | "multi" | "not_select" | "unsupported";
}
export interface QueryPaginationExecutionPlan {
sqlToExecute: string;
pageSql?: string;
pageLimit?: number;
pageOffset?: number;
countSql?: string;
useAgentResultSession: boolean;
}
const unsupportedPaginationTypes = new Set<DatabaseType | undefined>(["neo4j", "mongodb", "redis", "elasticsearch"]);
export function buildQueryPaginationExecutionPlan({
sql,
queryBaseSql,
databaseType,
pagination,
useAgentCursor,
}: {
sql: string;
queryBaseSql: string;
databaseType: DatabaseType | undefined;
pagination: { limit: number; offset: number; sessionId?: string };
useAgentCursor: boolean;
}): QueryPaginationExecutionPlan {
const plan: QueryPaginationExecutionPlan = {
sqlToExecute: sql,
useAgentResultSession: false,
};
const counted = buildCountQuerySql(queryBaseSql, databaseType);
if (counted.ok) {
plan.countSql = counted.sql;
}
if (pagination.sessionId) {
plan.pageLimit = pagination.limit;
plan.pageOffset = pagination.offset;
plan.useAgentResultSession = true;
return plan;
}
if (useAgentCursor && pagination.offset === 0) {
plan.sqlToExecute = queryBaseSql;
plan.pageLimit = pagination.limit;
plan.pageOffset = pagination.offset;
plan.useAgentResultSession = true;
return plan;
}
const paginated = buildPaginatedQuerySql(sql, databaseType, pagination.limit, pagination.offset);
if (paginated.ok) {
plan.sqlToExecute = paginated.sql;
plan.pageSql = paginated.sql;
plan.pageLimit = pagination.limit;
plan.pageOffset = pagination.offset;
}
return plan;
}
export function buildPaginatedQuerySql(
originalSql: string,
databaseType: DatabaseType | undefined,
limit: number,
offset: number,
): PaginatedQuerySqlResult | PaginatedQuerySqlError {
const statement = singleSelectableStatement(originalSql);
if (!statement.ok) return statement;
if (unsupportedPaginationTypes.has(databaseType)) return { ok: false, reason: "unsupported" };
const safeLimit = Math.max(1, Math.floor(limit));
const safeOffset = Math.max(0, Math.floor(offset));
const alias = quoteTableIdentifier(databaseType, "dbx_page");
const wrappedSql = databaseType === "sqlserver" ? sqlServerStatementForDerivedTable(statement.sql) : statement.sql;
const base = `SELECT * FROM (${wrappedSql}) ${alias}`;
if (databaseType === "sqlserver") {
if (safeOffset > 0) return { ok: false, reason: "unsupported" };
return {
ok: true,
sql: `SELECT TOP (${safeLimit}) * FROM (${wrappedSql}) ${alias}`,
};
}
if (usesFetchFirst(databaseType)) {
const offsetSql = safeOffset ? ` OFFSET ${safeOffset} ROWS` : "";
return { ok: true, sql: `${base}${offsetSql} FETCH FIRST ${safeLimit} ROWS ONLY` };
}
const offsetSql = safeOffset ? ` OFFSET ${safeOffset}` : "";
return { ok: true, sql: `${base} LIMIT ${safeLimit}${offsetSql};` };
}
export function buildCountQuerySql(
originalSql: string,
databaseType: DatabaseType | undefined,
): PaginatedQuerySqlResult | PaginatedQuerySqlError {
const statement = singleSelectableStatement(originalSql);
if (!statement.ok) return statement;
if (unsupportedPaginationTypes.has(databaseType)) return { ok: false, reason: "unsupported" };
const alias = quoteTableIdentifier(databaseType, "dbx_count");
const wrappedSql = databaseType === "sqlserver" ? sqlServerStatementForDerivedTable(statement.sql) : statement.sql;
return { ok: true, sql: `SELECT COUNT(*) AS dbx_total_rows FROM (${wrappedSql}) ${alias};` };
}
function singleSelectableStatement(
originalSql: string,
): { ok: true; sql: string } | Pick<PaginatedQuerySqlError, "ok" | "reason"> {
const baseSql = originalSql.trim();
if (!baseSql) return { ok: false, reason: "empty" };
const statement = findStatementAtCursor(baseSql, 0)
.trim()
.replace(/;+\s*$/, "")
.trim();
if (!statement) return { ok: false, reason: "empty" };
if (statement.length !== baseSql.replace(/;+\s*$/, "").trim().length) {
return { ok: false, reason: "multi" };
}
if (!/^\s*(SELECT|WITH)\b/i.test(statement)) {
return { ok: false, reason: "not_select" };
}
if (hasTopLevelSelectInto(statement)) {
return { ok: false, reason: "not_select" };
}
return { ok: true, sql: statement };
}
function hasTopLevelSelectInto(sql: string): boolean {
let sawSelect = false;
for (const token of topLevelSqlTokens(sql)) {
if (!sawSelect) {
sawSelect = token === "SELECT";
continue;
}
if (token === "INTO") return true;
}
return false;
}
function topLevelSqlTokens(sql: string): string[] {
const tokens: string[] = [];
let i = 0;
let depth = 0;
while (i < sql.length) {
const ch = sql[i];
const next = sql[i + 1];
if (ch === "-" && next === "-") {
i += 2;
while (i < sql.length && sql[i] !== "\n") i++;
continue;
}
if (ch === "/" && next === "*") {
i += 2;
while (i < sql.length - 1 && !(sql[i] === "*" && sql[i + 1] === "/")) i++;
i = Math.min(i + 2, sql.length);
continue;
}
if (ch === "'" || ch === '"' || ch === "`") {
i = skipSqlQuoted(sql, i, ch);
continue;
}
if (ch === "[") {
i = skipSqlBracketIdentifier(sql, i);
continue;
}
if (ch === "(") {
depth++;
i++;
continue;
}
if (ch === ")") {
depth = Math.max(0, depth - 1);
i++;
continue;
}
if (depth === 0 && isSqlTokenStart(ch)) {
const start = i;
i++;
while (i < sql.length && isSqlTokenPart(sql[i])) i++;
tokens.push(sql.slice(start, i).toUpperCase());
continue;
}
i++;
}
return tokens;
}
function skipSqlQuoted(sql: string, pos: number, quote: string): number {
let i = pos + 1;
while (i < sql.length) {
if (sql[i] === quote) {
if (sql[i + 1] === quote) {
i += 2;
continue;
}
return i + 1;
}
if (quote === "'" && sql[i] === "\\") {
i += 2;
continue;
}
i++;
}
return sql.length;
}
function skipSqlBracketIdentifier(sql: string, pos: number): number {
let i = pos + 1;
while (i < sql.length) {
if (sql[i] === "]") {
if (sql[i + 1] === "]") {
i += 2;
continue;
}
return i + 1;
}
i++;
}
return sql.length;
}
function isSqlTokenStart(ch: string): boolean {
return /[A-Za-z_]/.test(ch);
}
function isSqlTokenPart(ch: string): boolean {
return /[A-Za-z0-9_$#]/.test(ch);
}
-82
View File
@@ -1,82 +0,0 @@
import type { DatabaseType } from "../types/database.ts";
import { quoteTableIdentifier } from "./tableSelectSql.ts";
import { findStatementAtCursor } from "./sqlStatementSplit.ts";
import { sqlServerStatementForDerivedTable } from "./sqlServerQueryWrapping.ts";
export type QuerySortDirection = "asc" | "desc";
export interface SortedQuerySqlResult {
ok: true;
sql: string;
}
export interface SortedQuerySqlError {
ok: false;
reason: "empty" | "multi" | "not_select" | "with";
}
export function buildSortedQuerySql(
originalSql: string,
databaseType: DatabaseType | undefined,
resultColumns: string[],
columnIndex: number,
column: string,
direction: QuerySortDirection,
): SortedQuerySqlResult | SortedQuerySqlError {
const baseSql = originalSql.trim();
if (!baseSql) return { ok: false, reason: "empty" };
const statement = findStatementAtCursor(baseSql, 0)
.trim()
.replace(/;+\s*$/, "")
.trim();
if (!statement) return { ok: false, reason: "empty" };
if (statement.length !== baseSql.replace(/;+\s*$/, "").trim().length) {
return { ok: false, reason: "multi" };
}
if (/^\s*WITH\b/i.test(statement)) {
return { ok: false, reason: "with" };
}
if (!/^\s*SELECT\b/i.test(statement)) {
return { ok: false, reason: "not_select" };
}
const aliases = buildDerivedColumnAliases(resultColumns);
const useDerivedColumnAliases = supportsDerivedColumnAliasList(databaseType);
const sortAlias = useDerivedColumnAliases
? (aliases[columnIndex] ?? aliases[resultColumns.indexOf(column)] ?? fallbackAlias(columnIndex))
: (resultColumns[columnIndex] ?? column);
const quotedColumn = quoteTableIdentifier(databaseType, sortAlias);
const aliasList = aliases.map((alias) => quoteTableIdentifier(databaseType, alias)).join(", ");
const wrappedStatement = databaseType === "sqlserver" ? sqlServerStatementForDerivedTable(statement) : statement;
return {
ok: true,
sql: useDerivedColumnAliases
? `SELECT * FROM (${wrappedStatement}) t(${aliasList}) ORDER BY ${quotedColumn} ${direction.toUpperCase()};`
: `SELECT * FROM (${wrappedStatement}) t ORDER BY ${quotedColumn} ${direction.toUpperCase()};`,
};
}
function supportsDerivedColumnAliasList(databaseType: DatabaseType | undefined): boolean {
return databaseType !== "mysql";
}
function buildDerivedColumnAliases(resultColumns: string[]): string[] {
const seen = new Map<string, number>();
return resultColumns.map((column, index) => {
const base = normalizeAliasBase(column, index);
const count = (seen.get(base) ?? 0) + 1;
seen.set(base, count);
return count === 1 ? base : `${base}_${count}`;
});
}
function normalizeAliasBase(column: string, index: number): string {
const compact = column.trim().replace(/\s+/g, "_");
const safe = compact.replace(/[^\p{L}\p{N}_$]/gu, "_").replace(/^_+|_+$/g, "");
return safe || fallbackAlias(index);
}
function fallbackAlias(index: number): string {
return `column_${index + 1}`;
}
+17 -2
View File
@@ -1,4 +1,4 @@
import { findStatementAtCursor } from "./sqlStatementSplit";
import * as api from "./api";
export type ExecuteMode = "all" | "current";
@@ -11,7 +11,22 @@ export function resolveExecutableSql(
if (trimmedSelection) return trimmedSelection;
if (options?.mode === "current" && options.cursorPos !== undefined) {
return findStatementAtCursor(fullSql, options.cursorPos);
return fullSql;
}
return fullSql;
}
export async function resolveExecutableSqlWithBackend(
fullSql: string,
selectedSql: string,
options?: { mode?: ExecuteMode; cursorPos?: number },
): Promise<string> {
const trimmedSelection = selectedSql.trim();
if (trimmedSelection) return trimmedSelection;
if (options?.mode === "current" && options.cursorPos !== undefined) {
return await api.findStatementAtCursor(fullSql, options.cursorPos);
}
return fullSql;
-100
View File
@@ -1,100 +0,0 @@
export function findStatementAtCursor(sql: string, cursorPos: number): string {
const statements = splitStatements(sql);
for (const stmt of statements) {
if (cursorPos >= stmt.start && cursorPos <= stmt.end) {
return stmt.text;
}
}
if (statements.length > 0) {
const last = statements[statements.length - 1];
if (cursorPos >= last.end) return last.text;
}
return sql.trim();
}
interface StatementRange {
text: string;
start: number;
end: number;
}
function splitStatements(sql: string): StatementRange[] {
const results: StatementRange[] = [];
let i = 0;
let stmtStart = 0;
while (i < sql.length) {
const ch = sql[i];
if (ch === "-" && sql[i + 1] === "-") {
i = skipLineComment(sql, i);
continue;
}
if (ch === "/" && sql[i + 1] === "*") {
i = skipBlockComment(sql, i);
continue;
}
if (ch === "'" || ch === '"') {
i = skipQuoted(sql, i, ch);
continue;
}
if (ch === "`") {
i = skipQuoted(sql, i, "`");
continue;
}
if (ch === ";") {
const text = sql.slice(stmtStart, i).trim();
if (text) {
results.push({ text, start: stmtStart, end: i + 1 });
}
stmtStart = i + 1;
}
i++;
}
const trailing = sql.slice(stmtStart).trim();
if (trailing) {
results.push({ text: trailing, start: stmtStart, end: sql.length });
}
return results;
}
function skipLineComment(sql: string, pos: number): number {
let i = pos + 2;
while (i < sql.length && sql[i] !== "\n") i++;
return i + 1;
}
function skipBlockComment(sql: string, pos: number): number {
let i = pos + 2;
while (i < sql.length - 1) {
if (sql[i] === "*" && sql[i + 1] === "/") return i + 2;
i++;
}
return sql.length;
}
function skipQuoted(sql: string, pos: number, quote: string): number {
let i = pos + 1;
while (i < sql.length) {
if (sql[i] === quote) {
if (sql[i + 1] === quote) {
i += 2;
continue;
}
return i + 1;
}
if (sql[i] === "\\" && quote === "'") {
i += 2;
continue;
}
i++;
}
return sql.length;
}
+9 -1
View File
@@ -9,6 +9,14 @@ export interface FetchTableDataForExportOptions {
tableName: string;
columns?: string[];
pageSize?: number;
buildPageSql?: (options: {
databaseType?: DatabaseType;
schema?: string;
tableName: string;
columns?: string[];
limit: number;
offset: number;
}) => Promise<string> | string;
executePage: (sql: string) => Promise<QueryResult>;
}
@@ -20,7 +28,7 @@ export async function fetchTableDataForExport(options: FetchTableDataForExportOp
let executionTimeMs = 0;
while (true) {
const sql = buildTableSelectSql({
const sql = await (options.buildPageSql ?? buildTableSelectSql)({
databaseType: options.databaseType,
schema: options.schema,
tableName: options.tableName,
+4 -134
View File
@@ -1,6 +1,6 @@
import type { DatabaseType } from "../types/database.ts";
import { isSchemaAware, usesFetchFirst } from "./databaseCapabilities.ts";
import { DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN, DBX_TDENGINE_TBNAME_COLUMN } from "./tableEditing.ts";
import { isSchemaAware } from "./databaseCapabilities.ts";
import * as api from "./api.ts";
export interface BuildTableSelectSqlOptions {
databaseType?: DatabaseType;
@@ -29,17 +29,6 @@ function quoteCypherIdentifier(name: string): string {
return `\`${name.replace(/`/g, "``")}\``;
}
function isOracleRowId(databaseType: DatabaseType | undefined, name: string): boolean {
return databaseType === "oracle" && name.toUpperCase() === DBX_ROWID_COLUMN;
}
function quoteOrderIdentifier(databaseType: DatabaseType | undefined, name: string, tableAlias?: string): string {
if (isOracleRowId(databaseType, name)) return tableAlias ? `${tableAlias}.ROWID` : "ROWID";
if (isTdengineTbname(databaseType, name)) return DBX_TDENGINE_TBNAME_COLUMN;
const quoted = quoteTableIdentifier(databaseType, name);
return tableAlias ? `${tableAlias}.${quoted}` : quoted;
}
export function qualifiedTableName(
options: Pick<BuildTableSelectSqlOptions, "databaseType" | "schema" | "tableName">,
): string {
@@ -55,125 +44,6 @@ export function normalizeWhereInput(whereInput?: string): string {
return withoutSemicolon.replace(/^where\b/i, "").trim();
}
export function buildTableSelectSql(options: BuildTableSelectSqlOptions): string {
const databaseType = options.databaseType;
const limit = options.limit ?? 100;
if (databaseType === "neo4j") return buildNeo4jTableSelectSql(options, limit);
const table = qualifiedTableName(options);
const predicate = normalizeWhereInput(options.whereInput);
const where = predicate ? ` WHERE (${predicate})` : "";
const rowIdAlias = options.includeRowId && databaseType === "oracle" ? "t" : undefined;
const defaultOrderAlias = databaseType === "jdbc" ? "dbx_t" : rowIdAlias;
const defaultOrderBy = options.primaryKeys?.length
? options.primaryKeys.map((pk) => `${quoteOrderIdentifier(databaseType, pk, defaultOrderAlias)} ASC`).join(", ")
: options.fallbackOrderColumns?.length
? options.fallbackOrderColumns.map((column) => `${quoteTableIdentifier(databaseType, column)} ASC`).join(", ")
: undefined;
const orderBy = options.orderBy ?? defaultOrderBy;
const order = orderBy ? ` ORDER BY ${orderBy}` : "";
const selectColumns =
options.includeRowId && databaseType === "oracle"
? `ROWIDTOCHAR(t.ROWID) AS "${DBX_ROWID_COLUMN}", t.*`
: buildSelectColumns(databaseType, options.columns);
const tableAlias =
options.includeRowId && usesFetchFirst(databaseType)
? `${table} t`
: databaseType === "jdbc" && defaultOrderBy
? `${table} dbx_t`
: table;
if (usesFetchFirst(databaseType)) {
const offset = options.offset ? ` OFFSET ${options.offset} ROWS` : "";
return `SELECT ${selectColumns} FROM ${tableAlias}${where}${order}${offset} FETCH FIRST ${limit} ROWS ONLY`;
}
if (databaseType === "sqlserver") {
return buildSqlServerTableSelectSql({
table,
where,
orderBy: orderBy ?? "(SELECT NULL)",
columns: options.columns,
limit,
offset: options.offset ?? 0,
});
}
const offset = options.offset ? ` OFFSET ${options.offset}` : "";
return `SELECT ${selectColumns} FROM ${tableAlias}${where}${order} LIMIT ${limit}${offset};`;
}
function buildSelectColumns(databaseType: DatabaseType | undefined, columns?: string[]): string {
if (!columns?.length) return "*";
if (databaseType === "tdengine") {
const tdengineColumns = columns.some((column) => column.toLowerCase() === DBX_TDENGINE_TBNAME_COLUMN)
? columns
: [DBX_TDENGINE_TBNAME_COLUMN, ...columns];
return tdengineColumns
.map((column) => {
if (isTdengineTbname(databaseType, column)) return DBX_TDENGINE_TBNAME_COLUMN;
const ident = quoteTableIdentifier(databaseType, column);
return `${ident} AS ${ident}`;
})
.join(", ");
}
if (databaseType !== "hive") return "*";
return columns
.map((column) => {
const ident = quoteTableIdentifier(databaseType, column);
return `${ident} AS ${ident}`;
})
.join(", ");
}
function buildSqlServerTableSelectSql(options: {
table: string;
where: string;
orderBy: string;
columns?: string[];
limit: number;
offset: number;
}): string {
const columns = options.columns?.length
? options.columns.map((column) => quoteTableIdentifier("sqlserver", column)).join(", ")
: "*";
const order = options.orderBy === "(SELECT NULL)" ? "" : ` ORDER BY ${options.orderBy}`;
if (options.offset <= 0) {
return `SELECT TOP (${options.limit}) ${columns} FROM ${options.table}${options.where}${order}`;
}
const pageAlias = quoteTableIdentifier("sqlserver", "dbx_page");
const rowNumberAlias = quoteTableIdentifier("sqlserver", "__dbx_row_num");
const end = options.offset + options.limit;
const rowNumberOrder = `ORDER BY ${options.orderBy}`;
return (
`WITH ${pageAlias} AS (` +
`SELECT ${columns}, ROW_NUMBER() OVER (${rowNumberOrder}) AS ${rowNumberAlias} FROM ${options.table}${options.where}` +
`) SELECT ${columns} FROM ${pageAlias}` +
` WHERE ${rowNumberAlias} > ${options.offset} AND ${rowNumberAlias} <= ${end} ORDER BY ${rowNumberAlias}`
);
}
function isTdengineTbname(databaseType: DatabaseType | undefined, name: string): boolean {
return databaseType === "tdengine" && name.toLowerCase() === DBX_TDENGINE_TBNAME_COLUMN;
}
function buildNeo4jTableSelectSql(options: BuildTableSelectSqlOptions, limit: number): string {
const label = quoteCypherIdentifier(options.tableName);
const predicate = normalizeWhereInput(options.whereInput);
const where = predicate ? ` WHERE ${predicate}` : "";
const returnedColumns = options.columns?.length
? options.columns
.map((column) => `n.${quoteCypherIdentifier(column)} AS ${quoteCypherIdentifier(column)}`)
.join(", ")
: "n";
const returns = `elementId(n) AS ${quoteCypherIdentifier(DBX_NEO4J_ELEMENT_ID_COLUMN)}, ${returnedColumns}`;
const defaultOrderBy = options.primaryKeys?.length
? options.primaryKeys.map((pk) => `n.${quoteCypherIdentifier(pk)} ASC`).join(", ")
: undefined;
const orderBy = options.orderBy ?? defaultOrderBy;
const order = orderBy ? ` ORDER BY ${orderBy}` : "";
const skip = options.offset ? ` SKIP ${options.offset}` : "";
return `MATCH (n:${label})${where} RETURN ${returns}${order}${skip} LIMIT ${limit};`;
export async function buildTableSelectSql(options: BuildTableSelectSqlOptions): Promise<string> {
return api.buildTableSelectSql(options);
}
@@ -1,5 +1,4 @@
import type { ColumnInfo, DatabaseType, IndexInfo } from "../types/database.ts";
import { getTableStructureCapabilities, type TableStructureDialect } from "./tableStructureCapabilities.ts";
export interface EditableStructureColumn {
id: string;
@@ -40,689 +39,3 @@ export interface TableStructureChangeSql {
statements: string[];
warnings: string[];
}
type StructureSqlFlavor = DatabaseType | TableStructureDialect | undefined;
function quoteIdent(databaseType: StructureSqlFlavor, name: string): string {
if (
databaseType === "mysql" ||
databaseType === "doris" ||
databaseType === "starrocks" ||
databaseType === "goldendb" ||
databaseType === "sundb"
)
return `\`${name.replace(/`/g, "``")}\``;
if (databaseType === "sqlserver") return `[${name.replace(/\]/g, "]]")}]`;
return `"${name.replace(/"/g, '""')}"`;
}
function isOracleLike(databaseType: StructureSqlFlavor): boolean {
return databaseType === "oracle" || databaseType === "dameng" || databaseType === "oceanbase-oracle";
}
function qualifiedTable(databaseType: StructureSqlFlavor, schema: string | undefined, tableName: string): string {
if (
(databaseType === "postgres" ||
isOracleLike(databaseType) ||
databaseType === "sqlserver" ||
databaseType === "h2") &&
schema
) {
return `${quoteIdent(databaseType, schema)}.${quoteIdent(databaseType, tableName)}`;
}
return quoteIdent(databaseType, tableName);
}
function quoteString(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
function clean(value: string | null | undefined): string {
return (value ?? "").trim();
}
function normalizeDefault(value: string | null | undefined): string {
const trimmed = clean(value);
return trimmed.toLowerCase() === "null" ? "" : trimmed;
}
function unwrapClickHouseNullableType(dataType: string): string {
const match = dataType.trim().match(/^Nullable\s*\((.*)\)$/is);
return match ? match[1].trim() : dataType.trim();
}
function clickHouseColumnType(column: EditableStructureColumn): string {
const dataType = column.dataType.trim();
if (column.isNullable) {
return /^Nullable\s*\(/i.test(dataType) ? dataType : `Nullable(${dataType})`;
}
return unwrapClickHouseNullableType(dataType);
}
function columnDefinition(databaseType: StructureSqlFlavor, column: EditableStructureColumn): string {
const dataType = databaseType === "clickhouse" ? clickHouseColumnType(column) : column.dataType.trim();
const parts = [quoteIdent(databaseType, column.name), dataType];
if (!column.isNullable && !isOracleLike(databaseType) && databaseType !== "clickhouse") parts.push("NOT NULL");
const defaultValue = normalizeDefault(column.defaultValue);
if (defaultValue) parts.push(`DEFAULT ${defaultValue}`);
if (databaseType === "mysql" && clean(column.comment)) {
parts.push(`COMMENT ${quoteString(clean(column.comment))}`);
}
return parts.join(" ");
}
function originalDefault(column: EditableStructureColumn): string {
return normalizeDefault(column.original?.column_default);
}
function originalComment(column: EditableStructureColumn): string {
return clean(column.original?.comment);
}
function hasExistingColumnAttributeChange(column: EditableStructureColumn): boolean {
const original = column.original;
if (!original) return false;
return (
column.name !== original.name ||
column.dataType.trim() !== original.data_type.trim() ||
column.isNullable !== original.is_nullable ||
normalizeDefault(column.defaultValue) !== originalDefault(column) ||
clean(column.comment) !== originalComment(column)
);
}
function buildAddColumnSql(
databaseType: StructureSqlFlavor,
table: string,
column: EditableStructureColumn,
positionClause = "",
): string[] {
const addKeyword = databaseType === "sqlserver" ? "ADD" : "ADD COLUMN";
const definition = columnDefinition(databaseType, column);
const statements = isOracleLike(databaseType)
? [`ALTER TABLE ${table} ADD (${definition});`]
: [`ALTER TABLE ${table} ${addKeyword} ${definition}${positionClause};`];
if ((databaseType === "postgres" || isOracleLike(databaseType)) && clean(column.comment)) {
statements.push(
`COMMENT ON COLUMN ${table}.${quoteIdent(databaseType, column.name)} IS ${quoteString(clean(column.comment))};`,
);
}
if (databaseType === "clickhouse" && clean(column.comment)) {
statements.push(
`ALTER TABLE ${table} COMMENT COLUMN ${quoteIdent(databaseType, column.name)} ${quoteString(clean(column.comment))};`,
);
}
return statements;
}
function buildOracleLikeExistingColumnSql(
databaseType: StructureSqlFlavor,
table: string,
column: EditableStructureColumn,
): string[] {
const original = column.original;
if (!original) return [];
const statements: string[] = [];
let currentName = original.name;
if (column.name !== original.name) {
statements.push(
`ALTER TABLE ${table} RENAME COLUMN ${quoteIdent(databaseType, original.name)} TO ${quoteIdent(databaseType, column.name)};`,
);
currentName = column.name;
}
if (column.dataType.trim() !== original.data_type.trim()) {
statements.push(
`ALTER TABLE ${table} MODIFY (${quoteIdent(databaseType, currentName)} ${column.dataType.trim()});`,
);
}
if (column.isNullable !== original.is_nullable) {
const nullability = column.isNullable ? "NULL" : "NOT NULL";
statements.push(`ALTER TABLE ${table} MODIFY (${quoteIdent(databaseType, currentName)} ${nullability});`);
}
if (normalizeDefault(column.defaultValue) !== originalDefault(column)) {
const defaultValue = normalizeDefault(column.defaultValue) || "NULL";
statements.push(`ALTER TABLE ${table} MODIFY (${quoteIdent(databaseType, currentName)} DEFAULT ${defaultValue});`);
}
if (clean(column.comment) !== originalComment(column)) {
const commentValue = clean(column.comment) ? quoteString(clean(column.comment)) : "NULL";
statements.push(`COMMENT ON COLUMN ${table}.${quoteIdent(databaseType, currentName)} IS ${commentValue};`);
}
return statements;
}
function buildMysqlExistingColumnSql(table: string, column: EditableStructureColumn, positionClause = ""): string[] {
const originalName = column.original?.name ?? column.name;
const operation =
column.name === originalName
? `MODIFY COLUMN ${columnDefinition("mysql", column)}`
: `CHANGE COLUMN ${quoteIdent("mysql", originalName)} ${columnDefinition("mysql", column)}`;
return [`ALTER TABLE ${table} ${operation}${positionClause};`];
}
function buildPostgresExistingColumnSql(table: string, column: EditableStructureColumn): string[] {
const original = column.original;
if (!original) return [];
const statements: string[] = [];
const currentName = column.name;
if (column.name !== original.name) {
statements.push(
`ALTER TABLE ${table} RENAME COLUMN ${quoteIdent("postgres", original.name)} TO ${quoteIdent("postgres", column.name)};`,
);
}
if (column.dataType.trim() !== original.data_type.trim()) {
statements.push(
`ALTER TABLE ${table} ALTER COLUMN ${quoteIdent("postgres", currentName)} TYPE ${column.dataType.trim()};`,
);
}
if (column.isNullable !== original.is_nullable) {
const action = column.isNullable ? "DROP NOT NULL" : "SET NOT NULL";
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${quoteIdent("postgres", currentName)} ${action};`);
}
if (normalizeDefault(column.defaultValue) !== originalDefault(column)) {
const defaultValue = normalizeDefault(column.defaultValue);
const action = defaultValue ? `SET DEFAULT ${defaultValue}` : "DROP DEFAULT";
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${quoteIdent("postgres", currentName)} ${action};`);
}
if (clean(column.comment) !== originalComment(column)) {
const commentValue = clean(column.comment) ? quoteString(clean(column.comment)) : "NULL";
statements.push(`COMMENT ON COLUMN ${table}.${quoteIdent("postgres", currentName)} IS ${commentValue};`);
}
return statements;
}
function buildH2ExistingColumnSql(table: string, column: EditableStructureColumn): string[] {
const original = column.original;
if (!original) return [];
const statements: string[] = [];
let currentName = original.name;
if (column.name !== original.name) {
statements.push(
`ALTER TABLE ${table} ALTER COLUMN ${quoteIdent("h2", original.name)} RENAME TO ${quoteIdent("h2", column.name)};`,
);
currentName = column.name;
}
if (column.dataType.trim() !== original.data_type.trim()) {
statements.push(
`ALTER TABLE ${table} ALTER COLUMN ${quoteIdent("h2", currentName)} SET DATA TYPE ${column.dataType.trim()};`,
);
}
if (column.isNullable !== original.is_nullable) {
const action = column.isNullable ? "DROP NOT NULL" : "SET NOT NULL";
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${quoteIdent("h2", currentName)} ${action};`);
}
if (normalizeDefault(column.defaultValue) !== originalDefault(column)) {
const defaultValue = normalizeDefault(column.defaultValue);
const action = defaultValue ? `SET DEFAULT ${defaultValue}` : "DROP DEFAULT";
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${quoteIdent("h2", currentName)} ${action};`);
}
if (clean(column.comment) !== originalComment(column)) {
const commentValue = clean(column.comment) ? quoteString(clean(column.comment)) : "NULL";
statements.push(`COMMENT ON COLUMN ${table}.${quoteIdent("h2", currentName)} IS ${commentValue};`);
}
return statements;
}
function buildClickHouseExistingColumnSql(
table: string,
column: EditableStructureColumn,
positionClause = "",
): string[] {
const original = column.original;
if (!original) return [];
const statements: string[] = [];
let currentName = original.name;
if (column.name !== original.name) {
statements.push(
`ALTER TABLE ${table} RENAME COLUMN ${quoteIdent("clickhouse", original.name)} TO ${quoteIdent("clickhouse", column.name)};`,
);
currentName = column.name;
}
if (
clickHouseColumnType(column) !== original.data_type.trim() ||
normalizeDefault(column.defaultValue) !== originalDefault(column)
) {
const defaultValue = normalizeDefault(column.defaultValue);
if (defaultValue) {
statements.push(
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)} DEFAULT ${defaultValue}${positionClause};`,
);
} else if (originalDefault(column)) {
statements.push(`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} REMOVE DEFAULT;`);
if (clickHouseColumnType(column) !== original.data_type.trim()) {
statements.push(
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)}${positionClause};`,
);
} else if (positionClause) {
statements.push(
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)}${positionClause};`,
);
}
} else {
statements.push(
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)}${positionClause};`,
);
}
} else if (positionClause) {
statements.push(
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)}${positionClause};`,
);
}
if (clean(column.comment) !== originalComment(column)) {
statements.push(
`ALTER TABLE ${table} COMMENT COLUMN ${quoteIdent("clickhouse", currentName)} ${quoteString(clean(column.comment))};`,
);
}
return statements;
}
function buildSqliteExistingColumnSql(table: string, column: EditableStructureColumn, warnings: string[]): string[] {
const original = column.original;
if (!original) return [];
const statements: string[] = [];
const unsupportedChange =
column.dataType.trim() !== original.data_type.trim() ||
column.isNullable !== original.is_nullable ||
normalizeDefault(column.defaultValue) !== originalDefault(column) ||
clean(column.comment) !== originalComment(column);
if (column.name !== original.name) {
statements.push(
`ALTER TABLE ${table} RENAME COLUMN ${quoteIdent("sqlite", original.name)} TO ${quoteIdent("sqlite", column.name)};`,
);
}
if (unsupportedChange) {
warnings.push(`SQLite cannot safely alter existing column "${original.name}" without rebuilding the table.`);
}
return statements;
}
function columnPositionClause(
dialect: TableStructureDialect,
columns: EditableStructureColumn[],
index: number,
): string {
if (dialect !== "mysql" && dialect !== "clickhouse") return "";
if (index <= 0) return " FIRST";
return ` AFTER ${quoteIdent(dialect, columns[index - 1]?.name ?? "")}`;
}
function originalPreviousColumnName(
columns: EditableStructureColumn[],
column: EditableStructureColumn,
): string | null {
if (!column.original || typeof column.originalPosition !== "number") return null;
const originalColumns = columns
.filter((item) => item.original && typeof item.originalPosition === "number")
.sort((a, b) => (a.originalPosition ?? 0) - (b.originalPosition ?? 0));
const index = originalColumns.findIndex((item) => item.id === column.id);
if (index < 0) return null;
return index === 0 ? null : (originalColumns[index - 1]?.original?.name ?? null);
}
function currentPreviousOriginalColumnName(columns: EditableStructureColumn[], index: number): string | null {
if (index <= 0) return null;
const previous = columns[index - 1];
return previous?.original?.name ?? previous?.name ?? null;
}
function mysqlColumnPositionChanged(columns: EditableStructureColumn[], index: number): boolean {
const column = columns[index];
if (!column?.original || typeof column.originalPosition !== "number") return false;
return currentPreviousOriginalColumnName(columns, index) !== originalPreviousColumnName(columns, column);
}
function buildColumnSql(options: BuildTableStructureChangeSqlOptions, warnings: string[]): string[] {
const databaseType = options.databaseType;
const capabilities = getTableStructureCapabilities(databaseType);
const dialect = capabilities.dialect;
const table = qualifiedTable(dialect, options.schema, options.tableName);
const statements: string[] = [];
const databaseLabel = databaseType ?? "this database";
const activeColumns = options.columns.filter((column) => !column.markedForDrop);
const hasOriginalColumnPositions = activeColumns.some((column) => typeof column.originalPosition === "number");
for (const column of options.columns) {
if (column.markedForDrop) {
if (!column.original) continue;
if (!capabilities.dropColumn) {
warnings.push(`Dropping columns is not supported for ${databaseLabel} from this editor.`);
continue;
}
if (column.original.is_primary_key) {
warnings.push(`Primary key column "${column.original.name}" cannot be dropped from this editor.`);
continue;
}
statements.push(`ALTER TABLE ${table} DROP COLUMN ${quoteIdent(dialect, column.original.name)};`);
continue;
}
const activeIndex = activeColumns.indexOf(column);
const positionClause = hasOriginalColumnPositions ? columnPositionClause(dialect, activeColumns, activeIndex) : "";
const hasPositionChange =
hasOriginalColumnPositions &&
(dialect === "mysql" || dialect === "clickhouse") &&
!!column.original &&
mysqlColumnPositionChanged(activeColumns, activeIndex);
if (!column.original) {
if (!capabilities.addColumn) {
warnings.push(`Adding columns is not supported for ${databaseLabel} from this editor.`);
continue;
}
statements.push(...buildAddColumnSql(dialect, table, column, positionClause));
continue;
}
if (!hasExistingColumnAttributeChange(column) && !hasPositionChange) continue;
const original = column.original;
const hasRename = column.name !== original.name;
const hasAttributeChange =
column.dataType.trim() !== original.data_type.trim() ||
column.isNullable !== original.is_nullable ||
normalizeDefault(column.defaultValue) !== originalDefault(column) ||
clean(column.comment) !== originalComment(column);
if (hasPositionChange && !capabilities.reorderColumn) {
warnings.push(`Reordering columns is not supported for ${databaseLabel} from this editor.`);
}
if (hasRename && !capabilities.renameColumn) {
warnings.push(`Renaming columns is not supported for ${databaseLabel} from this editor.`);
}
if (hasAttributeChange && !capabilities.alterExistingColumn && dialect !== "sqlite") {
warnings.push(`Editing existing columns is not supported for ${databaseLabel} yet.`);
}
if (
(hasPositionChange && !capabilities.reorderColumn) ||
(hasRename && !capabilities.renameColumn) ||
(hasAttributeChange && !capabilities.alterExistingColumn && dialect !== "sqlite")
) {
continue;
}
if (dialect === "mysql") {
statements.push(...buildMysqlExistingColumnSql(table, column, hasPositionChange ? positionClause : ""));
} else if (dialect === "postgres") {
statements.push(...buildPostgresExistingColumnSql(table, column));
} else if (dialect === "oracle") {
statements.push(...buildOracleLikeExistingColumnSql(dialect, table, column));
} else if (dialect === "h2") {
statements.push(...buildH2ExistingColumnSql(table, column));
} else if (dialect === "clickhouse") {
statements.push(...buildClickHouseExistingColumnSql(table, column, hasPositionChange ? positionClause : ""));
} else if (dialect === "sqlite") {
statements.push(...buildSqliteExistingColumnSql(table, column, warnings));
} else {
warnings.push(`Editing existing columns is not supported for ${databaseLabel} yet.`);
}
}
return statements;
}
function buildDropIndexSql(
databaseType: StructureSqlFlavor,
table: string,
schema: string | undefined,
indexName: string,
): string {
if (databaseType === "mysql") return `DROP INDEX ${quoteIdent(databaseType, indexName)} ON ${table};`;
if (databaseType === "sqlserver") return `DROP INDEX ${quoteIdent(databaseType, indexName)} ON ${table};`;
if ((databaseType === "postgres" || isOracleLike(databaseType)) && schema) {
return `DROP INDEX ${quoteIdent(databaseType, schema)}.${quoteIdent(databaseType, indexName)};`;
}
return `DROP INDEX ${quoteIdent(databaseType, indexName)};`;
}
function indexListChanged(next: string[], previous: string[] | null | undefined): boolean {
const nextClean = next.map(clean).filter(Boolean);
const previousClean = (previous ?? []).map(clean).filter(Boolean);
return nextClean.length !== previousClean.length || nextClean.some((value, i) => value !== previousClean[i]);
}
function originalIndexType(index: EditableStructureIndex): string {
return clean(index.original?.index_type).toUpperCase();
}
function normalizedIndexType(index: EditableStructureIndex): string {
return clean(index.indexType).toUpperCase();
}
function hasExistingIndexChange(index: EditableStructureIndex): boolean {
const original = index.original;
if (!original) return false;
return (
clean(index.name) !== clean(original.name) ||
indexListChanged(index.columns, original.columns) ||
index.isUnique !== original.is_unique ||
normalizedIndexType(index) !== originalIndexType(index) ||
indexListChanged(index.includedColumns, original.included_columns) ||
clean(index.filter) !== clean(original.filter) ||
clean(index.comment) !== clean(original.comment)
);
}
function mysqlIndexParts(indexType: string): { prefix: string; usingClause: string } {
const upper = indexType.toUpperCase();
if (upper === "FULLTEXT" || upper === "SPATIAL") return { prefix: `${upper} `, usingClause: "" };
if (upper === "RTREE") return { prefix: "SPATIAL ", usingClause: "" };
if (upper === "BTREE" || upper === "HASH") return { prefix: "", usingClause: ` USING ${upper}` };
return { prefix: "", usingClause: "" };
}
function buildCreateIndexStatements(
databaseType: StructureSqlFlavor,
table: string,
index: EditableStructureIndex,
warnings: string[],
): string[] {
const capabilities = getTableStructureCapabilities(databaseType as DatabaseType | undefined);
const name = clean(index.name);
const columns = index.columns.map(clean).filter(Boolean);
if (!name || columns.length === 0) return [];
const unique = index.isUnique ? "UNIQUE " : "";
const cols = columns.map((column) => quoteIdent(databaseType, column)).join(", ");
const idxType = normalizedIndexType(index);
let typePrefix = "";
let usingClause = "";
if (idxType && capabilities.indexType) {
if (databaseType === "postgres") {
usingClause = ` USING ${idxType}`;
} else if (databaseType === "sqlserver") {
typePrefix = `${idxType} `;
} else if (databaseType === "mysql") {
const mysqlParts = mysqlIndexParts(idxType);
typePrefix = mysqlParts.prefix;
usingClause = mysqlParts.usingClause;
} else if (isOracleLike(databaseType) && idxType === "BITMAP") {
typePrefix = "BITMAP ";
}
}
const incCols = index.includedColumns.map(clean).filter(Boolean);
const includeClause =
incCols.length > 0 && capabilities.indexInclude && (databaseType === "postgres" || databaseType === "sqlserver")
? ` INCLUDE (${incCols.map((c) => quoteIdent(databaseType, c)).join(", ")})`
: "";
const filter = clean(index.filter);
const supportsWhere =
capabilities.indexFilter &&
(databaseType === "postgres" || databaseType === "sqlserver" || databaseType === "sqlite");
const whereClause = filter && supportsWhere ? ` WHERE ${filter}` : "";
const createSql =
databaseType === "postgres"
? `CREATE ${unique}${typePrefix}INDEX ${quoteIdent(databaseType, name)} ON ${table}${usingClause} (${cols})${includeClause}${whereClause};`
: `CREATE ${unique}${typePrefix}INDEX ${quoteIdent(databaseType, name)}${usingClause} ON ${table} (${cols})${includeClause}${whereClause};`;
const statements = [createSql];
const comment = clean(index.comment);
if (comment && capabilities.indexComment && databaseType === "postgres") {
statements.push(`COMMENT ON INDEX ${quoteIdent(databaseType, name)} IS ${quoteString(comment)};`);
} else if (comment && capabilities.indexComment && databaseType !== "postgres") {
warnings.push(`Index comments are not supported for ${databaseType ?? "this database"} from this editor.`);
}
return statements;
}
function buildIndexSql(options: BuildTableStructureChangeSqlOptions, warnings: string[]): string[] {
const databaseType = options.databaseType;
const capabilities = getTableStructureCapabilities(databaseType);
const dialect = capabilities.dialect;
const table = qualifiedTable(dialect, options.schema, options.tableName);
const statements: string[] = [];
const databaseLabel = databaseType ?? "this database";
for (const index of options.indexes) {
if (index.markedForDrop) {
if (!index.original) continue;
if (!capabilities.dropIndex) {
warnings.push(`Dropping indexes is not supported for ${databaseLabel} from this editor.`);
continue;
}
if (index.original.is_primary) {
warnings.push(`Primary index "${index.original.name}" cannot be dropped from this editor.`);
continue;
}
statements.push(buildDropIndexSql(dialect, table, options.schema, index.original.name));
continue;
}
if (index.original) {
if (!hasExistingIndexChange(index)) continue;
if (!capabilities.rebuildIndex || !capabilities.dropIndex || !capabilities.createIndex) {
warnings.push(`Editing existing indexes is not supported for ${databaseLabel} from this editor.`);
continue;
}
if (index.original.is_primary) {
warnings.push(`Primary index "${index.original.name}" cannot be edited from this editor.`);
continue;
}
statements.push(buildDropIndexSql(dialect, table, options.schema, index.original.name));
statements.push(...buildCreateIndexStatements(dialect, table, index, warnings));
continue;
}
if (!capabilities.createIndex) {
warnings.push(`Creating indexes is not supported for ${databaseLabel} from this editor.`);
continue;
}
statements.push(...buildCreateIndexStatements(dialect, table, index, warnings));
}
return statements;
}
function validateDraft(options: BuildTableStructureChangeSqlOptions): string[] {
const warnings: string[] = [];
const activeColumns = options.columns.filter((column) => !column.markedForDrop);
const names = new Set<string>();
for (const column of activeColumns) {
if (!clean(column.name)) warnings.push("Column name cannot be empty.");
if (!clean(column.dataType)) warnings.push(`Column "${column.name || "(new)"}" type cannot be empty.`);
const key = clean(column.name).toLowerCase();
if (key && names.has(key)) warnings.push(`Column "${column.name}" is duplicated.`);
if (key) names.add(key);
}
for (const index of options.indexes.filter(
(idx) => !idx.markedForDrop && (!idx.original || hasExistingIndexChange(idx)),
)) {
if (!clean(index.name)) warnings.push("Index name cannot be empty.");
if (index.columns.map(clean).filter(Boolean).length === 0) {
warnings.push(`Index "${index.name || "(new)"}" needs at least one column.`);
}
}
return warnings;
}
export function buildTableStructureChangeSql(options: BuildTableStructureChangeSqlOptions): TableStructureChangeSql {
const warnings = validateDraft(options);
const statements = [...buildColumnSql(options, warnings), ...buildIndexSql(options, warnings)];
return { statements, warnings };
}
export function buildCreateTableSql(options: BuildTableStructureChangeSqlOptions): TableStructureChangeSql {
const warnings: string[] = [];
if (!clean(options.tableName)) {
warnings.push("Table name is required.");
}
const activeColumns = options.columns.filter((c) => !c.markedForDrop);
if (activeColumns.length === 0) {
warnings.push("At least one column is required.");
}
const names = new Set<string>();
for (const col of activeColumns) {
if (!clean(col.name)) warnings.push("Column name cannot be empty.");
if (!clean(col.dataType)) warnings.push(`Column "${col.name || "(new)"}" type cannot be empty.`);
const key = clean(col.name).toLowerCase();
if (key && names.has(key)) warnings.push(`Column "${col.name}" is duplicated.`);
if (key) names.add(key);
}
if (warnings.length > 0) return { statements: [], warnings };
const databaseType = options.databaseType;
const capabilities = getTableStructureCapabilities(databaseType);
const dialect = capabilities.dialect;
const table = qualifiedTable(dialect, options.schema, options.tableName);
const statements: string[] = [];
const pkColumns = activeColumns.filter((c) => c.isPrimaryKey);
const colDefs = activeColumns.map((col) => {
const dataType = dialect === "clickhouse" ? clickHouseColumnType(col) : col.dataType.trim();
const parts = [quoteIdent(dialect, col.name), dataType];
if (!col.isNullable && !col.isPrimaryKey && dialect !== "clickhouse") parts.push("NOT NULL");
const defaultValue = normalizeDefault(col.defaultValue);
if (defaultValue) parts.push(`DEFAULT ${defaultValue}`);
if (dialect === "mysql" && capabilities.comment && clean(col.comment)) {
parts.push(`COMMENT ${quoteString(clean(col.comment))}`);
}
return parts.join(" ");
});
if (pkColumns.length > 0) {
const pkList = pkColumns.map((c) => quoteIdent(dialect, c.name)).join(", ");
colDefs.push(`PRIMARY KEY (${pkList})`);
}
statements.push(`CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n);`);
if (capabilities.comment && (dialect === "postgres" || dialect === "oracle" || dialect === "h2")) {
for (const col of activeColumns) {
if (clean(col.comment)) {
statements.push(
`COMMENT ON COLUMN ${table}.${quoteIdent(dialect, col.name)} IS ${quoteString(clean(col.comment))};`,
);
}
}
}
if (capabilities.comment && dialect === "clickhouse") {
for (const col of activeColumns) {
if (clean(col.comment)) {
statements.push(
`ALTER TABLE ${table} COMMENT COLUMN ${quoteIdent(dialect, col.name)} ${quoteString(clean(col.comment))};`,
);
}
}
}
for (const index of options.indexes.filter((idx) => !idx.markedForDrop && !idx.isPrimary)) {
if (!capabilities.createIndex) {
warnings.push(`Creating indexes is not supported for ${databaseType ?? "this database"} from this editor.`);
continue;
}
statements.push(...buildCreateIndexStatements(dialect, table, index, warnings));
}
return { statements, warnings };
}
+238 -1
View File
@@ -23,7 +23,15 @@ import type {
} from "@/types/database";
import type { AiConfig } from "@/stores/settingsStore";
import type { QueryEditability } from "@/lib/sqlAnalysis";
import type { DataGridSaveStatementOptions } from "@/lib/dataGridSql";
import type {
DataGridColumnValueFilterConditionOptions,
DataGridContextFilterConditionOptions,
DataGridCountSqlOptions,
DataGridCopyInsertStatementOptions,
DataGridCopyUpdateStatementOptions,
DataGridSaveStatementOptions,
HiveTablePropertiesSqlOptions,
} from "@/lib/dataGridSql";
import type {
DataCompareFromTablesOptions,
DataCompareFromTablesPreparation,
@@ -31,6 +39,21 @@ import type {
DataComparePreparationOptions,
} from "@/lib/dataCompare";
import type { SchemaDiffPreparation, SchemaDiffPreparationOptions, TableDiff } from "@/lib/schemaDiff";
import type { BuildTableStructureChangeSqlOptions, TableStructureChangeSql } from "@/lib/tableStructureEditorSql";
import type { BuildTableSelectSqlOptions } from "@/lib/tableSelectSql";
import type { DatabaseSearchSql, DatabaseSearchSqlOptions, SearchResultWhereOptions } from "@/lib/databaseSearch";
import type { BuildEditableObjectSourceSqlInput, BuildRoutineRenameObjectSourceInput } from "@/lib/objectSourceEditor";
import type { BuildViewDdlInput } from "@/lib/viewDdl";
import type { BuildRenameObjectSqlOptions } from "@/lib/objectRenameSql";
import type { CreateDatabaseSqlOptions } from "@/lib/createDatabaseSql";
import type {
DatabaseNameSqlOptions,
DropObjectSqlOptions,
DuplicateTableStructureSqlOptions,
SchemaNameSqlOptions,
TableAdminSqlOptions,
} from "@/lib/dbAdminSql";
import type { BuildDatabaseSqlExportOptions, BuildExportInsertStatementsOptions } from "@/lib/databaseExport";
export interface AgentDriverInfo {
db_type: string;
@@ -70,6 +93,62 @@ export interface DesktopSettings {
show_tray_icon: boolean;
}
export interface QueryPagination {
limit: number;
offset: number;
sessionId?: string;
}
export interface QueryPaginationExecutionPlanOptions {
sql: string;
queryBaseSql: string;
databaseType?: DatabaseType;
pagination: QueryPagination;
useAgentCursor: boolean;
}
export interface QueryPaginationExecutionPlan {
sqlToExecute: string;
pageSql?: string;
pageLimit?: number;
pageOffset?: number;
countSql?: string;
useAgentResultSession: boolean;
}
export type QuerySortDirection = "asc" | "desc";
export interface SortedQuerySqlOptions {
originalSql: string;
databaseType?: DatabaseType;
resultColumns: string[];
columnIndex: number;
column: string;
direction: QuerySortDirection;
}
export interface QuerySqlBuildResult {
ok: boolean;
sql?: string;
reason?: "empty" | "multi" | "not_select" | "unsupported" | "with";
}
export interface BuildExplainSqlOptions {
databaseType?: DatabaseType;
sql: string;
}
export interface ExplainSqlBuildResult {
ok: boolean;
sql?: string;
reason?: "unsupported" | "empty" | "unsafe";
}
export interface DroppedFilePreviewSqlOptions {
path: string;
limit?: number;
}
export type XlsxCellValue = string | number | boolean | null;
export interface DriverInstallProgress {
@@ -337,6 +416,117 @@ export async function analyzeSqlReferences(sql: string, dialect?: string): Promi
return invoke("analyze_sql_references", { sql, dialect });
}
export async function findStatementAtCursor(sql: string, cursorPos: number): Promise<string> {
return invoke("find_statement_at_cursor", { sql, cursorPos });
}
export async function prepareQueryPaginationExecutionPlan(
options: QueryPaginationExecutionPlanOptions,
): Promise<QueryPaginationExecutionPlan> {
return invoke("prepare_query_pagination_execution_plan", { options });
}
export async function buildSortedQuerySql(options: SortedQuerySqlOptions): Promise<QuerySqlBuildResult> {
return invoke("build_sorted_query_sql", { options });
}
export async function buildExplainSql(options: BuildExplainSqlOptions): Promise<ExplainSqlBuildResult> {
return invoke("build_explain_sql", { options });
}
export async function buildDroppedFilePreviewSql(options: DroppedFilePreviewSqlOptions): Promise<string | undefined> {
const result = await invoke<string | null>("build_dropped_file_preview_sql", { options });
return result ?? undefined;
}
export async function buildTableSelectSql(options: BuildTableSelectSqlOptions): Promise<string> {
return invoke("build_table_select_sql", { options });
}
export async function buildDatabaseSearchSql(options: DatabaseSearchSqlOptions): Promise<DatabaseSearchSql | null> {
return invoke("build_database_search_sql", { options });
}
export async function buildSearchResultWhere(options: SearchResultWhereOptions): Promise<string> {
return invoke("build_search_result_where", { options });
}
export async function buildRenameObjectSql(options: BuildRenameObjectSqlOptions): Promise<string> {
return invoke("build_rename_object_sql", { options });
}
export async function buildCreateDatabaseSql(options: CreateDatabaseSqlOptions): Promise<string> {
return invoke("build_create_database_sql", { options });
}
export async function buildDuckDbAttachDatabaseSql(path: string, name: string): Promise<string> {
return invoke("build_duckdb_attach_database_sql", { options: { path, name } });
}
export async function buildDropObjectSql(options: DropObjectSqlOptions): Promise<string> {
return invoke("build_drop_object_sql", { options });
}
export async function buildDropTableSql(options: TableAdminSqlOptions): Promise<string> {
return invoke("build_drop_table_sql", { options });
}
export async function buildEmptyTableSql(options: TableAdminSqlOptions): Promise<string> {
return invoke("build_empty_table_sql", { options });
}
export async function buildTruncateTableSql(options: TableAdminSqlOptions): Promise<string> {
return invoke("build_truncate_table_sql", { options });
}
export async function buildDropDatabaseSql(options: DatabaseNameSqlOptions): Promise<string> {
return invoke("build_drop_database_sql", { options });
}
export async function buildCreateSchemaSql(options: SchemaNameSqlOptions): Promise<string> {
return invoke("build_create_schema_sql", { options });
}
export async function buildDropSchemaSql(options: SchemaNameSqlOptions): Promise<string> {
return invoke("build_drop_schema_sql", { options });
}
export async function buildDuplicateTableStructureSql(options: DuplicateTableStructureSqlOptions): Promise<string> {
return invoke("build_duplicate_table_structure_sql", { options });
}
export async function buildExecutableObjectSourceStatements(
input: BuildEditableObjectSourceSqlInput,
): Promise<string[]> {
return invoke("build_executable_object_source_statements", { input });
}
export async function buildExecutableObjectSourceSql(input: BuildEditableObjectSourceSqlInput): Promise<string> {
return invoke("build_executable_object_source_sql", { input });
}
export async function buildRoutineRenameObjectSourceStatements(
input: BuildRoutineRenameObjectSourceInput,
): Promise<string[]> {
return invoke("build_routine_rename_object_source_statements", { input });
}
export async function buildViewDdlSql(input: BuildViewDdlInput): Promise<string> {
return invoke("build_view_ddl_sql", { input });
}
export async function buildTableStructureChangeSql(
options: BuildTableStructureChangeSqlOptions,
): Promise<TableStructureChangeSql> {
return invoke("build_table_structure_change_sql", { options });
}
export async function buildCreateTableSql(
options: BuildTableStructureChangeSqlOptions,
): Promise<TableStructureChangeSql> {
return invoke("build_create_table_sql", { options });
}
export async function analyzeEditableQueryEditability(sql: string): Promise<QueryEditability> {
return invoke("analyze_editable_query_editability", { sql });
}
@@ -352,6 +542,53 @@ export async function prepareDataGridSave(options: DataGridSaveStatementOptions)
return invoke("prepare_data_grid_save", { options });
}
export async function buildDataGridCopyUpdateStatements(
options: DataGridCopyUpdateStatementOptions,
): Promise<string[]> {
return invoke("build_data_grid_copy_update_statements", { options });
}
export async function buildDataGridCopyInsertStatement(
options: DataGridCopyInsertStatementOptions,
): Promise<string | undefined> {
const result = await invoke<string | null>("build_data_grid_copy_insert_statement", { options });
return result ?? undefined;
}
export async function buildDataGridContextFilterCondition(
options: DataGridContextFilterConditionOptions,
): Promise<string | undefined> {
const result = await invoke<string | null>("build_data_grid_context_filter_condition", { options });
return result ?? undefined;
}
export async function buildDataGridColumnValueFilterCondition(
options: DataGridColumnValueFilterConditionOptions,
): Promise<string | undefined> {
const result = await invoke<string | null>("build_data_grid_column_value_filter_condition", { options });
return result ?? undefined;
}
export async function buildDataGridCountSql(options: DataGridCountSqlOptions): Promise<string> {
return invoke("build_data_grid_count_sql", { options });
}
export async function buildHiveTablePropertiesSql(options: HiveTablePropertiesSqlOptions): Promise<string> {
return invoke("build_hive_table_properties_sql", { options });
}
export async function buildExportInsertStatements(options: BuildExportInsertStatementsOptions): Promise<string[]> {
return invoke("build_export_insert_statements", { options });
}
export async function buildExportSqlInsert(options: BuildExportInsertStatementsOptions): Promise<string> {
return invoke("build_export_sql_insert", { options });
}
export async function buildDatabaseSqlExport(options: BuildDatabaseSqlExportOptions): Promise<string> {
return invoke("build_database_sql_export", { options });
}
export async function prepareDataCompare(options: DataComparePreparationOptions): Promise<DataComparePreparation> {
return invoke("prepare_data_compare", { options });
}
+4 -41
View File
@@ -1,50 +1,13 @@
import type { DatabaseType } from "@/types/database";
import * as api from "@/lib/api";
type BuildViewDdlInput = {
export type BuildViewDdlInput = {
databaseType?: DatabaseType;
schema?: string | null;
name: string;
source: string;
};
const postgresLikeTypes = new Set<DatabaseType>([
"postgres",
"redshift",
"gaussdb",
"kingbase",
"highgo",
"vastbase",
"opengauss",
]);
function ensureSemicolon(sql: string): string {
const trimmed = sql.trim();
return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
}
function quotePostgresIdentifier(value: string): string {
return `"${value.replaceAll('"', '""')}"`;
}
function quoteMysqlIdentifier(value: string): string {
return `\`${value.replaceAll("`", "``")}\``;
}
function qualifiedName(input: BuildViewDdlInput): string {
const parts = [input.schema, input.name].filter(Boolean) as string[];
if (input.databaseType === "mysql" || input.databaseType === "goldendb") {
return parts.map(quoteMysqlIdentifier).join(".");
}
return parts.map(quotePostgresIdentifier).join(".");
}
export function buildViewDdl(input: BuildViewDdlInput): string {
const source = input.source.trim();
if (/^(?:CREATE|ALTER)\s+/i.test(source)) return ensureSemicolon(source);
if (!input.databaseType || postgresLikeTypes.has(input.databaseType)) {
return `CREATE OR REPLACE VIEW ${qualifiedName(input)} AS\n${ensureSemicolon(source)}`;
}
return `CREATE VIEW ${qualifiedName(input)} AS\n${ensureSemicolon(source)}`;
export function buildViewDdl(input: BuildViewDdlInput): Promise<string> {
return api.buildViewDdlSql(input);
}
+2 -3
View File
@@ -14,7 +14,6 @@ import {
parseMongoCountDocumentsCommand,
parseMongoFindCommand,
} from "@/lib/mongoShellCommand";
import { buildQueryPaginationExecutionPlan } from "@/lib/queryResultPagination";
import { AGENT_DRIVER_TYPES } from "@/lib/databaseCapabilities";
import { editablePrimaryKeys } from "@/lib/tableEditing";
import * as api from "@/lib/api";
@@ -484,7 +483,7 @@ export const useQueryStore = defineStore("query", () => {
await closeResultSession(tab, options?.pagination?.sessionId);
if (tab.mode === "query") {
const pagination = options?.pagination ?? { limit: settingsStore.editorSettings.pageSize, offset: 0 };
const plan = buildQueryPaginationExecutionPlan({
const plan = await api.prepareQueryPaginationExecutionPlan({
sql,
queryBaseSql,
databaseType: conn?.db_type,
@@ -659,7 +658,7 @@ export const useQueryStore = defineStore("query", () => {
const tab = tabs.value.find((t) => t.id === id);
if (!tab) return { ok: false as const, reason: "empty" as const };
const built = buildExplainSql(databaseType, sql);
const built = await buildExplainSql(databaseType, sql);
if (!built.ok) {
tab.explainPlan = undefined;
tab.explainError = built.reason;