mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
@@ -2246,6 +2246,7 @@ async function writeExternalSqlTab(tab: QueryTab, options: { closeAfterSave?: bo
|
||||
const result = await api.writeExternalSqlFile(tab.externalSqlPath, await formattedSqlForSave(tab), {
|
||||
expectedContentHash: options.expectedContentHash,
|
||||
expectedMissing: options.expectedMissing,
|
||||
encoding: tab.externalSqlEncoding === "auto" ? "utf8" : tab.externalSqlEncoding,
|
||||
});
|
||||
if (result.kind !== "written") return "retry";
|
||||
rememberExternalSqlFileTarget(tab.externalSqlPath, { connectionId: tab.connectionId, database: tab.database, catalog: tab.catalog, schema: tab.schema });
|
||||
@@ -2662,9 +2663,9 @@ async function saveExternalSqlTabAs(tab: QueryTab): Promise<boolean> {
|
||||
const isMongo = databaseType === "mongodb";
|
||||
const filterExtension = currentFileName.includes(".") ? currentFileName.split(".").pop()?.toLowerCase() : isMongo ? "js" : undefined;
|
||||
const defaultName = currentFileName || defaultSavedQueryFileName(tab.title, databaseType);
|
||||
const saved = await api.saveExternalSqlFile(defaultName, await formattedSqlForSave(tab), filterExtension);
|
||||
const saved = await api.saveExternalSqlFile(defaultName, await formattedSqlForSave(tab), filterExtension, tab.externalSqlEncoding === "auto" ? "utf8" : tab.externalSqlEncoding);
|
||||
if (!saved) return false;
|
||||
queryStore.linkExternalSqlPath(tab.id, saved.path, sqlFileTitleFromPath(saved.path), saved.version);
|
||||
queryStore.linkExternalSqlPath(tab.id, saved.path, sqlFileTitleFromPath(saved.path), saved.version, tab.externalSqlEncoding);
|
||||
rememberExternalSqlFileTarget(saved.path, { connectionId: tab.connectionId, database: tab.database, catalog: tab.catalog, schema: tab.schema });
|
||||
invalidateSaveSqlFolderSelection();
|
||||
showSaveSqlDialog.value = false;
|
||||
@@ -2718,9 +2719,9 @@ async function openSqlFile() {
|
||||
if (path) {
|
||||
const sqlPath = path as string;
|
||||
openedSqlPath = sqlPath;
|
||||
const snapshot = await api.readExternalSqlFileSnapshot(sqlPath, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb));
|
||||
const snapshot = await api.readExternalSqlFileSnapshot(sqlPath, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb), "auto");
|
||||
queryStore.updateSql(tab.id, snapshot.content);
|
||||
queryStore.linkExternalSqlPath(tab.id, sqlPath, sqlFileTitleFromPath(sqlPath), snapshot.version);
|
||||
queryStore.linkExternalSqlPath(tab.id, sqlPath, sqlFileTitleFromPath(sqlPath), snapshot.version, snapshot.encoding);
|
||||
applyExternalSqlFileTarget(tab, sqlPath);
|
||||
}
|
||||
} else {
|
||||
@@ -2778,11 +2779,11 @@ async function openSqlFilePath(path: string) {
|
||||
if (!isTauriRuntime()) return;
|
||||
try {
|
||||
await desktopOpenTabsRestorationBarrier?.settled;
|
||||
const snapshot = await api.readExternalSqlFileSnapshot(path, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb));
|
||||
const snapshot = await api.readExternalSqlFileSnapshot(path, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb), "auto");
|
||||
// Startup and OS-open events have no initiating editor tab. Keep new files
|
||||
// unassociated even if persisted tab restoration has since selected one.
|
||||
const target = resolveExternalSqlFileTarget(path, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), unassociatedExternalSqlFileTarget());
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, path, snapshot.content, snapshot.version, target.catalog, target.schema);
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, path, snapshot.content, snapshot.version, target.catalog, target.schema, undefined, snapshot.encoding);
|
||||
} catch (e: any) {
|
||||
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
}
|
||||
@@ -3555,7 +3556,7 @@ async function handleQuickOpenSelect(item: any) {
|
||||
try {
|
||||
const snapshot = await api.readExternalSqlFileSnapshot(item.filePath, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb));
|
||||
const target = resolveExternalSqlFileTargetForActiveTab(item.filePath, queryStore.tabs, queryStore.activeTabId, (connectionId) => connectionStore.getConfig(connectionId));
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, item.filePath, snapshot.content, snapshot.version, target.catalog, target.schema, { line: item.line ?? 1, column: item.column });
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, item.filePath, snapshot.content, snapshot.version, target.catalog, target.schema, { line: item.line ?? 1, column: item.column }, snapshot.encoding);
|
||||
} catch (e: any) {
|
||||
toast(
|
||||
externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)),
|
||||
@@ -3569,7 +3570,7 @@ async function handleQuickOpenSelect(item: any) {
|
||||
try {
|
||||
const snapshot = await api.readExternalSqlFileSnapshot(item.filePath, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb));
|
||||
const target = resolveExternalSqlFileTargetForActiveTab(item.filePath, queryStore.tabs, queryStore.activeTabId, (connectionId) => connectionStore.getConfig(connectionId));
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, item.filePath, snapshot.content, snapshot.version, target.catalog, target.schema);
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, item.filePath, snapshot.content, snapshot.version, target.catalog, target.schema, undefined, snapshot.encoding);
|
||||
} catch (e: any) {
|
||||
toast(
|
||||
externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from "vue-i18n";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import EditorGroupTabBar from "./EditorGroupTabBar.vue";
|
||||
import EditorToolbar from "./EditorToolbar.vue";
|
||||
import QueryEditorSurface from "./QueryEditorSurface.vue";
|
||||
@@ -14,6 +15,8 @@ import { isPreviewTab } from "@/lib/tabs/tabPresentation";
|
||||
import { resolveExecutableSql } from "@/lib/sql/sqlExecutionTarget";
|
||||
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { usesProvenReadOnlyStickyTransactionState } from "@/lib/database/databaseFeatureSupport";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { externalSqlEditorMaxBytes, externalSqlFileOpenErrorMessage } from "@/lib/sql/sqlFileOpen";
|
||||
import { GROUP_TAB_BAR_PORTAL } from "./groupTabBarPortal";
|
||||
import type { ContentAreaSurfaceEmits, ContentAreaSurfaceProps, QueryEditorSurfaceHandle, StatementRange } from "./querySurfaces";
|
||||
import type { QueryTab, TableInfoTab } from "@/types/database";
|
||||
@@ -99,6 +102,7 @@ const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { toast } = useToast();
|
||||
const toolbar = inject(EDITOR_TOOLBAR_ACTIONS, createNoopEditorToolbarActions());
|
||||
const tabBarPortal = inject(GROUP_TAB_BAR_PORTAL, null);
|
||||
const tabBarTarget = computed(() => {
|
||||
@@ -113,6 +117,27 @@ const groupTabs = computed(() => {
|
||||
const activeTab = computed(() => groupTabs.value.find((tab) => tab.id === props.activeTabId) ?? groupTabs.value[0] ?? null);
|
||||
const activeConnection = computed(() => (activeTab.value ? connectionStore.getConfig(activeTab.value.connectionId) : undefined));
|
||||
const showGroupToolbar = computed(() => activeTab.value?.mode === "query" && !activeTab.value.ddlViewer && !isPreviewTab(activeTab.value));
|
||||
|
||||
let encodingRequest = 0;
|
||||
async function changeExternalSqlEncoding(encoding: NonNullable<QueryTab["externalSqlEncoding"]>) {
|
||||
const tab = activeTab.value;
|
||||
if (!tab?.externalSqlPath) return;
|
||||
const request = ++encodingRequest;
|
||||
if (queryStore.isTabDirty(tab) && !window.confirm(t("externalSqlFile.unsavedWarning"))) return;
|
||||
const path = tab.externalSqlPath;
|
||||
const sql = tab.sql;
|
||||
const version = tab.externalSqlFileVersion;
|
||||
const stillCurrent = () => request === encodingRequest && queryStore.tabs.includes(tab) && tab.externalSqlPath === path && tab.sql === sql && tab.externalSqlFileVersion === version;
|
||||
try {
|
||||
const snapshot = await api.readExternalSqlFileSnapshot(tab.externalSqlPath, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb), encoding);
|
||||
if (!stillCurrent()) return;
|
||||
queryStore.applyExternalSqlFileSnapshot(tab.id, snapshot.content, snapshot.version);
|
||||
tab.externalSqlEncoding = snapshot.encoding ?? encoding;
|
||||
} catch (error) {
|
||||
if (!stillCurrent()) return;
|
||||
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(error, (key, params) => t(key, params)) }), 5000);
|
||||
}
|
||||
}
|
||||
const isGroupStickyManualTransaction = computed(() => usesProvenReadOnlyStickyTransactionState(effectiveDatabaseTypeForConnection(activeConnection.value)) && (activeTab.value?.autoCommit ?? true) === false);
|
||||
// Each group previews the executable SQL of its own active tab (selection
|
||||
// stored on the tab), not the focused tab's global selection.
|
||||
@@ -220,6 +245,7 @@ const groupExecutableSql = computed(() => {
|
||||
@unfold-all="activeSurfaceRef?.unfoldAll?.()"
|
||||
@toggle-sql-keyword-case="toolbar.toggleSqlKeywordCase()"
|
||||
@save-sql="(tabId: string) => toolbar.saveSql(tabId)"
|
||||
@change-encoding="changeExternalSqlEncoding"
|
||||
@open-sql="toolbar.openSqlFile()"
|
||||
@import-result-archive="toolbar.importResultArchive()"
|
||||
@paste-sql-in-condition="toolbar.pasteSqlInCondition()"
|
||||
|
||||
@@ -105,6 +105,7 @@ const emit = defineEmits<{
|
||||
unfoldAll: [];
|
||||
toggleSqlKeywordCase: [];
|
||||
saveSql: [tabId: string];
|
||||
"change-encoding": [value: NonNullable<QueryTab["externalSqlEncoding"]>];
|
||||
openSql: [];
|
||||
importResultArchive: [];
|
||||
pasteSqlInCondition: [];
|
||||
@@ -521,6 +522,14 @@ function onExecuteInNewResultTabClick(event: MouseEvent) {
|
||||
emit("toolbarExecuteInNewResultTab", event.detail > 0 ? "pointer" : "keyboard");
|
||||
}
|
||||
|
||||
function changeEncoding(event: Event) {
|
||||
const select = event.target as HTMLSelectElement;
|
||||
const encoding = select.value as NonNullable<QueryTab["externalSqlEncoding"]>;
|
||||
// Keep the visible selection committed until the asynchronous reload succeeds.
|
||||
select.value = props.activeTab.externalSqlEncoding ?? "auto";
|
||||
emit("change-encoding", encoding);
|
||||
}
|
||||
|
||||
async function changeCatalog(selectedCatalog: string) {
|
||||
const connection = props.activeConnection;
|
||||
if (!connection) return;
|
||||
@@ -1048,6 +1057,14 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
</SearchableSelect>
|
||||
</div>
|
||||
</div>
|
||||
<select v-if="activeTab.externalSqlPath" class="ml-1 h-6 rounded border border-border bg-background px-1 text-[11px]" :value="activeTab.externalSqlEncoding ?? 'auto'" :aria-label="t('toolbar.fileEncoding')" @change="changeEncoding">
|
||||
<option value="auto">{{ t("toolbar.encodingAuto") }}</option>
|
||||
<option value="utf8">UTF-8</option>
|
||||
<option value="utf8Bom">UTF-8 BOM</option>
|
||||
<option value="utf16le">UTF-16 LE</option>
|
||||
<option value="utf16be">UTF-16 BE</option>
|
||||
<option value="gbk">GBK / GB18030</option>
|
||||
</select>
|
||||
<div v-if="activeTab.mode === 'data' && activeTab.tableMeta" class="ml-2 inline-flex shrink-0 items-center gap-1 rounded border border-border bg-muted/30 px-2 py-0.5 font-medium text-muted-foreground tabular-nums">
|
||||
<Table2 class="h-3.5 w-3.5 shrink-0" />
|
||||
<span class="truncate">{{ activeTab.tableMeta.columns.length }} {{ t("tree.columns") }}</span>
|
||||
|
||||
@@ -258,7 +258,7 @@ async function openFile(path: string) {
|
||||
try {
|
||||
const snapshot = await api.readExternalSqlFileSnapshot(path, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb));
|
||||
const target = resolveExternalSqlFileTargetForActiveTab(path, queryStore.tabs, queryStore.activeTabId, (connectionId) => connectionStore.getConfig(connectionId));
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, path, snapshot.content, snapshot.version, target.catalog, target.schema);
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, path, snapshot.content, snapshot.version, target.catalog, target.schema, undefined, snapshot.encoding);
|
||||
} catch (e: any) {
|
||||
if (isExternalSqlFileTooLargeError(e) && isSqlFilePath(path)) {
|
||||
executeFile(path);
|
||||
|
||||
@@ -85,14 +85,14 @@ export function useExternalSqlFileChanges(options: UseExternalSqlFileChangesOpti
|
||||
return status;
|
||||
}
|
||||
|
||||
async function readStableSnapshot(path: string) {
|
||||
async function readStableSnapshot(path: string, encoding?: QueryTab["externalSqlEncoding"]) {
|
||||
const maxSizeBytes = externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb);
|
||||
try {
|
||||
return await api.readExternalSqlFileSnapshot(path, maxSizeBytes);
|
||||
return await api.readExternalSqlFileSnapshot(path, maxSizeBytes, encoding);
|
||||
} catch (firstError) {
|
||||
await delay(MISSING_FILE_RECHECK_DELAY_MS);
|
||||
try {
|
||||
return await api.readExternalSqlFileSnapshot(path, maxSizeBytes);
|
||||
return await api.readExternalSqlFileSnapshot(path, maxSizeBytes, encoding);
|
||||
} catch {
|
||||
throw firstError;
|
||||
}
|
||||
@@ -116,7 +116,7 @@ export function useExternalSqlFileChanges(options: UseExternalSqlFileChangesOpti
|
||||
|
||||
if (!forceContentCheck && !tab.externalSqlFileMissing && externalSqlFileMetadataMatches(tab.externalSqlFileVersion, status)) return null;
|
||||
|
||||
const snapshot = await readStableSnapshot(tab.externalSqlPath);
|
||||
const snapshot = await readStableSnapshot(tab.externalSqlPath, tab.externalSqlEncoding);
|
||||
if (externalSqlFileContentMatchesBaseline(tab, snapshot) || (!tab.externalSqlFileVersion && tab.sql === snapshot.content)) {
|
||||
queryStore.updateExternalSqlFileVersion(tab.id, snapshot.version);
|
||||
return null;
|
||||
@@ -153,7 +153,7 @@ export function useExternalSqlFileChanges(options: UseExternalSqlFileChangesOpti
|
||||
async function loadLatest(tab: QueryTab) {
|
||||
if (!tab.externalSqlPath) return false;
|
||||
try {
|
||||
const snapshot = await readStableSnapshot(tab.externalSqlPath);
|
||||
const snapshot = await readStableSnapshot(tab.externalSqlPath, tab.externalSqlEncoding);
|
||||
queryStore.applyExternalSqlFileSnapshot(tab.id, snapshot.content, snapshot.version);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -24,7 +24,7 @@ export function useFileDrop() {
|
||||
const { toast } = useToast();
|
||||
const { openInStreamingExecutorOnTooLarge } = useLargeSqlFileStreamingFallback();
|
||||
|
||||
async function openDroppedSqlFile(name: string, content: string, path?: string, version?: ExternalSqlFileVersion) {
|
||||
async function openDroppedSqlFile(name: string, content: string, path?: string, version?: ExternalSqlFileVersion, encoding?: import("@/types/database").QueryTab["externalSqlEncoding"]) {
|
||||
const options = { allowMongoScripts: !isSqlFilePath(name) };
|
||||
const target = path
|
||||
? resolveExternalSqlFileTargetForActiveTab(path, queryStore.tabs, queryStore.activeTabId, (connectionId) => connectionStore.getConfig(connectionId), options)
|
||||
@@ -33,7 +33,7 @@ export function useFileDrop() {
|
||||
// never bind to a relational SQL tab.
|
||||
if (!isSqlFilePath(name) && connectionStore.getConfig(target.connectionId)?.db_type !== "mongodb") return;
|
||||
if (path) {
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, path, content, version, target.catalog, target.schema);
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, path, content, version, target.catalog, target.schema, undefined, encoding);
|
||||
} else {
|
||||
const tabId = queryStore.createTab(target.connectionId, target.database, name, "query", target.schema, undefined, target.catalog);
|
||||
queryStore.updateSql(tabId, content);
|
||||
@@ -89,7 +89,7 @@ export function useFileDrop() {
|
||||
if (isScriptFilePath(path)) {
|
||||
try {
|
||||
const snapshot = await api.readExternalSqlFileSnapshot(path, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb));
|
||||
await openDroppedSqlFile(name, snapshot.content, path, snapshot.version);
|
||||
await openDroppedSqlFile(name, snapshot.content, path, snapshot.version, snapshot.encoding);
|
||||
} catch (e: any) {
|
||||
if (!openInStreamingExecutorOnTooLarge(path, e)) {
|
||||
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
|
||||
@@ -213,6 +213,8 @@ export default withEnglishFallback({
|
||||
logoutConfirm: "Çıxış etmək istədiyinizdən əminsiniz?",
|
||||
},
|
||||
toolbar: {
|
||||
fileEncoding: "Fayl kodlaşdırması",
|
||||
encodingAuto: "Avtomatik",
|
||||
commitOutcomeUnknown: "Təsdiqləmə nəticəsi məlum deyil və sessiya artıq mövcud deyil. SQL-i yenidən icra etməzdən əvvəl məlumatları yoxlayın.",
|
||||
moreActions: "Digər əməliyyatlar",
|
||||
newConnection: "Yeni əlaqə",
|
||||
|
||||
@@ -411,6 +411,8 @@ export default {
|
||||
logoutConfirm: "Are you sure you want to log out?",
|
||||
},
|
||||
toolbar: {
|
||||
fileEncoding: "File encoding",
|
||||
encodingAuto: "Auto",
|
||||
immediateSync: "Sync now",
|
||||
commitOutcomeUnknown: "Commit result is unknown and the session no longer exists. Verify the data before running this SQL again.",
|
||||
moreActions: "More actions",
|
||||
|
||||
@@ -404,6 +404,8 @@ export default withEnglishFallback({
|
||||
logoutConfirm: "¿Seguro que deseas cerrar sesión?",
|
||||
},
|
||||
toolbar: {
|
||||
fileEncoding: "Codificación del archivo",
|
||||
encodingAuto: "Automática",
|
||||
immediateSync: "Sincronizar ahora",
|
||||
commitOutcomeUnknown: "El resultado del commit es desconocido y la sesión ya no existe. Compruebe los datos antes de volver a ejecutar el SQL.",
|
||||
moreActions: "Más acciones",
|
||||
|
||||
@@ -333,6 +333,8 @@ export default withEnglishFallback({
|
||||
logoutConfirm: "Apakah Anda yakin ingin keluar?",
|
||||
},
|
||||
toolbar: {
|
||||
fileEncoding: "Pengodean berkas",
|
||||
encodingAuto: "Otomatis",
|
||||
commitOutcomeUnknown: "Hasil commit tidak diketahui dan sesi sudah tidak ada lagi. Verifikasi data sebelum menjalankan SQL ini lagi.",
|
||||
moreActions: "Tindakan lainnya",
|
||||
newConnection: "Koneksi Baru",
|
||||
|
||||
@@ -398,6 +398,8 @@ export default withEnglishFallback({
|
||||
logoutConfirm: "Sei sicuro di voler uscire?",
|
||||
},
|
||||
toolbar: {
|
||||
fileEncoding: "Codifica del file",
|
||||
encodingAuto: "Automatica",
|
||||
immediateSync: "Sincronizza ora",
|
||||
commitOutcomeUnknown: "L'esito del commit è sconosciuto e la sessione non esiste più. Verificare i dati prima di eseguire nuovamente SQL.",
|
||||
moreActions: "Altre azioni",
|
||||
|
||||
@@ -399,6 +399,8 @@ export default withEnglishFallback({
|
||||
logoutConfirm: "ログアウトしてもよろしいですか?",
|
||||
},
|
||||
toolbar: {
|
||||
fileEncoding: "ファイルの文字コード",
|
||||
encodingAuto: "自動",
|
||||
immediateSync: "今すぐ同期",
|
||||
commitOutcomeUnknown: "コミット結果は不明で、セッションは既に存在しません。SQL を再実行する前にデータを確認してください。",
|
||||
moreActions: "その他の操作",
|
||||
|
||||
@@ -407,6 +407,8 @@ export default withEnglishFallback({
|
||||
logoutConfirm: "로그아웃하시겠습니까?",
|
||||
},
|
||||
toolbar: {
|
||||
fileEncoding: "파일 인코딩",
|
||||
encodingAuto: "자동",
|
||||
immediateSync: "지금 동기화",
|
||||
commitOutcomeUnknown: "커밋 결과를 알 수 없고 세션이 더 이상 존재하지 않습니다. SQL을 다시 실행하기 전에 데이터를 확인하세요。",
|
||||
moreActions: "더 많은 작업",
|
||||
|
||||
@@ -399,6 +399,8 @@ export default withEnglishFallback({
|
||||
logoutConfirm: "Tem certeza de que deseja sair?",
|
||||
},
|
||||
toolbar: {
|
||||
fileEncoding: "Codificação do arquivo",
|
||||
encodingAuto: "Automática",
|
||||
immediateSync: "Sincronizar agora",
|
||||
commitOutcomeUnknown: "O resultado do commit é desconhecido e a sessão não existe mais. Verifique os dados antes de executar o SQL novamente.",
|
||||
moreActions: "Mais ações",
|
||||
|
||||
@@ -381,6 +381,8 @@ export default withEnglishFallback({
|
||||
logoutConfirm: "Вы действительно хотите выйти?",
|
||||
},
|
||||
toolbar: {
|
||||
fileEncoding: "Кодировка файла",
|
||||
encodingAuto: "Автоматически",
|
||||
immediateSync: "Синхронизировать сейчас",
|
||||
commitOutcomeUnknown: "Результат фиксации неизвестен, а сессия больше не существует. Проверьте данные перед повторным выполнением этого SQL.",
|
||||
moreActions: "Дополнительные действия",
|
||||
|
||||
@@ -213,6 +213,8 @@ export default withEnglishFallback({
|
||||
logoutConfirm: "Çıkış yapmak istediğinizden emin misiniz?",
|
||||
},
|
||||
toolbar: {
|
||||
fileEncoding: "Dosya kodlaması",
|
||||
encodingAuto: "Otomatik",
|
||||
commitOutcomeUnknown: "Onaylama sonucu bilinmiyor ve oturum artık mevcut değil. SQL'i yeniden çalıştırmadan önce verileri doğrulayın.",
|
||||
moreActions: "Diğer İşlemler",
|
||||
newConnection: "Yeni Bağlantı",
|
||||
|
||||
@@ -348,6 +348,8 @@ export default withEnglishFallback({
|
||||
logoutConfirm: "确定要退出登录吗?",
|
||||
},
|
||||
toolbar: {
|
||||
fileEncoding: "文件编码",
|
||||
encodingAuto: "自动",
|
||||
immediateSync: "立即同步",
|
||||
commitOutcomeUnknown: "提交结果未知,事务会话已不存在。请核对数据后再决定是否重新执行 SQL。",
|
||||
moreActions: "更多操作",
|
||||
|
||||
@@ -404,6 +404,8 @@ export default withEnglishFallback({
|
||||
logoutConfirm: "確定要登出嗎?",
|
||||
},
|
||||
toolbar: {
|
||||
fileEncoding: "檔案編碼",
|
||||
encodingAuto: "自動",
|
||||
commitOutcomeUnknown: "提交結果未知,交易工作階段已不存在。請核對資料後再決定是否重新執行 SQL。",
|
||||
moreActions: "更多操作",
|
||||
newConnection: "建立連線",
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("external SQL file API", () => {
|
||||
mocks.invoke.mockResolvedValue({ kind: "content", content: "select 1;", version });
|
||||
|
||||
await expect(readExternalSqlFile("/tmp/demo.sql")).resolves.toBe("select 1;");
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("read_external_sql_file", { path: "/tmp/demo.sql" });
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("read_external_sql_file", { path: "/tmp/demo.sql", maxSizeBytes: undefined, encoding: null });
|
||||
});
|
||||
|
||||
it("returns the disk version with an editor snapshot", async () => {
|
||||
@@ -51,6 +51,7 @@ describe("external SQL file API", () => {
|
||||
content: "select 2;",
|
||||
expectedContentHash: "abc123",
|
||||
expectedMissing: false,
|
||||
encoding: "utf8",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,6 +64,7 @@ describe("external SQL file API", () => {
|
||||
content: "select 2;",
|
||||
expectedContentHash: null,
|
||||
expectedMissing: true,
|
||||
encoding: "utf8",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2778,7 +2778,7 @@ export async function readExternalSqlFile(_path: string, _maxSizeBytes?: number)
|
||||
throw new Error("Opening external SQL file paths is only available in the desktop app");
|
||||
}
|
||||
|
||||
export async function readExternalSqlFileSnapshot(_path: string, _maxSizeBytes?: number): Promise<import("@/lib/backend/tauri").ExternalSqlFileSnapshot> {
|
||||
export async function readExternalSqlFileSnapshot(_path: string, _maxSizeBytes?: number, _encoding?: string): Promise<import("@/lib/backend/tauri").ExternalSqlFileSnapshot> {
|
||||
throw new Error("Opening external SQL file paths is only available in the desktop app");
|
||||
}
|
||||
|
||||
@@ -2786,11 +2786,11 @@ export async function inspectExternalSqlFile(_path: string): Promise<import("@/l
|
||||
throw new Error("Inspecting external SQL file paths is only available in the desktop app");
|
||||
}
|
||||
|
||||
export async function writeExternalSqlFile(_path: string, _content: string, _options: { expectedContentHash?: string; expectedMissing?: boolean } = {}): Promise<import("@/lib/backend/tauri").ExternalSqlFileWriteResult> {
|
||||
export async function writeExternalSqlFile(_path: string, _content: string, _options: { expectedContentHash?: string; expectedMissing?: boolean; encoding?: string } = {}): Promise<import("@/lib/backend/tauri").ExternalSqlFileWriteResult> {
|
||||
throw new Error("Saving external SQL file paths is only available in the desktop app");
|
||||
}
|
||||
|
||||
export async function saveExternalSqlFile(_defaultFileName: string, _content: string, _filterExtension?: string): Promise<{ path: string; version: import("@/types/database").ExternalSqlFileVersion } | null> {
|
||||
export async function saveExternalSqlFile(_defaultFileName: string, _content: string, _filterExtension?: string, _encoding?: string): Promise<{ path: string; version: import("@/types/database").ExternalSqlFileVersion } | null> {
|
||||
throw new Error("Saving SQL files locally is only available in the desktop app");
|
||||
}
|
||||
|
||||
|
||||
@@ -1246,39 +1246,41 @@ export async function pendingOpenPluginInstallLinks(): Promise<string[]> {
|
||||
export interface ExternalSqlFileSnapshot {
|
||||
content: string;
|
||||
version: ExternalSqlFileVersion;
|
||||
encoding?: import("@/types/database").QueryTab["externalSqlEncoding"];
|
||||
}
|
||||
|
||||
export type ExternalSqlFileStatus = { kind: "present"; sizeBytes: number; modifiedNs: string } | { kind: "missing" };
|
||||
|
||||
export type ExternalSqlFileWriteResult = { kind: "written"; version: ExternalSqlFileVersion } | { kind: "conflict"; currentVersion: ExternalSqlFileVersion } | { kind: "missing" };
|
||||
|
||||
export async function readExternalSqlFileSnapshot(path: string, maxSizeBytes?: number): Promise<ExternalSqlFileSnapshot> {
|
||||
const result = await invoke<{ kind: "content"; content: string; version: ExternalSqlFileVersion } | { kind: "tooLarge"; sizeBytes: number; maxSizeBytes: number }>("read_external_sql_file", { path, maxSizeBytes });
|
||||
export async function readExternalSqlFileSnapshot(path: string, maxSizeBytes?: number, encoding?: string): Promise<ExternalSqlFileSnapshot> {
|
||||
const result = await invoke<{ kind: "content"; content: string; version: ExternalSqlFileVersion; encoding?: ExternalSqlFileSnapshot["encoding"] } | { kind: "tooLarge"; sizeBytes: number; maxSizeBytes: number }>("read_external_sql_file", { path, maxSizeBytes, encoding: encoding ?? null });
|
||||
if (result.kind === "tooLarge") {
|
||||
throw new ExternalSqlFileTooLargeError(result.sizeBytes, result.maxSizeBytes);
|
||||
}
|
||||
return { content: result.content, version: result.version };
|
||||
return { content: result.content, version: result.version, ...(result.encoding ? { encoding: result.encoding } : {}) };
|
||||
}
|
||||
|
||||
export async function readExternalSqlFile(path: string, maxSizeBytes?: number): Promise<string> {
|
||||
return (await readExternalSqlFileSnapshot(path, maxSizeBytes)).content;
|
||||
export async function readExternalSqlFile(path: string, maxSizeBytes?: number, encoding?: string): Promise<string> {
|
||||
return (await readExternalSqlFileSnapshot(path, maxSizeBytes, encoding)).content;
|
||||
}
|
||||
|
||||
export async function inspectExternalSqlFile(path: string): Promise<ExternalSqlFileStatus> {
|
||||
return invoke("inspect_external_sql_file", { path });
|
||||
}
|
||||
|
||||
export async function writeExternalSqlFile(path: string, content: string, options: { expectedContentHash?: string; expectedMissing?: boolean } = {}): Promise<ExternalSqlFileWriteResult> {
|
||||
export async function writeExternalSqlFile(path: string, content: string, options: { expectedContentHash?: string; expectedMissing?: boolean; encoding?: string } = {}): Promise<ExternalSqlFileWriteResult> {
|
||||
return invoke("write_external_sql_file", {
|
||||
path,
|
||||
content,
|
||||
expectedContentHash: options.expectedContentHash ?? null,
|
||||
expectedMissing: options.expectedMissing ?? false,
|
||||
encoding: options.encoding ?? "utf8",
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveExternalSqlFile(defaultFileName: string, content: string, filterExtension?: string): Promise<{ path: string; version: ExternalSqlFileVersion } | null> {
|
||||
return invoke("save_external_sql_file", { defaultFileName, content, filterExtension });
|
||||
export async function saveExternalSqlFile(defaultFileName: string, content: string, filterExtension?: string, encoding?: string): Promise<{ path: string; version: ExternalSqlFileVersion } | null> {
|
||||
return invoke("save_external_sql_file", { defaultFileName, content, filterExtension, encoding: encoding ?? "utf8" });
|
||||
}
|
||||
|
||||
export interface SqlFileEntry {
|
||||
|
||||
@@ -2610,6 +2610,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
editorSelection: t.editorSelection,
|
||||
savedSqlId: t.savedSqlId,
|
||||
externalSqlPath: t.externalSqlPath,
|
||||
externalSqlEncoding: t.externalSqlEncoding,
|
||||
externalSqlFileVersion: t.externalSqlFileVersion,
|
||||
externalSqlIgnoredFileVersion: t.externalSqlIgnoredFileVersion,
|
||||
externalSqlFileMissing: t.externalSqlFileMissing,
|
||||
@@ -3249,7 +3250,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
});
|
||||
}
|
||||
|
||||
function openExternalSqlFile(connectionId: string, database: string, path: string, sql: string, version?: QueryTab["externalSqlFileVersion"], catalog?: string, schema?: string, reveal?: { line: number; column?: number }) {
|
||||
function openExternalSqlFile(connectionId: string, database: string, path: string, sql: string, version?: QueryTab["externalSqlFileVersion"], catalog?: string, schema?: string, reveal?: { line: number; column?: number }, encoding: QueryTab["externalSqlEncoding"] = "auto") {
|
||||
const normalizedPath = normalizeExternalSqlPath(path);
|
||||
const existing = tabs.value.find((tab) => tab.mode === "query" && tab.externalSqlPath && normalizeExternalSqlPath(tab.externalSqlPath) === normalizedPath);
|
||||
if (existing) {
|
||||
@@ -3276,6 +3277,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
sql,
|
||||
originalSql: sql,
|
||||
externalSqlPath: path,
|
||||
externalSqlEncoding: encoding,
|
||||
externalSqlFileVersion: version,
|
||||
editorRevealRequest: reveal ? { id: ++contentRevealSeq, line: reveal.line, column: reveal.column } : undefined,
|
||||
isExecuting: false,
|
||||
@@ -4726,6 +4728,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
originalSql: "",
|
||||
savedSqlId: undefined,
|
||||
externalSqlPath: undefined,
|
||||
externalSqlEncoding: undefined,
|
||||
lastExecutedSql: undefined,
|
||||
resultBaseSql: original.resultBaseSql,
|
||||
resultSortedSql: undefined,
|
||||
@@ -5444,6 +5447,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (!tab) return;
|
||||
tab.savedSqlId = savedSqlId;
|
||||
tab.externalSqlPath = undefined;
|
||||
tab.externalSqlEncoding = undefined;
|
||||
tab.externalSqlFileVersion = undefined;
|
||||
tab.externalSqlIgnoredFileVersion = undefined;
|
||||
tab.externalSqlFileMissing = undefined;
|
||||
@@ -5453,10 +5457,11 @@ export const useQueryStore = defineStore("query", () => {
|
||||
}
|
||||
}
|
||||
|
||||
function linkExternalSqlPath(id: string, path: string, title?: string, version?: QueryTab["externalSqlFileVersion"]) {
|
||||
function linkExternalSqlPath(id: string, path: string, title?: string, version?: QueryTab["externalSqlFileVersion"], encoding: QueryTab["externalSqlEncoding"] = "auto") {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab) return;
|
||||
tab.externalSqlPath = path;
|
||||
tab.externalSqlEncoding = encoding;
|
||||
tab.externalSqlFileVersion = version;
|
||||
tab.externalSqlIgnoredFileVersion = undefined;
|
||||
tab.externalSqlFileMissing = undefined;
|
||||
|
||||
@@ -1991,6 +1991,7 @@ export interface QueryTab {
|
||||
sql: string;
|
||||
savedSqlId?: string;
|
||||
externalSqlPath?: string;
|
||||
externalSqlEncoding?: "auto" | "utf8" | "utf8Bom" | "utf16le" | "utf16be" | "gbk";
|
||||
externalSqlFileVersion?: ExternalSqlFileVersion;
|
||||
externalSqlIgnoredFileVersion?: ExternalSqlFileVersion;
|
||||
externalSqlFileMissing?: boolean;
|
||||
|
||||
@@ -422,6 +422,54 @@ pub fn decode_sql_file_bytes(bytes: &[u8]) -> Result<String, String> {
|
||||
decode_sql_file_with_encoding(bytes, encoding_rs::GBK)
|
||||
}
|
||||
|
||||
/// Decode an SQL/text file using an explicit user-selected encoding.
|
||||
pub fn decode_sql_file_bytes_with_encoding(bytes: &[u8], encoding: &str) -> Result<String, String> {
|
||||
match encoding {
|
||||
"utf8" => {
|
||||
decode_sql_file_with_encoding(bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes), encoding_rs::UTF_8)
|
||||
}
|
||||
"utf8Bom" => {
|
||||
let payload = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes);
|
||||
decode_sql_file_with_encoding(payload, encoding_rs::UTF_8)
|
||||
}
|
||||
"utf16le" => {
|
||||
decode_sql_file_with_encoding(bytes.strip_prefix(&[0xFF, 0xFE]).unwrap_or(bytes), encoding_rs::UTF_16LE)
|
||||
}
|
||||
"utf16be" => {
|
||||
decode_sql_file_with_encoding(bytes.strip_prefix(&[0xFE, 0xFF]).unwrap_or(bytes), encoding_rs::UTF_16BE)
|
||||
}
|
||||
"gbk" => decode_sql_file_with_encoding(bytes, encoding_rs::GBK),
|
||||
"auto" | "" => decode_sql_file_bytes(bytes),
|
||||
_ => Err(format!("Unsupported SQL file encoding: {encoding}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode editor text for an explicit file encoding. The returned bytes include
|
||||
/// a BOM for UTF-8 BOM and UTF-16 variants.
|
||||
pub fn encode_sql_file_text(content: &str, encoding: &str) -> Result<Vec<u8>, String> {
|
||||
if encoding == "utf16le" || encoding == "utf16be" {
|
||||
let mut bytes = if encoding == "utf16le" { vec![0xFF, 0xFE] } else { vec![0xFE, 0xFF] };
|
||||
for unit in content.encode_utf16() {
|
||||
bytes.extend_from_slice(&if encoding == "utf16le" { unit.to_le_bytes() } else { unit.to_be_bytes() });
|
||||
}
|
||||
return Ok(bytes);
|
||||
}
|
||||
let (encoded, _, had_errors) = match encoding {
|
||||
"utf8" | "auto" | "" => encoding_rs::UTF_8.encode(content),
|
||||
"utf8Bom" => encoding_rs::UTF_8.encode(content),
|
||||
"gbk" => encoding_rs::GBK.encode(content),
|
||||
_ => return Err(format!("Unsupported SQL file encoding: {encoding}")),
|
||||
};
|
||||
if had_errors {
|
||||
return Err(sql_file_encoding_error());
|
||||
}
|
||||
let mut bytes = encoded.into_owned();
|
||||
if encoding == "utf8Bom" {
|
||||
bytes.splice(0..0, [0xEF, 0xBB, 0xBF]);
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn decode_sql_file_with_encoding(bytes: &[u8], encoding: &'static encoding_rs::Encoding) -> Result<String, String> {
|
||||
let (text, had_errors) = encoding.decode_without_bom_handling(bytes);
|
||||
if had_errors {
|
||||
@@ -5807,3 +5855,19 @@ delimiter ;";
|
||||
assert_eq!(find_statement_at_cursor_for_database(sql, cursor, DatabaseType::Mysql), "SELECT 1 # mysql comment");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod editor_encoding_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn utf16_bytes_and_round_trip() {
|
||||
for (encoding, expected) in [
|
||||
("utf16le", vec![0xff, 0xfe, 0x41, 0, 0x2d, 0x4e, 0x3d, 0xd8, 0, 0xde]),
|
||||
("utf16be", vec![0xfe, 0xff, 0, 0x41, 0x4e, 0x2d, 0xd8, 0x3d, 0xde, 0]),
|
||||
] {
|
||||
let bytes = encode_sql_file_text("A中😀", encoding).unwrap();
|
||||
assert_eq!(bytes, expected);
|
||||
assert_eq!(decode_sql_file_bytes_with_encoding(&bytes, encoding).unwrap(), "A中😀");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use dbx_core::sql::decode_sql_file_bytes;
|
||||
use dbx_core::sql::{decode_sql_file_bytes, decode_sql_file_bytes_with_encoding, encode_sql_file_text};
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::io::AsyncReadExt;
|
||||
@@ -25,7 +25,7 @@ fn exceeds_external_sql_editor_limit(size_bytes: u64, max_size_bytes: u64) -> bo
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
|
||||
pub enum ExternalSqlFileReadResult {
|
||||
Content { content: String, version: ExternalSqlFileVersion },
|
||||
Content { content: String, version: ExternalSqlFileVersion, encoding: String },
|
||||
TooLarge { size_bytes: u64, max_size_bytes: u64 },
|
||||
}
|
||||
|
||||
@@ -72,8 +72,14 @@ pub fn pending_open_sql_files(state: tauri::State<'_, ExternalSqlOpenState>) ->
|
||||
pub async fn read_external_sql_file(
|
||||
path: String,
|
||||
max_size_bytes: Option<u64>,
|
||||
encoding: Option<String>,
|
||||
) -> Result<ExternalSqlFileReadResult, String> {
|
||||
read_external_sql_file_content_async(PathBuf::from(path), clamp_external_sql_editor_limit(max_size_bytes)).await
|
||||
read_external_sql_file_content_async(
|
||||
PathBuf::from(path),
|
||||
clamp_external_sql_editor_limit(max_size_bytes),
|
||||
encoding.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -87,9 +93,17 @@ pub async fn write_external_sql_file(
|
||||
content: String,
|
||||
expected_content_hash: Option<String>,
|
||||
expected_missing: bool,
|
||||
encoding: Option<String>,
|
||||
) -> Result<ExternalSqlFileWriteResult, String> {
|
||||
write_external_sql_file_checked_async(PathBuf::from(path), content, expected_content_hash, expected_missing, false)
|
||||
.await
|
||||
write_external_sql_file_checked_async(
|
||||
PathBuf::from(path),
|
||||
content,
|
||||
expected_content_hash,
|
||||
expected_missing,
|
||||
false,
|
||||
encoding.as_deref().unwrap_or("utf8"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -98,6 +112,7 @@ pub async fn save_external_sql_file(
|
||||
default_file_name: String,
|
||||
content: String,
|
||||
filter_extension: Option<String>,
|
||||
encoding: Option<String>,
|
||||
) -> Result<Option<ExternalSqlFileSaveResult>, String> {
|
||||
let (sender, receiver) = tokio::sync::oneshot::channel();
|
||||
// Non-SQL external tabs (custom-filtered text files) keep their own
|
||||
@@ -120,7 +135,7 @@ pub async fn save_external_sql_file(
|
||||
|
||||
// Keep the native dialog result as a PathBuf until after the write so
|
||||
// Windows Unicode paths do not cross an extra frontend IPC boundary.
|
||||
save_external_sql_file_content_async(path, content).await
|
||||
save_external_sql_file_content_async(path, content, encoding.as_deref().unwrap_or("utf8")).await
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -169,6 +184,20 @@ pub fn is_sql_file_path(path: &Path) -> bool {
|
||||
path.extension().and_then(|ext| ext.to_str()).map(|ext| ext.eq_ignore_ascii_case("sql")).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn detected_encoding(bytes: &[u8]) -> &'static str {
|
||||
if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
|
||||
"utf8Bom"
|
||||
} else if bytes.starts_with(&[0xFF, 0xFE]) {
|
||||
"utf16le"
|
||||
} else if bytes.starts_with(&[0xFE, 0xFF]) {
|
||||
"utf16be"
|
||||
} else if std::str::from_utf8(bytes).is_ok() {
|
||||
"utf8"
|
||||
} else {
|
||||
"gbk"
|
||||
}
|
||||
}
|
||||
|
||||
fn modified_ns(metadata: &std::fs::Metadata) -> String {
|
||||
metadata
|
||||
.modified()
|
||||
@@ -210,12 +239,17 @@ fn read_external_sql_file_content_with_limit(
|
||||
}
|
||||
let bytes = std::fs::read(path).map_err(|e| format!("Failed to read SQL file: {e}"))?;
|
||||
let version = external_sql_file_version(&metadata, &bytes);
|
||||
decode_sql_file_bytes(&bytes).map(|content| ExternalSqlFileReadResult::Content { content, version })
|
||||
decode_sql_file_bytes(&bytes).map(|content| ExternalSqlFileReadResult::Content {
|
||||
content,
|
||||
version,
|
||||
encoding: detected_encoding(&bytes).to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn read_external_sql_file_content_async(
|
||||
path: PathBuf,
|
||||
max_size_bytes: u64,
|
||||
encoding: Option<&str>,
|
||||
) -> Result<ExternalSqlFileReadResult, String> {
|
||||
let file = tokio::fs::File::open(&path).await.map_err(|e| format!("Failed to read SQL file: {e}"))?;
|
||||
let metadata = file.metadata().await.map_err(|e| format!("Failed to inspect SQL file: {e}"))?;
|
||||
@@ -231,7 +265,15 @@ async fn read_external_sql_file_content_async(
|
||||
return Ok(ExternalSqlFileReadResult::TooLarge { size_bytes: bytes.len() as u64, max_size_bytes });
|
||||
}
|
||||
let version = external_sql_file_version(&metadata, &bytes);
|
||||
decode_sql_file_bytes(&bytes).map(|content| ExternalSqlFileReadResult::Content { content, version })
|
||||
let content = match encoding.filter(|value| *value != "auto") {
|
||||
Some(value) => decode_sql_file_bytes_with_encoding(&bytes, value),
|
||||
None => decode_sql_file_bytes(&bytes),
|
||||
}?;
|
||||
Ok(ExternalSqlFileReadResult::Content {
|
||||
content,
|
||||
version,
|
||||
encoding: encoding.filter(|v| *v != "auto").unwrap_or(detected_encoding(&bytes)).to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn inspect_external_sql_file_async(path: PathBuf) -> Result<ExternalSqlFileStatus, String> {
|
||||
@@ -281,6 +323,7 @@ async fn write_external_sql_file_checked_async(
|
||||
expected_content_hash: Option<String>,
|
||||
expected_missing: bool,
|
||||
force: bool,
|
||||
encoding: &str,
|
||||
) -> Result<ExternalSqlFileWriteResult, String> {
|
||||
if !force {
|
||||
match external_sql_file_version_async(&path).await? {
|
||||
@@ -296,8 +339,9 @@ async fn write_external_sql_file_checked_async(
|
||||
}
|
||||
}
|
||||
|
||||
let hash = content_hash(content.as_bytes());
|
||||
tokio::fs::write(&path, content).await.map_err(|error| format!("Failed to save SQL file: {error}"))?;
|
||||
let bytes = encode_sql_file_text(&content, encoding)?;
|
||||
let hash = content_hash(&bytes);
|
||||
tokio::fs::write(&path, &bytes).await.map_err(|error| format!("Failed to save SQL file: {error}"))?;
|
||||
let metadata =
|
||||
tokio::fs::metadata(&path).await.map_err(|error| format!("Failed to inspect saved SQL file: {error}"))?;
|
||||
Ok(ExternalSqlFileWriteResult::Written {
|
||||
@@ -312,11 +356,12 @@ async fn write_external_sql_file_checked_async(
|
||||
async fn save_external_sql_file_content_async(
|
||||
path: Option<PathBuf>,
|
||||
content: String,
|
||||
encoding: &str,
|
||||
) -> Result<Option<ExternalSqlFileSaveResult>, String> {
|
||||
let Some(path) = path else {
|
||||
return Ok(None);
|
||||
};
|
||||
let result = write_external_sql_file_checked_async(path.clone(), content, None, false, true).await?;
|
||||
let result = write_external_sql_file_checked_async(path.clone(), content, None, false, true, encoding).await?;
|
||||
let ExternalSqlFileWriteResult::Written { version } = result else {
|
||||
return Err("Failed to save SQL file".to_string());
|
||||
};
|
||||
@@ -392,7 +437,7 @@ mod tests {
|
||||
let result = read_external_sql_file_content(&path);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let ExternalSqlFileReadResult::Content { content, version } = result.unwrap() else {
|
||||
let ExternalSqlFileReadResult::Content { content, version, .. } = result.unwrap() else {
|
||||
panic!("expected SQL file content");
|
||||
};
|
||||
assert_eq!(content, "select 1;");
|
||||
@@ -408,7 +453,7 @@ mod tests {
|
||||
let result = read_external_sql_file_content(&path);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let ExternalSqlFileReadResult::Content { content, version } = result.unwrap() else {
|
||||
let ExternalSqlFileReadResult::Content { content, version, .. } = result.unwrap() else {
|
||||
panic!("expected SQL file content");
|
||||
};
|
||||
assert_eq!(content, "select '中文';");
|
||||
@@ -486,7 +531,7 @@ mod tests {
|
||||
let file_size = MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1;
|
||||
file.set_len(file_size).unwrap();
|
||||
|
||||
let result = read_external_sql_file_content_async(path.clone(), MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES).await;
|
||||
let result = read_external_sql_file_content_async(path.clone(), MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES, None).await;
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert_eq!(
|
||||
@@ -543,6 +588,7 @@ mod tests {
|
||||
Some(content_hash(b"select 1;")),
|
||||
false,
|
||||
false,
|
||||
"utf8",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -562,14 +608,16 @@ mod tests {
|
||||
Some(content_hash(b"select 1;")),
|
||||
false,
|
||||
false,
|
||||
"utf8",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(missing, ExternalSqlFileWriteResult::Missing);
|
||||
|
||||
let recreated = write_external_sql_file_checked_async(path.clone(), "select 2;".to_string(), None, true, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let recreated =
|
||||
write_external_sql_file_checked_async(path.clone(), "select 2;".to_string(), None, true, false, "utf8")
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(recreated, ExternalSqlFileWriteResult::Written { .. }));
|
||||
assert_eq!(std::fs::read_to_string(&path).unwrap(), "select 2;");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
@@ -580,10 +628,16 @@ mod tests {
|
||||
let path = std::env::temp_dir().join(format!("dbx-test-{}.sql", uuid::Uuid::new_v4()));
|
||||
std::fs::write(&path, "select external;").unwrap();
|
||||
|
||||
let result =
|
||||
write_external_sql_file_checked_async(path.clone(), "select editor;".to_string(), None, true, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let result = write_external_sql_file_checked_async(
|
||||
path.clone(),
|
||||
"select editor;".to_string(),
|
||||
None,
|
||||
true,
|
||||
false,
|
||||
"utf8",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(result, ExternalSqlFileWriteResult::Conflict { .. }));
|
||||
assert_eq!(std::fs::read_to_string(&path).unwrap(), "select external;");
|
||||
|
||||
Reference in New Issue
Block a user