feat(editor): include database name in generated SQL
This commit is contained in:
34 files changed
+379
-23
No files matched your search
@@ -1705,6 +1705,7 @@ async function newQuery() {
|
||||
databaseType: effectiveDatabaseTypeForConnection(conn),
|
||||
driverProfile: conn.driver_profile,
|
||||
identifierQuote: connectionStore.connectionIdentifierQuote?.(target.connectionId),
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
});
|
||||
const tabId = queryStore.createTab(conn.id, target.database, undefined, "query", target.schema, initialSql, target.catalog);
|
||||
if (initialSql) {
|
||||
|
||||
@@ -444,6 +444,7 @@ const editDisconnectTabHandlingMode = ref<DisconnectTabHandlingMode>(settingsSto
|
||||
const editDataTabReuseMode = ref<DataTabReuseMode>(settingsStore.editorSettings.dataTabReuseMode);
|
||||
const editOpenDataTabsNextToActive = ref(settingsStore.editorSettings.openDataTabsNextToActive);
|
||||
const editPrefillNewQueryWithSelect = ref(settingsStore.editorSettings.prefillNewQueryWithSelect);
|
||||
const editGenerateSqlIncludeDatabaseName = ref(settingsStore.editorSettings.generateSqlIncludeDatabaseName);
|
||||
const editClickTableNavigationTarget = ref<ClickTableNavigationTarget>(settingsStore.editorSettings.clickTableNavigationTarget);
|
||||
const editUpdateNotificationsEnabled = ref(settingsStore.editorSettings.updateNotificationsEnabled);
|
||||
const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n"));
|
||||
@@ -573,6 +574,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
||||
dataTabReuseMode: editDataTabReuseMode.value,
|
||||
openDataTabsNextToActive: editOpenDataTabsNextToActive.value,
|
||||
prefillNewQueryWithSelect: editPrefillNewQueryWithSelect.value,
|
||||
generateSqlIncludeDatabaseName: editGenerateSqlIncludeDatabaseName.value,
|
||||
updateNotificationsEnabled: editUpdateNotificationsEnabled.value,
|
||||
sidebarObjectInfoMode: editSidebarObjectInfoMode.value,
|
||||
sidebarAllowHorizontalScroll: editSidebarAllowHorizontalScroll.value,
|
||||
@@ -860,6 +862,7 @@ function syncEditorSettingsDraftFromStore() {
|
||||
editDataTabReuseMode.value = settingsStore.editorSettings.dataTabReuseMode;
|
||||
editOpenDataTabsNextToActive.value = settingsStore.editorSettings.openDataTabsNextToActive;
|
||||
editPrefillNewQueryWithSelect.value = settingsStore.editorSettings.prefillNewQueryWithSelect;
|
||||
editGenerateSqlIncludeDatabaseName.value = settingsStore.editorSettings.generateSqlIncludeDatabaseName;
|
||||
editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget;
|
||||
editUpdateNotificationsEnabled.value = settingsStore.editorSettings.updateNotificationsEnabled;
|
||||
editSidebarHiddenTablePrefixes.value = settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n");
|
||||
@@ -1107,6 +1110,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
editDataTabReuseMode.value = DEFAULT_EDITOR_SETTINGS.dataTabReuseMode;
|
||||
editOpenDataTabsNextToActive.value = DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive;
|
||||
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
|
||||
editGenerateSqlIncludeDatabaseName.value = DEFAULT_EDITOR_SETTINGS.generateSqlIncludeDatabaseName;
|
||||
editClickTableNavigationTarget.value = DEFAULT_EDITOR_SETTINGS.clickTableNavigationTarget;
|
||||
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
|
||||
editSidebarObjectInfoMode.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectInfoMode;
|
||||
@@ -1229,6 +1233,7 @@ function resetAllDefaults() {
|
||||
editDataTabReuseMode.value = DEFAULT_EDITOR_SETTINGS.dataTabReuseMode;
|
||||
editOpenDataTabsNextToActive.value = DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive;
|
||||
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
|
||||
editGenerateSqlIncludeDatabaseName.value = DEFAULT_EDITOR_SETTINGS.generateSqlIncludeDatabaseName;
|
||||
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
|
||||
editSidebarObjectInfoMode.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectInfoMode;
|
||||
editSidebarAllowHorizontalScroll.value = DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll;
|
||||
@@ -4094,6 +4099,16 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<Switch id="editor-prefill-new-query" v-model="editPrefillNewQueryWithSelect" class="mt-0.5" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="generate-sql-include-database-name">{{ t("settings.generateSqlIncludeDatabaseName") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.generateSqlIncludeDatabaseNameDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="generate-sql-include-database-name" v-model="editGenerateSqlIncludeDatabaseName" class="mt-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
@@ -83,7 +83,7 @@ import { formatElapsedSeconds } from "@/lib/common/elapsedTime";
|
||||
import { dataGridCellDisplayText, dataGridCellEditorText } from "@/lib/dataGrid/dataGridCellCoercion";
|
||||
import { createColumnDrafts } from "@/lib/table/tableStructureEditorState";
|
||||
import type { BuildSingleColumnAlterSqlOptions } from "@/lib/table/tableStructureEditorSql";
|
||||
import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
|
||||
import { buildTableSelectSql, qualifyTableReferencesInSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { generateCellValues, type CellValueGenerationKind } from "@/lib/dataGrid/cellValueGeneration";
|
||||
import { compactHeaderColumnType, isNumericColumnType, resolveDataGridTypeVisualKind, resolveHeaderColumnType, resolveResultColumnType } from "@/lib/dataGrid/dataGridColumnType";
|
||||
@@ -4931,6 +4931,7 @@ async function fetchLargeValueRequestChunk(columnIndex: number, requests: LargeV
|
||||
tableType: tableMeta.tableType,
|
||||
catalog: tableMeta.catalog,
|
||||
columns: selectedColumns,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
primaryKeys: tableMeta.primaryKeys,
|
||||
whereInput: predicates.map((predicate) => `(${predicate})`).join(" OR "),
|
||||
limit: requests.length,
|
||||
@@ -6214,6 +6215,7 @@ async function applyOrderBySearch() {
|
||||
tableName: tableMeta.tableName,
|
||||
tableType: tableMeta.tableType,
|
||||
columns: tableMeta.columns.map((column) => column.name),
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
primaryKeys: tableMeta.primaryKeys,
|
||||
...tableDataLargeValuePreviewOptions(resolvedDatabaseType.value, tableMeta.columns, tableMeta.primaryKeys, pageSize.value),
|
||||
orderBy: orderByClause,
|
||||
@@ -6251,6 +6253,7 @@ async function applyWhereFilter() {
|
||||
tableName: tableMeta.tableName,
|
||||
tableType: tableMeta.tableType,
|
||||
columns: tableMeta.columns.map((column) => column.name),
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
primaryKeys: tableMeta.primaryKeys,
|
||||
...tableDataLargeValuePreviewOptions(resolvedDatabaseType.value, tableMeta.columns, tableMeta.primaryKeys, pageSize.value),
|
||||
orderBy: orderByInput.value.trim() || (sortCol.value ? `${queryColumnRef(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined),
|
||||
@@ -7331,11 +7334,23 @@ async function cancelActiveExport() {
|
||||
const userFacingSql = ref("");
|
||||
let userFacingSqlGeneration = 0;
|
||||
|
||||
function sqlWithDisplayDatabaseName(sql: string): string {
|
||||
const database = props.tableMeta?.database ?? props.database;
|
||||
if (!settingsStore.editorSettings.generateSqlIncludeDatabaseName || !database) return sql;
|
||||
return qualifyTableReferencesInSql(sql, {
|
||||
databaseType: resolvedDatabaseType.value,
|
||||
database,
|
||||
includeDatabaseName: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function syncUserFacingSql() {
|
||||
const generation = ++userFacingSqlGeneration;
|
||||
const executionSql = props.sql?.trim() ?? "";
|
||||
if (props.context !== "table-data" || !executionSql.includes("__DBX_LARGE_VALUE_BYTES_") || !props.tableMeta?.tableName) {
|
||||
userFacingSql.value = executionSql;
|
||||
const includeDatabaseName = settingsStore.editorSettings.generateSqlIncludeDatabaseName;
|
||||
const shouldRebuildSql = executionSql.includes("__DBX_LARGE_VALUE_BYTES_") || includeDatabaseName;
|
||||
if (props.context !== "table-data" || !shouldRebuildSql || !props.tableMeta?.tableName) {
|
||||
userFacingSql.value = sqlWithDisplayDatabaseName(executionSql);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7350,19 +7365,20 @@ async function syncUserFacingSql() {
|
||||
schema: props.tableMeta.schema,
|
||||
tableName: props.tableMeta.tableName,
|
||||
tableType: props.tableMeta.tableType,
|
||||
includeDatabaseName,
|
||||
whereInput: currentWhereInput(),
|
||||
orderBy: currentOrderBy(),
|
||||
limit: props.pageLimit ?? pageSize.value,
|
||||
offset: props.pageOffset ?? Math.max(0, currentPage.value - 1) * pageSize.value,
|
||||
});
|
||||
if (generation === userFacingSqlGeneration) userFacingSql.value = sql;
|
||||
if (generation === userFacingSqlGeneration) userFacingSql.value = sqlWithDisplayDatabaseName(sql);
|
||||
} catch {
|
||||
if (generation === userFacingSqlGeneration) userFacingSql.value = executionSql;
|
||||
if (generation === userFacingSqlGeneration) userFacingSql.value = sqlWithDisplayDatabaseName(executionSql);
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.sql, props.context, props.tableMeta, props.pageLimit, props.pageOffset, currentWhereInput(), currentOrderBy()],
|
||||
() => [props.sql, props.context, props.tableMeta, props.pageLimit, props.pageOffset, currentWhereInput(), currentOrderBy(), settingsStore.editorSettings.generateSqlIncludeDatabaseName],
|
||||
() => void syncUserFacingSql(),
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -10380,6 +10396,7 @@ async function loadForeignKeyDisplayLabels() {
|
||||
schema,
|
||||
tableName: config.refTable,
|
||||
columns: [config.refColumn, config.displayColumn],
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
whereInput,
|
||||
limit: batch.length,
|
||||
});
|
||||
|
||||
@@ -1313,6 +1313,7 @@ async function openNewQuery(row: ObjectBrowserRow) {
|
||||
database: props.database,
|
||||
schema,
|
||||
tableName: row.name,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
limit: 100,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1656,6 +1656,7 @@ async function newQuery() {
|
||||
catalog: node.catalog,
|
||||
database: node.database,
|
||||
schema: node.schema,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableName: node.label,
|
||||
columns: [],
|
||||
});
|
||||
@@ -1738,6 +1739,7 @@ async function newSelectTemplate() {
|
||||
catalog: context.node.catalog,
|
||||
database: context.node.database,
|
||||
schema: context.tableSchema,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableName: context.node.label,
|
||||
columns: context.columns,
|
||||
});
|
||||
@@ -1758,6 +1760,7 @@ async function newInsertTemplate() {
|
||||
catalog: context.node.catalog,
|
||||
database: context.node.database,
|
||||
schema: context.tableSchema,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableName: context.node.label,
|
||||
columns: context.columns,
|
||||
tableType: context.tableType,
|
||||
@@ -1779,6 +1782,7 @@ async function newUpdateTemplate() {
|
||||
catalog: context.node.catalog,
|
||||
database: context.node.database,
|
||||
schema: context.tableSchema,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableName: context.node.label,
|
||||
columns: context.columns,
|
||||
});
|
||||
@@ -1799,6 +1803,7 @@ async function newDeleteTemplate() {
|
||||
catalog: context.node.catalog,
|
||||
database: context.node.database,
|
||||
schema: context.tableSchema,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableName: context.node.label,
|
||||
columns: context.columns,
|
||||
});
|
||||
|
||||
@@ -80,6 +80,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
|
||||
columns: realColumns?.map((column) => column.name),
|
||||
primaryKeys,
|
||||
...tableDataLargeValuePreviewOptions(effectiveDbType, realColumns ?? [], primaryKeys, limit),
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
includeRowId: useRowId,
|
||||
limit,
|
||||
...options,
|
||||
|
||||
@@ -118,6 +118,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
|
||||
catalog: target.catalog,
|
||||
database: target.database,
|
||||
tableName: target.tableName,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableType: targetTableType,
|
||||
columns: columns.map((column) => column.name),
|
||||
primaryKeys,
|
||||
@@ -162,6 +163,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
|
||||
catalog: target.catalog,
|
||||
database: target.database,
|
||||
tableName: target.tableName,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableType: targetTableType,
|
||||
columns: eagerColumns.map((column) => column.name),
|
||||
primaryKeys: eagerPrimaryKeys,
|
||||
@@ -209,6 +211,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
|
||||
catalog: target.catalog,
|
||||
database: target.database,
|
||||
tableName: target.tableName,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableType: targetTableType,
|
||||
whereInput: target.whereInput,
|
||||
limit: 0,
|
||||
@@ -254,6 +257,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
|
||||
catalog: target.catalog,
|
||||
database: target.database,
|
||||
tableName: target.tableName,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableType: targetTableType,
|
||||
whereInput: target.whereInput,
|
||||
primaryKeys,
|
||||
|
||||
@@ -347,6 +347,7 @@ export function useSidebarDataOpenRuntime() {
|
||||
columns: columns.map((column) => column.name),
|
||||
primaryKeys,
|
||||
...tableDataLargeValuePreviewOptions(effectiveDbType, columns, primaryKeys, limit),
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
limit,
|
||||
includeRowId,
|
||||
});
|
||||
|
||||
@@ -6320,6 +6320,8 @@ export default {
|
||||
savedSqlOpenTargetCurrentDescription: "Use the active tab’s connection, database, schema, and catalog when available; otherwise use the saved target.",
|
||||
prefillNewQueryWithSelect: "Prefill new query with SELECT *",
|
||||
prefillNewQueryWithSelectDescription: "When creating a new query, prefill the editor with SELECT * FROM <table> based on the active table tab or the table selected in the sidebar.",
|
||||
generateSqlIncludeDatabaseName: "Include database name in generated SQL",
|
||||
generateSqlIncludeDatabaseNameDescription: "When supported by the database, generated table SQL uses database.table instead of only the table name.",
|
||||
clickTableNavigationTarget: "Ctrl+Click Table Opens DDL",
|
||||
clickTableNavigationTargetDescription: "When enabled, Ctrl/Cmd+clicking a table name opens the table structure editor (DDL). When disabled, it opens the table data view.",
|
||||
sqlVariableSyntax: "SQL variable & placeholder substitution",
|
||||
|
||||
@@ -6009,6 +6009,8 @@ export default withEnglishFallback({
|
||||
savedSqlOpenTargetCurrentDescription: "Usar la conexión, la base de datos, el esquema y el catálogo de la pestaña activa cuando estén disponibles; de lo contrario, usar el destino guardado.",
|
||||
prefillNewQueryWithSelect: "Rellenar nueva consulta con SELECT *",
|
||||
prefillNewQueryWithSelectDescription: "Al crear una nueva consulta, rellena el editor con SELECT * FROM <tabla> según la pestaña de tabla activa o la tabla seleccionada en la barra lateral.",
|
||||
generateSqlIncludeDatabaseName: "Incluir el nombre de la base de datos en el SQL generado",
|
||||
generateSqlIncludeDatabaseNameDescription: "Cuando la base de datos lo admite, el SQL de tabla generado usa base_de_datos.tabla en lugar de solo el nombre de la tabla.",
|
||||
clickTableNavigationTarget: "Ctrl+Clic abre DDL de tabla",
|
||||
clickTableNavigationTargetDescription: "Cuando está activado, Ctrl/Cmd+clic en el nombre de la tabla abre el editor de estructura (DDL). Cuando está desactivado, abre la vista de datos.",
|
||||
sqlVariableSyntax: "Sustitución de variables y marcadores SQL",
|
||||
|
||||
@@ -6009,6 +6009,8 @@ export default withEnglishFallback({
|
||||
savedSqlOpenTargetCurrentDescription: "Usa la connessione, il database, lo schema e il catalogo della scheda attiva quando disponibili; altrimenti usa la destinazione salvata.",
|
||||
prefillNewQueryWithSelect: "Precompila nuova query con SELECT *",
|
||||
prefillNewQueryWithSelectDescription: "Alla creazione di una nuova query, precompila l'editor con SELECT * FROM <tabella> in base alla scheda tabella attiva o alla tabella selezionata nella barra laterale.",
|
||||
generateSqlIncludeDatabaseName: "Includi il nome del database nell'SQL generato",
|
||||
generateSqlIncludeDatabaseNameDescription: "Quando supportato dal database, l'SQL della tabella generato usa database.tabella anziché solo il nome della tabella.",
|
||||
clickTableNavigationTarget: "Ctrl+Clic apre DDL tabella",
|
||||
clickTableNavigationTargetDescription: "Quando attivato, Ctrl/Cmd+clic sul nome della tabella apre l'editor di struttura (DDL). Quando disattivato, apre la vista dati.",
|
||||
sqlVariableSyntax: "Sostituzione di variabili e segnaposto SQL",
|
||||
|
||||
@@ -6031,6 +6031,8 @@ export default withEnglishFallback({
|
||||
savedSqlOpenTargetCurrentDescription: "利用可能な場合は現在のタブの接続、データベース、スキーマ、カタログを使用し、それ以外は保存時の実行先を使用します。",
|
||||
prefillNewQueryWithSelect: "新規クエリに SELECT を自動入力",
|
||||
prefillNewQueryWithSelectDescription: "新規クエリ作成時、アクティブなテーブルタブまたはサイドバーで選択したテーブルに基づき、エディタに SELECT * FROM <テーブル名> を自動入力します。",
|
||||
generateSqlIncludeDatabaseName: "生成 SQL にデータベース名を含める",
|
||||
generateSqlIncludeDatabaseNameDescription: "データベースが対応している場合、生成するテーブル SQL でテーブル名のみではなく database.table を使用します。",
|
||||
clickTableNavigationTarget: "Ctrl+クリックでテーブル構造を開く",
|
||||
clickTableNavigationTargetDescription: "有効にすると、Ctrl/Cmd+クリックでテーブル構造エディタ(DDL)を開きます。無効の場合はテーブルデータビューを開きます。",
|
||||
sqlVariableSyntax: "SQL 変数・プレースホルダー置換",
|
||||
|
||||
@@ -5737,6 +5737,8 @@ export default withEnglishFallback({
|
||||
savedSqlOpenTargetCurrentDescription: "가능한 경우 현재 탭의 연결, 데이터베이스, 스키마 및 카탈로그를 사용하고, 그렇지 않으면 저장된 대상을 사용합니다.",
|
||||
prefillNewQueryWithSelect: "새 쿼리에 SELECT * 미리 채우기",
|
||||
prefillNewQueryWithSelectDescription: "새 쿼리를 만들 때 활성 테이블 탭이나 사이드바에서 선택한 테이블을 기반으로 편집기에 SELECT * FROM <table>을 미리 채웁니다.",
|
||||
generateSqlIncludeDatabaseName: "생성된 SQL에 데이터베이스 이름 포함",
|
||||
generateSqlIncludeDatabaseNameDescription: "데이터베이스가 지원하는 경우 생성된 테이블 SQL에서 테이블 이름만이 아니라 database.table을 사용합니다.",
|
||||
sqlVariableSyntax: "SQL 변수 및 자리표시자 치환",
|
||||
sqlVariableSyntaxDescription: "데이터베이스 유형별로 SQL을 실행하기 전에 DBX가 치환할 변수와 자리표시자 문법을 선택하세요. 모두 기본적으로 활성화됩니다.",
|
||||
sqlVariableSubstitutionEnabled: "치환 사용",
|
||||
|
||||
@@ -6011,6 +6011,8 @@ export default withEnglishFallback({
|
||||
savedSqlOpenTargetCurrentDescription: "Usar a conexão, o banco de dados, o esquema e o catálogo da aba ativa quando disponíveis; caso contrário, usar o destino salvo.",
|
||||
prefillNewQueryWithSelect: "Preencher nova consulta com SELECT *",
|
||||
prefillNewQueryWithSelectDescription: "Ao criar uma nova consulta, preenche o editor com SELECT * FROM <tabela> com base na aba de tabela ativa ou na tabela selecionada na barra lateral.",
|
||||
generateSqlIncludeDatabaseName: "Incluir nome do banco de dados no SQL gerado",
|
||||
generateSqlIncludeDatabaseNameDescription: "Quando compatível com o banco de dados, o SQL de tabela gerado usa banco.tabela em vez de somente o nome da tabela.",
|
||||
clickTableNavigationTarget: "Ctrl+Clique abre DDL da tabela",
|
||||
clickTableNavigationTargetDescription: "Quando ativado, Ctrl/Cmd+clicar no nome da tabela abre o editor de estrutura (DDL). Quando desativado, abre a visualização de dados.",
|
||||
sqlVariableSyntax: "Substituição de variáveis e espaços reservados SQL",
|
||||
|
||||
@@ -6305,6 +6305,8 @@ export default withEnglishFallback({
|
||||
savedSqlOpenTargetCurrentDescription: "优先使用当前标签页的连接、数据库、Schema 和 Catalog;不可用时回退到保存时的目标。",
|
||||
prefillNewQueryWithSelect: "新建查询时预填充 SELECT 语句",
|
||||
prefillNewQueryWithSelectDescription: "新建查询时,根据当前激活的数据表标签页或侧边栏选中的表,自动在编辑器中填充 SELECT * FROM <表名>。",
|
||||
generateSqlIncludeDatabaseName: "生成 SQL 时包含数据库名",
|
||||
generateSqlIncludeDatabaseNameDescription: "数据库支持时,生成的表 SQL 将使用“数据库名.表名”,而不只包含表名。",
|
||||
clickTableNavigationTarget: "Ctrl+点击表名打开 DDL",
|
||||
clickTableNavigationTargetDescription: "开启后,Ctrl/Cmd+点击表名将打开表结构编辑器(DDL);关闭则打开表数据视图。",
|
||||
sqlVariableSyntax: "SQL 变量与占位符替换",
|
||||
|
||||
@@ -5330,6 +5330,8 @@ export default withEnglishFallback({
|
||||
savedSqlOpenTargetCurrentDescription: "優先使用目前分頁的連線、資料庫、Schema 和 Catalog;無法使用時回復至儲存時的目標。",
|
||||
prefillNewQueryWithSelect: "新建查詢時預填 SELECT 語句",
|
||||
prefillNewQueryWithSelectDescription: "新建查詢時,根據目前啟用的資料表分頁或側邊欄選取的資料表,自動在編輯器中填入 SELECT * FROM <資料表名稱>。",
|
||||
generateSqlIncludeDatabaseName: "產生 SQL 時包含資料庫名稱",
|
||||
generateSqlIncludeDatabaseNameDescription: "資料庫支援時,產生的資料表 SQL 會使用「資料庫名稱.資料表名稱」,而非只包含資料表名稱。",
|
||||
clickTableNavigationTarget: "Ctrl+點擊表名開啟 DDL",
|
||||
clickTableNavigationTargetDescription: "開啟後,Ctrl/Cmd+點擊表名將開啟表結構編輯器(DDL);關閉則開啟表資料檢視。",
|
||||
sqlVariableSyntax: "SQL 變數與佔位符替換",
|
||||
|
||||
@@ -5,7 +5,7 @@ const dataGridSource = readFileSync(new URL("../../../components/grid/DataGrid.v
|
||||
|
||||
describe("DataGrid context menu state", () => {
|
||||
it("clears the context target whenever the result is replaced", () => {
|
||||
expect(dataGridSource).toMatch(/watch\(\s*\(\) => props\.result,[\s\S]*?clearRowSelection\(\);\s*invalidateSyntheticContextSelection\(\);[\s\S]*?exitTransaction\(\);/);
|
||||
expect(dataGridSource).toMatch(/watch\(\s*\(\) => props\.result,[\s\S]*?clearRowSelection\(\);\s*invalidateContextMenuTarget\(\);[\s\S]*?exitTransaction\(\);/);
|
||||
});
|
||||
|
||||
it("uses the actual copyable row count in the copy menu", () => {
|
||||
|
||||
@@ -200,6 +200,10 @@ describe("buildSelectAllSql", () => {
|
||||
expect(buildSelectAllSql("mysql", { schema: "mydb", tableName: "users" })).toBe("SELECT * FROM `users`");
|
||||
});
|
||||
|
||||
it("includes the MySQL database when requested", () => {
|
||||
expect(buildSelectAllSql("mysql", { database: "mydb", tableName: "users" }, undefined, undefined, true)).toBe("SELECT * FROM `mydb`.`users`");
|
||||
});
|
||||
|
||||
it("qualifies and quotes a PostgreSQL table with its schema", () => {
|
||||
expect(buildSelectAllSql("postgres", { schema: "public", tableName: "users" })).toBe('SELECT * FROM "public"."users"');
|
||||
});
|
||||
|
||||
+86
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { qualifiedTableName, quoteTableDataIdentifier, quoteTableIdentifier } from "@/lib/table/tableSelectSql";
|
||||
import { qualifiedTableName, qualifyTableReferencesInSql, quoteTableDataIdentifier, quoteTableIdentifier } from "@/lib/table/tableSelectSql";
|
||||
|
||||
describe("qualifiedTableName — Doris/StarRocks multi-catalog", () => {
|
||||
it("prefixes external catalog for Doris (no schema)", () => {
|
||||
@@ -28,6 +28,91 @@ describe("qualifiedTableName — Doris/StarRocks multi-catalog", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("qualifiedTableName — optional database qualification", () => {
|
||||
it("keeps MySQL table references unchanged unless enabled", () => {
|
||||
expect(qualifiedTableName({ databaseType: "mysql", database: "analytics", tableName: "events" })).toBe("`events`");
|
||||
expect(qualifiedTableName({ databaseType: "mysql", database: "analytics", tableName: "events", includeDatabaseName: true })).toBe("`analytics`.`events`");
|
||||
});
|
||||
|
||||
it("uses the dialect's identifier quoting for database-qualified names", () => {
|
||||
expect(qualifiedTableName({ databaseType: "clickhouse", database: "a`b", tableName: "c`d", includeDatabaseName: true })).toBe("`a``b`.`c``d`");
|
||||
});
|
||||
|
||||
it("uses MySQL-compatible quoting for GoldenDB", () => {
|
||||
expect(qualifiedTableName({ databaseType: "goldendb", database: "a`b", tableName: "c`d", includeDatabaseName: true })).toBe("`a``b`.`c``d`");
|
||||
});
|
||||
});
|
||||
|
||||
describe("qualifyTableReferencesInSql", () => {
|
||||
it("qualifies FROM and JOIN sources while preserving aliases", () => {
|
||||
expect(
|
||||
qualifyTableReferencesInSql("SELECT * FROM apis AS ap JOIN users AS u ON u.id = ap.user_id", {
|
||||
databaseType: "mysql",
|
||||
database: "aaa",
|
||||
includeDatabaseName: true,
|
||||
}),
|
||||
).toBe("SELECT * FROM `aaa`.`apis` AS ap JOIN `aaa`.`users` AS u ON u.id = ap.user_id");
|
||||
});
|
||||
|
||||
it("qualifies every source in a multi-line LEFT JOIN query", () => {
|
||||
const sql = "SELECT `u`.`id`, `g`.`group_name`\nFROM `users` AS `u`\nLEFT JOIN `group_users` AS `gu` ON `gu`.`user_id` = `u`.`id`\nLEFT JOIN `groups` AS `g` ON `g`.`id` = `gu`.`group_id`\nLEFT JOIN `user_roles` AS `ur` ON `ur`.`user_id` = `u`.`id`";
|
||||
const result = qualifyTableReferencesInSql(sql, { databaseType: "mysql", database: "aaa", includeDatabaseName: true });
|
||||
expect(result).toContain("FROM `aaa`.`users` AS `u`");
|
||||
expect(result).toContain("LEFT JOIN `aaa`.`group_users` AS `gu`");
|
||||
expect(result).toContain("LEFT JOIN `aaa`.`groups` AS `g`");
|
||||
expect(result).toContain("LEFT JOIN `aaa`.`user_roles` AS `ur`");
|
||||
});
|
||||
|
||||
it("leaves already-qualified sources unchanged", () => {
|
||||
const options = { databaseType: "mysql" as const, database: "aaa", includeDatabaseName: true };
|
||||
expect(qualifyTableReferencesInSql("SELECT * FROM `aaa`.`apis`", options)).toBe("SELECT * FROM `aaa`.`apis`");
|
||||
});
|
||||
|
||||
it("qualifies physical tables inside CTEs but never CTE references", () => {
|
||||
const sql = "WITH users AS (SELECT * FROM archived_users)\nSELECT * FROM users";
|
||||
expect(qualifyTableReferencesInSql(sql, { databaseType: "mysql", database: "aaa", includeDatabaseName: true })).toBe("WITH users AS (SELECT * FROM `aaa`.`archived_users`)\nSELECT * FROM users");
|
||||
});
|
||||
|
||||
it("preserves CTE references when the statement has a trailing semicolon", () => {
|
||||
const sql = "WITH users AS (SELECT * FROM archived_users) SELECT * FROM users;";
|
||||
expect(qualifyTableReferencesInSql(sql, { databaseType: "mysql", database: "aaa", includeDatabaseName: true })).toBe("WITH users AS (SELECT * FROM `aaa`.`archived_users`) SELECT * FROM users;");
|
||||
});
|
||||
|
||||
it("keeps CTE visibility scoped to each statement", () => {
|
||||
const sql = "WITH users AS (SELECT * FROM archived_users) SELECT * FROM users; WITH roles AS (SELECT * FROM archived_roles) SELECT * FROM roles;";
|
||||
expect(qualifyTableReferencesInSql(sql, { databaseType: "mysql", database: "aaa", includeDatabaseName: true })).toBe("WITH users AS (SELECT * FROM `aaa`.`archived_users`) SELECT * FROM users; WITH roles AS (SELECT * FROM `aaa`.`archived_roles`) SELECT * FROM roles;");
|
||||
});
|
||||
|
||||
it("does not treat a later CTE name as visible inside an earlier CTE", () => {
|
||||
const sql = "WITH first AS (SELECT * FROM second), second AS (SELECT * FROM archived_users) SELECT * FROM first";
|
||||
expect(qualifyTableReferencesInSql(sql, { databaseType: "mysql", database: "aaa", includeDatabaseName: true })).toBe("WITH first AS (SELECT * FROM `aaa`.`second`), second AS (SELECT * FROM `aaa`.`archived_users`) SELECT * FROM first");
|
||||
});
|
||||
|
||||
it("does not leak a nested CTE name into the outer query", () => {
|
||||
const sql = "SELECT * FROM (WITH users AS (SELECT * FROM archived_users) SELECT * FROM users) nested JOIN users ON 1 = 1";
|
||||
expect(qualifyTableReferencesInSql(sql, { databaseType: "mysql", database: "aaa", includeDatabaseName: true })).toBe("SELECT * FROM (WITH users AS (SELECT * FROM `aaa`.`archived_users`) SELECT * FROM users) nested JOIN `aaa`.`users` ON 1 = 1");
|
||||
});
|
||||
|
||||
it("keeps outer CTEs visible inside nested WITH queries", () => {
|
||||
const sql = "WITH users AS (SELECT * FROM archived_users) SELECT * FROM (WITH roles AS (SELECT * FROM users) SELECT * FROM roles) nested";
|
||||
expect(qualifyTableReferencesInSql(sql, { databaseType: "mysql", database: "aaa", includeDatabaseName: true })).toBe("WITH users AS (SELECT * FROM `aaa`.`archived_users`) SELECT * FROM (WITH roles AS (SELECT * FROM users) SELECT * FROM roles) nested");
|
||||
});
|
||||
|
||||
it("does not let a nested CTE suppress an earlier outer physical table", () => {
|
||||
const sql = "SELECT * FROM users WHERE EXISTS (WITH users AS (SELECT * FROM archived_users) SELECT * FROM users)";
|
||||
expect(qualifyTableReferencesInSql(sql, { databaseType: "mysql", database: "aaa", includeDatabaseName: true })).toBe("SELECT * FROM `aaa`.`users` WHERE EXISTS (WITH users AS (SELECT * FROM `aaa`.`archived_users`) SELECT * FROM users)");
|
||||
});
|
||||
|
||||
it("does not rewrite FROM and JOIN text inside literals or comments", () => {
|
||||
const sql = "SELECT 'FROM users JOIN groups' AS note\nFROM users -- JOIN groups";
|
||||
expect(qualifyTableReferencesInSql(sql, { databaseType: "mysql", database: "aaa", includeDatabaseName: true })).toBe("SELECT 'FROM users JOIN groups' AS note\nFROM `aaa`.`users` -- JOIN groups");
|
||||
});
|
||||
|
||||
it("uses GoldenDB's MySQL-compatible identifier quoting", () => {
|
||||
expect(qualifyTableReferencesInSql("SELECT * FROM users", { databaseType: "goldendb", database: "aaa", includeDatabaseName: true })).toBe("SELECT * FROM `aaa`.`users`");
|
||||
});
|
||||
});
|
||||
|
||||
describe("qualifiedTableName — SQLite attached databases", () => {
|
||||
it("qualifies tables with the attached database alias", () => {
|
||||
expect(qualifiedTableName({ databaseType: "sqlite", schema: "analytics", tableName: "events" })).toBe('"analytics"."events"');
|
||||
|
||||
@@ -1291,7 +1291,7 @@ export async function buildDroppedFilePreviewSql(options: DroppedFilePreviewSqlO
|
||||
}
|
||||
|
||||
export async function buildTableSelectSql(options: BuildTableSelectSqlOptions): Promise<string> {
|
||||
return post("/api/query/build-table-select-sql", { options });
|
||||
return post("/api/query/build-table-select-sql", { options, includeDatabaseName: options.includeDatabaseName === true });
|
||||
}
|
||||
|
||||
export async function buildDatabaseSearchSql(options: DatabaseSearchSqlOptions): Promise<DatabaseSearchSql | null> {
|
||||
|
||||
@@ -1452,7 +1452,7 @@ export async function buildDroppedFilePreviewSql(options: DroppedFilePreviewSqlO
|
||||
}
|
||||
|
||||
export async function buildTableSelectSql(options: BuildTableSelectSqlOptions): Promise<string> {
|
||||
return invoke("build_table_select_sql", { options });
|
||||
return invoke("build_table_select_sql", { options, includeDatabaseName: options.includeDatabaseName === true });
|
||||
}
|
||||
|
||||
export async function buildDatabaseSearchSql(options: DatabaseSearchSqlOptions): Promise<DatabaseSearchSql | null> {
|
||||
|
||||
@@ -186,6 +186,7 @@ describe("settings search", () => {
|
||||
{ titleKey: "settings.completionTriggerMode", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.autoAliasTables", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.clickTableNavigationTarget", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.generateSqlIncludeDatabaseName", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.sqlFormatterKeywordCase", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterFunctionCase", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterDataTypeCase", category: "formatter", targetId: "formatter" },
|
||||
|
||||
@@ -65,6 +65,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"dataTabReuseMode",
|
||||
"openDataTabsNextToActive",
|
||||
"prefillNewQueryWithSelect",
|
||||
"generateSqlIncludeDatabaseName",
|
||||
"updateNotificationsEnabled",
|
||||
"sidebarObjectInfoMode",
|
||||
"sidebarAllowHorizontalScroll",
|
||||
|
||||
@@ -120,6 +120,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
|
||||
{ id: "editor-unsaved-close", category: "editor", titleKey: "settings.confirmUnsavedSqlClose", descriptionKey: "settings.confirmUnsavedSqlCloseDescription", targetId: "editor" },
|
||||
{ id: "editor-app-close-unsaved-tabs", category: "editor", titleKey: "settings.appCloseUnsavedTabsMode", descriptionKey: "settings.appCloseUnsavedTabsModeDescription", targetId: "editor" },
|
||||
{ id: "editor-prefill-query", category: "editor", titleKey: "settings.prefillNewQueryWithSelect", descriptionKey: "settings.prefillNewQueryWithSelectDescription", targetId: "editor" },
|
||||
{ id: "editor-generate-sql-include-database", category: "editor", titleKey: "settings.generateSqlIncludeDatabaseName", descriptionKey: "settings.generateSqlIncludeDatabaseNameDescription", targetId: "editor" },
|
||||
{ id: "editor-diagnostics", category: "editor", titleKey: "settings.sqlSemanticDiagnosticsEnabled", descriptionKey: "settings.sqlSemanticDiagnosticsEnabledDescription", targetId: "editor" },
|
||||
{ id: "editor-sql-variables", category: "editor", titleKey: "settings.sqlVariableSyntax", descriptionKey: "settings.sqlVariableSyntaxDescription", targetId: "editor" },
|
||||
{ id: "editor-saved-sql-target", category: "editor", titleKey: "settings.savedSqlOpenTarget", targetId: "editor" },
|
||||
|
||||
@@ -92,6 +92,7 @@ export interface ResolveNewQueryInitialSqlInput extends ResolveNewQueryTableInpu
|
||||
databaseType?: DatabaseType;
|
||||
driverProfile?: string;
|
||||
identifierQuote?: string;
|
||||
includeDatabaseName?: boolean;
|
||||
}
|
||||
|
||||
// Database types whose "table" view does not use standard SQL `SELECT * FROM <table>`
|
||||
@@ -147,9 +148,9 @@ export function resolveNewQueryTable(input: ResolveNewQueryTableInput): NewQuery
|
||||
* the same per-dialect identifier quoting and schema/catalog qualification used
|
||||
* by the table-data view.
|
||||
*/
|
||||
export function buildSelectAllSql(databaseType: DatabaseType | undefined, table: Pick<NewQueryTable, "schema" | "catalog" | "tableName"> & Partial<Pick<NewQueryTable, "database">>, identifierQuote?: string, driverProfile?: string): string {
|
||||
export function buildSelectAllSql(databaseType: DatabaseType | undefined, table: Pick<NewQueryTable, "schema" | "catalog" | "tableName"> & Partial<Pick<NewQueryTable, "database">>, identifierQuote?: string, driverProfile?: string, includeDatabaseName = false): string {
|
||||
if (databaseType === "victoriametrics") return metricRangeQuery(table.tableName);
|
||||
const ref = qualifiedTableName({ databaseType, driverProfile, identifierQuote, database: table.database, schema: table.schema, catalog: table.catalog, tableName: table.tableName });
|
||||
const ref = qualifiedTableName({ databaseType, driverProfile, identifierQuote, database: table.database, schema: table.schema, catalog: table.catalog, tableName: table.tableName, includeDatabaseName });
|
||||
return `SELECT * FROM ${ref}`;
|
||||
}
|
||||
|
||||
@@ -164,5 +165,5 @@ export function resolveNewQueryInitialSql(input: ResolveNewQueryInitialSqlInput)
|
||||
const table = resolveNewQueryTable(input);
|
||||
if (!table || table.connectionId !== input.targetConnectionId || table.database !== input.targetDatabase) return undefined;
|
||||
|
||||
return buildSelectAllSql(input.databaseType, table, input.identifierQuote, input.driverProfile);
|
||||
return buildSelectAllSql(input.databaseType, table, input.identifierQuote, input.driverProfile, input.includeDatabaseName);
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import { jdbcDriverProfileUsesSchemaQualification } from "@/lib/database/jdbcDia
|
||||
import * as api from "@/lib/backend/api.ts";
|
||||
import { parseSqlServerLinkedSchema, sqlServerLinkedTableName } from "@/lib/database/sqlServerLinkedServers.ts";
|
||||
import { isExplicitlyQuotedSqlIdentifier, quoteGaussDbJdbcIdentifier } from "@/lib/sql/sqlIdentifier.ts";
|
||||
import { sqlSemanticDialectFor } from "@/lib/sql/semantic/dialect";
|
||||
import { sqlSemanticTableNameSpans } from "@/lib/sql/semantic/model";
|
||||
import { tokenIsIdentifier, tokenizeSqlSemantic, unquoteSqlSemanticIdentifier } from "@/lib/sql/semantic/tokens";
|
||||
import type { SqlSemanticToken } from "@/lib/sql/semantic/types";
|
||||
|
||||
export interface BuildTableSelectSqlOptions {
|
||||
databaseType?: DatabaseType;
|
||||
@@ -25,6 +29,30 @@ export interface BuildTableSelectSqlOptions {
|
||||
includeRowId?: boolean;
|
||||
catalog?: string;
|
||||
database?: string;
|
||||
/** Include the active database when this dialect supports `database.table` references. */
|
||||
includeDatabaseName?: boolean;
|
||||
}
|
||||
|
||||
const DATABASE_QUALIFIED_TABLE_TYPES = new Set<DatabaseType>(["mysql", "clickhouse", "doris", "starrocks", "goldendb"]);
|
||||
|
||||
function sqlStatementSpans(sql: string, dialectId: string): Array<{ start: number; end: number }> {
|
||||
const spans: Array<{ start: number; end: number }> = [];
|
||||
let start = 0;
|
||||
const appendSpan = (end: number) => {
|
||||
let trimmedStart = start;
|
||||
let trimmedEnd = end;
|
||||
while (trimmedStart < trimmedEnd && /\s/.test(sql[trimmedStart] ?? "")) trimmedStart += 1;
|
||||
while (trimmedEnd > trimmedStart && /\s/.test(sql[trimmedEnd - 1] ?? "")) trimmedEnd -= 1;
|
||||
if (trimmedStart < trimmedEnd) spans.push({ start: trimmedStart, end: trimmedEnd });
|
||||
};
|
||||
|
||||
for (const token of tokenizeSqlSemantic(sql, dialectId)) {
|
||||
if (token.kind !== "punctuation" || token.text !== ";" || token.depth !== 0) continue;
|
||||
appendSpan(token.span.start);
|
||||
start = token.span.end;
|
||||
}
|
||||
appendSpan(sql.length);
|
||||
return spans;
|
||||
}
|
||||
|
||||
export function quoteTableIdentifier(databaseType: DatabaseType | undefined, name: string): string {
|
||||
@@ -49,7 +77,8 @@ export function quoteTableIdentifier(databaseType: DatabaseType | undefined, nam
|
||||
databaseType === "tdengine" ||
|
||||
databaseType === "access" ||
|
||||
databaseType === "doris" ||
|
||||
databaseType === "starrocks"
|
||||
databaseType === "starrocks" ||
|
||||
databaseType === "goldendb"
|
||||
)
|
||||
return `\`${name.replace(/`/g, "``")}\``;
|
||||
if (databaseType === "informix" && /^[A-Za-z_][A-Za-z0-9_$]*$/.test(name)) return name;
|
||||
@@ -75,8 +104,8 @@ function quoteCypherIdentifier(name: string): string {
|
||||
return `\`${name.replace(/`/g, "``")}\``;
|
||||
}
|
||||
|
||||
export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "databaseType" | "driverProfile" | "identifierQuote" | "schema" | "tableName" | "catalog" | "database">): string {
|
||||
const { databaseType, driverProfile, identifierQuote, schema, tableName, catalog, database } = options;
|
||||
export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "databaseType" | "driverProfile" | "identifierQuote" | "schema" | "tableName" | "catalog" | "database" | "includeDatabaseName">): string {
|
||||
const { databaseType, driverProfile, identifierQuote, schema, tableName, catalog, database, includeDatabaseName } = options;
|
||||
if (databaseType === "informix" && driverProfile?.trim().toLowerCase() === "gbase8s") {
|
||||
return quoteTableDataIdentifier(databaseType, tableName, identifierQuote);
|
||||
}
|
||||
@@ -143,9 +172,111 @@ export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "da
|
||||
}
|
||||
return `${quoteTableIdentifier(databaseType, schema)}.${quoteTableIdentifier(databaseType, tableName)}`;
|
||||
}
|
||||
// MySQL-style engines use the selected database as their table namespace.
|
||||
// Keep this opt-in so existing generated SQL remains unchanged by default.
|
||||
const trimmedDatabase = database?.trim();
|
||||
if (includeDatabaseName && trimmedDatabase && databaseType && DATABASE_QUALIFIED_TABLE_TYPES.has(databaseType)) {
|
||||
return `${quoteTableIdentifier(databaseType, trimmedDatabase)}.${quoteTableIdentifier(databaseType, tableName)}`;
|
||||
}
|
||||
return quoteTableIdentifier(databaseType, tableName);
|
||||
}
|
||||
|
||||
interface SqlCteVisibility {
|
||||
name: string;
|
||||
visibleFrom: number;
|
||||
visibleUntil: number;
|
||||
}
|
||||
|
||||
function matchingSqlParenthesisToken(tokens: readonly SqlSemanticToken[], openIndex: number): number {
|
||||
const open = tokens[openIndex];
|
||||
if (open?.text !== "(") return -1;
|
||||
for (let index = openIndex + 1; index < tokens.length; index += 1) {
|
||||
if (tokens[index]?.text === ")" && tokens[index]?.depth === open.depth) return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function sqlCteVisibilities(tokens: readonly SqlSemanticToken[], sqlLength: number): SqlCteVisibility[] {
|
||||
const visibilities: SqlCteVisibility[] = [];
|
||||
for (let withIndex = 0; withIndex < tokens.length; withIndex += 1) {
|
||||
const withToken = tokens[withIndex];
|
||||
if (withToken?.kind !== "word" || withToken.normalized !== "with") continue;
|
||||
const depth = withToken.depth;
|
||||
const scopeEnd = tokens.find((token, index) => index > withIndex && token.depth < depth)?.span.start ?? sqlLength;
|
||||
let index = withIndex + 1;
|
||||
if (tokens[index]?.depth === depth && tokens[index]?.normalized === "recursive") index += 1;
|
||||
|
||||
while (index < tokens.length) {
|
||||
while (tokens[index]?.depth === depth && tokens[index]?.text === ",") index += 1;
|
||||
const nameToken = tokens[index];
|
||||
if (!nameToken || nameToken.depth !== depth || !tokenIsIdentifier(nameToken)) break;
|
||||
index += 1;
|
||||
|
||||
if (tokens[index]?.depth === depth && tokens[index]?.text === "(") {
|
||||
const columnsClose = matchingSqlParenthesisToken(tokens, index);
|
||||
if (columnsClose < 0) break;
|
||||
index = columnsClose + 1;
|
||||
}
|
||||
if (tokens[index]?.depth === depth && tokens[index]?.normalized === "as") index += 1;
|
||||
if (tokens[index]?.depth !== depth || tokens[index]?.text !== "(") break;
|
||||
const bodyOpen = index;
|
||||
const bodyClose = matchingSqlParenthesisToken(tokens, bodyOpen);
|
||||
if (bodyClose < 0) break;
|
||||
visibilities.push({
|
||||
name: unquoteSqlSemanticIdentifier(nameToken),
|
||||
visibleFrom: tokens[bodyOpen]!.span.end,
|
||||
visibleUntil: scopeEnd,
|
||||
});
|
||||
index = bodyClose + 1;
|
||||
if (tokens[index]?.depth !== depth || tokens[index]?.text !== ",") break;
|
||||
}
|
||||
}
|
||||
return visibilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Qualifies physical table sources shown in a result footer without changing
|
||||
* the SQL that was actually executed. The semantic model deliberately skips
|
||||
* CTE names, strings, and comments that can happen to contain FROM/JOIN text.
|
||||
*/
|
||||
export function qualifyTableReferencesInSql(sql: string, options: Pick<BuildTableSelectSqlOptions, "databaseType" | "database" | "includeDatabaseName">): string {
|
||||
if (!options.includeDatabaseName || !options.databaseType || !DATABASE_QUALIFIED_TABLE_TYPES.has(options.databaseType) || !options.database?.trim()) return sql;
|
||||
const database = quoteTableIdentifier(options.databaseType, options.database.trim());
|
||||
// Build replacements from right to left so that every semantic span still
|
||||
// points at the original source text. Only one-part physical table names
|
||||
// need the active database prefix; CTEs and already-qualified tables do not.
|
||||
const semanticOptions = {
|
||||
databaseType: options.databaseType,
|
||||
dialect: options.databaseType === "goldendb" ? "mysql" : undefined,
|
||||
} as const;
|
||||
const dialectId = sqlSemanticDialectFor(semanticOptions).id;
|
||||
const replacements = sqlStatementSpans(sql, dialectId)
|
||||
.flatMap(({ start, end }) => {
|
||||
const statementSql = sql.slice(start, end);
|
||||
const tokens = tokenizeSqlSemantic(statementSql, dialectId);
|
||||
const cteVisibilities = sqlCteVisibilities(tokens, statementSql.length);
|
||||
const isCteReference = (name: string, span: { start: number; end: number }): boolean => cteVisibilities.some((cte) => cte.name.toLowerCase() === name.toLowerCase() && span.start >= cte.visibleFrom && span.end <= cte.visibleUntil);
|
||||
const tokensBySpan = new Map(tokens.map((token) => [`${token.span.start}:${token.span.end}`, token]));
|
||||
|
||||
return sqlSemanticTableNameSpans(statementSql, semanticOptions)
|
||||
.map((span) => ({ span, token: tokensBySpan.get(`${span.start}:${span.end}`) }))
|
||||
.filter(({ span, token }) => {
|
||||
if (!token || isCteReference(unquoteSqlSemanticIdentifier(token), span)) return false;
|
||||
// sqlSemanticTableNameSpans returns the final segment in a qualified
|
||||
// name, so a preceding dot identifies a database-qualified source.
|
||||
return !statementSql.slice(0, span.start).trimEnd().endsWith(".");
|
||||
})
|
||||
.map(({ span, token }) => ({
|
||||
span: { start: start + span.start, end: start + span.end },
|
||||
tableName: unquoteSqlSemanticIdentifier(token!),
|
||||
}));
|
||||
})
|
||||
.filter(({ span }, index, all) => all.findIndex((candidate) => candidate.span.start === span.start && candidate.span.end === span.end) === index)
|
||||
.sort((left, right) => right.span.start - left.span.start);
|
||||
|
||||
return replacements.reduce((qualifiedSql, { span, tableName }) => `${qualifiedSql.slice(0, span.start)}${database}.${quoteTableIdentifier(options.databaseType, tableName)}${qualifiedSql.slice(span.end)}`, sql);
|
||||
}
|
||||
|
||||
export function metricSelector(metricName: string): string {
|
||||
const escaped = metricName.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\n", "\\n");
|
||||
return `{__name__="${escaped}"}`;
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface TableSqlTemplateOptions {
|
||||
schema?: string;
|
||||
catalog?: string;
|
||||
database?: string;
|
||||
includeDatabaseName?: boolean;
|
||||
tableName: string;
|
||||
columns?: ColumnInfo[];
|
||||
tableType?: string;
|
||||
@@ -105,6 +106,7 @@ function templateTableName(options: TableSqlTemplateOptions): string {
|
||||
identifierQuote: options.identifierQuote,
|
||||
catalog: options.catalog,
|
||||
database: options.database,
|
||||
includeDatabaseName: options.includeDatabaseName,
|
||||
schema: options.schema,
|
||||
tableName: options.tableName,
|
||||
});
|
||||
|
||||
@@ -2898,6 +2898,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
columns: tableMeta.columns.map((column) => column.name),
|
||||
primaryKeys,
|
||||
...tableDataLargeValuePreviewOptions(effectiveDbType, tableMeta.columns, primaryKeys, limit),
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
includeRowId: usesSyntheticRowIdKey(effectiveDbType, primaryKeys, tableMeta.tableType),
|
||||
whereInput: tab.whereInput,
|
||||
orderBy,
|
||||
|
||||
@@ -601,6 +601,7 @@ export interface EditorSettings {
|
||||
dataTabReuseMode: DataTabReuseMode;
|
||||
openDataTabsNextToActive: boolean;
|
||||
prefillNewQueryWithSelect: boolean;
|
||||
generateSqlIncludeDatabaseName: boolean;
|
||||
updateNotificationsEnabled: boolean;
|
||||
sidebarHiddenTablePrefixes: string[];
|
||||
sidebarObjectInfoMode: SidebarObjectInfoMode;
|
||||
@@ -807,6 +808,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
dataTabReuseMode: DEFAULT_DATA_TAB_REUSE_MODE,
|
||||
openDataTabsNextToActive: false,
|
||||
prefillNewQueryWithSelect: true,
|
||||
generateSqlIncludeDatabaseName: false,
|
||||
updateNotificationsEnabled: true,
|
||||
sidebarHiddenTablePrefixes: [],
|
||||
sidebarObjectInfoMode: "comment-inline",
|
||||
@@ -1199,6 +1201,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
),
|
||||
openDataTabsNextToActive: typeof settings.openDataTabsNextToActive === "boolean" ? settings.openDataTabsNextToActive : DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive,
|
||||
prefillNewQueryWithSelect: typeof settings.prefillNewQueryWithSelect === "boolean" ? settings.prefillNewQueryWithSelect : DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect,
|
||||
generateSqlIncludeDatabaseName: settings.generateSqlIncludeDatabaseName === true,
|
||||
updateNotificationsEnabled: settings.updateNotificationsEnabled ?? DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled,
|
||||
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
|
||||
sidebarObjectInfoMode: normalizeSidebarObjectInfoMode(
|
||||
@@ -1788,6 +1791,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.dataTabReuseMode !== undefined) editorSettings.value.dataTabReuseMode = normalizeDataTabReuseMode(partial.dataTabReuseMode);
|
||||
if (partial.openDataTabsNextToActive !== undefined) editorSettings.value.openDataTabsNextToActive = partial.openDataTabsNextToActive === true;
|
||||
if (partial.prefillNewQueryWithSelect !== undefined) editorSettings.value.prefillNewQueryWithSelect = partial.prefillNewQueryWithSelect;
|
||||
if (partial.generateSqlIncludeDatabaseName !== undefined) editorSettings.value.generateSqlIncludeDatabaseName = partial.generateSqlIncludeDatabaseName === true;
|
||||
if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled;
|
||||
if (partial.sidebarHiddenTablePrefixes !== undefined) editorSettings.value.sidebarHiddenTablePrefixes = normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes);
|
||||
if (partial.sidebarObjectInfoMode !== undefined) editorSettings.value.sidebarObjectInfoMode = normalizeSidebarObjectInfoMode(partial.sidebarObjectInfoMode);
|
||||
|
||||
Reference in new issue
Block a user