feat(postgres): add support hash index types (#812)

* feat(postgres): add support for hash index type with single column constraint

* some fixes

* some fixes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
This commit is contained in:
Jonathan Fishner
2025-08-10 20:24:34 +03:00
committed by GitHub
parent 19fd94c6bd
commit 0d623a86b1
27 changed files with 115 additions and 11 deletions

View File

@@ -18,7 +18,7 @@ import {
import { useStorage } from '@/hooks/use-storage';
import { useChartDB } from '@/hooks/use-chartdb';
import { filterTable } from '@/lib/domain/diagram-filter/filter';
import { schemaNameToSchemaId } from '@/lib/domain';
import { databasesWithSchemas, schemaNameToSchemaId } from '@/lib/domain';
import { defaultSchemas } from '@/lib/data/default-schemas';
import type { ChartDBEvent } from '../chartdb-context/chartdb-context';
@@ -246,7 +246,7 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
const toggleTableFilter: DiagramFilterContext['toggleTableFilter'] =
useCallback(
(tableId: string) => {
if (!defaultSchemas[databaseType]) {
if (!databasesWithSchemas.includes(databaseType)) {
// No schemas, toggle table filter without schema context
toggleTableFilterForNoSchema(tableId);
return;

View File

@@ -149,6 +149,7 @@ export const ar: LanguageTranslation = {
title: 'خصائص الفهرس',
name: 'الإسم',
unique: 'فريد',
index_type: 'نوع الفهرس',
delete_index: 'حذف الفهرس',
},
table_actions: {

View File

@@ -151,6 +151,7 @@ export const bn: LanguageTranslation = {
title: 'ইনডেক্স কর্ম',
name: 'নাম',
unique: 'অদ্বিতীয়',
index_type: 'ইনডেক্স ধরন',
delete_index: 'ইনডেক্স মুছুন',
},
table_actions: {

View File

@@ -152,6 +152,7 @@ export const de: LanguageTranslation = {
title: 'Indexattribute',
name: 'Name',
unique: 'Eindeutig',
index_type: 'Indextyp',
delete_index: 'Index löschen',
},
table_actions: {

View File

@@ -145,6 +145,7 @@ export const en = {
title: 'Index Attributes',
name: 'Name',
unique: 'Unique',
index_type: 'Index Type',
delete_index: 'Delete Index',
},
table_actions: {

View File

@@ -150,6 +150,7 @@ export const es: LanguageTranslation = {
title: 'Atributos del Índice',
name: 'Nombre',
unique: 'Único',
index_type: 'Tipo de Índice',
delete_index: 'Eliminar Índice',
},
table_actions: {

View File

@@ -148,6 +148,7 @@ export const fr: LanguageTranslation = {
title: "Attributs de l'Index",
name: 'Nom',
unique: 'Unique',
index_type: "Type d'index",
delete_index: "Supprimer l'Index",
},
table_actions: {

View File

@@ -152,6 +152,7 @@ export const gu: LanguageTranslation = {
title: 'ઇન્ડેક્સ લક્ષણો',
name: 'નામ',
unique: 'અદ્વિતીય',
index_type: 'ઇન્ડેક્સ પ્રકાર',
delete_index: 'ઇન્ડેક્સ કાઢી નાખો',
},
table_actions: {

View File

@@ -151,6 +151,7 @@ export const hi: LanguageTranslation = {
title: 'सूचकांक विशेषताएँ',
name: 'नाम',
unique: 'अद्वितीय',
index_type: 'इंडेक्स प्रकार',
delete_index: 'सूचकांक हटाएँ',
},
table_actions: {

View File

@@ -146,6 +146,7 @@ export const hr: LanguageTranslation = {
title: 'Atributi indeksa',
name: 'Naziv',
unique: 'Jedinstven',
index_type: 'Vrsta indeksa',
delete_index: 'Izbriši indeks',
},
table_actions: {

View File

@@ -150,6 +150,7 @@ export const id_ID: LanguageTranslation = {
title: 'Atribut Indeks',
name: 'Nama',
unique: 'Unik',
index_type: 'Tipe Indeks',
delete_index: 'Hapus Indeks',
},
table_actions: {

View File

@@ -154,6 +154,7 @@ export const ja: LanguageTranslation = {
title: 'インデックス属性',
name: '名前',
unique: 'ユニーク',
index_type: 'インデックスタイプ',
delete_index: 'インデックスを削除',
},
table_actions: {

View File

@@ -150,6 +150,7 @@ export const ko_KR: LanguageTranslation = {
title: '인덱스 속성',
name: '인덱스 명',
unique: '유니크 여부',
index_type: '인덱스 타입',
delete_index: '인덱스 삭제',
},
table_actions: {

View File

@@ -153,6 +153,7 @@ export const mr: LanguageTranslation = {
title: 'इंडेक्स गुणधर्म',
name: 'नाव',
unique: 'युनिक',
index_type: 'इंडेक्स प्रकार',
delete_index: 'इंडेक्स हटवा',
},
table_actions: {

View File

@@ -151,6 +151,7 @@ export const ne: LanguageTranslation = {
title: 'सूचक विशेषताहरू',
name: 'नाम',
unique: 'अनन्य',
index_type: 'इन्डेक्स प्रकार',
delete_index: 'सूचक हटाउनुहोस्',
},
table_actions: {

View File

@@ -151,6 +151,7 @@ export const pt_BR: LanguageTranslation = {
title: 'Atributos do Índice',
name: 'Nome',
unique: 'Único',
index_type: 'Tipo de Índice',
delete_index: 'Excluir Índice',
},
table_actions: {

View File

@@ -147,6 +147,7 @@ export const ru: LanguageTranslation = {
title: 'Атрибуты индекса',
name: 'Имя',
unique: 'Уникальный',
index_type: 'Тип индекса',
delete_index: 'Удалить индекс',
},
table_actions: {

View File

@@ -151,6 +151,7 @@ export const te: LanguageTranslation = {
title: 'ఇండెక్స్ గుణాలు',
name: 'పేరు',
unique: 'అద్వితీయ',
index_type: 'ఇండెక్స్ రకం',
delete_index: 'ఇండెక్స్ తొలగించు',
},
table_actions: {

View File

@@ -150,6 +150,7 @@ export const tr: LanguageTranslation = {
title: 'İndeks Özellikleri',
name: 'Ad',
unique: 'Tekil',
index_type: 'İndeks Türü',
delete_index: 'İndeksi Sil',
},
table_actions: {

View File

@@ -149,6 +149,7 @@ export const uk: LanguageTranslation = {
title: 'Атрибути індексу',
name: 'Назва індекса',
unique: 'Унікальний',
index_type: 'Тип індексу',
delete_index: 'Видалити індекс',
},
table_actions: {

View File

@@ -150,6 +150,7 @@ export const vi: LanguageTranslation = {
title: 'Thuộc tính chỉ mục',
name: 'Tên',
unique: 'Giá trị duy nhất',
index_type: 'Loại chỉ mục',
delete_index: 'Xóa chỉ mục',
},
table_actions: {

View File

@@ -147,6 +147,7 @@ export const zh_CN: LanguageTranslation = {
title: '索引属性',
name: '名称',
unique: '唯一',
index_type: '索引类型',
delete_index: '删除索引',
},
table_actions: {

View File

@@ -147,6 +147,7 @@ export const zh_TW: LanguageTranslation = {
title: '索引屬性',
name: '名稱',
unique: '唯一',
index_type: '索引類型',
delete_index: '刪除索引',
},
table_actions: {

View File

@@ -405,7 +405,7 @@ export function exportPostgreSQL({
.filter(Boolean);
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldNames.join(', ')});`
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName}${index.type && index.type !== 'btree' ? ` USING ${index.type.toUpperCase()}` : ''} (${indexFieldNames.join(', ')});`
: '';
})
.filter(Boolean);

View File

@@ -2,6 +2,17 @@ import { z } from 'zod';
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
import { generateId } from '../utils';
import type { DBField } from './db-field';
import { DatabaseType } from './database-type';
export const INDEX_TYPES = [
'btree',
'hash',
'gist',
'gin',
'spgist',
'brin',
] as const;
export type IndexType = (typeof INDEX_TYPES)[number];
export interface DBIndex {
id: string;
@@ -9,6 +20,7 @@ export interface DBIndex {
unique: boolean;
fieldIds: string[];
createdAt: number;
type?: IndexType | null;
}
export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
@@ -17,6 +29,7 @@ export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
unique: z.boolean(),
fieldIds: z.array(z.string()),
createdAt: z.number(),
type: z.enum(INDEX_TYPES).optional(),
});
export const createIndexesFromMetadata = ({
@@ -36,5 +49,10 @@ export const createIndexesFromMetadata = ({
.map((c) => fields.find((f) => f.name === c.name)?.id)
.filter((id): id is string => id !== undefined),
createdAt: Date.now(),
type: idx.index_type?.toLowerCase() as IndexType,
})
);
export const databaseIndexTypes: { [key in DatabaseType]?: IndexType[] } = {
[DatabaseType.POSTGRESQL]: ['btree', 'hash'],
};

View File

@@ -16,7 +16,6 @@ import type { TreeNode } from '@/components/tree-view/tree';
import { ScrollArea } from '@/components/scroll-area/scroll-area';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
import { defaultSchemas } from '@/lib/data/default-schemas';
import type {
GroupingMode,
NodeContext,
@@ -26,6 +25,7 @@ import type {
} from './types';
import { generateTreeDataByAreas, generateTreeDataBySchemas } from './utils';
import { FilterItemActions } from './filter-item-actions';
import { databasesWithSchemas } from '@/lib/domain';
export interface CanvasFilterProps {
onClose: () => void;
@@ -63,7 +63,7 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
);
const databaseWithSchemas = useMemo(
() => !!defaultSchemas[databaseType],
() => databasesWithSchemas.includes(databaseType),
[databaseType]
);

View File

@@ -1,7 +1,11 @@
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import { Ellipsis, Trash2 } from 'lucide-react';
import { Button } from '@/components/button/button';
import type { DBIndex } from '@/lib/domain/db-index';
import {
databaseIndexTypes,
type DBIndex,
type IndexType,
} from '@/lib/domain/db-index';
import type { DBField } from '@/lib/domain/db-field';
import {
Popover,
@@ -20,6 +24,7 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useChartDB } from '@/hooks/use-chartdb';
export interface TableIndexProps {
index: DBIndex;
@@ -28,6 +33,11 @@ export interface TableIndexProps {
fields: DBField[];
}
const allIndexTypeOptions: { label: string; value: IndexType }[] = [
{ label: 'B-tree (default)', value: 'btree' },
{ label: 'Hash', value: 'hash' },
];
export const TableIndex: React.FC<TableIndexProps> = ({
fields,
index,
@@ -35,14 +45,51 @@ export const TableIndex: React.FC<TableIndexProps> = ({
removeIndex,
}) => {
const { t } = useTranslation();
const { databaseType } = useChartDB();
const fieldOptions = fields.map((field) => ({
label: field.name,
value: field.id,
}));
const updateIndexFields = (fieldIds: string | string[]) => {
const ids = Array.isArray(fieldIds) ? fieldIds : [fieldIds];
updateIndex({ fieldIds: ids });
};
const updateIndexFields = useCallback(
(fieldIds: string | string[]) => {
const ids = Array.isArray(fieldIds) ? fieldIds : [fieldIds];
// For hash indexes, only keep the last selected field
if (index.type === 'hash' && ids.length > 0) {
updateIndex({ fieldIds: [ids[ids.length - 1]] });
} else {
updateIndex({ fieldIds: ids });
}
},
[index.type, updateIndex]
);
const indexTypeOptions = useMemo(
() =>
allIndexTypeOptions.filter((option) =>
databaseIndexTypes[databaseType]?.includes(option.value)
),
[databaseType]
);
const updateIndexType = useCallback(
(value: string | string[]) => {
{
const newType = value as IndexType;
// If switching to hash and multiple fields are selected, keep only the first
if (newType === 'hash' && index.fieldIds.length > 1) {
updateIndex({
type: newType,
fieldIds: [index.fieldIds[0]],
});
} else {
updateIndex({ type: newType });
}
}
},
[updateIndex, index.fieldIds]
);
return (
<div className="flex flex-1 flex-row justify-between gap-2 p-1">
<SelectBox
@@ -135,6 +182,23 @@ export const TableIndex: React.FC<TableIndexProps> = ({
}
/>
</div>
{indexTypeOptions.length > 0 ? (
<div className="mt-2 flex flex-col gap-2">
<Label
htmlFor="indexType"
className="text-subtitle"
>
{t(
'side_panel.tables_section.table.index_actions.index_type'
)}
</Label>
<SelectBox
options={indexTypeOptions}
value={index.type || 'btree'}
onChange={updateIndexType}
/>
</div>
) : null}
<Separator orientation="horizontal" />
<Button
variant="outline"