diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
index 80dd8027..dd0f7399 100644
--- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx
+++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
@@ -49,7 +49,10 @@ const storeState = vi.hoisted(() => ({
saveQuery: vi.fn(),
theme: 'light',
languagePreference: 'zh-CN' as 'zh-CN' | 'en-US',
- appearance: { uiVersion: 'legacy' as 'legacy' | 'v2' },
+ appearance: {
+ uiVersion: 'legacy' as 'legacy' | 'v2',
+ newQuerySqlTemplate: null as string | null,
+ },
sqlFormatOptions: { keywordCase: 'upper' as const },
setSqlFormatOptions: vi.fn(),
queryOptions: {
@@ -705,6 +708,7 @@ describe('QueryEditor external SQL save', () => {
storeState.aiPanelVisible = false;
storeState.setAIPanelVisible.mockReset();
storeState.appearance.uiVersion = 'legacy';
+ storeState.appearance.newQuerySqlTemplate = null;
storeState.queryOptions = {
maxRows: 5000,
showColumnComment: true,
@@ -833,6 +837,26 @@ describe('QueryEditor external SQL save', () => {
expect(editorState.value).toBe('SELECT * FROM ');
});
+ it('uses the customized new query template for a fresh blank query tab', async () => {
+ storeState.appearance.newQuerySqlTemplate = 'SELECT id,\n name\nFROM users;';
+
+ await act(async () => {
+ create();
+ });
+
+ expect(editorState.value).toBe('SELECT id,\n name\nFROM users;');
+ });
+
+ it('allows a blank new query template when the default content is cleared', async () => {
+ storeState.appearance.newQuerySqlTemplate = '';
+
+ await act(async () => {
+ create();
+ });
+
+ expect(editorState.value).toBe('');
+ });
+
it('keeps the query results panel hidden by default on first entry', async () => {
storeState.appearance.uiVersion = 'v2';
diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx
index 222fb8bc..6a1e2ccc 100644
--- a/frontend/src/components/QueryEditor.tsx
+++ b/frontend/src/components/QueryEditor.tsx
@@ -124,6 +124,7 @@ import {
queryCompletionMetadataRowsBySpecs,
readSidebarSqlDropText,
matchLeadingSelectTableReference,
+ resolveNewQueryDefaultTemplate,
resolveEventTargetNode,
resolveNextResultSetIndex,
resolveOracleExactCaseTableReference,
@@ -759,7 +760,11 @@ const resetSharedQueryEditorMetadata = () => {
const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isActive = true }) => {
- const [query, setQuery] = useState(getInitialEditorQuery(tab));
+ const appearance = useStore(state => state.appearance);
+ const [query, setQuery] = useState(() => getInitialEditorQuery(
+ tab,
+ resolveNewQueryDefaultTemplate(appearance.newQuerySqlTemplate),
+ ));
const isExternalSQLFileTab = Boolean(String(tab.filePath || '').trim());
const isObjectEditQueryTab = tab.type === 'query' && tab.queryMode === 'object-edit';
@@ -860,7 +865,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const theme = useStore(state => state.theme);
const languagePreference = useStore((state) => state.languagePreference);
void languagePreference;
- const appearance = useStore(state => state.appearance);
const darkMode = theme === 'dark';
const isV2Ui = appearance.uiVersion === 'v2';
const sqlFormatOptions = useStore(state => state.sqlFormatOptions);
diff --git a/frontend/src/components/queryEditor/QueryEditorHelpers.ts b/frontend/src/components/queryEditor/QueryEditorHelpers.ts
index 0c12d703..ef4309f7 100644
--- a/frontend/src/components/queryEditor/QueryEditorHelpers.ts
+++ b/frontend/src/components/queryEditor/QueryEditorHelpers.ts
@@ -682,11 +682,25 @@ export const splitCompletionSchemaAndTable = (qualified: string): { schema: stri
export const DEFAULT_QUERY_TEMPLATE = 'SELECT * FROM ';
+export const resolveNewQueryDefaultTemplate = (
+ template: string | null | undefined,
+): string => {
+ if (template === null || template === undefined) {
+ return DEFAULT_QUERY_TEMPLATE;
+ }
+ return String(template)
+ .replace(/\r\n/g, '\n')
+ .replace(/\r/g, '\n');
+};
+
export const getTabQueryValue = (tab: TabData): string => (
typeof tab.query === 'string' ? tab.query : ''
);
-export const getInitialEditorQuery = (tab: TabData): string => {
+export const getInitialEditorQuery = (
+ tab: TabData,
+ defaultQueryTemplate: string | null | undefined = DEFAULT_QUERY_TEMPLATE,
+): string => {
if (hasQueryTabDraft(tab.id)) {
return getQueryTabDraft(tab.id);
}
@@ -694,7 +708,7 @@ export const getInitialEditorQuery = (tab: TabData): string => {
if (tabQuery || tab.filePath || tab.savedQueryId || tab.readOnly) {
return tabQuery;
}
- return DEFAULT_QUERY_TEMPLATE;
+ return resolveNewQueryDefaultTemplate(defaultQueryTemplate);
};
export const resolveNextResultSetIndex = (sets: Array<{ key?: string }>): number => {
diff --git a/frontend/src/i18n/catalog.test.ts b/frontend/src/i18n/catalog.test.ts
index 004e29e4..e4cee93e 100644
--- a/frontend/src/i18n/catalog.test.ts
+++ b/frontend/src/i18n/catalog.test.ts
@@ -200,6 +200,10 @@ describe("i18n catalog", () => {
"app.theme.nav.theme.description",
"app.theme.nav.theme.title",
"app.theme.navigation_title",
+ "app.theme.query_template.description",
+ "app.theme.query_template.hint",
+ "app.theme.query_template.reset_default",
+ "app.theme.query_template.title",
"app.theme.theme_settings_description",
"app.theme.theme_settings_title",
"app.theme.ui_version.beta_warning",
diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts
index c36e1c94..e5fb1d66 100644
--- a/frontend/src/store.test.ts
+++ b/frontend/src/store.test.ts
@@ -82,6 +82,7 @@ describe('store appearance persistence', () => {
expect(appearance.sidebarTreeFontSizeFollowGlobal).toBe(true);
expect(appearance.customUIFontFamily).toBeNull();
expect(appearance.customMonoFontFamily).toBeNull();
+ expect(appearance.newQuerySqlTemplate).toBeNull();
expect(appearance.tabDisplay).toEqual({
layout: 'single',
primaryElements: ['connection', 'kind', 'object'],
@@ -260,6 +261,50 @@ describe('store appearance persistence', () => {
expect(appearance.customMonoFontFamily).toBeNull();
});
+ it('persists the new query SQL template while preserving blank and trailing-space overrides', async () => {
+ const { useStore } = await importStore();
+
+ useStore.getState().setAppearance({
+ newQuerySqlTemplate: 'SELECT * FROM ',
+ });
+ expect(useStore.getState().appearance.newQuerySqlTemplate).toBe('SELECT * FROM ');
+
+ useStore.getState().setAppearance({
+ newQuerySqlTemplate: 'SELECT id,\r\n name\nFROM users;\r',
+ });
+
+ let persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
+ expect(persisted.state.appearance.newQuerySqlTemplate).toBe('SELECT id,\n name\nFROM users;\n');
+
+ vi.resetModules();
+ let reloaded = await importStore();
+ expect(reloaded.useStore.getState().appearance.newQuerySqlTemplate).toBe('SELECT id,\n name\nFROM users;\n');
+
+ reloaded.useStore.getState().setAppearance({
+ newQuerySqlTemplate: '',
+ });
+
+ persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
+ expect(persisted.state.appearance.newQuerySqlTemplate).toBe('');
+
+ vi.resetModules();
+ reloaded = await importStore();
+ expect(reloaded.useStore.getState().appearance.newQuerySqlTemplate).toBe('');
+
+ storage.setItem('lite-db-storage', JSON.stringify({
+ state: {
+ appearance: {
+ newQuerySqlTemplate: 123,
+ },
+ },
+ version: 13,
+ }));
+
+ vi.resetModules();
+ reloaded = await importStore();
+ expect(reloaded.useStore.getState().appearance.newQuerySqlTemplate).toBeNull();
+ });
+
it('persists v2 sidebar search preferences and sanitizes filter text', async () => {
const { useStore } = await importStore();
diff --git a/frontend/src/store.ts b/frontend/src/store.ts
index 1f2244d6..2a7abb45 100644
--- a/frontend/src/store.ts
+++ b/frontend/src/store.ts
@@ -105,6 +105,7 @@ export interface AppearanceSettings extends DataGridDisplaySettings {
v2SidebarRailScale: number;
customUIFontFamily: string | null;
customMonoFontFamily: string | null;
+ newQuerySqlTemplate: string | null;
tabDisplay: TabDisplaySettings;
redisDbAliases: RedisDbAliasMap;
}
@@ -126,6 +127,7 @@ export const DEFAULT_APPEARANCE: AppearanceSettings = {
v2SidebarRailScale: DEFAULT_V2_SIDEBAR_RAIL_SCALE,
customUIFontFamily: null,
customMonoFontFamily: null,
+ newQuerySqlTemplate: null,
tabDisplay: DEFAULT_TAB_DISPLAY_SETTINGS,
redisDbAliases: DEFAULT_REDIS_DB_ALIASES,
...DEFAULT_DATA_GRID_DISPLAY_SETTINGS,
@@ -140,6 +142,7 @@ const DEFAULT_STARTUP_FULLSCREEN = false;
const LEGACY_DEFAULT_OPACITY = 0.95;
const OPACITY_EPSILON = 1e-6;
const MAX_SIDEBAR_PERSISTED_FILTER_LENGTH = 120;
+const MAX_NEW_QUERY_SQL_TEMPLATE_LENGTH = 32 * 1024;
const sanitizeV2SidebarSearchMode = (
value: unknown,
@@ -160,6 +163,19 @@ const sanitizeV2SidebarPersistedFilter = (value: unknown): string => {
return value.trim().slice(0, MAX_SIDEBAR_PERSISTED_FILTER_LENGTH);
};
+const sanitizeNewQuerySqlTemplate = (value: unknown): string | null => {
+ if (value === null || value === undefined) {
+ return DEFAULT_APPEARANCE.newQuerySqlTemplate;
+ }
+ if (typeof value !== "string") {
+ return DEFAULT_APPEARANCE.newQuerySqlTemplate;
+ }
+ return value
+ .replace(/\r\n/g, "\n")
+ .replace(/\r/g, "\n")
+ .slice(0, MAX_NEW_QUERY_SQL_TEMPLATE_LENGTH);
+};
+
export const sanitizeV2SidebarRailScale = (value: unknown): number => {
return normalizeFloatInRange(
value,
@@ -2214,6 +2230,7 @@ const sanitizeAppearance = (
),
customUIFontFamily: sanitizeFontFamilyInput(appearance.customUIFontFamily),
customMonoFontFamily: sanitizeFontFamilyInput(appearance.customMonoFontFamily),
+ newQuerySqlTemplate: sanitizeNewQuerySqlTemplate(appearance.newQuerySqlTemplate),
tabDisplay: sanitizeTabDisplaySettings(appearance.tabDisplay),
redisDbAliases: sanitizeRedisDbAliases(appearance.redisDbAliases),
showDataTableVerticalBorders:
diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json
index 7cadcfe1..b944cee3 100644
--- a/shared/i18n/de-DE.json
+++ b/shared/i18n/de-DE.json
@@ -2715,6 +2715,10 @@
"app.theme.font_family.mono_title": "Monospace-Schriftfamilie",
"app.theme.font_family.title": "Schriftfamilie",
"app.theme.font_family.ui_title": "UI-Schriftfamilie",
+ "app.theme.query_template.description": "Legt fest, welcher SQL-Inhalt beim Öffnen eines neuen Abfrage-Tabs vorbefüllt wird.",
+ "app.theme.query_template.hint": "Leeren Sie den Text, damit neue Abfrage-Tabs leer starten. Mit Standard wiederherstellen kehren Sie zu SELECT * FROM zurück.",
+ "app.theme.query_template.reset_default": "Standard wiederherstellen",
+ "app.theme.query_template.title": "Standard-SQL für neue Abfragen",
"app.theme.mac_window.restart_hint": "* Benutzerdefinierte Schaltflächen oben rechts werden ausgeblendet. Falls der Systemfensterstil nicht sofort aktualisiert wird, starten Sie die App neu.",
"app.theme.mac_window.title": "macOS-Fenstersteuerung",
"app.theme.mac_window.use_native_controls": "Native macOS-Fenstersteuerung verwenden",
diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json
index 98600194..e645ae34 100644
--- a/shared/i18n/en-US.json
+++ b/shared/i18n/en-US.json
@@ -2715,6 +2715,10 @@
"app.theme.font_family.mono_title": "Mono Font Family",
"app.theme.font_family.title": "Font Family",
"app.theme.font_family.ui_title": "UI Font Family",
+ "app.theme.query_template.description": "Configure the SQL that is prefilled when you open a fresh query tab.",
+ "app.theme.query_template.hint": "Clear the text to keep new query tabs blank. Use Restore default to go back to SELECT * FROM .",
+ "app.theme.query_template.reset_default": "Restore default",
+ "app.theme.query_template.title": "New Query Default SQL",
"app.theme.mac_window.restart_hint": "* Custom top-right buttons are hidden; restart the app if the system window style does not refresh immediately",
"app.theme.mac_window.title": "macOS Window Controls",
"app.theme.mac_window.use_native_controls": "Use macOS Native Window Controls",
diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json
index 09902d0c..680fab9e 100644
--- a/shared/i18n/ja-JP.json
+++ b/shared/i18n/ja-JP.json
@@ -2715,6 +2715,10 @@
"app.theme.font_family.mono_title": "等幅フォントファミリー",
"app.theme.font_family.title": "フォントファミリー",
"app.theme.font_family.ui_title": "UI フォントファミリー",
+ "app.theme.query_template.description": "新しいクエリタブを開いたときに自動入力する SQL を設定します。",
+ "app.theme.query_template.hint": "空にすると新しいクエリタブは空白のまま開きます。既定に戻すと SELECT * FROM に戻ります。",
+ "app.theme.query_template.reset_default": "既定に戻す",
+ "app.theme.query_template.title": "新規クエリの既定 SQL",
"app.theme.mac_window.restart_hint": "* 右上のカスタムボタンは非表示に同期されています。システムウィンドウのスタイルがすぐに更新されない場合は、アプリを再起動して確認してください",
"app.theme.mac_window.title": "macOS ウィンドウ制御",
"app.theme.mac_window.use_native_controls": "macOS ネイティブウィンドウ制御を使用",
diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json
index 84e69e3a..1608b3b2 100644
--- a/shared/i18n/ru-RU.json
+++ b/shared/i18n/ru-RU.json
@@ -2715,6 +2715,10 @@
"app.theme.font_family.mono_title": "Моноширинное семейство шрифтов",
"app.theme.font_family.title": "Семейство шрифтов",
"app.theme.font_family.ui_title": "Семейство шрифтов UI",
+ "app.theme.query_template.description": "Настройте SQL, который будет автоматически подставляться при открытии новой вкладки запроса.",
+ "app.theme.query_template.hint": "Очистите текст, чтобы новые вкладки запроса открывались пустыми. Кнопка восстановления вернет SELECT * FROM .",
+ "app.theme.query_template.reset_default": "Восстановить по умолчанию",
+ "app.theme.query_template.title": "SQL по умолчанию для нового запроса",
"app.theme.mac_window.restart_hint": "* Пользовательские кнопки справа вверху скрыты. Если системный стиль окна не обновился сразу, перезапустите приложение.",
"app.theme.mac_window.title": "Управление окном macOS",
"app.theme.mac_window.use_native_controls": "Использовать нативные элементы управления окном macOS",
diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json
index 7ecef403..27b41f01 100644
--- a/shared/i18n/zh-CN.json
+++ b/shared/i18n/zh-CN.json
@@ -2715,6 +2715,10 @@
"app.theme.font_family.mono_title": "代码字体 (Mono Font Family)",
"app.theme.font_family.title": "字体族",
"app.theme.font_family.ui_title": "界面字体 (UI Font Family)",
+ "app.theme.query_template.description": "配置新建查询标签页时默认预填的 SQL 内容。",
+ "app.theme.query_template.hint": "清空后新建查询将保持空白;点击“恢复默认”可回到 SELECT * FROM 。",
+ "app.theme.query_template.reset_default": "恢复默认",
+ "app.theme.query_template.title": "新建查询默认 SQL",
"app.theme.mac_window.restart_hint": "* 已同步隐藏右上角自定义按钮;如系统窗口样式未立即刷新,可重启应用后再确认",
"app.theme.mac_window.title": "macOS 窗口控制",
"app.theme.mac_window.use_native_controls": "使用 macOS 原生窗口控制",
diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json
index 424b85b7..f35e5b97 100644
--- a/shared/i18n/zh-TW.json
+++ b/shared/i18n/zh-TW.json
@@ -2715,6 +2715,10 @@
"app.theme.font_family.mono_title": "程式碼字型 (Mono Font Family)",
"app.theme.font_family.title": "字型族",
"app.theme.font_family.ui_title": "介面字型 (UI Font Family)",
+ "app.theme.query_template.description": "設定新建查詢分頁時預先填入的 SQL 內容。",
+ "app.theme.query_template.hint": "清空後新建查詢會保持空白;點擊「恢復預設」可回到 SELECT * FROM 。",
+ "app.theme.query_template.reset_default": "恢復預設",
+ "app.theme.query_template.title": "新建查詢預設 SQL",
"app.theme.mac_window.restart_hint": "* 已同步隐藏右上角自定义按钮;如系统視窗样式未立即重新整理,可重启套用后再确认",
"app.theme.mac_window.title": "macOS 視窗控制",
"app.theme.mac_window.use_native_controls": "使用 macOS 原生視窗控制",