From 4f1997d8eb5f49a3703b786841260b35bf2e3cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Sun, 12 Jul 2026 13:28:46 +0800 Subject: [PATCH] feat(types): add database type help --- .../src/components/redis/RedisKeyBrowser.vue | 93 +++++++- .../structure/TableStructureEditor.vue | 36 ++- .../ui/option-help-panel/OptionHelpPanel.vue | 21 ++ .../components/ui/option-help-panel/index.ts | 1 + .../option-help-panel/optionHelpPanel.spec.ts | 13 ++ .../ui/option-help-panel/optionHelpPanel.ts | 1 + .../ui/searchable-select/SearchableSelect.vue | 206 ++++++++++++------ apps/desktop/src/i18n/locales/en.ts | 110 ++++++++++ apps/desktop/src/i18n/locales/es.ts | 110 ++++++++++ apps/desktop/src/i18n/locales/it.ts | 110 ++++++++++ apps/desktop/src/i18n/locales/ja.ts | 110 ++++++++++ apps/desktop/src/i18n/locales/pt-BR.ts | 110 ++++++++++ .../i18n/locales/typeHelpOverrides.spec.ts | 109 +++++++++ apps/desktop/src/i18n/locales/zh-CN.ts | 110 ++++++++++ apps/desktop/src/i18n/locales/zh-TW.ts | 110 ++++++++++ .../common/optionHelpPanelOffset.spec.ts | 20 ++ .../common/searchableSelectTooltip.spec.ts | 36 +++ .../redis/redisCreateKeyTypeHelp.spec.ts | 44 ++++ .../__tests__/table/mysqlDataTypeHelp.spec.ts | 96 ++++++++ .../table/postgresDataTypeHelp.spec.ts | 64 ++++++ .../table/sqliteDataTypeHelp.spec.ts | 63 ++++++ .../table/tableStructureEditorState.spec.ts | 26 +++ .../src/lib/common/optionHelpPanelOffset.ts | 13 ++ .../src/lib/common/searchableSelectTooltip.ts | 19 ++ .../src/lib/redis/redisCreateKeyTypeHelp.ts | 41 ++++ .../src/lib/table/mysqlDataTypeHelp.ts | 199 +++++++++++++++++ .../src/lib/table/postgresDataTypeHelp.ts | 155 +++++++++++++ .../src/lib/table/sqliteDataTypeHelp.ts | 24 ++ .../lib/table/tableStructureEditorState.ts | 25 ++- apps/desktop/src/styles/globals.css | 10 + 30 files changed, 2006 insertions(+), 79 deletions(-) create mode 100644 apps/desktop/src/components/ui/option-help-panel/OptionHelpPanel.vue create mode 100644 apps/desktop/src/components/ui/option-help-panel/index.ts create mode 100644 apps/desktop/src/components/ui/option-help-panel/optionHelpPanel.spec.ts create mode 100644 apps/desktop/src/components/ui/option-help-panel/optionHelpPanel.ts create mode 100644 apps/desktop/src/i18n/locales/typeHelpOverrides.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/common/optionHelpPanelOffset.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/common/searchableSelectTooltip.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/redis/redisCreateKeyTypeHelp.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/table/mysqlDataTypeHelp.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/table/postgresDataTypeHelp.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/table/sqliteDataTypeHelp.spec.ts create mode 100644 apps/desktop/src/lib/common/optionHelpPanelOffset.ts create mode 100644 apps/desktop/src/lib/common/searchableSelectTooltip.ts create mode 100644 apps/desktop/src/lib/redis/redisCreateKeyTypeHelp.ts create mode 100644 apps/desktop/src/lib/table/mysqlDataTypeHelp.ts create mode 100644 apps/desktop/src/lib/table/postgresDataTypeHelp.ts create mode 100644 apps/desktop/src/lib/table/sqliteDataTypeHelp.ts diff --git a/apps/desktop/src/components/redis/RedisKeyBrowser.vue b/apps/desktop/src/components/redis/RedisKeyBrowser.vue index 1c19b898f6..0c9ea006a8 100644 --- a/apps/desktop/src/components/redis/RedisKeyBrowser.vue +++ b/apps/desktop/src/components/redis/RedisKeyBrowser.vue @@ -11,6 +11,7 @@ import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { OptionHelpPanel } from "@/components/ui/option-help-panel"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Switch } from "@/components/ui/switch"; import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue"; @@ -33,6 +34,8 @@ import { useEditorFontFamilyStyle } from "@/composables/useEditorFontFamilyStyle import { useToast } from "@/composables/useToast"; import { redisKeySearchPattern } from "@/lib/redis/redisKeyPattern"; import { REDIS_SCAN_PAGE_SIZE_DEFAULT } from "@/lib/redis/redisKeyPattern"; +import { getRedisCreateKeyTypeHelp, redisCreateKeyTypeHelpOptionOnOpen, shouldActivateRedisCreateKeyTypeHelpOnFocus } from "@/lib/redis/redisCreateKeyTypeHelp"; +import { optionHelpPanelOffsetTop } from "@/lib/common/optionHelpPanelOffset"; const { t } = useI18n(); const { toast } = useToast(); @@ -100,6 +103,12 @@ const createKeyRawMode = ref(false); const createKeyEntryId = ref("*"); const jsonModuleAvailable = ref(null); const checkingJsonModule = ref(false); +const activeCreateKeyTypeHelp = ref(); +const createKeyTypeKeyboardNavigation = ref(false); +const createKeyTypeOpenedByArrow = ref(false); +const createKeyTypeListCard = ref(); +const createKeyTypeHelpPanel = ref<{ element?: HTMLElement }>(); +const createKeyTypeHelpOffsetTop = ref(0); let nextEntryId = 0; let searchRequestId = 0; let redisBrowserIsActive = true; @@ -166,6 +175,67 @@ const createKeyTypeOptions = computed<{ value: RedisCreateKeyType; label: string { value: "stream", label: "Stream" }, { value: "json", label: "JSON" }, ]); +function createKeyTypeTooltip(type: RedisCreateKeyType): string | undefined { + const help = getRedisCreateKeyTypeHelp(type); + return help ? t(`redis.createKeyTypeHelp.${help.key}`) : undefined; +} +const activeCreateKeyTypeHelpContent = computed(() => (activeCreateKeyTypeHelp.value ? createKeyTypeTooltip(activeCreateKeyTypeHelp.value) : undefined)); + +function activateCreateKeyTypeHelp(type: RedisCreateKeyType) { + activeCreateKeyTypeHelp.value = createKeyTypeTooltip(type) ? type : undefined; +} + +function onCreateKeyTypeSelectOpen(open: boolean) { + if (open) { + activeCreateKeyTypeHelp.value = redisCreateKeyTypeHelpOptionOnOpen(createKeyType.value); + return; + } + activeCreateKeyTypeHelp.value = undefined; + createKeyTypeKeyboardNavigation.value = false; + createKeyTypeOpenedByArrow.value = false; +} + +function onCreateKeyTypeTriggerKeydown(event: KeyboardEvent) { + if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return; + createKeyTypeOpenedByArrow.value = true; + createKeyTypeKeyboardNavigation.value = true; +} + +function onCreateKeyTypeSelectKeydown(event: KeyboardEvent) { + if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return; + createKeyTypeKeyboardNavigation.value = true; +} + +function onCreateKeyTypeOptionFocus(type: RedisCreateKeyType) { + if (!shouldActivateRedisCreateKeyTypeHelpOnFocus({ openedByArrow: createKeyTypeOpenedByArrow.value, keyboardNavigating: createKeyTypeKeyboardNavigation.value })) return; + activateCreateKeyTypeHelp(type); + createKeyTypeOpenedByArrow.value = false; + createKeyTypeKeyboardNavigation.value = false; +} + +async function updateCreateKeyTypeHelpOffset() { + if (!activeCreateKeyTypeHelp.value) { + createKeyTypeHelpOffsetTop.value = 0; + return; + } + await nextTick(); + const card = createKeyTypeListCard.value; + const panel = createKeyTypeHelpPanel.value?.element; + const option = card?.querySelector(`[data-option-help-value="${activeCreateKeyTypeHelp.value}"]`); + if (!card || !panel || !option) { + createKeyTypeHelpOffsetTop.value = 0; + return; + } + createKeyTypeHelpOffsetTop.value = optionHelpPanelOffsetTop({ + activeItemTop: option.getBoundingClientRect().top - card.getBoundingClientRect().top, + listCardHeight: card.clientHeight, + panelHeight: panel.clientHeight, + }); +} + +watch(activeCreateKeyTypeHelp, () => { + void updateCreateKeyTypeHelpOffset(); +}); const visibleRows = computed(() => { const rows = useFlatKeySearchRows.value ? flatKeys.value.map((key) => redisKeyToFlatTreeRow(key, props.db)) : flattenVisibleRedisKeyTree(treeKeys.value, expandedGroupIds.value); return rows.map((row) => ({ @@ -632,6 +702,9 @@ function resetCreateKeyForm() { createKeyEntryId.value = "*"; jsonModuleAvailable.value = null; checkingJsonModule.value = false; + activeCreateKeyTypeHelp.value = undefined; + createKeyTypeKeyboardNavigation.value = false; + createKeyTypeOpenedByArrow.value = false; resetEntries(); } @@ -640,6 +713,9 @@ function onCreateKeyTypeChange(type: any) { createKeyRawMode.value = false; jsonModuleAvailable.value = null; checkingJsonModule.value = false; + activeCreateKeyTypeHelp.value = undefined; + createKeyTypeKeyboardNavigation.value = false; + createKeyTypeOpenedByArrow.value = false; resetEntries(); if (createKeyType.value === "json") { createKeyError.value = ""; @@ -1267,14 +1343,19 @@ defineExpose({ focusSearch }); diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index ba57da5fa0..3c4f191a91 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -27,6 +27,9 @@ import { queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout"; import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage"; import { type BuildTableStructureChangeSqlOptions, type EditableStructureColumn, type EditableStructureForeignKey, type EditableStructureIndex, type EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql"; import { PRESET_FIELDS_TEMPLATE_ID, createTableColumnTemplateDrafts } from "@/lib/table/tableColumnTemplates"; +import { getMysqlDataTypeHelp } from "@/lib/table/mysqlDataTypeHelp"; +import { getPostgresDataTypeHelp } from "@/lib/table/postgresDataTypeHelp"; +import { getSqliteDataTypeHelp } from "@/lib/table/sqliteDataTypeHelp"; import { getTableMetadataCapabilities, firstStructureMetadataTab, isStructureMetadataTabSupported } from "@/lib/table/tableMetadataCapabilities"; import { canAddTableStructureColumn, getTableStructureCapabilities, hasLocalTableColumnOrderChange, isPhysicalTableColumnOrderChange, supportsLocalTableColumnReorder } from "@/lib/table/tableStructureCapabilities"; import { orderedColumnIndexes, uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder"; @@ -43,6 +46,7 @@ import { createIndexDrafts, createTriggerDrafts, dataTypeLengthInputValue, + defaultNewColumnDataType, generateIndexName, generateUniqueIndexName, getColumnEditorControls, @@ -599,6 +603,7 @@ function onIndexColResize(e: MouseEvent, col: number) { const connection = computed(() => (props.connectionId ? store.getConfig(props.connectionId) : undefined)); const databaseType = computed(() => tableStructureDatabaseTypeForConnection(connection.value)); +const usesMysql8SafeDefaults = computed(() => databaseType.value === "mysql" && connection.value?.db_type === "mysql" && connection.value.driver_profile === "mysql"); const structureCapabilities = computed(() => getTableStructureCapabilities(databaseType.value, connection.value?.db_type)); const tableMetadataCapabilities = computed(() => getTableMetadataCapabilities(databaseType.value)); const structureDialect = computed(() => structureCapabilities.value.dialect); @@ -1008,6 +1013,32 @@ function mergeDataTypeOptions(primary: readonly string[], fallback: readonly str return result; } +function mysqlDataTypeTooltip(option: string): string | undefined { + if (databaseType.value !== "mysql") return undefined; + const product = connection.value?.driver_profile === "mariadb" ? "mariadb" : connection.value?.driver_profile === "mysql" ? "mysql" : undefined; + const help = getMysqlDataTypeHelp(option, { product }); + return help ? [help.key, ...(help.warningKeys ?? [])].map((key) => t(`structureEditor.mysqlDataTypeHelp.${key}`)).join("\n\n") : undefined; +} + +function postgresDataTypeTooltip(option: string): string | undefined { + if (databaseType.value !== "postgres") return undefined; + const help = getPostgresDataTypeHelp(option); + return help ? t(`structureEditor.postgresDataTypeHelp.${help.key}`) : undefined; +} + +function sqliteDataTypeTooltip(option: string): string | undefined { + if (databaseType.value !== "sqlite") return undefined; + const help = getSqliteDataTypeHelp(option); + return help ? t(`structureEditor.sqliteDataTypeHelp.${help.key}`) : undefined; +} + +function dataTypeTooltip(option: string): string | undefined { + if (databaseType.value === "mysql") return mysqlDataTypeTooltip(option); + if (databaseType.value === "postgres") return postgresDataTypeTooltip(option); + if (databaseType.value === "sqlite") return sqliteDataTypeTooltip(option); + return undefined; +} + async function loadDynamicDataTypeOptions() { const requestId = ++dataTypeOptionsRequestId; const connectionId = props.connectionId; @@ -1278,7 +1309,7 @@ async function refreshStructureAfterSave(scope: StructureRefreshScope) { async function addColumn() { if (!canAddColumn.value) return; activeTab.value = "columns"; - const dataType = databaseType.value === "manticoresearch" ? combineDataTypeForDatabase(databaseType.value, dataTypeOptions.value[0] ?? "text", getDefaultLengthForType(databaseType.value, dataTypeOptions.value[0] ?? "text")) : "varchar(255)"; + const dataType = defaultNewColumnDataType(databaseType.value, dataTypeOptions.value); const column: EditableStructureColumn = { id: `new:${uuid()}`, name: "", @@ -1517,7 +1548,7 @@ function updateColumnDataType(column: EditableStructureColumn, baseType: string) if (!column.enumValues?.length) column.enumValues = [""]; column.dataType = mysqlEnumDataType(column.enumValues); } else { - column.dataType = combineDataTypeForDatabase(databaseType.value, baseType, getDefaultLengthForType(databaseType.value, baseType)); + column.dataType = combineDataTypeForDatabase(databaseType.value, baseType, getDefaultLengthForType(databaseType.value, baseType, { omitMysqlDeprecatedDefaults: usesMysql8SafeDefaults.value })); } syncSqlServerIdentityForDataType(column); syncDamengIdentityForDataType(column); @@ -2567,6 +2598,7 @@ watch([activeTab, ddlLoading], ([tab, loading]) => { :empty-text="t('structureEditor.noMatchingType')" :loading-text="t('common.loading')" :allow-custom="true" + :option-tooltip="dataTypeTooltip" :trigger-class="[structureMonoControlClass, 'w-full']" @update:model-value="(v: string) => updateColumnDataType(column, v)" /> diff --git a/apps/desktop/src/components/ui/option-help-panel/OptionHelpPanel.vue b/apps/desktop/src/components/ui/option-help-panel/OptionHelpPanel.vue new file mode 100644 index 0000000000..254f928a6a --- /dev/null +++ b/apps/desktop/src/components/ui/option-help-panel/OptionHelpPanel.vue @@ -0,0 +1,21 @@ + + + diff --git a/apps/desktop/src/components/ui/option-help-panel/index.ts b/apps/desktop/src/components/ui/option-help-panel/index.ts new file mode 100644 index 0000000000..5afc69798f --- /dev/null +++ b/apps/desktop/src/components/ui/option-help-panel/index.ts @@ -0,0 +1 @@ +export { default as OptionHelpPanel } from "./OptionHelpPanel.vue"; diff --git a/apps/desktop/src/components/ui/option-help-panel/optionHelpPanel.spec.ts b/apps/desktop/src/components/ui/option-help-panel/optionHelpPanel.spec.ts new file mode 100644 index 0000000000..8620b3efb6 --- /dev/null +++ b/apps/desktop/src/components/ui/option-help-panel/optionHelpPanel.spec.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import { OPTION_HELP_PANEL_CLASS } from "./optionHelpPanel"; + +describe("OptionHelpPanel layout", () => { + it("keeps the panel compact at the top and scrolls long help content", () => { + expect(OPTION_HELP_PANEL_CLASS).toContain("self-start"); + expect(OPTION_HELP_PANEL_CLASS).toContain("max-h-64"); + expect(OPTION_HELP_PANEL_CLASS).toContain("overflow-y-auto"); + expect(OPTION_HELP_PANEL_CLASS).toContain("rounded-md"); + expect(OPTION_HELP_PANEL_CLASS).toContain("shadow-md"); + expect(OPTION_HELP_PANEL_CLASS).toContain("sm:mt-[var(--option-help-offset)]"); + }); +}); diff --git a/apps/desktop/src/components/ui/option-help-panel/optionHelpPanel.ts b/apps/desktop/src/components/ui/option-help-panel/optionHelpPanel.ts new file mode 100644 index 0000000000..68bafd5ce4 --- /dev/null +++ b/apps/desktop/src/components/ui/option-help-panel/optionHelpPanel.ts @@ -0,0 +1 @@ +export const OPTION_HELP_PANEL_CLASS = "w-full shrink-0 self-start max-h-64 overflow-y-auto rounded-md border bg-popover px-3 py-2 text-xs leading-relaxed text-muted-foreground shadow-md sm:mt-[var(--option-help-offset)] sm:w-[min(26rem,calc(100vw-5rem))]"; diff --git a/apps/desktop/src/components/ui/searchable-select/SearchableSelect.vue b/apps/desktop/src/components/ui/searchable-select/SearchableSelect.vue index 2d5abf1589..88ff1dbc23 100644 --- a/apps/desktop/src/components/ui/searchable-select/SearchableSelect.vue +++ b/apps/desktop/src/components/ui/searchable-select/SearchableSelect.vue @@ -5,9 +5,12 @@ import { Check, ChevronDown, Search, X } from "@lucide/vue"; import { Button } from "@/components/ui/button"; import type { ButtonVariants } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { OptionHelpPanel } from "@/components/ui/option-help-panel"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { filterDatabaseOptions } from "@/lib/database/databaseOptionSearch"; import { cn } from "@/lib/common/utils"; +import { optionHelpPanelOffsetTop } from "@/lib/common/optionHelpPanelOffset"; +import { SEARCHABLE_SELECT_HELP_PANEL_ALIGN, searchableSelectKeyboardTooltipOption, searchableSelectSelectedOrFirstHelpOption } from "@/lib/common/searchableSelectTooltip"; const props = withDefaults( defineProps<{ @@ -26,6 +29,7 @@ const props = withDefaults( contentClass?: HTMLAttributes["class"]; itemClass?: HTMLAttributes["class"]; displayName?: (option: string) => string; + optionTooltip?: (option: string) => string | undefined; normalizeCustom?: (value: string) => string; clearable?: boolean; }>(), @@ -38,6 +42,7 @@ const props = withDefaults( triggerVariant: "ghost", triggerIconClass: "h-3 w-3", displayName: (option: string) => option, + optionTooltip: () => undefined, normalizeCustom: (value: string) => value, }, ); @@ -57,7 +62,11 @@ const open = ref(false); const searchText = ref(""); const searchInput = ref>(); const listContainer = ref(); +const listCard = ref(); +const helpPanel = ref<{ element?: HTMLElement }>(); const highlightIndex = ref(-1); +const activeHelpOption = ref(); +const helpPanelOffsetTop = ref(0); const selectedLabel = computed(() => { if (!props.modelValue && !props.options.includes("")) return props.placeholder; @@ -86,6 +95,7 @@ async function scrollHighlightedOptionIntoView() { const buttons = container.querySelectorAll("button"); const target = buttons[highlightIndex.value]; target?.scrollIntoView({ block: "nearest" }); + void updateHelpPanelOffset(); } function highlightAndScrollSelectedOption() { @@ -93,33 +103,79 @@ function highlightAndScrollSelectedOption() { void scrollHighlightedOptionIntoView(); } +function activateInitialHelpOption() { + activeHelpOption.value = searchableSelectSelectedOrFirstHelpOption(filteredOptions.value, props.modelValue, props.optionTooltip); +} + watch(open, async (value) => { emit("update:open", value); if (!value) { searchText.value = ""; highlightIndex.value = -1; + activeHelpOption.value = undefined; return; } await nextTick(); const input = searchInput.value?.$el as HTMLInputElement | undefined; input?.focus(); highlightAndScrollSelectedOption(); + activateInitialHelpOption(); }); watch(searchText, () => { highlightIndex.value = 0; + activeHelpOption.value = searchableSelectKeyboardTooltipOption(filteredOptions.value, 0, props.optionTooltip); }); watch( () => [props.modelValue, props.options], () => { if (!open.value || searchText.value) return; + activeHelpOption.value = undefined; highlightAndScrollSelectedOption(); + activateInitialHelpOption(); }, { deep: true }, ); -watch(highlightIndex, scrollHighlightedOptionIntoView); +watch([highlightIndex, filteredOptions], () => { + void scrollHighlightedOptionIntoView(); +}); + +const activeHelpContent = computed(() => (activeHelpOption.value ? props.optionTooltip(activeHelpOption.value) : undefined)); + +function activateHelpForHighlightedOption() { + activeHelpOption.value = searchableSelectKeyboardTooltipOption(filteredOptions.value, highlightIndex.value, props.optionTooltip); +} + +function activateHelpForOption(option: string) { + activeHelpOption.value = props.optionTooltip(option) ? option : undefined; +} + +async function updateHelpPanelOffset() { + if (!activeHelpOption.value) { + helpPanelOffsetTop.value = 0; + return; + } + await nextTick(); + const card = listCard.value; + const panel = helpPanel.value?.element; + const optionIndex = filteredOptions.value.indexOf(activeHelpOption.value); + const option = optionIndex >= 0 ? listContainer.value?.querySelectorAll("button")[optionIndex] : undefined; + if (!card || !panel || !option) { + helpPanelOffsetTop.value = 0; + return; + } + helpPanelOffsetTop.value = optionHelpPanelOffsetTop({ + activeItemTop: option.getBoundingClientRect().top - card.getBoundingClientRect().top, + listCardHeight: card.clientHeight, + panelHeight: panel.clientHeight, + }); +} + +watch([activeHelpOption, filteredOptions], () => { + void updateHelpPanelOffset(); +}); function selectOption(option: string) { emit("update:modelValue", option); @@ -146,11 +202,13 @@ function handleKeydown(event: KeyboardEvent) { const total = optionCount(); if (total === 0) return; highlightIndex.value = highlightIndex.value < total - 1 ? highlightIndex.value + 1 : 0; + activateHelpForHighlightedOption(); } else if (event.key === "ArrowUp") { event.preventDefault(); const total = optionCount(); if (total === 0) return; highlightIndex.value = highlightIndex.value > 0 ? highlightIndex.value - 1 : total - 1; + activateHelpForHighlightedOption(); } else if (event.key === "Enter") { if (highlightIndex.value < 0 || highlightIndex.value >= optionCount()) return; event.preventDefault(); @@ -176,76 +234,84 @@ function handleKeydown(event: KeyboardEvent) { - -
- - {{ searchPlaceholder }} - -
-
-
- {{ loadingText }} -
- - -
- {{ emptyText }} + +
+
+
+ + {{ searchPlaceholder }} + +
+
+
+ {{ loadingText }} +
+ + +
+ {{ emptyText }} +
+
+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 9bfe7eaafe..99a3735ac6 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1848,6 +1848,107 @@ export default { dataType: "Type", typePlaceholder: "Select or type...", noMatchingType: "No matching type", + mysqlDataTypeHelp: { + tinyint: "1-byte integer. Signed range: -128 to 127; unsigned range: 0 to 255.", + smallint: "2-byte integer. Signed range: -32,768 to 32,767; unsigned range: 0 to 65,535.", + mediumint: "3-byte integer. Signed range: -8,388,608 to 8,388,607; unsigned range: 0 to 16,777,215.", + int: "4-byte integer. Signed range: -2,147,483,648 to 2,147,483,647; unsigned range: 0 to 4,294,967,295.", + bigint: "8-byte integer. Signed range: -2^63 to 2^63-1; unsigned range: 0 to 2^64-1.", + decimal: "Exact fixed-point number. MySQL supports precision up to 65 digits and scale up to 30 digits; MariaDB version limits can differ. Use it for money and other exact values.", + float: "Single-precision approximate number. Use DECIMAL when exact decimal values are required.", + double: "Double-precision approximate number. Use DECIMAL when exact decimal values are required.", + real: "Alias for DOUBLE by default; REAL_AS_FLOAT SQL mode makes it an alias for FLOAT.", + bit: "Bit field of 1 to 64 bits. The default width is 1 bit.", + boolean: "Alias for TINYINT(1): 0 represents false and nonzero values represent true.", + serial: "Convenience alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.", + date: "Calendar date. Supported range: 1000-01-01 through 9999-12-31.", + datetime: "Date and time without time-zone conversion. Supported range: 1000-01-01 00:00:00 through 9999-12-31 23:59:59; fractional-seconds precision is 0 to 6 (default 0).", + timestamp: "Date and time converted between the session time zone and UTC. Supported range: 1970-01-01 00:00:01 UTC through 2038-01-19 03:14:07 UTC; fractional-seconds precision is 0 to 6 (default 0).", + time: "Time of day or elapsed time. Supported range: -838:59:59 through 838:59:59; fractional-seconds precision is 0 to 6 (default 0).", + year: "Year value. Four-digit values range from 1901 to 2155, plus 0000.", + char: "Fixed-length character string, up to 255 characters. Stored values are right-padded with spaces.", + varchar: "Variable-length character string. Its practical maximum is constrained by row size and character set.", + tinytext: "Variable-length text, up to 255 bytes. Character set, row-size, and max_allowed_packet limits can impose lower practical limits.", + text: "Variable-length text, up to 65,535 bytes. Character set, row-size, and max_allowed_packet limits can impose lower practical limits.", + mediumtext: "Variable-length text, up to 16,777,215 bytes. Character set, row-size, and max_allowed_packet limits can impose lower practical limits.", + longtext: "Variable-length text, up to 4,294,967,295 bytes (about 4 GiB). Character set, row-size, and max_allowed_packet limits can impose lower practical limits.", + binary: "Fixed-length binary byte string, up to 255 bytes. Stored values are padded with 0x00.", + varbinary: "Variable-length binary byte string. Its practical maximum is constrained by row size.", + tinyblob: "Binary large object, up to 255 bytes. Row-size and max_allowed_packet limits can impose lower practical limits.", + blob: "Binary large object, up to 65,535 bytes. Row-size and max_allowed_packet limits can impose lower practical limits.", + mediumblob: "Binary large object, up to 16,777,215 bytes. Row-size and max_allowed_packet limits can impose lower practical limits.", + longblob: "Binary large object, up to 4,294,967,295 bytes (about 4 GiB). Row-size and max_allowed_packet limits can impose lower practical limits.", + enum: "One value selected from up to 65,535 declared literals. Changing literal order can change stored value semantics.", + set: "Zero or more values selected from up to 64 declared members.", + json: "Validated JSON document. Storage representation and JSON feature support depend on the connected server product and version; document size is also limited by max_allowed_packet.", + jsonMysql: "MySQL's native binary JSON document type with JSON functions and validation. Document size is limited by max_allowed_packet.", + jsonMariaDb: "MariaDB JSON is an alias for LONGTEXT with a JSON_VALID constraint and utf8mb4_bin collation; document size is limited by max_allowed_packet.", + nationalCharacter: "National character string alias. In MySQL it uses the predefined national character set (utf8mb3, deprecated); explicitly choose utf8mb4 for new schemas.", + geometry: "Spatial value of any supported geometry type. A MySQL InnoDB SPATIAL index requires a NOT NULL column with an SRID attribute.", + point: "Spatial point with one coordinate pair. A MySQL InnoDB SPATIAL index requires a NOT NULL column with an SRID attribute.", + linestring: "Spatial curve composed of one or more connected points. A MySQL InnoDB SPATIAL index requires a NOT NULL column with an SRID attribute.", + polygon: "Spatial surface enclosed by one or more rings. A MySQL InnoDB SPATIAL index requires a NOT NULL column with an SRID attribute.", + multipoint: "Collection of spatial points. A MySQL InnoDB SPATIAL index requires a NOT NULL column with an SRID attribute.", + multilinestring: "Collection of spatial curves. A MySQL InnoDB SPATIAL index requires a NOT NULL column with an SRID attribute.", + multipolygon: "Collection of spatial polygons. A MySQL InnoDB SPATIAL index requires a NOT NULL column with an SRID attribute.", + geometrycollection: "Collection that can contain mixed spatial geometry types. A MySQL InnoDB SPATIAL index requires a NOT NULL column with an SRID attribute.", + inet4: "MariaDB IPv4 address type (available from MariaDB 10.10); it stores an IPv4 address in four bytes.", + inet6: "MariaDB-compatible address type for IPv6 and mapped IPv4 values.", + uuid: "128-bit universally unique identifier type supplied by compatible servers.", + vector: "Vector data type for similarity or machine-learning workloads; availability and dimensions depend on the server version.", + xmltype: "XML data type supplied by compatible servers.", + integerDisplayWidth: "MySQL integer display width (for example INT(11)) is deprecated and does not limit the stored range. Do not add it to new MySQL 8 schemas.", + zerofill: "ZEROFILL is deprecated in MySQL 8. It also implicitly makes the column UNSIGNED; use explicit formatting in the application instead.", + unsignedNumeric: "UNSIGNED for DECIMAL, FLOAT, or DOUBLE is deprecated in MySQL 8. Use a CHECK constraint when a nonnegative domain is required.", + floatPrecision: "For FLOAT(p), p from 25 through 53 selects DOUBLE precision rather than FLOAT. Use DOUBLE explicitly when that is intended.", + floatingPointDisplay: "FLOAT(M,D) and DOUBLE(M,D) are deprecated in MySQL 8. Use FLOAT/DOUBLE without display parameters, or DECIMAL(M,D) for exact values.", + }, + postgresDataTypeHelp: { + smallint: "2-byte signed integer, from -32,768 to 32,767.", + integer: "4-byte signed integer, from -2,147,483,648 to 2,147,483,647.", + bigint: "8-byte signed integer, from -2^63 to 2^63-1.", + serial: "Convenience syntax that creates an integer column, sequence, NOT NULL, and nextval default. It is not unique by itself and sequence values can have gaps; prefer IDENTITY for new schemas.", + numeric: "Exact decimal number with user-selected precision and scale. Use it for money and other values that must not use binary floating-point rounding.", + real: "4-byte single-precision floating-point number. FLOAT(p) with p from 1 through 24 selects this type.", + double: "8-byte double-precision floating-point number. FLOAT without precision is its alias; FLOAT(p) with p from 25 through 53 selects it.", + money: "Locale-formatted currency amount. Its output and parsing depend on lc_monetary; NUMERIC is usually safer for portable financial data.", + boolean: "Logical true, false, or null value. BOOL is an alias for BOOLEAN.", + char: "Fixed-length blank-padded character string. CHARACTER is an alias; avoid it when trailing spaces are significant.", + varchar: "Variable-length character string with an optional character limit. CHARACTER VARYING is an alias.", + text: "Variable-length character string without a declared limit; it has the same storage behavior as VARCHAR without a limit.", + bytea: "Variable-length binary string. Large values can be stored out of line by TOAST.", + date: "Calendar date without a time of day or time zone.", + time: "Time of day without a time zone, with optional fractional seconds precision.", + timetz: "Time of day plus a UTC offset. It has limited usefulness and is generally discouraged; use timestamptz for an instant in time.", + timestamp: "Date and time without time-zone conversion. Use it for local wall-clock values whose time zone is not part of the data.", + timestamptz: "Instant in time stored internally in UTC and displayed in the session time zone. It does not retain the original named time zone.", + interval: "Duration expressed in months, days, and microseconds. Calendar units are not fixed-length durations.", + uuid: "128-bit universally unique identifier.", + json: "JSON text preserved in the original input representation. Parsing is validated, but processing is generally slower than JSONB.", + jsonb: "Decomposed binary JSON representation optimized for processing and indexing. It does not preserve insignificant whitespace, key order, or duplicate keys.", + xml: "XML value with optional document or content validation depending on the XML option used.", + bit: "Fixed- or variable-length bit string. BIT VARYING and VARBIT are variable-length aliases.", + tsvector: "Normalized lexemes used as the searchable document side of PostgreSQL full-text search; commonly indexed with GIN or GiST.", + tsquery: "Boolean full-text search query evaluated against a tsvector.", + cidr: "IPv4 or IPv6 network with host bits constrained to zero.", + inet: "IPv4 or IPv6 host address, optionally with a network mask; unlike CIDR, host bits may be nonzero.", + macaddr: "MAC address. MACADDR stores 6-byte addresses and MACADDR8 stores 8-byte addresses.", + geometry: "Built-in two-dimensional geometric type. PostgreSQL geometry is planar and has no SRID; use PostGIS for geographic coordinates, projections, or advanced spatial operations.", + range: "Single continuous range with inclusive/exclusive bounds. Range values require a compatible subtype; use multirange types for multiple disjoint ranges.", + oid: "Unsigned object identifier. It is a system identifier, not a durable application primary key; use an ordinary integer or UUID for application data.", + array: "PostgreSQL array of any element type, including multi-dimensional arrays. Array dimensions are not enforced by the declared type and arrays are often less flexible than normalized child rows.", + multirange: "Set of non-overlapping ranges of the matching range subtype. PostgreSQL normalizes adjacent or overlapping members according to the subtype rules.", + jsonpath: "SQL/JSON path expression used by JSONB query and predicate functions.", + pgLsn: "Write-ahead log location used for replication and recovery coordinates; it is not an application sequence number.", + pgSnapshot: "Transaction visibility snapshot. Its text representation is intended for database snapshot operations, not general application data.", + }, + sqliteDataTypeHelp: { + integer: "SQLite uses dynamic typing and INTEGER affinity; an ordinary column does not enforce the INTEGER storage class. Values are stored as signed integers when possible; INTEGER PRIMARY KEY (exact spelling) is the special alias for the rowid.", + real: "REAL affinity stores IEEE 754 floating-point values. It is appropriate for approximate measurements, not exact money or decimal calculations.", + text: "TEXT affinity stores text in the database encoding. VARCHAR(n) and CHAR(n) do not enforce n in SQLite; add a CHECK constraint when a length limit matters.", + blob: "BLOB affinity stores bytes exactly as supplied, without type conversion.", + numeric: "NUMERIC affinity attempts to convert well-formed numeric text to INTEGER first, then REAL; it is not a separate SQLite storage class. DATE, DATETIME, and TIME declarations also have NUMERIC affinity; store ISO-8601 TEXT, Julian-day REAL, or Unix-time INTEGER consistently.", + }, length: "Length", enumValues: "Enum values", enumValueCount: "{count} values", @@ -2078,6 +2179,15 @@ export default { createKeyName: "Key", createKeyNamePlaceholder: "user:1", createKeyType: "Type", + createKeyTypeHelp: { + string: "Single binary-safe value up to 512 MB. Best for cached values, counters, flags, and simple serialized data.", + hash: "Flat field-value map. Efficient for individual fields, but reading a large hash in full is O(N).", + list: "Ordered sequence. Head and tail operations are efficient; random access in the middle is linear in the list length.", + set: "Unordered collection of unique members. Single-member operations are O(1), while enumerating or combining large sets is linear.", + zset: "Unique members ordered by floating-point scores. Writes are O(log N); members with equal scores are ordered lexicographically.", + stream: "Append-only event log. Consumer groups need XACK and pending-entry handling; trim the stream to control retention and memory.", + json: "Path-addressable JSON document. Requires RedisJSON/Redis Stack JSON.* support and usually has more overhead than String or Hash.", + }, createValue: "Value", createMember: "Member", createField: "Field", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 033f13bce0..8068f4a84b 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1749,6 +1749,107 @@ export default withEnglishFallback({ sortBy: "ordenar por", }, structureEditor: { + mysqlDataTypeHelp: { + tinyint: "Entero de 1 byte: con signo -128 a 127; sin signo 0 a 255.", + smallint: "Entero de 2 bytes: con signo -32.768 a 32.767; sin signo 0 a 65.535.", + mediumint: "Entero de 3 bytes: con signo -8.388.608 a 8.388.607; sin signo 0 a 16.777.215.", + int: "Entero de 4 bytes: con signo -2.147.483.648 a 2.147.483.647; sin signo 0 a 4.294.967.295.", + bigint: "Entero de 8 bytes: con signo -2^63 a 2^63-1; sin signo 0 a 2^64-1.", + decimal: "Número decimal exacto; MySQL admite hasta 65 dígitos y escala 30. Úselo para dinero.", + float: "Número aproximado de precisión simple; para decimales exactos use DECIMAL.", + double: "Número aproximado de doble precisión; para decimales exactos use DECIMAL.", + real: "Alias de DOUBLE por defecto; REAL_AS_FLOAT lo convierte en alias de FLOAT.", + bit: "Campo de 1 a 64 bits; el ancho predeterminado es 1.", + boolean: "Alias de TINYINT(1): 0 es falso y distinto de cero es verdadero.", + serial: "Alias práctico de BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.", + date: "Fecha de calendario, de 1000-01-01 a 9999-12-31.", + datetime: "Fecha y hora sin conversión de zona; precisión fraccionaria 0 a 6, predeterminada 0.", + timestamp: "Fecha y hora convertida entre zona de sesión y UTC; precisión fraccionaria 0 a 6.", + time: "Hora o intervalo de -838:59:59 a 838:59:59; precisión fraccionaria 0 a 6.", + year: "Año de cuatro dígitos de 1901 a 2155, además de 0000.", + char: "Cadena de longitud fija de hasta 255 caracteres, rellenada con espacios.", + varchar: "Cadena de longitud variable; el máximo real depende de fila y juego de caracteres.", + tinytext: "Texto variable de hasta 255 bytes; fila, charset y max_allowed_packet pueden limitarlo.", + text: "Texto variable de hasta 65.535 bytes; fila, charset y max_allowed_packet pueden limitarlo.", + mediumtext: "Texto variable de hasta 16.777.215 bytes; fila, charset y max_allowed_packet pueden limitarlo.", + longtext: "Texto variable de hasta 4.294.967.295 bytes; fila, charset y max_allowed_packet pueden limitarlo.", + binary: "Cadena binaria fija de hasta 255 bytes, rellenada con 0x00.", + varbinary: "Cadena binaria variable; el máximo real depende del tamaño de fila.", + tinyblob: "Objeto binario de hasta 255 bytes; max_allowed_packet puede limitarlo.", + blob: "Objeto binario de hasta 65.535 bytes; max_allowed_packet puede limitarlo.", + mediumblob: "Objeto binario de hasta 16.777.215 bytes; max_allowed_packet puede limitarlo.", + longblob: "Objeto binario de hasta 4.294.967.295 bytes; max_allowed_packet puede limitarlo.", + enum: "Un valor entre hasta 65.535 literales; cambiar el orden puede alterar la semántica almacenada.", + set: "Cero o más valores entre hasta 64 miembros declarados.", + json: "Documento JSON validado; representación y funciones dependen del producto/versión; limitado por max_allowed_packet.", + jsonMysql: "JSON binario nativo de MySQL con validación y funciones JSON; limitado por max_allowed_packet.", + jsonMariaDb: "En MariaDB JSON es alias de LONGTEXT con JSON_VALID y colación utf8mb4_bin.", + nationalCharacter: "Alias de cadena nacional; MySQL usa utf8mb3, obsoleto; prefiera utf8mb4.", + geometry: "Geometría espacial; un índice SPATIAL InnoDB requiere NOT NULL y atributo SRID.", + point: "Punto espacial; SPATIAL InnoDB requiere NOT NULL y SRID.", + linestring: "Curva espacial; SPATIAL InnoDB requiere NOT NULL y SRID.", + polygon: "Superficie espacial; SPATIAL InnoDB requiere NOT NULL y SRID.", + multipoint: "Colección de puntos; SPATIAL InnoDB requiere NOT NULL y SRID.", + multilinestring: "Colección de curvas; SPATIAL InnoDB requiere NOT NULL y SRID.", + multipolygon: "Colección de polígonos; SPATIAL InnoDB requiere NOT NULL y SRID.", + geometrycollection: "Colección de geometrías mixtas; SPATIAL InnoDB requiere NOT NULL y SRID.", + inet4: "Tipo IPv4 de MariaDB desde 10.10; almacena la dirección en cuatro bytes.", + inet6: "Tipo compatible MariaDB para IPv6 e IPv4 mapeada.", + uuid: "Identificador único universal de 128 bits proporcionado por servidores compatibles.", + vector: "Tipo vector para similitud o ML; disponibilidad y dimensiones dependen de versión.", + xmltype: "Tipo XML proporcionado por servidores compatibles.", + integerDisplayWidth: "El ancho de visualización entero, por ejemplo INT(11), está obsoleto en MySQL y no limita el rango.", + zerofill: "ZEROFILL está obsoleto en MySQL 8 e implica UNSIGNED; formatee en la aplicación.", + unsignedNumeric: "UNSIGNED para DECIMAL, FLOAT o DOUBLE está obsoleto en MySQL 8; use CHECK para no negativos.", + floatPrecision: "En FLOAT(p), p de 25 a 53 selecciona DOUBLE; use DOUBLE explícitamente.", + floatingPointDisplay: "FLOAT(M,D) y DOUBLE(M,D) están obsoletos; use FLOAT/DOUBLE sin parámetros o DECIMAL(M,D).", + }, + postgresDataTypeHelp: { + smallint: "Entero con signo de 2 bytes, de -32.768 a 32.767.", + integer: "Entero con signo de 4 bytes, de -2.147.483.648 a 2.147.483.647.", + bigint: "Entero con signo de 8 bytes, de -2^63 a 2^63-1.", + serial: "Sintaxis práctica que crea entero, secuencia, NOT NULL y nextval; no es único por sí mismo y puede tener huecos; prefiera IDENTITY.", + numeric: "Decimal exacto con precisión y escala; úselo para dinero y valores sin redondeo binario.", + real: "Flotante de precisión simple de 4 bytes; FLOAT(p) de 1 a 24 lo selecciona.", + double: "Flotante de doble precisión de 8 bytes; FLOAT sin precisión es alias y FLOAT(p) de 25 a 53 lo selecciona.", + money: "Importe monetario dependiente de lc_monetary; NUMERIC suele ser más seguro para datos portables.", + boolean: "Valor lógico verdadero, falso o nulo; BOOL es alias de BOOLEAN.", + char: "Cadena fija rellenada con espacios; CHARACTER es alias.", + varchar: "Cadena variable con límite opcional; CHARACTER VARYING es alias.", + text: "Cadena variable sin límite declarado; se almacena como VARCHAR sin límite.", + bytea: "Cadena binaria variable; TOAST puede almacenar valores grandes fuera de fila.", + date: "Fecha de calendario sin hora ni zona horaria.", + time: "Hora sin zona, con precisión fraccionaria opcional.", + timetz: "Hora con desplazamiento UTC; uso limitado, prefiera timestamptz para un instante.", + timestamp: "Fecha y hora sin conversión de zona para valores locales.", + timestamptz: "Instante almacenado en UTC y mostrado en la zona de sesión; no conserva la zona original.", + interval: "Duración en meses, días y microsegundos; las unidades de calendario no tienen duración fija.", + uuid: "Identificador único universal de 128 bits.", + json: "Texto JSON en representación original; se valida al analizar, pero suele ser más lento que JSONB.", + jsonb: "JSON binario descompuesto para procesamiento e índices; no conserva espacios, orden ni claves duplicadas.", + xml: "Valor XML con validación de documento o contenido según la opción.", + bit: "Cadena de bits fija o variable; BIT VARYING y VARBIT son alias variables.", + tsvector: "Lexemas normalizados para búsqueda de texto completo; suele indexarse con GIN o GiST.", + tsquery: "Consulta booleana de texto completo evaluada contra tsvector.", + cidr: "Red IPv4 o IPv6 con bits de host forzados a cero.", + inet: "Host IPv4 o IPv6 con máscara opcional; a diferencia de CIDR puede tener bits de host distintos de cero.", + macaddr: "Dirección MAC; MACADDR usa 6 bytes y MACADDR8 usa 8 bytes.", + geometry: "Geometría plana bidimensional sin SRID; use PostGIS para coordenadas geográficas y operaciones espaciales avanzadas.", + range: "Rango continuo con límites inclusivos/exclusivos; use multirange para rangos separados.", + oid: "Identificador de objeto sin signo del sistema; no es una clave primaria estable de aplicación.", + array: "Arreglo PostgreSQL de cualquier elemento, incluso multidimensional; las dimensiones declaradas no se fuerzan.", + multirange: "Conjunto de rangos no superpuestos; PostgreSQL normaliza miembros adyacentes o solapados.", + jsonpath: "Expresión de ruta SQL/JSON usada por consultas y predicados JSONB.", + pgLsn: "Ubicación WAL para replicación y recuperación; no es un número de secuencia de aplicación.", + pgSnapshot: "Instantánea de visibilidad de transacciones para operaciones de base de datos, no datos generales.", + }, + sqliteDataTypeHelp: { + integer: "Afinidad INTEGER; una columna normal no fuerza esa clase. INTEGER PRIMARY KEY exacto es alias de rowid.", + real: "Afinidad REAL con IEEE 754; no apta para dinero exacto.", + text: "Afinidad TEXT; VARCHAR(n) y CHAR(n) no fuerzan n; use CHECK si importa la longitud.", + blob: "Afinidad BLOB: almacena bytes sin conversión.", + numeric: "Afinidad NUMERIC convierte texto numérico primero a INTEGER y luego a REAL; DATE, DATETIME y TIME también la usan.", + }, title: "Editar estructura de tabla", createTitle: "Crear tabla", editTabTitle: "Editar: {tableName}", @@ -1941,6 +2042,15 @@ export default withEnglishFallback({ summarySize: "tamaño", }, redis: { + createKeyTypeHelp: { + string: "Valor único de hasta 512 MB; útil para caché y contadores.", + hash: "Mapa plano campo-valor; leer un Hash grande completo es O(N).", + list: "Secuencia ordenada; acceso intermedio lineal.", + set: "Miembros únicos sin orden; enumerar conjuntos grandes es lineal.", + zset: "Miembros únicos con puntuación flotante; escritura O(log N).", + stream: "Registro append-only; gestione XACK, pendientes y recorte.", + json: "JSON por ruta; requiere RedisJSON/Redis Stack y suele costar más que String o Hash.", + }, selectKey: "Selecciona una clave para ver su valor", noKeys: "No se encontraron claves", noKeysInScanHint: "No se encontraron claves en el rango de escaneo actual", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 2172f7b1f6..ff94f88808 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1747,6 +1747,107 @@ export default withEnglishFallback({ sortBy: "Ordina per", }, structureEditor: { + mysqlDataTypeHelp: { + tinyint: "Intero da 1 byte: con segno da -128 a 127, senza segno da 0 a 255.", + smallint: "Intero da 2 byte: con segno da -32.768 a 32.767, senza segno da 0 a 65.535.", + mediumint: "Intero da 3 byte: con segno da -8.388.608 a 8.388.607, senza segno da 0 a 16.777.215.", + int: "Intero da 4 byte: con segno da -2.147.483.648 a 2.147.483.647, senza segno da 0 a 4.294.967.295.", + bigint: "Intero da 8 byte: con segno da -2^63 a 2^63-1, senza segno da 0 a 2^64-1.", + decimal: "Numero decimale esatto; MySQL supporta fino a 65 cifre e scala 30. Usarlo per denaro.", + float: "Numero approssimato a precisione singola; per decimali esatti usare DECIMAL.", + double: "Numero approssimato a doppia precisione; per decimali esatti usare DECIMAL.", + real: "Alias di DOUBLE per impostazione predefinita; REAL_AS_FLOAT lo rende alias di FLOAT.", + bit: "Campo da 1 a 64 bit; larghezza predefinita 1.", + boolean: "Alias di TINYINT(1): 0 è falso e un valore non zero è vero.", + serial: "Alias pratico di BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.", + date: "Data di calendario da 1000-01-01 a 9999-12-31.", + datetime: "Data e ora senza conversione di fuso; precisione frazionaria da 0 a 6, predefinita 0.", + timestamp: "Data e ora convertita tra fuso sessione e UTC; precisione frazionaria da 0 a 6.", + time: "Ora o intervallo da -838:59:59 a 838:59:59; precisione frazionaria da 0 a 6.", + year: "Anno a quattro cifre da 1901 a 2155, oltre a 0000.", + char: "Stringa a lunghezza fissa fino a 255 caratteri, riempita con spazi.", + varchar: "Stringa a lunghezza variabile; il massimo dipende da riga e set di caratteri.", + tinytext: "Testo variabile fino a 255 byte; riga, charset e max_allowed_packet possono ridurlo.", + text: "Testo variabile fino a 65.535 byte; riga, charset e max_allowed_packet possono ridurlo.", + mediumtext: "Testo variabile fino a 16.777.215 byte; riga, charset e max_allowed_packet possono ridurlo.", + longtext: "Testo variabile fino a 4.294.967.295 byte; riga, charset e max_allowed_packet possono ridurlo.", + binary: "Stringa binaria fissa fino a 255 byte, riempita con 0x00.", + varbinary: "Stringa binaria variabile; il massimo dipende dalla dimensione della riga.", + tinyblob: "Oggetto binario fino a 255 byte; max_allowed_packet può limitarlo.", + blob: "Oggetto binario fino a 65.535 byte; max_allowed_packet può limitarlo.", + mediumblob: "Oggetto binario fino a 16.777.215 byte; max_allowed_packet può limitarlo.", + longblob: "Oggetto binario fino a 4.294.967.295 byte; max_allowed_packet può limitarlo.", + enum: "Un valore tra fino a 65.535 letterali; cambiare l'ordine può alterare il significato memorizzato.", + set: "Zero o più valori tra fino a 64 membri dichiarati.", + json: "Documento JSON validato; rappresentazione e funzioni dipendono da prodotto/versione; limitato da max_allowed_packet.", + jsonMysql: "JSON binario nativo MySQL con validazione e funzioni JSON; limitato da max_allowed_packet.", + jsonMariaDb: "In MariaDB JSON è alias di LONGTEXT con JSON_VALID e collazione utf8mb4_bin.", + nationalCharacter: "Alias di stringa nazionale; MySQL usa utf8mb3, deprecato; preferire utf8mb4.", + geometry: "Geometria spaziale; un indice SPATIAL InnoDB richiede NOT NULL e attributo SRID.", + point: "Punto spaziale; SPATIAL InnoDB richiede NOT NULL e SRID.", + linestring: "Curva spaziale; SPATIAL InnoDB richiede NOT NULL e SRID.", + polygon: "Superficie spaziale; SPATIAL InnoDB richiede NOT NULL e SRID.", + multipoint: "Raccolta di punti; SPATIAL InnoDB richiede NOT NULL e SRID.", + multilinestring: "Raccolta di curve; SPATIAL InnoDB richiede NOT NULL e SRID.", + multipolygon: "Raccolta di poligoni; SPATIAL InnoDB richiede NOT NULL e SRID.", + geometrycollection: "Raccolta di geometrie miste; SPATIAL InnoDB richiede NOT NULL e SRID.", + inet4: "Tipo IPv4 MariaDB disponibile da 10.10; memorizza l'indirizzo in quattro byte.", + inet6: "Tipo compatibile MariaDB per IPv6 e IPv4 mappato.", + uuid: "Identificatore universale univoco a 128 bit fornito da server compatibili.", + vector: "Tipo vettore per similarità o ML; disponibilità e dimensioni dipendono dalla versione.", + xmltype: "Tipo XML fornito da server compatibili.", + integerDisplayWidth: "La larghezza di visualizzazione intera, ad esempio INT(11), è deprecata e non limita l'intervallo.", + zerofill: "ZEROFILL è deprecato in MySQL 8 e implica UNSIGNED; formattare nell'applicazione.", + unsignedNumeric: "UNSIGNED per DECIMAL, FLOAT o DOUBLE è deprecato in MySQL 8; usare CHECK per valori non negativi.", + floatPrecision: "In FLOAT(p), p da 25 a 53 seleziona DOUBLE; usare DOUBLE esplicitamente.", + floatingPointDisplay: "FLOAT(M,D) e DOUBLE(M,D) sono deprecati; usare FLOAT/DOUBLE senza parametri o DECIMAL(M,D).", + }, + postgresDataTypeHelp: { + smallint: "Intero con segno da 2 byte, da -32.768 a 32.767.", + integer: "Intero con segno da 4 byte, da -2.147.483.648 a 2.147.483.647.", + bigint: "Intero con segno da 8 byte, da -2^63 a 2^63-1.", + serial: "Sintassi pratica che crea intero, sequenza, NOT NULL e default nextval; non è unico da solo e può avere lacune; preferire IDENTITY.", + numeric: "Decimale esatto con precisione e scala; usarlo per denaro e valori senza arrotondamento binario.", + real: "Floating point a precisione singola da 4 byte; FLOAT(p) da 1 a 24 lo seleziona.", + double: "Floating point a doppia precisione da 8 byte; FLOAT senza precisione è alias e FLOAT(p) da 25 a 53 lo seleziona.", + money: "Importo monetario dipendente da lc_monetary; NUMERIC è più sicuro per dati finanziari portabili.", + boolean: "Valore logico vero, falso o nullo; BOOL è alias di BOOLEAN.", + char: "Stringa fissa riempita di spazi; CHARACTER è alias.", + varchar: "Stringa variabile con limite opzionale; CHARACTER VARYING è alias.", + text: "Stringa variabile senza limite dichiarato; stessa memorizzazione di VARCHAR senza limite.", + bytea: "Stringa binaria variabile; TOAST può memorizzare valori grandi fuori riga.", + date: "Data di calendario senza ora né fuso.", + time: "Ora senza fuso, con precisione frazionaria opzionale.", + timetz: "Ora con offset UTC; uso limitato, preferire timestamptz per un istante.", + timestamp: "Data e ora senza conversione di fuso per valori locali.", + timestamptz: "Istante memorizzato in UTC e mostrato nel fuso della sessione; non conserva il fuso originale.", + interval: "Durata in mesi, giorni e microsecondi; le unità di calendario non hanno durata fissa.", + uuid: "Identificatore universale univoco a 128 bit.", + json: "Testo JSON nella rappresentazione originale; validato al parsing ma generalmente più lento di JSONB.", + jsonb: "JSON binario decomposto ottimizzato per elaborazione e indici; non conserva spazi, ordine o chiavi duplicate.", + xml: "Valore XML con validazione di documento o contenuto secondo l'opzione.", + bit: "Stringa di bit fissa o variabile; BIT VARYING e VARBIT sono alias variabili.", + tsvector: "Lessemi normalizzati per ricerca full-text; spesso indicizzati con GIN o GiST.", + tsquery: "Query full-text booleana valutata contro un tsvector.", + cidr: "Rete IPv4 o IPv6 con bit host vincolati a zero.", + inet: "Host IPv4 o IPv6 con maschera opzionale; diversamente da CIDR può avere bit host non zero.", + macaddr: "Indirizzo MAC; MACADDR usa 6 byte e MACADDR8 usa 8 byte.", + geometry: "Tipo geometrico piano bidimensionale senza SRID; usare PostGIS per coordinate geografiche e operazioni avanzate.", + range: "Intervallo continuo con limiti inclusivi/esclusivi; usare multirange per intervalli separati.", + oid: "Identificatore oggetto senza segno di sistema; non è una chiave primaria applicativa durevole.", + array: "Array PostgreSQL di qualunque elemento, anche multidimensionale; le dimensioni dichiarate non sono imposte.", + multirange: "Insieme di intervalli non sovrapposti; PostgreSQL normalizza membri adiacenti o sovrapposti.", + jsonpath: "Espressione percorso SQL/JSON usata da query e predicati JSONB.", + pgLsn: "Posizione WAL per replica e recupero; non è un numero di sequenza applicativo.", + pgSnapshot: "Snapshot di visibilità delle transazioni per operazioni database, non dati applicativi generali.", + }, + sqliteDataTypeHelp: { + integer: "Affinità INTEGER; una colonna normale non impone questa classe. INTEGER PRIMARY KEY esatto è alias di rowid.", + real: "Affinità REAL con IEEE 754; non adatta a denaro esatto.", + text: "Affinità TEXT; VARCHAR(n) e CHAR(n) non impongono n; usare CHECK se la lunghezza conta.", + blob: "Affinità BLOB: memorizza byte senza conversione.", + numeric: "Affinità NUMERIC converte testo numerico prima in INTEGER e poi in REAL; anche DATE, DATETIME e TIME la usano.", + }, title: "Modifica Struttura Tabella", createTitle: "Crea Tabella", editTabTitle: "Modifica: {tableName}", @@ -1939,6 +2040,15 @@ export default withEnglishFallback({ summarySize: "dimensione", }, redis: { + createKeyTypeHelp: { + string: "Valore singolo fino a 512 MB; utile per cache e contatori.", + hash: "Mappa campo-valore piatta; leggere un Hash grande intero è O(N).", + list: "Sequenza ordinata; accesso intermedio lineare.", + set: "Membri unici non ordinati; enumerare insiemi grandi è lineare.", + zset: "Membri unici con punteggio floating point; scrittura O(log N).", + stream: "Log append-only; gestire XACK, pendenti e trimming.", + json: "JSON per percorso; richiede RedisJSON/Redis Stack e costa di solito più di String o Hash.", + }, selectKey: "Seleziona una chiave per visualizzarne il valore", noKeys: "Nessuna chiave trovata", noKeysInScanHint: "Nessuna chiave trovata nell'intervallo di scansione corrente", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index ae22084a47..0efa4c0357 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1782,6 +1782,107 @@ export default withEnglishFallback({ sortBy: "並べ替え", }, structureEditor: { + mysqlDataTypeHelp: { + tinyint: "1 バイト整数。符号付きは -128~127、符号なしは 0~255 です。", + smallint: "2 バイト整数。符号付きは -32,768~32,767、符号なしは 0~65,535 です。", + mediumint: "3 バイト整数。符号付きは -8,388,608~8,388,607、符号なしは 0~16,777,215 です。", + int: "4 バイト整数。符号付きは -2,147,483,648~2,147,483,647、符号なしは 0~4,294,967,295 です。", + bigint: "8 バイト整数。符号付きは -2^63~2^63-1、符号なしは 0~2^64-1 です。", + decimal: "精度と小数部桁数を指定できる正確な 10 進数です。MySQL では最大 65 桁、小数部は最大 30 桁で、金額に適します。", + float: "単精度の近似数値です。正確な小数値には DECIMAL を使用してください。", + double: "倍精度の近似数値です。正確な小数値には DECIMAL を使用してください。", + real: "既定では DOUBLE の別名です。REAL_AS_FLOAT を有効にすると FLOAT の別名になります。", + bit: "1~64 ビットのビット列です。既定の幅は 1 です。", + boolean: "TINYINT(1) の別名です。0 は偽、0 以外は真として扱われます。", + serial: "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE の簡略別名です。", + date: "1000-01-01~9999-12-31 の暦日です。", + datetime: "タイムゾーン変換を行わない日時です。小数秒精度は 0~6、既定値は 0 です。", + timestamp: "セッションのタイムゾーンと UTC の間で変換される日時です。小数秒精度は 0~6 です。", + time: "-838:59:59~838:59:59 の時刻または時間間隔です。小数秒精度は 0~6 です。", + year: "1901~2155 と 0000 を格納する 4 桁の年です。", + char: "最大 255 文字の固定長文字列です。末尾は空白で埋められます。", + varchar: "可変長文字列です。実際の最大長は行サイズと文字セットに依存します。", + tinytext: "最大 255 バイトの可変長テキストです。行サイズ、文字セット、max_allowed_packet により制限される場合があります。", + text: "最大 65,535 バイトの可変長テキストです。行サイズ、文字セット、max_allowed_packet により制限される場合があります。", + mediumtext: "最大 16,777,215 バイトの可変長テキストです。行サイズ、文字セット、max_allowed_packet により制限される場合があります。", + longtext: "最大 4,294,967,295 バイトの可変長テキストです。行サイズ、文字セット、max_allowed_packet により制限される場合があります。", + binary: "最大 255 バイトの固定長バイナリ文字列です。末尾は 0x00 で埋められます。", + varbinary: "可変長バイナリ文字列です。実際の最大長は行サイズに依存します。", + tinyblob: "最大 255 バイトのバイナリオブジェクトです。max_allowed_packet により制限される場合があります。", + blob: "最大 65,535 バイトのバイナリオブジェクトです。max_allowed_packet により制限される場合があります。", + mediumblob: "最大 16,777,215 バイトのバイナリオブジェクトです。max_allowed_packet により制限される場合があります。", + longblob: "最大 4,294,967,295 バイトのバイナリオブジェクトです。max_allowed_packet により制限される場合があります。", + enum: "最大 65,535 個のリテラルから 1 つを選ぶ値です。定義順を変えると保存済み値の意味が変わることがあります。", + set: "宣言した最大 64 個のメンバーから、0 個以上を選ぶ値です。", + json: "検証済み JSON ドキュメントです。内部表現と関数は製品・バージョンに依存し、max_allowed_packet の制限を受けます。", + jsonMysql: "MySQL のネイティブなバイナリ JSON です。検証と JSON 関数を備え、max_allowed_packet の制限を受けます。", + jsonMariaDb: "MariaDB では JSON は JSON_VALID と utf8mb4_bin 照合順序を持つ LONGTEXT の別名です。", + nationalCharacter: "国別文字列型の別名です。MySQL では非推奨の utf8mb3 を使用するため、utf8mb4 を推奨します。", + geometry: "空間ジオメトリです。InnoDB の SPATIAL インデックスには NOT NULL と SRID 属性が必要です。", + point: "空間上の点です。InnoDB の SPATIAL インデックスには NOT NULL と SRID が必要です。", + linestring: "空間上の曲線です。InnoDB の SPATIAL インデックスには NOT NULL と SRID が必要です。", + polygon: "空間上の面です。InnoDB の SPATIAL インデックスには NOT NULL と SRID が必要です。", + multipoint: "点のコレクションです。InnoDB の SPATIAL インデックスには NOT NULL と SRID が必要です。", + multilinestring: "曲線のコレクションです。InnoDB の SPATIAL インデックスには NOT NULL と SRID が必要です。", + multipolygon: "ポリゴンのコレクションです。InnoDB の SPATIAL インデックスには NOT NULL と SRID が必要です。", + geometrycollection: "異なる種類のジオメトリのコレクションです。InnoDB の SPATIAL インデックスには NOT NULL と SRID が必要です。", + inet4: "MariaDB 10.10 以降で利用できる IPv4 型です。アドレスを 4 バイトで保存します。", + inet6: "IPv6 および IPv4 マップドアドレス向けの MariaDB 互換型です。", + uuid: "対応サーバーが提供する 128 ビットの UUID です。", + vector: "類似検索や機械学習向けのベクトル型です。利用可否と次元数はバージョンに依存します。", + xmltype: "対応サーバーが提供する XML 型です。", + integerDisplayWidth: "INT(11) のような整数の表示幅は MySQL で非推奨であり、値の範囲を制限しません。", + zerofill: "ZEROFILL は MySQL 8 で非推奨で、UNSIGNED を暗黙に指定します。表示はアプリケーション側で整形してください。", + unsignedNumeric: "DECIMAL、FLOAT、DOUBLE の UNSIGNED は MySQL 8 で非推奨です。非負値の制約には CHECK を使用してください。", + floatPrecision: "FLOAT(p) では p が 25~53 の場合に DOUBLE が選ばれます。DOUBLE を明示的に使用してください。", + floatingPointDisplay: "FLOAT(M,D) と DOUBLE(M,D) は非推奨です。引数なしの FLOAT/DOUBLE または DECIMAL(M,D) を使用してください。", + }, + postgresDataTypeHelp: { + smallint: "2 バイトの符号付き整数で、範囲は -32,768~32,767 です。", + integer: "4 バイトの符号付き整数で、範囲は -2,147,483,648~2,147,483,647 です。", + bigint: "8 バイトの符号付き整数で、範囲は -2^63~2^63-1 です。", + serial: "整数型、シーケンス、NOT NULL、nextval の既定値を作る簡略構文です。単独では一意でなく欠番も発生するため、IDENTITY を推奨します。", + numeric: "精度と小数部桁数を持つ正確な 10 進数です。金額やバイナリ丸めを避ける値に適します。", + real: "4 バイトの単精度浮動小数点数です。FLOAT(p) の p が 1~24 の場合に選ばれます。", + double: "8 バイトの倍精度浮動小数点数です。精度指定なしの FLOAT は別名で、FLOAT(p) の p が 25~53 の場合に選ばれます。", + money: "lc_monetary に依存する通貨額です。移植性のある金額データには NUMERIC の方が安全です。", + boolean: "真、偽、NULL を取る論理値です。BOOL は BOOLEAN の別名です。", + char: "空白で埋める固定長文字列です。CHARACTER は別名です。", + varchar: "任意の長さ上限を持てる可変長文字列です。CHARACTER VARYING は別名です。", + text: "宣言上の長さ上限がない可変長文字列です。長さ指定なしの VARCHAR と同じ形式で保存されます。", + bytea: "可変長バイナリ文字列です。TOAST により大きな値を行外に保存できます。", + date: "時刻やタイムゾーンを含まない暦日です。", + time: "タイムゾーンを含まない時刻で、小数秒精度は任意です。", + timetz: "UTC オフセットを持つ時刻です。用途は限定的で、ある時点を表すには timestamptz を推奨します。", + timestamp: "ローカルな日時に用いる、タイムゾーン変換を行わない日時です。", + timestamptz: "UTC で保存しセッションのタイムゾーンで表示する時点です。元のタイムゾーン名は保持しません。", + interval: "月、日、マイクロ秒で表す期間です。暦上の単位には固定の長さがありません。", + uuid: "128 ビットの UUID です。", + json: "元の表現を保持する JSON テキストです。解析時に検証されますが、通常は JSONB より低速です。", + jsonb: "処理とインデックス用に分解されたバイナリ JSON です。空白、キーの順序、重複キーは保持しません。", + xml: "オプションに応じて文書またはコンテンツを検証する XML 値です。", + bit: "固定長または可変長のビット列です。BIT VARYING と VARBIT は可変長の別名です。", + tsvector: "全文検索用に正規化された語彙素です。通常は GIN または GiST でインデックスします。", + tsquery: "tsvector に対して評価する全文検索の論理クエリです。", + cidr: "ホスト部ビットを 0 に固定した IPv4 または IPv6 ネットワークです。", + inet: "任意のマスクを持てる IPv4 または IPv6 ホストです。CIDR と異なり、ホスト部ビットは 0 以外にできます。", + macaddr: "MAC アドレスです。MACADDR は 6 バイト、MACADDR8 は 8 バイトを使用します。", + geometry: "SRID を持たない 2 次元平面ジオメトリです。地理座標や高度な空間演算には PostGIS を使用してください。", + range: "包含・非包含の境界を持つ連続範囲です。分離した範囲には multirange を使用してください。", + oid: "システムの符号なしオブジェクト識別子です。安定したアプリケーションの主キーではありません。", + array: "任意の要素型の PostgreSQL 配列で、多次元にもできます。宣言した次元数は強制されません。", + multirange: "重複しない範囲の集合です。PostgreSQL は隣接または重複する要素を正規化します。", + jsonpath: "JSONB のクエリや述語に使う SQL/JSON パス式です。", + pgLsn: "レプリケーションと復旧に使う WAL の位置です。アプリケーション用の連番ではありません。", + pgSnapshot: "一般的なアプリケーションデータではなく、データベース操作で使うトランザクション可視性のスナップショットです。", + }, + sqliteDataTypeHelp: { + integer: "INTEGER アフィニティです。通常の列はこのストレージクラスを強制しません。正確に INTEGER PRIMARY KEY と宣言すると rowid の別名になります。", + real: "IEEE 754 を用いる REAL アフィニティです。正確な金額には適しません。", + text: "TEXT アフィニティです。VARCHAR(n) と CHAR(n) は n を強制しないため、長さが重要なら CHECK を使用してください。", + blob: "BLOB アフィニティです。バイト列を変換せずに保存します。", + numeric: "NUMERIC アフィニティは数値テキストをまず INTEGER、次に REAL に変換します。DATE、DATETIME、TIME もこのアフィニティを持ちます。", + }, title: "テーブル構造を編集", createTitle: "テーブルを作成", editTabTitle: "編集: {tableName}", @@ -1940,6 +2041,15 @@ export default withEnglishFallback({ base64Readonly: "Base64値はこのバージョンでは読み取り専用です。", }, redis: { + createKeyTypeHelp: { + string: "最大 512 MB の単一値。キャッシュやカウンター向けです。", + hash: "フラットなフィールド値マップ。大きな Hash 全読込は O(N) です。", + list: "順序付き列。途中へのランダムアクセスは線形です。", + set: "順不同の一意メンバー集合。大集合の列挙は線形です。", + zset: "浮動小数スコア付き一意メンバー。書込みは O(log N) です。", + stream: "追記専用ログ。XACK、pending、トリミングを管理します。", + json: "パス指定 JSON。RedisJSON/Redis Stack が必要で、String/Hash より負荷が高めです。", + }, selectKey: "キーを選択して値を表示", noKeys: "キーが見つかりません", pattern: "パターン(例: user:*)", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index e1e3d720db..35e5141202 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1749,6 +1749,107 @@ export default withEnglishFallback({ sortBy: "Ordenar por", }, structureEditor: { + mysqlDataTypeHelp: { + tinyint: "Inteiro de 1 byte: com sinal, de -128 a 127; sem sinal, de 0 a 255.", + smallint: "Inteiro de 2 bytes: com sinal, de -32.768 a 32.767; sem sinal, de 0 a 65.535.", + mediumint: "Inteiro de 3 bytes: com sinal, de -8.388.608 a 8.388.607; sem sinal, de 0 a 16.777.215.", + int: "Inteiro de 4 bytes: com sinal, de -2.147.483.648 a 2.147.483.647; sem sinal, de 0 a 4.294.967.295.", + bigint: "Inteiro de 8 bytes: com sinal, de -2^63 a 2^63-1; sem sinal, de 0 a 2^64-1.", + decimal: "Número decimal exato; o MySQL aceita até 65 dígitos e escala 30. Use para valores monetários.", + float: "Número aproximado de precisão simples; para decimais exatos, use DECIMAL.", + double: "Número aproximado de dupla precisão; para decimais exatos, use DECIMAL.", + real: "Alias de DOUBLE por padrão; REAL_AS_FLOAT faz dele um alias de FLOAT.", + bit: "Campo de 1 a 64 bits; a largura padrão é 1.", + boolean: "Alias de TINYINT(1): 0 é falso e qualquer valor diferente de zero é verdadeiro.", + serial: "Alias prático de BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.", + date: "Data de calendário de 1000-01-01 a 9999-12-31.", + datetime: "Data e hora sem conversão de fuso; precisão fracionária de 0 a 6, padrão 0.", + timestamp: "Data e hora convertida entre o fuso da sessão e UTC; precisão fracionária de 0 a 6.", + time: "Hora ou intervalo de -838:59:59 a 838:59:59; precisão fracionária de 0 a 6.", + year: "Ano de quatro dígitos de 1901 a 2155, além de 0000.", + char: "String de tamanho fixo de até 255 caracteres, preenchida com espaços.", + varchar: "String de tamanho variável; o máximo efetivo depende da linha e do conjunto de caracteres.", + tinytext: "Texto variável de até 255 bytes; linha, charset e max_allowed_packet podem reduzi-lo.", + text: "Texto variável de até 65.535 bytes; linha, charset e max_allowed_packet podem reduzi-lo.", + mediumtext: "Texto variável de até 16.777.215 bytes; linha, charset e max_allowed_packet podem reduzi-lo.", + longtext: "Texto variável de até 4.294.967.295 bytes; linha, charset e max_allowed_packet podem reduzi-lo.", + binary: "String binária fixa de até 255 bytes, preenchida com 0x00.", + varbinary: "String binária de tamanho variável; o máximo efetivo depende do tamanho da linha.", + tinyblob: "Objeto binário de até 255 bytes; max_allowed_packet pode limitá-lo.", + blob: "Objeto binário de até 65.535 bytes; max_allowed_packet pode limitá-lo.", + mediumblob: "Objeto binário de até 16.777.215 bytes; max_allowed_packet pode limitá-lo.", + longblob: "Objeto binário de até 4.294.967.295 bytes; max_allowed_packet pode limitá-lo.", + enum: "Um valor entre até 65.535 literais; alterar a ordem pode mudar o significado armazenado.", + set: "Zero ou mais valores entre até 64 membros declarados.", + json: "Documento JSON validado; a representação e as funções dependem do produto e da versão; limitado por max_allowed_packet.", + jsonMysql: "JSON binário nativo do MySQL, com validação e funções JSON; limitado por max_allowed_packet.", + jsonMariaDb: "No MariaDB, JSON é um alias de LONGTEXT com JSON_VALID e collation utf8mb4_bin.", + nationalCharacter: "Alias de string nacional; o MySQL usa utf8mb3, que está obsoleto; prefira utf8mb4.", + geometry: "Geometria espacial; um índice SPATIAL do InnoDB requer NOT NULL e atributo SRID.", + point: "Ponto espacial; SPATIAL do InnoDB requer NOT NULL e SRID.", + linestring: "Curva espacial; SPATIAL do InnoDB requer NOT NULL e SRID.", + polygon: "Superfície espacial; SPATIAL do InnoDB requer NOT NULL e SRID.", + multipoint: "Coleção de pontos; SPATIAL do InnoDB requer NOT NULL e SRID.", + multilinestring: "Coleção de curvas; SPATIAL do InnoDB requer NOT NULL e SRID.", + multipolygon: "Coleção de polígonos; SPATIAL do InnoDB requer NOT NULL e SRID.", + geometrycollection: "Coleção de geometrias mistas; SPATIAL do InnoDB requer NOT NULL e SRID.", + inet4: "Tipo IPv4 do MariaDB disponível desde a versão 10.10; armazena o endereço em quatro bytes.", + inet6: "Tipo compatível com MariaDB para IPv6 e IPv4 mapeado.", + uuid: "Identificador universalmente único de 128 bits fornecido por servidores compatíveis.", + vector: "Tipo de vetor para similaridade ou ML; disponibilidade e dimensões dependem da versão.", + xmltype: "Tipo XML fornecido por servidores compatíveis.", + integerDisplayWidth: "A largura de exibição de inteiros, como INT(11), está obsoleta no MySQL e não limita o intervalo.", + zerofill: "ZEROFILL está obsoleto no MySQL 8 e implica UNSIGNED; formate na aplicação.", + unsignedNumeric: "UNSIGNED para DECIMAL, FLOAT ou DOUBLE está obsoleto no MySQL 8; use CHECK para valores não negativos.", + floatPrecision: "Em FLOAT(p), p de 25 a 53 seleciona DOUBLE; use DOUBLE explicitamente.", + floatingPointDisplay: "FLOAT(M,D) e DOUBLE(M,D) estão obsoletos; use FLOAT/DOUBLE sem parâmetros ou DECIMAL(M,D).", + }, + postgresDataTypeHelp: { + smallint: "Inteiro com sinal de 2 bytes, de -32.768 a 32.767.", + integer: "Inteiro com sinal de 4 bytes, de -2.147.483.648 a 2.147.483.647.", + bigint: "Inteiro com sinal de 8 bytes, de -2^63 a 2^63-1.", + serial: "Sintaxe prática que cria inteiro, sequência, NOT NULL e nextval; não é único por si só e pode ter lacunas; prefira IDENTITY.", + numeric: "Decimal exato com precisão e escala; use para valores monetários e valores sem arredondamento binário.", + real: "Ponto flutuante de precisão simples de 4 bytes; FLOAT(p) de 1 a 24 o seleciona.", + double: "Ponto flutuante de dupla precisão de 8 bytes; FLOAT sem precisão é um alias e FLOAT(p) de 25 a 53 o seleciona.", + money: "Valor monetário dependente de lc_monetary; NUMERIC costuma ser mais seguro para dados portáveis.", + boolean: "Valor lógico verdadeiro, falso ou nulo; BOOL é um alias de BOOLEAN.", + char: "String fixa preenchida com espaços; CHARACTER é um alias.", + varchar: "String de tamanho variável com limite opcional; CHARACTER VARYING é um alias.", + text: "String de tamanho variável sem limite declarado; é armazenada como VARCHAR sem limite.", + bytea: "String binária de tamanho variável; TOAST pode armazenar valores grandes fora da linha.", + date: "Data de calendário sem hora nem fuso horário.", + time: "Hora sem fuso horário, com precisão fracionária opcional.", + timetz: "Hora com deslocamento UTC; tem uso limitado, prefira timestamptz para representar um instante.", + timestamp: "Data e hora sem conversão de fuso para valores locais.", + timestamptz: "Instante armazenado em UTC e exibido no fuso da sessão; não preserva o fuso original.", + interval: "Duração em meses, dias e microssegundos; unidades de calendário não têm duração fixa.", + uuid: "Identificador universalmente único de 128 bits.", + json: "Texto JSON na representação original; é validado ao ser analisado, mas costuma ser mais lento que JSONB.", + jsonb: "JSON binário decomposto para processamento e índices; não preserva espaços, ordem nem chaves duplicadas.", + xml: "Valor XML com validação de documento ou conteúdo conforme a opção.", + bit: "String de bits fixa ou variável; BIT VARYING e VARBIT são aliases de tamanho variável.", + tsvector: "Lexemas normalizados para busca de texto completo; normalmente indexados com GIN ou GiST.", + tsquery: "Consulta booleana de texto completo avaliada contra tsvector.", + cidr: "Rede IPv4 ou IPv6 com os bits de host forçados a zero.", + inet: "Host IPv4 ou IPv6 com máscara opcional; ao contrário de CIDR, pode ter bits de host diferentes de zero.", + macaddr: "Endereço MAC; MACADDR usa 6 bytes e MACADDR8 usa 8 bytes.", + geometry: "Geometria plana bidimensional sem SRID; use PostGIS para coordenadas geográficas e operações espaciais avançadas.", + range: "Intervalo contínuo com limites inclusivos ou exclusivos; use multirange para intervalos separados.", + oid: "Identificador de objeto sem sinal do sistema; não é uma chave primária estável para aplicações.", + array: "Array PostgreSQL de qualquer tipo de elemento, inclusive multidimensional; as dimensões declaradas não são impostas.", + multirange: "Conjunto de intervalos não sobrepostos; o PostgreSQL normaliza membros adjacentes ou sobrepostos.", + jsonpath: "Expressão de caminho SQL/JSON usada em consultas e predicados JSONB.", + pgLsn: "Posição do WAL para replicação e recuperação; não é um número de sequência da aplicação.", + pgSnapshot: "Instantâneo de visibilidade de transações para operações de banco de dados, não para dados gerais.", + }, + sqliteDataTypeHelp: { + integer: "Afinidade INTEGER; uma coluna comum não impõe essa classe de armazenamento. INTEGER PRIMARY KEY exato é um alias de rowid.", + real: "Afinidade REAL com IEEE 754; não é adequada para valores monetários exatos.", + text: "Afinidade TEXT; VARCHAR(n) e CHAR(n) não impõem n; use CHECK se o tamanho for importante.", + blob: "Afinidade BLOB: armazena bytes sem conversão.", + numeric: "A afinidade NUMERIC converte texto numérico primeiro em INTEGER e depois em REAL; DATE, DATETIME e TIME também a usam.", + }, title: "Editar estrutura da tabela", createTitle: "Criar tabela", editTabTitle: "Editar: {tableName}", @@ -1941,6 +2042,15 @@ export default withEnglishFallback({ summarySize: "tamanho", }, redis: { + createKeyTypeHelp: { + string: "Valor único de até 512 MB; útil para cache e contadores.", + hash: "Mapa plano campo-valor; ler um Hash grande inteiro é O(N).", + list: "Sequência ordenada; acesso no meio é linear.", + set: "Membros únicos sem ordem; enumerar conjuntos grandes é linear.", + zset: "Membros únicos com pontuação de ponto flutuante; escrita O(log N).", + stream: "Log somente de acréscimo; gerencie XACK, pendências e corte.", + json: "JSON por caminho; requer RedisJSON/Redis Stack e costuma custar mais que String ou Hash.", + }, selectKey: "Selecione uma chave para ver seu valor", noKeys: "Nenhuma chave encontrada", noKeysInScanHint: "Nenhuma chave encontrada no intervalo atual de verificação", diff --git a/apps/desktop/src/i18n/locales/typeHelpOverrides.spec.ts b/apps/desktop/src/i18n/locales/typeHelpOverrides.spec.ts new file mode 100644 index 0000000000..768396eb7b --- /dev/null +++ b/apps/desktop/src/i18n/locales/typeHelpOverrides.spec.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from "vitest"; +import en from "./en"; +import esMessages from "./es"; +import itMessages from "./it"; +import jaMessages from "./ja"; +import ptBRMessages from "./pt-BR"; +import zhTWMessages from "./zh-TW"; + +type TypeHelpLocale = { structureEditor: Record>; redis: Record> }; + +const english = en as unknown as TypeHelpLocale; +const es = esMessages as unknown as TypeHelpLocale; +const itLocale = itMessages as unknown as TypeHelpLocale; +const ja = jaMessages as unknown as TypeHelpLocale; +const ptBR = ptBRMessages as unknown as TypeHelpLocale; +const zhTW = zhTWMessages as unknown as TypeHelpLocale; +const locales = { es, it: itLocale, ja, "pt-BR": ptBR, "zh-TW": zhTW }; +const directories = [ + ["structureEditor", "mysqlDataTypeHelp"], + ["structureEditor", "postgresDataTypeHelp"], + ["structureEditor", "sqliteDataTypeHelp"], + ["redis", "createKeyTypeHelp"], +] as const; + +describe("localized type help", () => { + it("provides every English help key directly in each supported locale", () => { + for (const [localeName, locale] of Object.entries(locales)) { + for (const [section, directory] of directories) { + const localized = (locale as unknown as Record>>)[section]?.[directory] ?? {}; + for (const key of Object.keys(english[section]![directory]!)) { + expect(localized[key], `${localeName}.${section}.${directory}.${key}`).toEqual(expect.any(String)); + } + } + } + }); + + it("does not silently use the English copy for representative entries", () => { + for (const locale of Object.values(locales)) { + expect(locale.structureEditor.mysqlDataTypeHelp.int).not.toBe(english.structureEditor.mysqlDataTypeHelp.int); + expect(locale.structureEditor.postgresDataTypeHelp.jsonb).not.toBe(english.structureEditor.postgresDataTypeHelp.jsonb); + expect(locale.structureEditor.sqliteDataTypeHelp.integer).not.toBe(english.structureEditor.sqliteDataTypeHelp.integer); + expect(locale.redis.createKeyTypeHelp.string).not.toBe(english.redis.createKeyTypeHelp.string); + } + }); + + it("keeps concrete database facts in the Spanish translations", () => { + expect(es.structureEditor.mysqlDataTypeHelp.int).toContain("4 bytes"); + expect(es.structureEditor.mysqlDataTypeHelp.jsonMysql).toContain("max_allowed_packet"); + expect(es.structureEditor.mysqlDataTypeHelp.geometry).toContain("SRID"); + expect(es.structureEditor.postgresDataTypeHelp.jsonb).toContain("índices"); + expect(es.structureEditor.sqliteDataTypeHelp.integer).toContain("rowid"); + expect(es.redis.createKeyTypeHelp.stream).toContain("XACK"); + }); + + it("keeps concrete database facts in the Italian translations", () => { + expect(itLocale.structureEditor.mysqlDataTypeHelp.int).toContain("4 byte"); + expect(itLocale.structureEditor.mysqlDataTypeHelp.jsonMysql).toContain("max_allowed_packet"); + expect(itLocale.structureEditor.mysqlDataTypeHelp.geometry).toContain("SRID"); + expect(itLocale.structureEditor.postgresDataTypeHelp.jsonb).toContain("indici"); + expect(itLocale.structureEditor.sqliteDataTypeHelp.integer).toContain("rowid"); + expect(itLocale.redis.createKeyTypeHelp.stream).toContain("XACK"); + }); + + it("keeps concrete database facts in the Japanese translations", () => { + expect(ja.structureEditor.mysqlDataTypeHelp.int).toContain("4 バイト"); + expect(ja.structureEditor.mysqlDataTypeHelp.jsonMariaDb).toContain("LONGTEXT"); + expect(ja.structureEditor.mysqlDataTypeHelp.geometry).toContain("SRID"); + expect(ja.structureEditor.mysqlDataTypeHelp.integerDisplayWidth).toContain("非推奨"); + expect(ja.structureEditor.postgresDataTypeHelp.jsonb).toContain("重複キー"); + expect(ja.structureEditor.postgresDataTypeHelp.pgLsn).toContain("WAL"); + expect(ja.structureEditor.sqliteDataTypeHelp.integer).toContain("rowid"); + expect(ja.structureEditor.sqliteDataTypeHelp.text).toContain("CHECK"); + expect(ja.redis.createKeyTypeHelp.stream).toContain("XACK"); + }); + + it("keeps concrete database facts in the Brazilian Portuguese translations", () => { + for (const directory of [ptBR.structureEditor.mysqlDataTypeHelp, ptBR.structureEditor.postgresDataTypeHelp, ptBR.structureEditor.sqliteDataTypeHelp]) { + expect(Object.values(directory)).not.toContain(expect.stringContaining("{type}")); + } + expect(ptBR.structureEditor.mysqlDataTypeHelp.int).toContain("4 bytes"); + expect(ptBR.structureEditor.mysqlDataTypeHelp.jsonMysql).toContain("max_allowed_packet"); + expect(ptBR.structureEditor.mysqlDataTypeHelp.jsonMariaDb).toContain("LONGTEXT"); + expect(ptBR.structureEditor.mysqlDataTypeHelp.geometry).toContain("SRID"); + expect(ptBR.structureEditor.mysqlDataTypeHelp.integerDisplayWidth).toContain("obsoleta"); + expect(ptBR.structureEditor.postgresDataTypeHelp.jsonb).toContain("chaves duplicadas"); + expect(ptBR.structureEditor.postgresDataTypeHelp.bytea).toContain("TOAST"); + expect(ptBR.structureEditor.postgresDataTypeHelp.pgLsn).toContain("WAL"); + expect(ptBR.structureEditor.sqliteDataTypeHelp.integer).toContain("rowid"); + expect(ptBR.structureEditor.sqliteDataTypeHelp.text).toContain("CHECK"); + expect(ptBR.redis.createKeyTypeHelp.stream).toContain("XACK"); + }); + + it("keeps concrete database facts in the Traditional Chinese translations", () => { + for (const directory of [zhTW.structureEditor.mysqlDataTypeHelp, zhTW.structureEditor.postgresDataTypeHelp, zhTW.structureEditor.sqliteDataTypeHelp]) { + expect(Object.values(directory)).not.toContain(expect.stringContaining("{type}")); + } + expect(zhTW.structureEditor.mysqlDataTypeHelp.int).toContain("4 位元組"); + expect(zhTW.structureEditor.mysqlDataTypeHelp.jsonMysql).toContain("max_allowed_packet"); + expect(zhTW.structureEditor.mysqlDataTypeHelp.jsonMariaDb).toContain("LONGTEXT"); + expect(zhTW.structureEditor.mysqlDataTypeHelp.geometry).toContain("SRID"); + expect(zhTW.structureEditor.mysqlDataTypeHelp.integerDisplayWidth).toContain("淘汰"); + expect(zhTW.structureEditor.postgresDataTypeHelp.jsonb).toContain("重複鍵"); + expect(zhTW.structureEditor.postgresDataTypeHelp.bytea).toContain("TOAST"); + expect(zhTW.structureEditor.postgresDataTypeHelp.pgLsn).toContain("WAL"); + expect(zhTW.structureEditor.sqliteDataTypeHelp.integer).toContain("rowid"); + expect(zhTW.structureEditor.sqliteDataTypeHelp.text).toContain("CHECK"); + expect(zhTW.redis.createKeyTypeHelp.stream).toContain("XACK"); + }); +}); diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index cf3054707e..94f6a53e7b 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1853,6 +1853,107 @@ export default withEnglishFallback({ enumValuePlaceholder: "枚举值(允许空字符串)", typePlaceholder: "选择或输入...", noMatchingType: "无匹配类型", + mysqlDataTypeHelp: { + tinyint: "1 字节整数。有符号范围:-128 到 127;无符号范围:0 到 255。", + smallint: "2 字节整数。有符号范围:-32768 到 32767;无符号范围:0 到 65535。", + mediumint: "3 字节整数。有符号范围:-8388608 到 8388607;无符号范围:0 到 16777215。", + int: "4 字节整数。有符号范围:-2147483648 到 2147483647;无符号范围:0 到 4294967295。", + bigint: "8 字节整数。有符号范围:-2^63 到 2^63-1;无符号范围:0 到 2^64-1。", + decimal: "精确的定点数。MySQL 总精度最多 65 位、小数位最多 30 位;MariaDB 的版本限制可能不同。金额等需要精确计算的值应使用此类型。", + float: "单精度近似数值。需要精确小数值时请使用 DECIMAL。", + double: "双精度近似数值。需要精确小数值时请使用 DECIMAL。", + real: "默认是 DOUBLE 的别名;启用 REAL_AS_FLOAT SQL 模式后是 FLOAT 的别名。", + bit: "位字段,长度为 1 到 64 位,默认长度为 1 位。", + boolean: "TINYINT(1) 的别名:0 表示假,非零表示真。", + serial: "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE 的便捷别名。", + date: "日历日期,支持范围为 1000-01-01 到 9999-12-31。", + datetime: "不进行时区转换的日期和时间,支持范围为 1000-01-01 00:00:00 到 9999-12-31 23:59:59;小数秒精度为 0 到 6,默认 0。", + timestamp: "会在会话时区和 UTC 间转换的日期时间,支持范围为 1970-01-01 00:00:01 UTC 到 2038-01-19 03:14:07 UTC;小数秒精度为 0 到 6,默认 0。", + time: "时间或时间间隔,支持范围为 -838:59:59 到 838:59:59;小数秒精度为 0 到 6,默认 0。", + year: "年份值。四位年份范围为 1901 到 2155,另支持 0000。", + char: "定长字符串,最多 255 个字符;存储时会在右侧补空格。", + varchar: "变长字符串;实际最大长度受行大小和字符集约束。", + tinytext: "变长文本,最多 255 字节;字符集、行大小和 max_allowed_packet 可能使实际限制更小。", + text: "变长文本,最多 65535 字节;字符集、行大小和 max_allowed_packet 可能使实际限制更小。", + mediumtext: "变长文本,最多 16777215 字节;字符集、行大小和 max_allowed_packet 可能使实际限制更小。", + longtext: "变长文本,最多 4294967295 字节(约 4 GiB);字符集、行大小和 max_allowed_packet 可能使实际限制更小。", + binary: "定长二进制字节串,最多 255 字节;存储时以 0x00 补齐。", + varbinary: "变长二进制字节串;实际最大长度受行大小约束。", + tinyblob: "二进制大对象,最多 255 字节;行大小和 max_allowed_packet 可能使实际限制更小。", + blob: "二进制大对象,最多 65535 字节;行大小和 max_allowed_packet 可能使实际限制更小。", + mediumblob: "二进制大对象,最多 16777215 字节;行大小和 max_allowed_packet 可能使实际限制更小。", + longblob: "二进制大对象,最多 4294967295 字节(约 4 GiB);行大小和 max_allowed_packet 可能使实际限制更小。", + enum: "从最多 65535 个声明字面量中选择一个值;调整字面量顺序可能改变已存储值的语义。", + set: "从最多 64 个声明成员中选择零个或多个值。", + json: "经过校验的 JSON 文档;存储形式和 JSON 功能取决于所连接的服务端产品及版本,文档大小还受 max_allowed_packet 限制。", + jsonMysql: "MySQL 原生二进制 JSON 文档类型,支持 JSON 函数和有效性校验;文档大小受 max_allowed_packet 限制。", + jsonMariaDb: "MariaDB 的 JSON 是带 JSON_VALID 约束和 utf8mb4_bin 排序规则的 LONGTEXT 别名;文档大小受 max_allowed_packet 限制。", + nationalCharacter: "国家字符集字符串别名。在 MySQL 中会使用预定义国家字符集 utf8mb3(已弃用);新建表应显式选择 utf8mb4。", + geometry: "可存储任意受支持空间几何类型的值。MySQL InnoDB 的 SPATIAL 索引要求列为 NOT NULL 且指定 SRID 属性。", + point: "由一对坐标构成的空间点。MySQL InnoDB 的 SPATIAL 索引要求列为 NOT NULL 且指定 SRID 属性。", + linestring: "由一个或多个相连点构成的空间曲线。MySQL InnoDB 的 SPATIAL 索引要求列为 NOT NULL 且指定 SRID 属性。", + polygon: "由一个或多个环围成的空间区域。MySQL InnoDB 的 SPATIAL 索引要求列为 NOT NULL 且指定 SRID 属性。", + multipoint: "空间点的集合。MySQL InnoDB 的 SPATIAL 索引要求列为 NOT NULL 且指定 SRID 属性。", + multilinestring: "空间曲线的集合。MySQL InnoDB 的 SPATIAL 索引要求列为 NOT NULL 且指定 SRID 属性。", + multipolygon: "空间多边形的集合。MySQL InnoDB 的 SPATIAL 索引要求列为 NOT NULL 且指定 SRID 属性。", + geometrycollection: "可包含混合空间几何类型的集合。MySQL InnoDB 的 SPATIAL 索引要求列为 NOT NULL 且指定 SRID 属性。", + inet4: "MariaDB 的 IPv4 地址类型(MariaDB 10.10 起提供),以 4 字节存储 IPv4 地址。", + inet6: "MariaDB 兼容的地址类型,用于 IPv6 和映射后的 IPv4 值。", + uuid: "兼容服务提供的 128 位通用唯一标识符类型。", + vector: "用于相似度检索或机器学习负载的向量类型;是否可用及维度取决于服务端版本。", + xmltype: "兼容服务提供的 XML 数据类型。", + integerDisplayWidth: "MySQL 整数显示宽度(如 INT(11))已弃用,且不会限制实际存储范围;MySQL 8 新表不应再添加此参数。", + zerofill: "ZEROFILL 已在 MySQL 8 中弃用,并会隐式使字段成为 UNSIGNED;应改在应用层处理补零展示。", + unsignedNumeric: "DECIMAL、FLOAT 或 DOUBLE 的 UNSIGNED 已在 MySQL 8 中弃用;需要非负取值范围时请使用 CHECK 约束。", + floatPrecision: "FLOAT(p) 中 p 为 25 到 53 时实际选择 DOUBLE 精度,而不是 FLOAT;需要双精度时应显式使用 DOUBLE。", + floatingPointDisplay: "FLOAT(M,D) 和 DOUBLE(M,D) 已在 MySQL 8 中弃用;请使用无显示参数的 FLOAT/DOUBLE,或对精确数值使用 DECIMAL(M,D)。", + }, + postgresDataTypeHelp: { + smallint: "2 字节有符号整数,范围为 -32768 到 32767。", + integer: "4 字节有符号整数,范围为 -2147483648 到 2147483647。", + bigint: "8 字节有符号整数,范围为 -2^63 到 2^63-1。", + serial: "便利语法:创建整数列、序列、NOT NULL 和 nextval 默认值。它本身不保证唯一且序列值可能断号;新建表建议使用 IDENTITY。", + numeric: "可指定精度和小数位的精确十进制数。金额等不能接受二进制浮点舍入的值应使用此类型。", + real: "4 字节单精度浮点数。FLOAT(p) 的 p 为 1 到 24 时选择此类型。", + double: "8 字节双精度浮点数。未指定精度的 FLOAT 是它的别名;FLOAT(p) 的 p 为 25 到 53 时选择它。", + money: "受区域格式影响的货币金额,输出和解析依赖 lc_monetary;需要可移植财务数据时通常应使用 NUMERIC。", + boolean: "逻辑真、假或空值;BOOL 是 BOOLEAN 的别名。", + char: "定长、以空格补齐的字符串。CHARACTER 是别名;若尾随空格有业务含义应谨慎使用。", + varchar: "可选长度限制的变长字符串;CHARACTER VARYING 是其别名。", + text: "无声明长度限制的变长字符串;存储行为与无长度限制的 VARCHAR 相同。", + bytea: "变长二进制字符串;大值可由 TOAST 移到行外存储。", + date: "不含时间和时区的日历日期。", + time: "不含时区的时间,可指定小数秒精度。", + timetz: "带 UTC 偏移的时间;用途有限且通常不推荐。表示时间点应使用 timestamptz。", + timestamp: "不进行时区转换的日期时间。适用于时区不属于数据本身的本地钟表时间。", + timestamptz: "时间点内部以 UTC 存储,并按会话时区显示;不会保留原始命名时区。", + interval: "以月、日和微秒表示的时间间隔;日历单位不是固定时长。", + uuid: "128 位通用唯一标识符。", + json: "保留原始输入表示的 JSON 文本,解析会校验有效性,但处理通常比 JSONB 慢。", + jsonb: "适合处理和索引的分解二进制 JSON;不会保留无意义空白、键顺序或重复键。", + xml: "XML 值;是否按文档或内容校验取决于所用 XML 选项。", + bit: "定长或变长位串。BIT VARYING 和 VARBIT 是变长形式的别名。", + tsvector: "PostgreSQL 全文检索中用于被检索文档的标准化词位;通常使用 GIN 或 GiST 索引。", + tsquery: "针对 tsvector 求值的布尔全文检索查询。", + cidr: "IPv4 或 IPv6 网络,主机位必须全为零。", + inet: "IPv4 或 IPv6 主机地址,可带网络掩码;与 CIDR 不同,主机位可以非零。", + macaddr: "MAC 地址。MACADDR 存储 6 字节地址,MACADDR8 存储 8 字节地址。", + geometry: "内置二维平面几何类型,无 SRID。地理坐标、投影或高级空间操作应使用 PostGIS。", + range: "带开闭边界的单一连续范围,值必须使用兼容子类型;多个不连续范围应使用 multirange。", + oid: "无符号对象标识符,是系统标识而非稳定的业务主键;业务数据应使用普通整数或 UUID。", + array: "任意元素类型的 PostgreSQL 数组,支持多维。声明类型不会强制数组维度;相较规范化子表,数组通常灵活性更低。", + multirange: "同一范围子类型的一组不重叠范围。PostgreSQL 会按子类型规则规范化相邻或重叠成员。", + jsonpath: "供 JSONB 查询和谓词函数使用的 SQL/JSON 路径表达式。", + pgLsn: "用于复制和恢复坐标的 WAL 位置,不是业务序列号。", + pgSnapshot: "事务可见性快照,其文本表示用于数据库快照操作,不适合作为一般业务数据。", + }, + sqliteDataTypeHelp: { + integer: "SQLite 使用动态类型和 INTEGER 亲和性;普通列并不强制 INTEGER 存储类。可转换时按有符号整数存储;精确拼写的 INTEGER PRIMARY KEY 是 rowid 的特殊别名。", + real: "REAL 亲和性存储 IEEE 754 浮点数。适合近似测量值,不适合精确金额或十进制计算。", + text: "TEXT 亲和性按数据库编码存储文本。SQLite 中 VARCHAR(n) 和 CHAR(n) 不会强制 n;若长度重要,请增加 CHECK 约束。", + blob: "BLOB 亲和性原样存储字节,不进行类型转换。", + numeric: "NUMERIC 亲和性会尝试先将格式正确的数字文本转换为 INTEGER,再转换为 REAL;它不是独立的 SQLite 存储类。DATE、DATETIME 和 TIME 声明同样具有 NUMERIC 亲和性;应统一使用 ISO-8601 TEXT、儒略日 REAL 或 Unix 时间 INTEGER。", + }, nullable: "可为空", primaryKey: "主键", defaultValue: "默认值", @@ -2077,6 +2178,15 @@ export default withEnglishFallback({ createKeyName: "Key", createKeyNamePlaceholder: "user:1", createKeyType: "类型", + createKeyTypeHelp: { + string: "单个二进制安全值,最大 512 MB。适合缓存值、计数器、标记及简单序列化数据。", + hash: "扁平的字段-值映射。单字段操作高效,但完整读取大 Hash 是 O(N)。", + list: "有序序列。头尾操作高效;中间随机访问的复杂度随列表长度线性增长。", + set: "无序且成员唯一的集合。单成员操作是 O(1),枚举或运算大集合则为线性复杂度。", + zset: "按浮点分值排序的唯一成员集合。写入为 O(log N);同分成员按字典序排序。", + stream: "追加式事件日志。消费组需要处理 XACK 与待处理消息;应裁剪 Stream 以控制保留量和内存。", + json: "可按路径读写的 JSON 文档。需要 RedisJSON/Redis Stack 的 JSON.* 支持,通常比 String 或 Hash 开销更高。", + }, createValue: "值", createMember: "成员", createField: "字段", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 950312eceb..17c14bfd5e 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1657,6 +1657,107 @@ export default withEnglishFallback({ sortBy: "排序方式", }, structureEditor: { + mysqlDataTypeHelp: { + tinyint: "1 位元組整數:有號範圍 -128 至 127;無號範圍 0 至 255。", + smallint: "2 位元組整數:有號範圍 -32,768 至 32,767;無號範圍 0 至 65,535。", + mediumint: "3 位元組整數:有號範圍 -8,388,608 至 8,388,607;無號範圍 0 至 16,777,215。", + int: "4 位元組整數:有號範圍 -2,147,483,648 至 2,147,483,647;無號範圍 0 至 4,294,967,295。", + bigint: "8 位元組整數:有號範圍 -2^63 至 2^63-1;無號範圍 0 至 2^64-1。", + decimal: "精確定點十進位數。MySQL 最多支援 65 位精度與 30 位小數位;MariaDB 的版本限制可能不同。適用於金額及其他精確值。", + float: "單精度近似數值;需要精確十進位值時請使用 DECIMAL。", + double: "雙精度近似數值;需要精確十進位值時請使用 DECIMAL。", + real: "預設為 DOUBLE 的別名;REAL_AS_FLOAT SQL 模式會使其成為 FLOAT 的別名。", + bit: "1 至 64 位元的位元欄位;預設寬度為 1 位元。", + boolean: "TINYINT(1) 的別名:0 表示 false,非 0 值表示 true。", + serial: "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE 的便利別名。", + date: "日曆日期;支援範圍為 1000-01-01 至 9999-12-31。", + datetime: "不進行時區轉換的日期與時間;支援範圍為 1000-01-01 00:00:00 至 9999-12-31 23:59:59,小數秒精度為 0 至 6(預設 0)。", + timestamp: "在工作階段時區與 UTC 間轉換的日期與時間;支援範圍為 1970-01-01 00:00:01 UTC 至 2038-01-19 03:14:07 UTC,小數秒精度為 0 至 6(預設 0)。", + time: "一天中的時間或已耗時間;支援範圍為 -838:59:59 至 838:59:59,小數秒精度為 0 至 6(預設 0)。", + year: "年份值;四位數範圍為 1901 至 2155,另包含 0000。", + char: "固定長度字串,最多 255 個字元;儲存值會在右側以空白補齊。", + varchar: "可變長度字串;實際最大長度受列大小與字元集限制。", + tinytext: "可變長度文字,最多 255 位元組;字元集、列大小與 max_allowed_packet 可能使實際上限更低。", + text: "可變長度文字,最多 65,535 位元組;字元集、列大小與 max_allowed_packet 可能使實際上限更低。", + mediumtext: "可變長度文字,最多 16,777,215 位元組;字元集、列大小與 max_allowed_packet 可能使實際上限更低。", + longtext: "可變長度文字,最多 4,294,967,295 位元組(約 4 GiB);字元集、列大小與 max_allowed_packet 可能使實際上限更低。", + binary: "固定長度二進位位元組字串,最多 255 位元組;儲存值會以 0x00 補齊。", + varbinary: "可變長度二進位位元組字串;實際最大長度受列大小限制。", + tinyblob: "二進位大型物件,最多 255 位元組;列大小與 max_allowed_packet 可能使實際上限更低。", + blob: "二進位大型物件,最多 65,535 位元組;列大小與 max_allowed_packet 可能使實際上限更低。", + mediumblob: "二進位大型物件,最多 16,777,215 位元組;列大小與 max_allowed_packet 可能使實際上限更低。", + longblob: "二進位大型物件,最多 4,294,967,295 位元組(約 4 GiB);列大小與 max_allowed_packet 可能使實際上限更低。", + enum: "從最多 65,535 個已宣告常值中選擇一個值;改變常值順序可能改變已儲存值的語意。", + set: "從最多 64 個已宣告成員中選擇零個或多個值。", + json: "經驗證的 JSON 文件;儲存表示與 JSON 功能支援取決於所連線伺服器的產品及版本;文件大小亦受 max_allowed_packet 限制。", + jsonMysql: "MySQL 原生的二進位 JSON 文件類型,具備 JSON 函式與驗證;文件大小受 max_allowed_packet 限制。", + jsonMariaDb: "MariaDB 的 JSON 是帶有 JSON_VALID 約束與 utf8mb4_bin 定序的 LONGTEXT 別名;文件大小受 max_allowed_packet 限制。", + nationalCharacter: "國家字元字串別名;MySQL 使用預先定義的國家字元集(已淘汰的 utf8mb3),新綱要應明確選用 utf8mb4。", + geometry: "任何受支援幾何類型的空間值;MySQL InnoDB SPATIAL 索引要求欄位為 NOT NULL 並具有 SRID 屬性。", + point: "含一組座標的空間點;MySQL InnoDB SPATIAL 索引要求欄位為 NOT NULL 並具有 SRID 屬性。", + linestring: "由一或多個相連點組成的空間曲線;MySQL InnoDB SPATIAL 索引要求欄位為 NOT NULL 並具有 SRID 屬性。", + polygon: "由一或多個環封閉的空間面;MySQL InnoDB SPATIAL 索引要求欄位為 NOT NULL 並具有 SRID 屬性。", + multipoint: "空間點集合;MySQL InnoDB SPATIAL 索引要求欄位為 NOT NULL 並具有 SRID 屬性。", + multilinestring: "空間曲線集合;MySQL InnoDB SPATIAL 索引要求欄位為 NOT NULL 並具有 SRID 屬性。", + multipolygon: "空間多邊形集合;MySQL InnoDB SPATIAL 索引要求欄位為 NOT NULL 並具有 SRID 屬性。", + geometrycollection: "可包含混合空間幾何類型的集合;MySQL InnoDB SPATIAL 索引要求欄位為 NOT NULL 並具有 SRID 屬性。", + inet4: "MariaDB IPv4 位址類型(MariaDB 10.10 起可用);以四個位元組儲存 IPv4 位址。", + inet6: "MariaDB 相容的 IPv6 與對應 IPv4 值位址類型。", + uuid: "相容伺服器提供的 128 位元通用唯一識別碼類型。", + vector: "用於相似度或機器學習工作負載的向量資料類型;可用性與維度取決於伺服器版本。", + xmltype: "相容伺服器提供的 XML 資料類型。", + integerDisplayWidth: "MySQL 整數顯示寬度(例如 INT(11))已淘汰,且不會限制儲存範圍;不要在新的 MySQL 8 綱要中使用。", + zerofill: "ZEROFILL 在 MySQL 8 已淘汰,且會隱含使欄位成為 UNSIGNED;請改在應用程式中明確格式化。", + unsignedNumeric: "DECIMAL、FLOAT 或 DOUBLE 的 UNSIGNED 在 MySQL 8 已淘汰;需要非負值範圍時請使用 CHECK 約束。", + floatPrecision: "對於 FLOAT(p),p 為 25 至 53 時會選擇 DOUBLE 精度而非 FLOAT;若需要此行為,請明確使用 DOUBLE。", + floatingPointDisplay: "FLOAT(M,D) 與 DOUBLE(M,D) 在 MySQL 8 已淘汰;請使用不含顯示參數的 FLOAT/DOUBLE,或以 DECIMAL(M,D) 表示精確值。", + }, + postgresDataTypeHelp: { + smallint: "2 位元組有號整數,範圍為 -32,768 至 32,767。", + integer: "4 位元組有號整數,範圍為 -2,147,483,648 至 2,147,483,647。", + bigint: "8 位元組有號整數,範圍為 -2^63 至 2^63-1。", + serial: "便利語法,會建立整數欄位、序列、NOT NULL 與 nextval 預設值;本身不保證唯一且序列值可能有缺口,新綱要建議使用 IDENTITY。", + numeric: "可自訂精度與小數位的精確十進位數;適合金額及不能使用二進位浮點捨入的值。", + real: "4 位元組單精度浮點數;FLOAT(p) 的 p 為 1 至 24 時會選擇此類型。", + double: "8 位元組雙精度浮點數;未指定精度的 FLOAT 是其別名,FLOAT(p) 的 p 為 25 至 53 時會選擇此類型。", + money: "依地區格式化的貨幣金額;輸出與解析取決於 lc_monetary,NUMERIC 通常更適合可攜的財務資料。", + boolean: "邏輯 true、false 或 null 值;BOOL 是 BOOLEAN 的別名。", + char: "以空白補齊的固定長度字串;CHARACTER 是別名,尾端空白有意義時應避免使用。", + varchar: "可選擇字元長度上限的可變長度字串;CHARACTER VARYING 是別名。", + text: "未宣告長度上限的可變長度字串;儲存行為與不設長度上限的 VARCHAR 相同。", + bytea: "可變長度二進位字串;大型值可由 TOAST 儲存在列外。", + date: "不含一天中時間或時區的日曆日期。", + time: "不含時區的一天中時間,可選擇小數秒精度。", + timetz: "一天中時間加上 UTC 位移;實用性有限且通常不建議使用,表示某個時間點應使用 timestamptz。", + timestamp: "不進行時區轉換的日期與時間;適合時區不屬於資料一部分的本地時鐘值。", + timestamptz: "內部以 UTC 儲存、依工作階段時區顯示的時間點;不保留原始具名時區。", + interval: "以月、日和微秒表示的持續時間;日曆單位不是固定長度的持續時間。", + uuid: "128 位元通用唯一識別碼。", + json: "保留原始輸入表示的 JSON 文字;會驗證解析,但處理速度通常慢於 JSONB。", + jsonb: "為處理與索引最佳化的分解式二進位 JSON 表示;不保留無意義空白、鍵順序或重複鍵。", + xml: "XML 值;是否驗證文件或內容取決於所使用的 XML 選項。", + bit: "固定或可變長度位元字串;BIT VARYING 與 VARBIT 是可變長度別名。", + tsvector: "PostgreSQL 全文搜尋中作為可搜尋文件端的正規化詞素;通常以 GIN 或 GiST 建立索引。", + tsquery: "針對 tsvector 評估的布林全文搜尋查詢。", + cidr: "主機位元被限制為零的 IPv4 或 IPv6 網路。", + inet: "可選擇網路遮罩的 IPv4 或 IPv6 主機位址;不同於 CIDR,主機位元可以非零。", + macaddr: "MAC 位址;MACADDR 儲存 6 位元組位址,MACADDR8 儲存 8 位元組位址。", + geometry: "內建二維幾何類型;PostgreSQL 的 geometry 為平面且沒有 SRID,地理座標、投影或進階空間操作請使用 PostGIS。", + range: "具有含括/不含括邊界的單一連續範圍;範圍值需要相容子類型,多個不連續範圍請使用 multirange 類型。", + oid: "無號物件識別碼;它是系統識別碼而非持久的應用程式主鍵,應用資料請使用一般整數或 UUID。", + array: "任何元素類型的 PostgreSQL 陣列,包含多維陣列;宣告類型不會強制陣列維度,且陣列通常不如正規化子列彈性。", + multirange: "相符範圍子類型的不重疊範圍集合;PostgreSQL 會依子類型規則正規化相鄰或重疊成員。", + jsonpath: "供 JSONB 查詢與述詞函式使用的 SQL/JSON 路徑運算式。", + pgLsn: "用於複寫與復原座標的預寫式日誌(WAL)位置;不是應用程式序號。", + pgSnapshot: "交易可見性快照;其文字表示供資料庫快照操作使用,並非一般應用程式資料。", + }, + sqliteDataTypeHelp: { + integer: "SQLite 使用動態型別與 INTEGER 親和性;一般欄位不會強制 INTEGER 儲存類別。值在可能時會儲存為有號整數;精確拼寫的 INTEGER PRIMARY KEY 是 rowid 的特殊別名。", + real: "REAL 親和性儲存 IEEE 754 浮點值;適合近似量測,不適合精確金額或十進位計算。", + text: "TEXT 親和性會以資料庫編碼儲存文字。SQLite 中 VARCHAR(n) 與 CHAR(n) 不會強制 n;長度上限重要時請加上 CHECK 約束。", + blob: "BLOB 親和性會完全依原樣儲存位元組,不進行型別轉換。", + numeric: "NUMERIC 親和性會先嘗試將格式正確的數值文字轉為 INTEGER,再轉為 REAL;它不是獨立的 SQLite 儲存類別。DATE、DATETIME 與 TIME 宣告也具有 NUMERIC 親和性;應一致地儲存 ISO-8601 TEXT、儒略日 REAL 或 Unix 時間 INTEGER。", + }, title: "編輯資料表結構", createTitle: "建立資料表", editTabTitle: "編輯: {tableName}", @@ -1796,6 +1897,15 @@ export default withEnglishFallback({ noJoinSql: "沒有可複製的關係 SQL", }, redis: { + createKeyTypeHelp: { + string: "最大 512 MB 的單一值;適合快取與計數器。", + hash: "扁平欄位值映射;完整讀取大型 Hash 為 O(N)。", + list: "有序序列;中間隨機存取為線性。", + set: "無序唯一成員集合;列舉大型集合為線性。", + zset: "含浮點分數的唯一成員;寫入為 O(log N)。", + stream: "追加式日誌;應處理 XACK、待處理項目與裁剪。", + json: "可依路徑存取 JSON;需要 RedisJSON/Redis Stack,通常比 String 或 Hash 開銷高。", + }, selectKey: "選擇一個 key 檢視值", noKeys: "未找到 key", noKeysInScanHint: "目前掃描範圍內未找到匹配的 key", diff --git a/apps/desktop/src/lib/__tests__/common/optionHelpPanelOffset.spec.ts b/apps/desktop/src/lib/__tests__/common/optionHelpPanelOffset.spec.ts new file mode 100644 index 0000000000..e89a0e2e60 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/common/optionHelpPanelOffset.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { optionHelpPanelOffsetTop } from "@/lib/common/optionHelpPanelOffset"; + +describe("optionHelpPanelOffsetTop", () => { + it("aligns a help panel with the active option when there is room", () => { + expect(optionHelpPanelOffsetTop({ activeItemTop: 48, listCardHeight: 260, panelHeight: 80 })).toBe(48); + }); + + it("does not produce a negative offset", () => { + expect(optionHelpPanelOffsetTop({ activeItemTop: -12, listCardHeight: 260, panelHeight: 80 })).toBe(0); + }); + + it("clamps the panel upward at the list card bottom", () => { + expect(optionHelpPanelOffsetTop({ activeItemTop: 210, listCardHeight: 260, panelHeight: 80 })).toBe(180); + }); + + it("pins a panel taller than the list card to the top", () => { + expect(optionHelpPanelOffsetTop({ activeItemTop: 48, listCardHeight: 80, panelHeight: 120 })).toBe(0); + }); +}); diff --git a/apps/desktop/src/lib/__tests__/common/searchableSelectTooltip.spec.ts b/apps/desktop/src/lib/__tests__/common/searchableSelectTooltip.spec.ts new file mode 100644 index 0000000000..2e87be7ef9 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/common/searchableSelectTooltip.spec.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { SEARCHABLE_SELECT_HELP_PANEL_ALIGN, searchableSelectKeyboardTooltipOption, searchableSelectSelectedOrFirstHelpOption } from "@/lib/common/searchableSelectTooltip"; + +describe("searchableSelectKeyboardTooltipOption", () => { + it("anchors the help panel to the trigger's left edge", () => { + expect(SEARCHABLE_SELECT_HELP_PANEL_ALIGN).toBe("start"); + }); + + it("activates the highlighted option help during Arrow-key navigation", () => { + const optionTooltip = (option: string) => (option === "json" ? "Validated JSON document" : undefined); + + // ArrowDown moves highlight from int (index 0) to json (index 1) while the search input retains focus. + expect(searchableSelectKeyboardTooltipOption(["int", "json"], 0, optionTooltip)).toBeUndefined(); + expect(searchableSelectKeyboardTooltipOption(["int", "json"], 1, optionTooltip)).toBe("json"); + }); + + it("does not activate help for the initial unhighlighted list state", () => { + const optionTooltip = () => "Type help"; + + expect(searchableSelectKeyboardTooltipOption(["int"], -1, optionTooltip)).toBeUndefined(); + }); + + it("activates selected help on open and the first matching option after search", () => { + const optionTooltip = (option: string) => (option === "json" || option === "int" ? `${option} help` : undefined); + + expect(searchableSelectSelectedOrFirstHelpOption(["int", "json"], "json", optionTooltip)).toBe("json"); + expect(searchableSelectSelectedOrFirstHelpOption(["int", "json"], "unknown", optionTooltip)).toBe("int"); + expect(searchableSelectSelectedOrFirstHelpOption(["custom"], "unknown", optionTooltip)).toBeUndefined(); + expect(searchableSelectSelectedOrFirstHelpOption(["dynamic_unknown", "json"], "dynamic_unknown", optionTooltip)).toBe("json"); + }); + + it("does not activate a tooltip when the optional callback is absent or returns no content", () => { + expect(searchableSelectKeyboardTooltipOption(["int", "json"], 1)).toBeUndefined(); + expect(searchableSelectKeyboardTooltipOption(["int", "json"], 0, () => undefined)).toBeUndefined(); + }); +}); diff --git a/apps/desktop/src/lib/__tests__/redis/redisCreateKeyTypeHelp.spec.ts b/apps/desktop/src/lib/__tests__/redis/redisCreateKeyTypeHelp.spec.ts new file mode 100644 index 0000000000..2cead83d6a --- /dev/null +++ b/apps/desktop/src/lib/__tests__/redis/redisCreateKeyTypeHelp.spec.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { REDIS_CREATE_KEY_TYPE_VALUES, getRedisCreateKeyTypeHelp, redisCreateKeyTypeHelpOptionOnOpen, shouldActivateRedisCreateKeyTypeHelpOnFocus } from "@/lib/redis/redisCreateKeyTypeHelp"; +import en from "@/i18n/locales/en"; +import zhCN from "@/i18n/locales/zh-CN"; + +function redisCreateTypeHelpMessages(locale: unknown): Record { + const messages = locale as { redis?: { createKeyTypeHelp?: Record } }; + return messages.redis?.createKeyTypeHelp ?? {}; +} + +describe("getRedisCreateKeyTypeHelp", () => { + it("covers every key type offered by the Create key dialog", () => { + for (const type of REDIS_CREATE_KEY_TYPE_VALUES) { + expect(getRedisCreateKeyTypeHelp(type)).toEqual({ key: type }); + } + }); + + it("normalizes casing and leaves unsupported types undocumented", () => { + expect(getRedisCreateKeyTypeHelp(" ZSET ")).toEqual({ key: "zset" }); + expect(getRedisCreateKeyTypeHelp("bitmap")).toBeUndefined(); + }); + + it("activates help only for focus reached through Arrow navigation", () => { + expect(shouldActivateRedisCreateKeyTypeHelpOnFocus({ openedByArrow: true, keyboardNavigating: true })).toBe(true); + expect(shouldActivateRedisCreateKeyTypeHelpOnFocus({ openedByArrow: false, keyboardNavigating: true })).toBe(true); + expect(shouldActivateRedisCreateKeyTypeHelpOnFocus({ openedByArrow: false, keyboardNavigating: false })).toBe(false); + }); + + it("shows the selected offered type's help when the Select opens", () => { + expect(redisCreateKeyTypeHelpOptionOnOpen("string")).toBe("string"); + expect(redisCreateKeyTypeHelpOptionOnOpen(" JSON ")).toBe("json"); + expect(redisCreateKeyTypeHelpOptionOnOpen("bitmap")).toBeUndefined(); + }); + + it("has English and Simplified Chinese copy for every offered type", () => { + const english = redisCreateTypeHelpMessages(en); + const chinese = redisCreateTypeHelpMessages(zhCN); + + for (const type of REDIS_CREATE_KEY_TYPE_VALUES) { + expect(english[type], `English: ${type}`).toEqual(expect.any(String)); + expect(chinese[type], `Simplified Chinese: ${type}`).toEqual(expect.any(String)); + } + }); +}); diff --git a/apps/desktop/src/lib/__tests__/table/mysqlDataTypeHelp.spec.ts b/apps/desktop/src/lib/__tests__/table/mysqlDataTypeHelp.spec.ts new file mode 100644 index 0000000000..2c0e241d79 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/table/mysqlDataTypeHelp.spec.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from "vitest"; +import { DATA_TYPE_OPTIONS } from "@/lib/table/tableStructureEditorState"; +import { getMysqlDataTypeHelp } from "@/lib/table/mysqlDataTypeHelp"; +import en from "@/i18n/locales/en"; +import zhCN from "@/i18n/locales/zh-CN"; + +function mysqlHelpMessages(locale: unknown): Record { + const messages = locale as { structureEditor?: { mysqlDataTypeHelp?: Record } }; + return messages.structureEditor?.mysqlDataTypeHelp ?? {}; +} + +describe("getMysqlDataTypeHelp", () => { + it("normalizes MySQL attributes, aliases, parameters, casing, and whitespace", () => { + expect(getMysqlDataTypeHelp(" INT(11) UNSIGNED ")).toEqual({ key: "int", warningKeys: ["integerDisplayWidth"] }); + expect(getMysqlDataTypeHelp("integer zerofill")).toEqual({ key: "int", warningKeys: ["zerofill"] }); + expect(getMysqlDataTypeHelp("NUMERIC(10, 2)")).toEqual({ key: "decimal" }); + expect(getMysqlDataTypeHelp(" double precision ")).toEqual({ key: "double" }); + expect(getMysqlDataTypeHelp("BOOL")).toEqual({ key: "boolean" }); + expect(getMysqlDataTypeHelp("DEC(20, 6)")).toEqual({ key: "decimal" }); + expect(getMysqlDataTypeHelp("CHARACTER VARYING(120)")).toEqual({ key: "varchar" }); + expect(getMysqlDataTypeHelp("CHARACTER(12)")).toEqual({ key: "char" }); + expect(getMysqlDataTypeHelp("CHAR VARYING(120)")).toEqual({ key: "varchar" }); + expect(getMysqlDataTypeHelp("CHAR BYTE")).toEqual({ key: "char" }); + }); + + it("covers phpMyAdmin's additional MySQL type aliases for dynamic type lists", () => { + expect(getMysqlDataTypeHelp("FIXED")?.key).toBe("decimal"); + expect(getMysqlDataTypeHelp("FLOAT4")?.key).toBe("float"); + expect(getMysqlDataTypeHelp("FLOAT8")?.key).toBe("double"); + expect(getMysqlDataTypeHelp("INT1")?.key).toBe("tinyint"); + expect(getMysqlDataTypeHelp("INT2")?.key).toBe("smallint"); + expect(getMysqlDataTypeHelp("INT3")?.key).toBe("mediumint"); + expect(getMysqlDataTypeHelp("INT4")?.key).toBe("int"); + expect(getMysqlDataTypeHelp("INT8")?.key).toBe("bigint"); + expect(getMysqlDataTypeHelp("MIDDLEINT")?.key).toBe("mediumint"); + expect(getMysqlDataTypeHelp("LONG")?.key).toBe("mediumtext"); + expect(getMysqlDataTypeHelp("LONG VARCHAR")?.key).toBe("mediumtext"); + expect(getMysqlDataTypeHelp("LONG VARBINARY")?.key).toBe("mediumblob"); + expect(getMysqlDataTypeHelp("GEOMCOLLECTION")?.key).toBe("geometrycollection"); + expect(getMysqlDataTypeHelp("NCHAR(30)")?.key).toBe("nationalCharacter"); + expect(getMysqlDataTypeHelp("NVARCHAR(30)")?.key).toBe("nationalCharacter"); + expect(getMysqlDataTypeHelp("NATIONAL CHARACTER VARYING(30)")?.key).toBe("nationalCharacter"); + }); + + it("documents numeric, text, binary, enum, temporal, JSON, and spatial types", () => { + expect(getMysqlDataTypeHelp("bigint unsigned")?.key).toBe("bigint"); + expect(getMysqlDataTypeHelp("varchar")?.key).toBe("varchar"); + expect(getMysqlDataTypeHelp("mediumblob")?.key).toBe("mediumblob"); + expect(getMysqlDataTypeHelp("enum")?.key).toBe("enum"); + expect(getMysqlDataTypeHelp("timestamp")?.key).toBe("timestamp"); + expect(getMysqlDataTypeHelp("json")?.key).toBe("json"); + expect(getMysqlDataTypeHelp("geometrycollection")?.key).toBe("geometrycollection"); + }); + + it("uses product-specific JSON help when the connection profile identifies the server", () => { + expect(getMysqlDataTypeHelp("json")).toEqual({ key: "json" }); + expect(getMysqlDataTypeHelp("json", { product: "mysql" })).toEqual({ key: "jsonMysql" }); + expect(getMysqlDataTypeHelp("json", { product: "mariadb" })).toEqual({ key: "jsonMariaDb" }); + }); + + it("warns about MySQL 8 numeric syntax based on the exact raw type", () => { + expect(getMysqlDataTypeHelp("INT(11) ZEROFILL")).toEqual({ key: "int", warningKeys: ["integerDisplayWidth", "zerofill"] }); + expect(getMysqlDataTypeHelp("DECIMAL(10,2) UNSIGNED")).toEqual({ key: "decimal", warningKeys: ["unsignedNumeric"] }); + expect(getMysqlDataTypeHelp("FLOAT(30)")).toEqual({ key: "float", warningKeys: ["floatPrecision"] }); + expect(getMysqlDataTypeHelp("DOUBLE(10,2) UNSIGNED")).toEqual({ key: "double", warningKeys: ["unsignedNumeric", "floatingPointDisplay"] }); + }); + + it("also recognizes compatible-server extensions and leaves unknown custom types alone", () => { + expect(getMysqlDataTypeHelp("inet4")?.key).toBe("inet4"); + expect(getMysqlDataTypeHelp("inet6")?.key).toBe("inet6"); + expect(getMysqlDataTypeHelp("uuid")?.key).toBe("uuid"); + expect(getMysqlDataTypeHelp("vector(1536)")?.key).toBe("vector"); + expect(getMysqlDataTypeHelp("xmltype")?.key).toBe("xmltype"); + expect(getMysqlDataTypeHelp("my_private_type")).toBeUndefined(); + }); + + it("covers every built-in MySQL type option", () => { + const undocumented = DATA_TYPE_OPTIONS.mysql.filter((type) => !getMysqlDataTypeHelp(type)); + expect(undocumented).toEqual([]); + }); + + it("has an English and Simplified Chinese message for every supported help key", () => { + const options = [...DATA_TYPE_OPTIONS.mysql, "inet4", "inet6", "uuid", "vector", "xmltype", "character", "char varying", "char byte", "geomcollection", "nchar", "nvarchar", "national character varying", "int(11) zerofill", "decimal(10,2) unsigned", "float(30)", "double(10,2)"]; + const english = mysqlHelpMessages(en); + const chinese = mysqlHelpMessages(zhCN); + + for (const option of options) { + const key = getMysqlDataTypeHelp(option)?.key; + expect(key, option).toBeTruthy(); + for (const helpKey of [key!, ...(getMysqlDataTypeHelp(option)?.warningKeys ?? [])]) { + expect(english[helpKey], `English: ${option}`).toEqual(expect.any(String)); + expect(chinese[helpKey], `Simplified Chinese: ${option}`).toEqual(expect.any(String)); + } + } + }); +}); diff --git a/apps/desktop/src/lib/__tests__/table/postgresDataTypeHelp.spec.ts b/apps/desktop/src/lib/__tests__/table/postgresDataTypeHelp.spec.ts new file mode 100644 index 0000000000..29b91db056 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/table/postgresDataTypeHelp.spec.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "vitest"; +import { DATA_TYPE_OPTIONS } from "@/lib/table/tableStructureEditorState"; +import { getPostgresDataTypeHelp } from "@/lib/table/postgresDataTypeHelp"; +import en from "@/i18n/locales/en"; +import zhCN from "@/i18n/locales/zh-CN"; + +function postgresHelpMessages(locale: unknown): Record { + const messages = locale as { structureEditor?: { postgresDataTypeHelp?: Record } }; + return messages.structureEditor?.postgresDataTypeHelp ?? {}; +} + +describe("getPostgresDataTypeHelp", () => { + it("covers every built-in PostgreSQL type option", () => { + expect(DATA_TYPE_OPTIONS.postgres.filter((type) => !getPostgresDataTypeHelp(type))).toEqual([]); + }); + + it("normalizes aliases and float precision parameters", () => { + expect(getPostgresDataTypeHelp("int2")).toEqual({ key: "smallint" }); + expect(getPostgresDataTypeHelp("INT4")).toEqual({ key: "integer" }); + expect(getPostgresDataTypeHelp("int8")).toEqual({ key: "bigint" }); + expect(getPostgresDataTypeHelp("decimal(10, 2)")).toEqual({ key: "numeric" }); + expect(getPostgresDataTypeHelp("float")).toEqual({ key: "double" }); + expect(getPostgresDataTypeHelp("float(24)")).toEqual({ key: "real" }); + expect(getPostgresDataTypeHelp("float(25)")).toEqual({ key: "double" }); + expect(getPostgresDataTypeHelp("float(0)")).toBeUndefined(); + expect(getPostgresDataTypeHelp("float(54)")).toBeUndefined(); + expect(getPostgresDataTypeHelp("float(1.5)")).toBeUndefined(); + expect(getPostgresDataTypeHelp("float(precision)")).toBeUndefined(); + expect(getPostgresDataTypeHelp("character varying(255)")).toEqual({ key: "varchar" }); + }); + + it("documents serial, temporal aliases, dynamic built-ins, and arrays", () => { + expect(getPostgresDataTypeHelp("bigserial")).toEqual({ key: "serial" }); + expect(getPostgresDataTypeHelp("timestamp with time zone")).toEqual({ key: "timestamptz" }); + expect(getPostgresDataTypeHelp("int4multirange")).toEqual({ key: "multirange" }); + expect(getPostgresDataTypeHelp("jsonpath")).toEqual({ key: "jsonpath" }); + expect(getPostgresDataTypeHelp("pg_lsn")).toEqual({ key: "pgLsn" }); + expect(getPostgresDataTypeHelp("pg_snapshot")).toEqual({ key: "pgSnapshot" }); + expect(getPostgresDataTypeHelp("uuid[][]")).toEqual({ key: "array" }); + expect(getPostgresDataTypeHelp("my_schema.order_status[]")).toEqual({ key: "array" }); + }); + + it("leaves extensions and named user types undocumented", () => { + expect(getPostgresDataTypeHelp("vector(1536)")).toBeUndefined(); + expect(getPostgresDataTypeHelp("geometry(Point, 4326)")).toBeUndefined(); + expect(getPostgresDataTypeHelp("citext")).toBeUndefined(); + expect(getPostgresDataTypeHelp("hstore")).toBeUndefined(); + expect(getPostgresDataTypeHelp("order_status")).toBeUndefined(); + expect(getPostgresDataTypeHelp("order_status_multirange")).toBeUndefined(); + }); + + it("has English and Simplified Chinese copy for every supported static key", () => { + const english = postgresHelpMessages(en); + const chinese = postgresHelpMessages(zhCN); + const dynamic = ["int4multirange", "jsonpath", "pg_lsn", "pg_snapshot", "any_type[]"]; + + for (const option of [...DATA_TYPE_OPTIONS.postgres, ...dynamic]) { + const key = getPostgresDataTypeHelp(option)?.key; + expect(key, option).toBeTruthy(); + expect(english[key!], `English: ${option}`).toEqual(expect.any(String)); + expect(chinese[key!], `Simplified Chinese: ${option}`).toEqual(expect.any(String)); + } + }); +}); diff --git a/apps/desktop/src/lib/__tests__/table/sqliteDataTypeHelp.spec.ts b/apps/desktop/src/lib/__tests__/table/sqliteDataTypeHelp.spec.ts new file mode 100644 index 0000000000..dc79e74bc6 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/table/sqliteDataTypeHelp.spec.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { DATA_TYPE_OPTIONS } from "@/lib/table/tableStructureEditorState"; +import { getSqliteDataTypeHelp } from "@/lib/table/sqliteDataTypeHelp"; +import en from "@/i18n/locales/en"; +import zhCN from "@/i18n/locales/zh-CN"; + +function sqliteHelpMessages(locale: unknown): Record { + const messages = locale as { structureEditor?: { sqliteDataTypeHelp?: Record } }; + return messages.structureEditor?.sqliteDataTypeHelp ?? {}; +} + +describe("getSqliteDataTypeHelp", () => { + it("covers every built-in SQLite type option", () => { + expect(DATA_TYPE_OPTIONS.sqlite.filter((type) => !getSqliteDataTypeHelp(type))).toEqual([]); + }); + + it("normalizes whitespace, casing, and declared type parameters", () => { + expect(getSqliteDataTypeHelp(" INTEGER ")).toEqual({ key: "integer" }); + expect(getSqliteDataTypeHelp("VARCHAR(255)")).toEqual({ key: "text" }); + expect(getSqliteDataTypeHelp(" decimal(10, 2) ")).toEqual({ key: "numeric" }); + }); + + it("applies SQLite's ordered declared-type affinity rules", () => { + expect(getSqliteDataTypeHelp("char(12)")).toEqual({ key: "text" }); + expect(getSqliteDataTypeHelp("boolean")).toEqual({ key: "numeric" }); + expect(getSqliteDataTypeHelp("unsigned big int")).toEqual({ key: "integer" }); + expect(getSqliteDataTypeHelp("double precision")).toEqual({ key: "real" }); + expect(getSqliteDataTypeHelp("date")).toEqual({ key: "numeric" }); + expect(getSqliteDataTypeHelp("datetime")).toEqual({ key: "numeric" }); + expect(getSqliteDataTypeHelp("time")).toEqual({ key: "numeric" }); + expect(getSqliteDataTypeHelp("VARCHAR2")).toEqual({ key: "text" }); + expect(getSqliteDataTypeHelp("CHARACTER")).toEqual({ key: "text" }); + expect(getSqliteDataTypeHelp("INT64")).toEqual({ key: "integer" }); + expect(getSqliteDataTypeHelp("FLOATING POINT")).toEqual({ key: "integer" }); + expect(getSqliteDataTypeHelp("STRING")).toEqual({ key: "numeric" }); + expect(getSqliteDataTypeHelp("application_status")).toEqual({ key: "numeric" }); + expect(getSqliteDataTypeHelp("")).toEqual({ key: "blob" }); + }); + + it("has English and Simplified Chinese copy for static and common help keys", () => { + const english = sqliteHelpMessages(en); + const chinese = sqliteHelpMessages(zhCN); + const types = [...DATA_TYPE_OPTIONS.sqlite, "varchar", "boolean", "datetime"]; + + for (const type of types) { + const key = getSqliteDataTypeHelp(type)?.key; + expect(key, type).toBeTruthy(); + expect(english[key!], `English: ${type}`).toEqual(expect.any(String)); + expect(chinese[key!], `Simplified Chinese: ${type}`).toEqual(expect.any(String)); + } + }); + + it("calls out SQLite affinity and its important storage exceptions", () => { + const english = sqliteHelpMessages(en); + + expect(english.integer).toContain("rowid"); + expect(english.integer).toContain("does not enforce"); + expect(english.real).toContain("IEEE 754"); + expect(english.text).toContain("do not enforce n"); + expect(english.blob).toContain("without type conversion"); + expect(english.numeric).toContain("not a separate SQLite storage class"); + }); +}); diff --git a/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts b/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts index 59d03a041e..87bd1e95b2 100644 --- a/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts @@ -3,6 +3,8 @@ import { combineDataTypeForDatabase, createColumnDrafts, dataTypeLengthInputValue, + DATA_TYPE_OPTIONS, + defaultNewColumnDataType, getDefaultLengthForType, hasExistingColumnTypeChange, isDataTypeLengthDisabled, @@ -109,6 +111,30 @@ describe("tableStructureEditorState", () => { expect(getDefaultLengthForType("mysql", "integer")).toBe("11"); }); + it("uses MySQL 8-safe defaults only when the native MySQL profile is known", () => { + const mysql8Defaults = { omitMysqlDeprecatedDefaults: true }; + + expect(getDefaultLengthForType("mysql", "int", mysql8Defaults)).toBe(""); + expect(getDefaultLengthForType("mysql", "bigint unsigned", mysql8Defaults)).toBe(""); + expect(getDefaultLengthForType("mysql", "float", mysql8Defaults)).toBe(""); + expect(getDefaultLengthForType("mysql", "double", mysql8Defaults)).toBe(""); + expect(combineDataTypeForDatabase("mysql", "int", getDefaultLengthForType("mysql", "int", mysql8Defaults))).toBe("int"); + expect(combineDataTypeForDatabase("mysql", "float", getDefaultLengthForType("mysql", "float", mysql8Defaults))).toBe("float"); + expect(getDefaultLengthForType("mysql", "decimal", mysql8Defaults)).toBe("10,0"); + + // A compatibility profile cannot be version-identified, so its existing behavior is retained. + expect(getDefaultLengthForType("mysql", "int")).toBe("11"); + expect(getDefaultLengthForType("mysql", "float")).toBe("10,2"); + }); + + it("uses TEXT for a new native SQLite column without changing compatible defaults", () => { + expect(DATA_TYPE_OPTIONS.sqlite).toContain("text"); + expect(defaultNewColumnDataType("sqlite")).toBe("text"); + expect(defaultNewColumnDataType("rqlite")).toBe("varchar(255)"); + expect(defaultNewColumnDataType("turso")).toBe("varchar(255)"); + expect(defaultNewColumnDataType("mysql")).toBe("varchar(255)"); + }); + it("requires a SQLite rebuild only for a retained existing column type change", () => { const [column] = createColumnDrafts( [ diff --git a/apps/desktop/src/lib/common/optionHelpPanelOffset.ts b/apps/desktop/src/lib/common/optionHelpPanelOffset.ts new file mode 100644 index 0000000000..cfa1c6bc7f --- /dev/null +++ b/apps/desktop/src/lib/common/optionHelpPanelOffset.ts @@ -0,0 +1,13 @@ +export interface OptionHelpPanelOffsetInput { + activeItemTop: number; + listCardHeight: number; + panelHeight: number; +} + +/** Aligns a help card to its active row without allowing it past the list card. */ +export function optionHelpPanelOffsetTop({ activeItemTop, listCardHeight, panelHeight }: OptionHelpPanelOffsetInput): number { + if (![activeItemTop, listCardHeight, panelHeight].every(Number.isFinite)) return 0; + const desiredTop = Math.max(0, activeItemTop); + const maxTop = Math.max(0, listCardHeight - Math.max(0, panelHeight)); + return Math.min(desiredTop, maxTop); +} diff --git a/apps/desktop/src/lib/common/searchableSelectTooltip.ts b/apps/desktop/src/lib/common/searchableSelectTooltip.ts new file mode 100644 index 0000000000..cd6a232d2f --- /dev/null +++ b/apps/desktop/src/lib/common/searchableSelectTooltip.ts @@ -0,0 +1,19 @@ +export type SearchableSelectOptionTooltip = (option: string) => string | undefined; + +/** Keep the option list aligned with its trigger while help expands rightward. */ +export const SEARCHABLE_SELECT_HELP_PANEL_ALIGN = "start"; + +/** + * Returns the option whose help panel is activated by keyboard navigation + * while focus remains in the searchable input. Empty-string options are valid. + */ +export function searchableSelectKeyboardTooltipOption(options: readonly string[], highlightIndex: number, optionTooltip?: SearchableSelectOptionTooltip): string | undefined { + const option = options[highlightIndex]; + return option === undefined || !optionTooltip?.(option) ? undefined : option; +} + +/** Returns the selected option's help, or the first available option's help. */ +export function searchableSelectSelectedOrFirstHelpOption(options: readonly string[], selectedOption: string, optionTooltip?: SearchableSelectOptionTooltip): string | undefined { + if (options.includes(selectedOption) && optionTooltip?.(selectedOption)) return selectedOption; + return options.find((option) => !!optionTooltip?.(option)); +} diff --git a/apps/desktop/src/lib/redis/redisCreateKeyTypeHelp.ts b/apps/desktop/src/lib/redis/redisCreateKeyTypeHelp.ts new file mode 100644 index 0000000000..1663cc3ae5 --- /dev/null +++ b/apps/desktop/src/lib/redis/redisCreateKeyTypeHelp.ts @@ -0,0 +1,41 @@ +export const REDIS_CREATE_KEY_TYPE_VALUES = ["string", "hash", "list", "set", "zset", "stream", "json"] as const; + +export type RedisCreateKeyTypeValue = (typeof REDIS_CREATE_KEY_TYPE_VALUES)[number]; +export type RedisCreateKeyTypeHelpKey = RedisCreateKeyTypeValue; + +export interface RedisCreateKeyTypeHelp { + key: RedisCreateKeyTypeHelpKey; +} + +export interface RedisCreateKeyTypeHelpFocusState { + openedByArrow: boolean; + keyboardNavigating: boolean; +} + +const REDIS_CREATE_KEY_TYPE_HELP: Readonly> = { + string: "string", + hash: "hash", + list: "list", + set: "set", + zset: "zset", + stream: "stream", + json: "json", +}; + +/** Returns help for a Redis data type offered by the Create key dialog. */ +export function getRedisCreateKeyTypeHelp(rawType: string): RedisCreateKeyTypeHelp | undefined { + const type = rawType.trim().toLowerCase() as RedisCreateKeyTypeValue; + const key = REDIS_CREATE_KEY_TYPE_HELP[type]; + return key ? { key } : undefined; +} + +/** Returns the selected offered type whose help is shown when the Select opens. */ +export function redisCreateKeyTypeHelpOptionOnOpen(rawType: string): RedisCreateKeyTypeValue | undefined { + const type = rawType.trim().toLowerCase() as RedisCreateKeyTypeValue; + return REDIS_CREATE_KEY_TYPE_HELP[type] ? type : undefined; +} + +/** Determines whether a SelectItem focus event was caused by Arrow navigation. */ +export function shouldActivateRedisCreateKeyTypeHelpOnFocus(state: RedisCreateKeyTypeHelpFocusState): boolean { + return state.openedByArrow || state.keyboardNavigating; +} diff --git a/apps/desktop/src/lib/table/mysqlDataTypeHelp.ts b/apps/desktop/src/lib/table/mysqlDataTypeHelp.ts new file mode 100644 index 0000000000..e26c729103 --- /dev/null +++ b/apps/desktop/src/lib/table/mysqlDataTypeHelp.ts @@ -0,0 +1,199 @@ +export type MysqlDataTypeHelpKey = + | "tinyint" + | "smallint" + | "mediumint" + | "int" + | "bigint" + | "decimal" + | "float" + | "double" + | "real" + | "bit" + | "boolean" + | "serial" + | "date" + | "datetime" + | "timestamp" + | "time" + | "year" + | "char" + | "varchar" + | "tinytext" + | "text" + | "mediumtext" + | "longtext" + | "binary" + | "varbinary" + | "tinyblob" + | "blob" + | "mediumblob" + | "longblob" + | "enum" + | "set" + | "json" + | "jsonMysql" + | "jsonMariaDb" + | "nationalCharacter" + | "geometry" + | "point" + | "linestring" + | "polygon" + | "multipoint" + | "multilinestring" + | "multipolygon" + | "geometrycollection" + | "inet4" + | "inet6" + | "uuid" + | "vector" + | "xmltype" + | "integerDisplayWidth" + | "zerofill" + | "unsignedNumeric" + | "floatPrecision" + | "floatingPointDisplay"; + +export interface MysqlDataTypeHelp { + key: MysqlDataTypeHelpKey; + warningKeys?: MysqlDataTypeHelpKey[]; +} + +export interface MysqlDataTypeHelpOptions { + /** Set only when the selected connection profile identifies the server product. */ + product?: "mysql" | "mariadb"; +} + +const MYSQL_TYPE_HELP_KEYS: Readonly> = { + tinyint: "tinyint", + smallint: "smallint", + mediumint: "mediumint", + int: "int", + bigint: "bigint", + decimal: "decimal", + float: "float", + double: "double", + real: "real", + bit: "bit", + boolean: "boolean", + nationalCharacter: "nationalCharacter", + serial: "serial", + date: "date", + datetime: "datetime", + timestamp: "timestamp", + time: "time", + year: "year", + char: "char", + varchar: "varchar", + tinytext: "tinytext", + text: "text", + mediumtext: "mediumtext", + longtext: "longtext", + binary: "binary", + varbinary: "varbinary", + tinyblob: "tinyblob", + blob: "blob", + mediumblob: "mediumblob", + longblob: "longblob", + enum: "enum", + set: "set", + json: "json", + geometry: "geometry", + point: "point", + linestring: "linestring", + polygon: "polygon", + multipoint: "multipoint", + multilinestring: "multilinestring", + multipolygon: "multipolygon", + geometrycollection: "geometrycollection", + inet4: "inet4", + inet6: "inet6", + uuid: "uuid", + vector: "vector", + xmltype: "xmltype", +}; + +const MYSQL_TYPE_ALIASES: Readonly> = { + integer: "int", + numeric: "decimal", + dec: "decimal", + fixed: "decimal", + bool: "boolean", + "character varying": "varchar", + character: "char", + "char varying": "varchar", + "char byte": "char", + nchar: "nationalCharacter", + nvarchar: "nationalCharacter", + "nchar varying": "nationalCharacter", + "national char": "nationalCharacter", + "national character": "nationalCharacter", + "national varchar": "nationalCharacter", + "national char varying": "nationalCharacter", + "national character varying": "nationalCharacter", + "double precision": "double", + float4: "float", + float8: "double", + int1: "tinyint", + int2: "smallint", + int3: "mediumint", + int4: "int", + int8: "bigint", + middleint: "mediumint", + long: "mediumtext", + "long varchar": "mediumtext", + "long varbinary": "mediumblob", + geomcollection: "geometrycollection", +}; + +/** + * Returns help for a MySQL-family type name without making a database request. + * The caller is responsible for limiting this to connections that use MySQL + * semantics, since several compatible databases accept similarly named types + * with different limits. + */ +export function getMysqlDataTypeHelp(rawType: string, options: MysqlDataTypeHelpOptions = {}): MysqlDataTypeHelp | undefined { + const normalized = normalizeMysqlType(rawType); + const resolvedType = MYSQL_TYPE_ALIASES[normalized] ?? normalized; + const baseKey = MYSQL_TYPE_HELP_KEYS[resolvedType]; + if (!baseKey) return undefined; + + const key = resolvedType === "json" ? jsonHelpKey(options.product) : baseKey; + const warningKeys = mysqlDataTypeWarningKeys(rawType, resolvedType); + return warningKeys.length > 0 ? { key, warningKeys } : { key }; +} + +function jsonHelpKey(product: MysqlDataTypeHelpOptions["product"]): MysqlDataTypeHelpKey { + if (product === "mysql") return "jsonMysql"; + if (product === "mariadb") return "jsonMariaDb"; + return "json"; +} + +function mysqlDataTypeWarningKeys(rawType: string, resolvedType: string): MysqlDataTypeHelpKey[] { + const lowered = rawType.trim().toLowerCase().replace(/\s+/g, " "); + const warnings: MysqlDataTypeHelpKey[] = []; + const isInteger = ["tinyint", "smallint", "mediumint", "int", "bigint"].includes(resolvedType); + const isNumeric = isInteger || ["decimal", "float", "double", "real"].includes(resolvedType); + + if (isInteger && /\(\s*\d+\s*\)/.test(lowered)) warnings.push("integerDisplayWidth"); + if (isNumeric && /\bzerofill\b/.test(lowered)) warnings.push("zerofill"); + if (["decimal", "float", "double", "real"].includes(resolvedType) && /\bunsigned\b/.test(lowered)) warnings.push("unsignedNumeric"); + + const parameters = lowered.match(/\(([^)]*)\)/)?.[1]?.trim() ?? ""; + if (resolvedType === "float" && /^\d+$/.test(parameters)) { + const precision = Number(parameters); + if (precision >= 25 && precision <= 53) warnings.push("floatPrecision"); + } + if (["float", "double"].includes(resolvedType) && /^\d+\s*,\s*\d+$/.test(parameters)) warnings.push("floatingPointDisplay"); + + return warnings; +} + +function normalizeMysqlType(rawType: string): string { + return rawType + .trim() + .toLowerCase() + .replace(/\([^)]*\)/g, " ") + .replace(/\b(?:unsigned|signed|zerofill)\b/g, " ") + .replace(/\s+/g, " ") + .trim(); +} diff --git a/apps/desktop/src/lib/table/postgresDataTypeHelp.ts b/apps/desktop/src/lib/table/postgresDataTypeHelp.ts new file mode 100644 index 0000000000..d4218c5a5b --- /dev/null +++ b/apps/desktop/src/lib/table/postgresDataTypeHelp.ts @@ -0,0 +1,155 @@ +export type PostgresDataTypeHelpKey = + | "smallint" + | "integer" + | "bigint" + | "serial" + | "numeric" + | "real" + | "double" + | "money" + | "boolean" + | "char" + | "varchar" + | "text" + | "bytea" + | "date" + | "time" + | "timetz" + | "timestamp" + | "timestamptz" + | "interval" + | "uuid" + | "json" + | "jsonb" + | "xml" + | "bit" + | "tsvector" + | "tsquery" + | "cidr" + | "inet" + | "macaddr" + | "geometry" + | "range" + | "oid" + | "array" + | "multirange" + | "jsonpath" + | "pgLsn" + | "pgSnapshot"; + +export interface PostgresDataTypeHelp { + key: PostgresDataTypeHelpKey; +} + +const POSTGRES_TYPE_HELP_KEYS: Readonly> = { + smallint: "smallint", + integer: "integer", + bigint: "bigint", + smallserial: "serial", + serial: "serial", + bigserial: "serial", + numeric: "numeric", + real: "real", + "double precision": "double", + money: "money", + boolean: "boolean", + char: "char", + varchar: "varchar", + text: "text", + bytea: "bytea", + date: "date", + time: "time", + timetz: "timetz", + timestamp: "timestamp", + timestamptz: "timestamptz", + interval: "interval", + uuid: "uuid", + json: "json", + jsonb: "jsonb", + xml: "xml", + bit: "bit", + "bit varying": "bit", + tsvector: "tsvector", + tsquery: "tsquery", + cidr: "cidr", + inet: "inet", + macaddr: "macaddr", + macaddr8: "macaddr", + point: "geometry", + line: "geometry", + lseg: "geometry", + box: "geometry", + path: "geometry", + polygon: "geometry", + circle: "geometry", + int4range: "range", + int8range: "range", + numrange: "range", + tsrange: "range", + tstzrange: "range", + daterange: "range", + oid: "oid", + jsonpath: "jsonpath", + pg_lsn: "pgLsn", + pg_snapshot: "pgSnapshot", +}; + +const POSTGRES_TYPE_ALIASES: Readonly> = { + int2: "smallint", + int: "integer", + int4: "integer", + int8: "bigint", + decimal: "numeric", + float4: "real", + float8: "double precision", + bool: "boolean", + character: "char", + "character varying": "varchar", + "time without time zone": "time", + "time with time zone": "timetz", + "timestamp without time zone": "timestamp", + "timestamp with time zone": "timestamptz", + varbit: "bit varying", +}; + +const POSTGRES_MULTIRANGE_TYPES = new Set(["int4multirange", "int8multirange", "nummultirange", "tsmultirange", "tstzmultirange", "datemultirange"]); + +/** + * Returns static PostgreSQL type help without querying the server. Extension + * types and user-defined types intentionally remain undocumented. + */ +export function getPostgresDataTypeHelp(rawType: string): PostgresDataTypeHelp | undefined { + const normalized = normalizePostgresType(rawType); + if (isPostgresArrayType(normalized)) return { key: "array" }; + + const baseType = normalized + .replace(/\([^)]*\)/g, " ") + .replace(/\s+/g, " ") + .trim(); + if (baseType === "float") { + const key = floatHelpKey(normalized); + return key ? { key } : undefined; + } + if (POSTGRES_MULTIRANGE_TYPES.has(baseType)) return { key: "multirange" }; + + const canonicalType = POSTGRES_TYPE_ALIASES[baseType] ?? baseType; + const key = POSTGRES_TYPE_HELP_KEYS[canonicalType]; + return key ? { key } : undefined; +} + +function normalizePostgresType(rawType: string): string { + return rawType.trim().toLowerCase().replace(/\s+/g, " "); +} + +function isPostgresArrayType(normalizedType: string): boolean { + return /(?:\[\s*\]\s*)+$/.test(normalizedType); +} + +function floatHelpKey(normalizedType: string): "real" | "double" | undefined { + if (normalizedType === "float") return "double"; + const precision = normalizedType.match(/^float\(\s*(\d+)\s*\)$/)?.[1]; + if (!precision) return undefined; + const value = Number(precision); + if (value >= 1 && value <= 24) return "real"; + return value >= 25 && value <= 53 ? "double" : undefined; +} diff --git a/apps/desktop/src/lib/table/sqliteDataTypeHelp.ts b/apps/desktop/src/lib/table/sqliteDataTypeHelp.ts new file mode 100644 index 0000000000..63d74fbcb4 --- /dev/null +++ b/apps/desktop/src/lib/table/sqliteDataTypeHelp.ts @@ -0,0 +1,24 @@ +export type SqliteDataTypeHelpKey = "integer" | "real" | "text" | "blob" | "numeric"; + +export interface SqliteDataTypeHelp { + key: SqliteDataTypeHelpKey; +} + +/** + * Maps declared types using SQLite's ordered affinity rules. The order matters: + * for example, FLOATING POINT has INTEGER affinity because it contains "INT". + */ +export function getSqliteDataTypeHelp(rawType: string): SqliteDataTypeHelp | undefined { + const normalized = rawType + .trim() + .toLowerCase() + .replace(/\([^)]*\)/g, " ") + .replace(/\s+/g, " ") + .trim(); + if (!normalized) return { key: "blob" }; + if (normalized.includes("int")) return { key: "integer" }; + if (normalized.includes("char") || normalized.includes("clob") || normalized.includes("text")) return { key: "text" }; + if (normalized.includes("blob")) return { key: "blob" }; + if (normalized.includes("real") || normalized.includes("floa") || normalized.includes("doub")) return { key: "real" }; + return { key: "numeric" }; +} diff --git a/apps/desktop/src/lib/table/tableStructureEditorState.ts b/apps/desktop/src/lib/table/tableStructureEditorState.ts index 1f0d648e35..aa566321bd 100644 --- a/apps/desktop/src/lib/table/tableStructureEditorState.ts +++ b/apps/desktop/src/lib/table/tableStructureEditorState.ts @@ -965,8 +965,17 @@ function isValidTemporalPrecision(dbType: DatabaseType | undefined, params: stri return Number.isInteger(value) && value >= 0 && value <= max && String(value) === params; } -export function getDefaultLengthForType(_dbType: DatabaseType | undefined, baseType: string): string { +export interface DataTypeDefaultOptions { + /** + * Native MySQL profiles use MySQL 8-safe defaults. Compatibility profiles + * retain their existing DDL defaults because their server/version is unknown. + */ + omitMysqlDeprecatedDefaults?: boolean; +} + +export function getDefaultLengthForType(_dbType: DatabaseType | undefined, baseType: string, options: DataTypeDefaultOptions = {}): string { const key = baseType.trim().toLowerCase(); + if (_dbType === "mysql" && options.omitMysqlDeprecatedDefaults && isMysqlDeprecatedDefaultParameterType(key)) return ""; if (_dbType === "sqlite" || _dbType === "rqlite" || _dbType === "turso") { return ""; } else if (_dbType === "questdb") { @@ -978,6 +987,20 @@ export function getDefaultLengthForType(_dbType: DatabaseType | undefined, baseT } } +/** Default data type for a newly added structure-editor column. */ +export function defaultNewColumnDataType(dbType: DatabaseType | undefined, dataTypeOptions: readonly string[] = []): string { + if (dbType === "manticoresearch") { + const baseType = dataTypeOptions[0] ?? "text"; + return combineDataTypeForDatabase(dbType, baseType, getDefaultLengthForType(dbType, baseType)); + } + return dbType === "sqlite" ? "text" : "varchar(255)"; +} + +function isMysqlDeprecatedDefaultParameterType(baseType: string): boolean { + const typeName = baseType.split(/\s+/)[0]; + return ["tinyint", "smallint", "mediumint", "int", "integer", "bigint", "float", "double", "real"].includes(typeName ?? ""); +} + export function isDataTypeLengthDisabled(_dbType: DatabaseType | undefined, baseType: string): boolean { const key = baseType.trim().toLowerCase(); if (_dbType === "questdb") { diff --git a/apps/desktop/src/styles/globals.css b/apps/desktop/src/styles/globals.css index b6937da96d..91e3629d6a 100644 --- a/apps/desktop/src/styles/globals.css +++ b/apps/desktop/src/styles/globals.css @@ -1952,6 +1952,16 @@ body.dbx-table-reference-dragging * { box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45) !important; } +/* A few compound pickers render their own separate surface cards. */ +[data-slot="select-content"][data-naked-surface], +.dark [data-slot="select-content"][data-naked-surface] { + background: transparent !important; + border: 0 !important; + box-shadow: none !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; +} + [data-slot="context-menu-item"]:hover, [data-slot="context-menu-item"]:focus, [data-slot="context-menu-item"][data-highlighted],