feat(types): add database type help

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-07-12 13:28:46 +08:00
1 parent fdb33ef477
commit 4f1997d8eb
30 files changed
+2006 -79

No files matched your search

@@ -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<boolean | null>(null);
const checkingJsonModule = ref(false);
const activeCreateKeyTypeHelp = ref<RedisCreateKeyType>();
const createKeyTypeKeyboardNavigation = ref(false);
const createKeyTypeOpenedByArrow = ref(false);
const createKeyTypeListCard = ref<HTMLElement>();
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<HTMLElement>(`[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 });
<label class="grid gap-1.5 text-xs font-medium">
<span>{{ t("redis.createKeyType") }}</span>
<Select :model-value="createKeyType" @update:model-value="onCreateKeyTypeChange">
<SelectTrigger class="h-8 text-xs">
<Select :model-value="createKeyType" @update:open="onCreateKeyTypeSelectOpen" @update:model-value="onCreateKeyTypeChange">
<SelectTrigger class="h-8 text-xs" @keydown.capture="onCreateKeyTypeTriggerKeydown">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="option in createKeyTypeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</SelectItem>
<SelectContent data-naked-surface class="w-auto max-w-[calc(100vw-1rem)] border-0 bg-transparent p-0 shadow-none ring-0">
<div class="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-start" @keydown.capture="onCreateKeyTypeSelectKeydown">
<div ref="createKeyTypeListCard" class="min-w-40 rounded-md border bg-popover p-1 shadow-md" @scroll="updateCreateKeyTypeHelpOffset">
<SelectItem v-for="option in createKeyTypeOptions" :key="option.value" :value="option.value" :data-option-help-value="option.value" @pointerenter="activateCreateKeyTypeHelp(option.value)" @focus="onCreateKeyTypeOptionFocus(option.value)">
{{ option.label }}
</SelectItem>
</div>
<OptionHelpPanel v-if="activeCreateKeyTypeHelpContent" ref="createKeyTypeHelpPanel" :content="activeCreateKeyTypeHelpContent" :offset-top="createKeyTypeHelpOffsetTop" />
</div>
</SelectContent>
</Select>
</label>
@@ -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)"
/>
@@ -0,0 +1,21 @@
<script setup lang="ts">
import { ref } from "vue";
import { OPTION_HELP_PANEL_CLASS } from "./optionHelpPanel";
const props = withDefaults(
defineProps<{
content: string;
offsetTop?: number;
}>(),
{ offsetTop: 0 },
);
const element = ref<HTMLElement>();
defineExpose({ element });
</script>
<template>
<aside ref="element" data-slot="option-help-panel" role="note" :class="OPTION_HELP_PANEL_CLASS" :style="{ '--option-help-offset': `${offsetTop}px` }">
{{ content }}
</aside>
</template>
@@ -0,0 +1 @@
export { default as OptionHelpPanel } from "./OptionHelpPanel.vue";
@@ -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)]");
});
});
@@ -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))]";
@@ -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<InstanceType<typeof Input>>();
const listContainer = ref<HTMLDivElement>();
const listCard = ref<HTMLElement>();
const helpPanel = ref<{ element?: HTMLElement }>();
const highlightIndex = ref(-1);
const activeHelpOption = ref<string>();
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) {
<ChevronDown v-else :class="cn('shrink-0 opacity-60', triggerIconClass)" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" :class="cn('w-52 gap-1 p-1.5', contentClass)">
<div class="relative rounded-sm border bg-background">
<Search class="pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
<span v-if="!searchText" class="pointer-events-none absolute left-[25px] top-1/2 -translate-y-1/2 text-sm text-muted-foreground">{{ searchPlaceholder }}</span>
<Input ref="searchInput" :model-value="searchText" class="h-6 border-0 pl-6 pr-2 text-sm caret-foreground shadow-none focus-visible:ring-0" @update:model-value="(value) => (searchText = String(value))" @keydown="handleKeydown" />
</div>
<div ref="listContainer" class="max-h-64 overflow-y-auto py-1">
<div v-if="loading" class="px-2 py-2 text-sm text-muted-foreground">
{{ loadingText }}
</div>
<template v-else-if="filteredOptions.length">
<button
v-for="(option, index) in filteredOptions"
:key="option"
type="button"
:title="optionTitle(option)"
:class="
cn(
'flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none',
props.itemClass,
index === highlightIndex && 'bg-accent text-accent-foreground',
)
"
@click="selectOption(option)"
>
<Check :class="cn('h-3.5 w-3.5 shrink-0', option === modelValue ? 'opacity-100' : 'opacity-0')" />
<slot name="option-label" :option="option" :label="displayName?.(option)">
<span class="truncate">{{ displayName?.(option) }}</span>
</slot>
</button>
<button
v-if="canSelectCustom"
type="button"
:title="customOptionValue"
:class="
cn(
'flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none',
props.itemClass,
filteredOptions.length === highlightIndex && 'bg-accent text-accent-foreground',
)
"
@click="selectCustomOption"
>
<Check class="h-3.5 w-3.5 shrink-0 opacity-0" />
<slot name="custom-option-label" :value="customOptionValue">
<span class="truncate">{{ customOptionValue }}</span>
</slot>
</button>
</template>
<button
v-else-if="canSelectCustom"
type="button"
:title="customOptionValue"
:class="
cn(
'flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none',
props.itemClass,
0 === highlightIndex && 'bg-accent text-accent-foreground',
)
"
@click="selectCustomOption"
>
<Check class="h-3.5 w-3.5 shrink-0 opacity-0" />
<slot name="custom-option-label" :value="customOptionValue">
<span class="truncate">{{ customOptionValue }}</span>
</slot>
</button>
<div v-else class="px-2 py-2 text-sm text-muted-foreground">
{{ emptyText }}
<PopoverContent :align="SEARCHABLE_SELECT_HELP_PANEL_ALIGN" :class="cn('w-auto max-w-[calc(100vw-1rem)] border-0 bg-transparent p-0 shadow-none ring-0', contentClass)">
<div class="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-start">
<div ref="listCard" class="w-52 shrink-0 rounded-md border bg-popover p-1.5 shadow-md">
<div class="relative rounded-sm border bg-background">
<Search class="pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
<span v-if="!searchText" class="pointer-events-none absolute left-[25px] top-1/2 -translate-y-1/2 text-sm text-muted-foreground">{{ searchPlaceholder }}</span>
<Input ref="searchInput" :model-value="searchText" class="h-6 border-0 pl-6 pr-2 text-sm caret-foreground shadow-none focus-visible:ring-0" @update:model-value="(value) => (searchText = String(value))" @keydown="handleKeydown" />
</div>
<div ref="listContainer" class="max-h-64 overflow-y-auto py-1" @scroll="updateHelpPanelOffset">
<div v-if="loading" class="px-2 py-2 text-sm text-muted-foreground">
{{ loadingText }}
</div>
<template v-else-if="filteredOptions.length">
<button
v-for="(option, index) in filteredOptions"
:key="option"
type="button"
:title="optionTooltip(option) ? undefined : optionTitle(option)"
:class="
cn(
'flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none',
props.itemClass,
index === highlightIndex && 'bg-accent text-accent-foreground',
)
"
@pointerenter="activateHelpForOption(option)"
@click="selectOption(option)"
>
<Check :class="cn('h-3.5 w-3.5 shrink-0', option === modelValue ? 'opacity-100' : 'opacity-0')" />
<slot name="option-label" :option="option" :label="displayName?.(option)">
<span class="truncate">{{ displayName?.(option) }}</span>
</slot>
</button>
<button
v-if="canSelectCustom"
type="button"
:title="customOptionValue"
:class="
cn(
'flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none',
props.itemClass,
filteredOptions.length === highlightIndex && 'bg-accent text-accent-foreground',
)
"
@pointerenter="activateHelpForOption(customOptionValue)"
@click="selectCustomOption"
>
<Check class="h-3.5 w-3.5 shrink-0 opacity-0" />
<slot name="custom-option-label" :value="customOptionValue">
<span class="truncate">{{ customOptionValue }}</span>
</slot>
</button>
</template>
<button
v-else-if="canSelectCustom"
type="button"
:title="customOptionValue"
:class="
cn(
'flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none',
props.itemClass,
0 === highlightIndex && 'bg-accent text-accent-foreground',
)
"
@pointerenter="activateHelpForOption(customOptionValue)"
@click="selectCustomOption"
>
<Check class="h-3.5 w-3.5 shrink-0 opacity-0" />
<slot name="custom-option-label" :value="customOptionValue">
<span class="truncate">{{ customOptionValue }}</span>
</slot>
</button>
<div v-else class="px-2 py-2 text-sm text-muted-foreground">
{{ emptyText }}
</div>
</div>
</div>
<OptionHelpPanel v-if="activeHelpContent" ref="helpPanel" :content="activeHelpContent" :offset-top="helpPanelOffsetTop" />
</div>
</PopoverContent>
</Popover>
+110
View File
@@ -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",
+110
View File
@@ -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",
+110
View File
@@ -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",
+110
View File
@@ -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:*)",
+110
View File
@@ -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",
+109
View File
@@ -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<string, Record<string, string>>; redis: Record<string, Record<string, string>> };
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<string, Record<string, Record<string, unknown>>>)[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");
});
});
+110
View File
@@ -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: "字段",
+110
View File
@@ -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",
@@ -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);
});
});
@@ -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();
});
});
@@ -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<string, unknown> {
const messages = locale as { redis?: { createKeyTypeHelp?: Record<string, unknown> } };
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));
}
});
});
@@ -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<string, unknown> {
const messages = locale as { structureEditor?: { mysqlDataTypeHelp?: Record<string, unknown> } };
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));
}
}
});
});
@@ -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<string, unknown> {
const messages = locale as { structureEditor?: { postgresDataTypeHelp?: Record<string, unknown> } };
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));
}
});
});
@@ -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<string, unknown> {
const messages = locale as { structureEditor?: { sqliteDataTypeHelp?: Record<string, unknown> } };
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");
});
});
@@ -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(
[
@@ -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);
}
@@ -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));
}
@@ -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<Record<RedisCreateKeyTypeValue, RedisCreateKeyTypeHelpKey>> = {
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;
}
@@ -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<Record<string, MysqlDataTypeHelpKey>> = {
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<Record<string, string>> = {
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();
}
@@ -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<Record<string, PostgresDataTypeHelpKey>> = {
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<Record<string, string>> = {
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;
}
@@ -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" };
}
@@ -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") {
+10
View File
@@ -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],