feat: add GIN index and array type support for PostgreSQL/CockroachDB (#1048)

* feat: add GIN index and array type support for PostgreSQL/CockroachDB

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
This commit is contained in:
Jonathan Fishner
2026-01-07 18:00:42 +02:00
committed by GitHub
parent 63febde3f7
commit de26f731be
10 changed files with 504 additions and 79 deletions

View File

@@ -59,6 +59,7 @@ export interface SelectBoxProps {
commandOnMouseDown?: (e: React.MouseEvent) => void;
commandOnClick?: (e: React.MouseEvent) => void;
onSearchChange?: (search: string) => void;
modal?: boolean;
}
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
@@ -89,6 +90,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
commandOnMouseDown,
commandOnClick,
onSearchChange,
modal = true,
},
ref
) => {
@@ -312,7 +314,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
);
return (
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
<Popover open={isOpen} onOpenChange={onOpenChange} modal={modal}>
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
<div
className={cn(

View File

@@ -56,6 +56,7 @@ export interface SQLIndex {
name: string;
columns: string[];
unique: boolean;
type?: string; // Index type (btree, hash, gin, gist, etc.)
}
export interface SQLForeignKey {
@@ -614,6 +615,9 @@ export const typeAffinity: Record<string, Record<string, string>> = {
},
};
// CockroachDB uses PostgreSQL-compatible types - reference dynamically
typeAffinity[DatabaseType.COCKROACHDB] = typeAffinity[DatabaseType.POSTGRESQL];
// Convert SQLParserResult to ChartDB Diagram structure
export function convertToChartDBDiagram(
parserResult: SQLParserResult,
@@ -636,9 +640,18 @@ export function convertToChartDBDiagram(
// Use special case handling for specific database types to ensure correct mapping
let mappedType: DataType;
// Detect and handle array types (e.g., int[], text[], varchar[])
const isArrayType = column.type.endsWith('[]');
const baseColumnType = isArrayType
? column.type.slice(0, -2)
: column.type;
// Create a modified column object with the base type for mapping
const columnForMapping = { ...column, type: baseColumnType };
// SQLite-specific handling for numeric types
if (sourceDatabaseType === DatabaseType.SQLITE) {
const normalizedType = column.type.toLowerCase();
const normalizedType = columnForMapping.type.toLowerCase();
if (normalizedType === 'integer' || normalizedType === 'int') {
// Ensure integer types are preserved
@@ -658,7 +671,7 @@ export function convertToChartDBDiagram(
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
column.type,
columnForMapping.type,
sourceDatabaseType
);
}
@@ -668,7 +681,7 @@ export function convertToChartDBDiagram(
sourceDatabaseType === DatabaseType.MYSQL ||
sourceDatabaseType === DatabaseType.MARIADB
) {
const normalizedType = column.type
const normalizedType = columnForMapping.type
.toLowerCase()
.replace(/\(\d+\)/, '')
.trim();
@@ -690,17 +703,18 @@ export function convertToChartDBDiagram(
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
column.type,
columnForMapping.type,
sourceDatabaseType
);
}
}
// Handle PostgreSQL integer type specifically
// Handle PostgreSQL/CockroachDB integer type specifically
else if (
sourceDatabaseType === DatabaseType.POSTGRESQL &&
(column.type.toLowerCase() === 'integer' ||
column.type.toLowerCase() === 'int' ||
column.type.toLowerCase() === 'int4')
(sourceDatabaseType === DatabaseType.POSTGRESQL ||
sourceDatabaseType === DatabaseType.COCKROACHDB) &&
(columnForMapping.type.toLowerCase() === 'integer' ||
columnForMapping.type.toLowerCase() === 'int' ||
columnForMapping.type.toLowerCase() === 'int4')
) {
// Ensure integer types are preserved
mappedType = { id: 'integer', name: 'integer' };
@@ -708,21 +722,25 @@ export function convertToChartDBDiagram(
supportsCustomTypes(sourceDatabaseType) &&
parserResult.enums &&
parserResult.enums.some(
(e) => e.name.toLowerCase() === column.type.toLowerCase()
(e) =>
e.name.toLowerCase() ===
columnForMapping.type.toLowerCase()
)
) {
// If the column type matches a custom enum type, preserve it
mappedType = {
id: column.type.toLowerCase(),
name: column.type,
id: columnForMapping.type.toLowerCase(),
name: columnForMapping.type,
};
}
// Handle PostgreSQL-specific types (not in genericDataTypes)
// Handle PostgreSQL/CockroachDB-specific types (not in genericDataTypes)
else if (
sourceDatabaseType === DatabaseType.POSTGRESQL &&
targetDatabaseType === DatabaseType.POSTGRESQL
(sourceDatabaseType === DatabaseType.POSTGRESQL ||
sourceDatabaseType === DatabaseType.COCKROACHDB) &&
(targetDatabaseType === DatabaseType.POSTGRESQL ||
targetDatabaseType === DatabaseType.COCKROACHDB)
) {
const normalizedType = column.type.toLowerCase();
const normalizedType = columnForMapping.type.toLowerCase();
// Preserve PostgreSQL-specific types that don't exist in genericDataTypes
// Serial types are PostgreSQL-specific syntax (not true data types)
@@ -738,7 +756,7 @@ export function convertToChartDBDiagram(
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
column.type,
columnForMapping.type,
sourceDatabaseType
);
}
@@ -748,7 +766,7 @@ export function convertToChartDBDiagram(
sourceDatabaseType === DatabaseType.SQL_SERVER &&
targetDatabaseType === DatabaseType.SQL_SERVER
) {
const normalizedType = column.type.toLowerCase();
const normalizedType = columnForMapping.type.toLowerCase();
// Preserve SQL Server specific types when target is also SQL Server
if (
@@ -770,14 +788,14 @@ export function convertToChartDBDiagram(
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
column.type,
columnForMapping.type,
sourceDatabaseType
);
}
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
column.type,
columnForMapping.type,
sourceDatabaseType
);
}
@@ -803,6 +821,7 @@ export function convertToChartDBDiagram(
default: column.default || '',
createdAt: Date.now(),
increment: column.increment,
isArray: isArrayType || undefined,
};
// Add type arguments if present
@@ -899,13 +918,20 @@ export function convertToChartDBDiagram(
return null;
}
return {
const index: DBIndex = {
id: generateId(),
name: sqlIndex.name,
fieldIds,
unique: sqlIndex.unique,
createdAt: Date.now(),
};
// Add type if specified (for GIN, HASH, etc.)
if (sqlIndex.type) {
index.type = sqlIndex.type as DBIndex['type'];
}
return index;
})
.filter((idx): idx is DBIndex => idx !== null);

View File

@@ -0,0 +1,171 @@
import { describe, it, expect } from 'vitest';
import { fromPostgres } from '../postgresql';
import { convertToChartDBDiagram } from '../../../common';
import { DatabaseType } from '@/lib/domain/database-type';
describe('Array Type Conversion', () => {
it('should correctly parse and convert array types with isArray flag', async () => {
const sql = `
CREATE TABLE test_arrays (
id bigint NOT NULL PRIMARY KEY,
int_array int[],
text_array text[],
varchar_array varchar(255)[],
jsonb_data jsonb,
regular_int int
);
CREATE INDEX idx_int_array ON test_arrays USING GIN (int_array);
`;
// Parse SQL
const parserResult = await fromPostgres(sql);
// Verify parser correctly captures array notation in type string
const intArrayCol = parserResult.tables[0].columns.find(
(c) => c.name === 'int_array'
);
// The parser normalizes int to integer, but should preserve []
expect(intArrayCol?.type).toMatch(/\[\]$/);
const textArrayCol = parserResult.tables[0].columns.find(
(c) => c.name === 'text_array'
);
expect(textArrayCol?.type).toBe('text[]');
const varcharArrayCol = parserResult.tables[0].columns.find(
(c) => c.name === 'varchar_array'
);
expect(varcharArrayCol?.type).toBe('varchar(255)[]');
// Convert to diagram
const diagram = convertToChartDBDiagram(
parserResult,
DatabaseType.POSTGRESQL,
DatabaseType.POSTGRESQL
);
const table = diagram.tables?.find((t) => t.name === 'test_arrays');
expect(table).toBeDefined();
// Check int[] field - should have isArray=true and base type (PostgreSQL uses 'int' as the canonical form)
const intArrayField = table!.fields.find((f) => f.name === 'int_array');
expect(intArrayField).toBeDefined();
expect(intArrayField!.isArray).toBe(true);
expect(intArrayField!.type.id).toBe('int');
// Check text[] field - should have isArray=true and type=text
const textArrayField = table!.fields.find(
(f) => f.name === 'text_array'
);
expect(textArrayField).toBeDefined();
expect(textArrayField!.isArray).toBe(true);
expect(textArrayField!.type.id).toBe('text');
// Check varchar[] field - should have isArray=true and type=varchar
const varcharArrayField = table!.fields.find(
(f) => f.name === 'varchar_array'
);
expect(varcharArrayField).toBeDefined();
expect(varcharArrayField!.isArray).toBe(true);
expect(varcharArrayField!.type.id).toBe('varchar');
// Check regular jsonb field - should NOT have isArray
const jsonbField = table!.fields.find((f) => f.name === 'jsonb_data');
expect(jsonbField).toBeDefined();
expect(jsonbField!.isArray).toBeUndefined();
expect(jsonbField!.type.id).toBe('jsonb');
// Check regular int field - should NOT have isArray (PostgreSQL uses 'int' as the canonical form)
const regularIntField = table!.fields.find(
(f) => f.name === 'regular_int'
);
expect(regularIntField).toBeDefined();
expect(regularIntField!.isArray).toBeUndefined();
expect(regularIntField!.type.id).toBe('int');
});
it('should handle multi-dimensional arrays', async () => {
const sql = `
CREATE TABLE matrix_data (
id serial PRIMARY KEY,
matrix int[][]
);
`;
const parserResult = await fromPostgres(sql);
// Check parser captures the type with array notation
const matrixCol = parserResult.tables[0].columns.find(
(c) => c.name === 'matrix'
);
// Multi-dimensional arrays should still have array notation
expect(matrixCol?.type).toMatch(/\[\]/);
});
it('should correctly parse GIN index type from USING clause', async () => {
const sql = `
CREATE TABLE test_gin_index (
id bigint NOT NULL PRIMARY KEY,
tags text[]
);
CREATE INDEX idx_tags ON test_gin_index USING GIN (tags);
CREATE INDEX idx_tags_btree ON test_gin_index USING BTREE (id);
CREATE INDEX idx_tags_hash ON test_gin_index USING HASH (id);
`;
// Parse SQL
const parserResult = await fromPostgres(sql);
// Check that the parser captures the index type
const table = parserResult.tables[0];
expect(table.indexes).toHaveLength(3);
const ginIndex = table.indexes.find((idx) => idx.name === 'idx_tags');
expect(ginIndex).toBeDefined();
expect(ginIndex!.type).toBe('gin');
const btreeIndex = table.indexes.find(
(idx) => idx.name === 'idx_tags_btree'
);
expect(btreeIndex).toBeDefined();
expect(btreeIndex!.type).toBe('btree');
const hashIndex = table.indexes.find(
(idx) => idx.name === 'idx_tags_hash'
);
expect(hashIndex).toBeDefined();
expect(hashIndex!.type).toBe('hash');
// Convert to diagram and verify index types are preserved
const diagram = convertToChartDBDiagram(
parserResult,
DatabaseType.POSTGRESQL,
DatabaseType.POSTGRESQL
);
const diagramTable = diagram.tables?.find(
(t) => t.name === 'test_gin_index'
);
expect(diagramTable).toBeDefined();
const diagramGinIndex = diagramTable!.indexes.find(
(idx) => idx.name === 'idx_tags'
);
expect(diagramGinIndex).toBeDefined();
expect(diagramGinIndex!.type).toBe('gin');
const diagramBtreeIndex = diagramTable!.indexes.find(
(idx) => idx.name === 'idx_tags_btree'
);
expect(diagramBtreeIndex).toBeDefined();
expect(diagramBtreeIndex!.type).toBe('btree');
const diagramHashIndex = diagramTable!.indexes.find(
(idx) => idx.name === 'idx_tags_hash'
);
expect(diagramHashIndex).toBeDefined();
expect(diagramHashIndex!.type).toBe('hash');
});
});

View File

@@ -1096,6 +1096,14 @@ export async function fromPostgres(
| number
| undefined;
// Check if this is an array type (node-sql-parser stores this separately)
const arrayInfo = definition?.array as
| { dimension?: number }
| undefined;
const isArrayType =
arrayInfo?.dimension !== undefined ||
rawDataType.endsWith('[]');
// Normalize the type (pass length to handle parser quirks like INT with length=8)
let finalDataType = normalizePostgreSQLType(
rawDataType,
@@ -1163,6 +1171,12 @@ export async function fromPostgres(
}
}
// Add array suffix if this is an array type
// (only if not already present from rawDataType)
if (isArrayType && !finalDataType.endsWith('[]')) {
finalDataType = `${finalDataType}[]`;
}
if (columnName) {
const isPrimaryKey =
columnDef.primary_key === 'primary key' ||
@@ -2172,12 +2186,29 @@ export async function fromPostgres(
createIndexStmt.index_name ||
`idx_${tableName}_${columns.join('_')}`;
// Extract index type from USING clause (e.g., USING GIN, USING HASH)
// The parser may store this in different properties
let indexType: string | undefined;
const indexUsing = createIndexStmt.index_using;
if (typeof indexUsing === 'string') {
indexType = indexUsing.toLowerCase();
} else if (
indexUsing &&
typeof indexUsing === 'object' &&
'type' in indexUsing
) {
indexType = String(
(indexUsing as { type: unknown }).type
).toLowerCase();
}
table.indexes.push({
name: indexName,
columns,
unique:
createIndexStmt.index_type === 'unique' ||
createIndexStmt.unique === true,
type: indexType,
});
}
}

View File

@@ -197,7 +197,9 @@ export async function sqlImportToDiagram({
// Select the appropriate parser based on database type
switch (sourceDatabaseType) {
case DatabaseType.POSTGRESQL:
case DatabaseType.COCKROACHDB:
// Check if the SQL is from pg_dump and use the appropriate parser
// CockroachDB uses PostgreSQL-compatible syntax
if (isPgDumpFormat(sqlContent)) {
parserResult = await fromPostgresDump(sqlContent);
} else {
@@ -278,7 +280,8 @@ export async function parseSQLError({
// Validate SQL based on the database type
switch (sourceDatabaseType) {
case DatabaseType.POSTGRESQL:
// PostgreSQL validation - check format and use appropriate parser
case DatabaseType.COCKROACHDB:
// PostgreSQL/CockroachDB validation - check format and use appropriate parser
if (isPgDumpFormat(sqlContent)) {
await fromPostgresDump(sqlContent);
} else {

View File

@@ -30,6 +30,8 @@ export function validateSQL(
): ValidationResult {
switch (databaseType) {
case DatabaseType.POSTGRESQL:
case DatabaseType.COCKROACHDB:
// CockroachDB uses PostgreSQL-compatible syntax
return validatePostgreSQLDialect(sql);
case DatabaseType.MYSQL:

View File

@@ -2,6 +2,7 @@ import { z } from 'zod';
import { generateId } from '../utils';
import { DatabaseType } from './database-type';
import type { DBTable } from './db-table';
import type { DBField } from './db-field';
export const INDEX_TYPES = [
'btree',
@@ -41,16 +42,66 @@ export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
isPrimaryKey: z.boolean().or(z.null()).optional(),
});
export const databaseIndexTypes: { [key in DatabaseType]?: IndexType[] } = {
[DatabaseType.POSTGRESQL]: ['btree', 'hash'],
export const databaseIndexTypes: Record<DatabaseType, IndexType[] | undefined> =
{
[DatabaseType.POSTGRESQL]: ['btree', 'hash', 'gin'],
[DatabaseType.COCKROACHDB]: ['btree', 'hash', 'gin'],
[DatabaseType.MYSQL]: undefined,
[DatabaseType.MARIADB]: undefined,
[DatabaseType.SQL_SERVER]: undefined,
[DatabaseType.SQLITE]: undefined,
[DatabaseType.CLICKHOUSE]: undefined,
[DatabaseType.ORACLE]: undefined,
[DatabaseType.GENERIC]: undefined,
};
export const defaultIndexTypeForDatabase: Record<
DatabaseType,
IndexType | undefined
> = {
[DatabaseType.POSTGRESQL]: 'btree',
[DatabaseType.COCKROACHDB]: 'btree',
[DatabaseType.MYSQL]: undefined,
[DatabaseType.MARIADB]: undefined,
[DatabaseType.SQL_SERVER]: undefined,
[DatabaseType.SQLITE]: undefined,
[DatabaseType.CLICKHOUSE]: undefined,
[DatabaseType.ORACLE]: undefined,
[DatabaseType.GENERIC]: undefined,
};
export const defaultIndexTypeForDatabase: {
[key in DatabaseType]?: IndexType;
} = {
[DatabaseType.POSTGRESQL]: 'btree',
// Data types that support GIN indexes in PostgreSQL/CockroachDB
const GIN_SUPPORTED_TYPES = ['jsonb', 'json', 'tsvector', 'hstore'] as const;
export const supportsGinIndex = (field: DBField): boolean => {
if (field.isArray) return true;
const typeLower = field.type.id.toLowerCase();
return GIN_SUPPORTED_TYPES.includes(
typeLower as (typeof GIN_SUPPORTED_TYPES)[number]
);
};
export const canFieldsUseGinIndex = (fields: DBField[]): boolean => {
return fields.length > 0 && fields.every(supportsGinIndex);
};
export interface IndexTypeConfig {
label: string;
value: IndexType;
disabledTooltip?: string;
}
export const INDEX_TYPE_CONFIGS: IndexTypeConfig[] = [
{ label: 'B-tree (default)', value: 'btree' },
{ label: 'Hash', value: 'hash' },
{
label: 'GIN',
value: 'gin',
disabledTooltip:
'GIN indexes require array, jsonb, json, tsvector, or hstore types',
},
];
export const getTablePrimaryKeyIndex = ({
table,
}: {

View File

@@ -142,6 +142,7 @@ export const TableField: React.FC<TableFieldProps> = ({
'side_panel.tables_section.table.no_types_found'
)}
readonly={readonly}
modal={false}
/>
</span>
</TooltipTrigger>

View File

@@ -0,0 +1,114 @@
import React from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
import { Button } from '@/components/button/button';
import type { IndexType, IndexTypeConfig } from '@/lib/domain/db-index';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/popover/popover';
import { Label } from '@/components/label/label';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { cn } from '@/lib/utils';
export interface IndexTypeSelectorProps {
options: Array<IndexTypeConfig & { disabled: boolean }>;
value: IndexType;
label: string;
onChange: (value: IndexType) => void;
readonly?: boolean;
t: (key: string) => string;
}
export const IndexTypeSelector: React.FC<IndexTypeSelectorProps> = ({
options,
value,
label,
onChange,
readonly,
t,
}) => (
<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>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full justify-between font-normal"
disabled={readonly}
>
{label}
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[180px] p-1">
<div className="flex flex-col">
{options.map((option) => (
<IndexTypeOption
key={option.value}
option={option}
isSelected={value === option.value}
onSelect={() => {
if (!option.disabled) {
onChange(option.value);
}
}}
/>
))}
</div>
</PopoverContent>
</Popover>
</div>
);
interface IndexTypeOptionProps {
option: IndexTypeConfig & { disabled: boolean };
isSelected: boolean;
onSelect: () => void;
}
const IndexTypeOption: React.FC<IndexTypeOptionProps> = ({
option,
isSelected,
onSelect,
}) => {
const content = (
<div
className={cn(
'flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm',
option.disabled
? 'cursor-not-allowed opacity-50'
: 'hover:bg-accent',
isSelected && 'bg-accent'
)}
onClick={onSelect}
>
<Check
className={cn(
'mr-2 h-4 w-4',
isSelected ? 'opacity-100' : 'opacity-0'
)}
/>
{option.label}
</div>
);
if (option.disabled && option.disabledTooltip) {
return (
<Tooltip>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent side="right">
{option.disabledTooltip}
</TooltipContent>
</Tooltip>
);
}
return content;
};

View File

@@ -2,7 +2,9 @@ import React, { useCallback, useMemo } from 'react';
import { Ellipsis, Trash2, KeyRound } from 'lucide-react';
import { Button } from '@/components/button/button';
import {
canFieldsUseGinIndex,
databaseIndexTypes,
INDEX_TYPE_CONFIGS,
type DBIndex,
type IndexType,
} from '@/lib/domain/db-index';
@@ -25,6 +27,7 @@ import {
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useChartDB } from '@/hooks/use-chartdb';
import { IndexTypeSelector } from './index-type-selector';
export interface TableIndexProps {
index: DBIndex;
@@ -33,11 +36,6 @@ 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,
@@ -46,10 +44,39 @@ export const TableIndex: React.FC<TableIndexProps> = ({
}) => {
const { t } = useTranslation();
const { databaseType, readonly } = useChartDB();
const fieldOptions = fields.map((field) => ({
label: field.name,
value: field.id,
}));
const fieldOptions = useMemo(
() =>
fields.map((field) => ({
label: field.name,
value: field.id,
})),
[fields]
);
const selectedFields = useMemo(
() => fields.filter((f) => index.fieldIds.includes(f.id)),
[fields, index.fieldIds]
);
const canUseGin = useMemo(
() => canFieldsUseGinIndex(selectedFields),
[selectedFields]
);
const availableIndexTypes = databaseIndexTypes[databaseType];
const indexTypeOptions = useMemo(() => {
if (!availableIndexTypes) return [];
return INDEX_TYPE_CONFIGS.filter((config) =>
availableIndexTypes.includes(config.value)
).map((config) => ({
...config,
disabled: config.value === 'gin' && !canUseGin,
}));
}, [availableIndexTypes, canUseGin]);
const updateIndexFields = useCallback(
(fieldIds: string | string[]) => {
const ids = Array.isArray(fieldIds) ? fieldIds : [fieldIds];
@@ -57,39 +84,43 @@ export const TableIndex: React.FC<TableIndexProps> = ({
// For hash indexes, only keep the last selected field
if (index.type === 'hash' && ids.length > 0) {
updateIndex({ fieldIds: [ids[ids.length - 1]] });
return;
}
// Check if GIN is still valid with new field selection
const newSelectedFields = fields.filter((f) => ids.includes(f.id));
const ginStillValid = canFieldsUseGinIndex(newSelectedFields);
if (index.type === 'gin' && !ginStillValid) {
// Reset to btree if GIN is no longer valid
updateIndex({ fieldIds: ids, type: 'btree' });
} else {
updateIndex({ fieldIds: ids });
}
},
[index.type, updateIndex]
);
const indexTypeOptions = useMemo(
() =>
allIndexTypeOptions.filter((option) =>
databaseIndexTypes[databaseType]?.includes(option.value)
),
[databaseType]
[index.type, updateIndex, fields]
);
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 });
}
(newType: 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]
);
const currentIndexType = index.type || 'btree';
const currentTypeLabel =
indexTypeOptions.find((opt) => opt.value === currentIndexType)?.label ||
'Select type';
return (
<div className="flex flex-1 flex-row justify-between gap-2 p-1">
<SelectBox
@@ -108,6 +139,7 @@ export const TableIndex: React.FC<TableIndexProps> = ({
keepOrder
disabled={index.isPrimaryKey ?? false}
readonly={readonly}
modal={false}
/>
<div className="flex shrink-0 gap-1">
{index.isPrimaryKey ? (
@@ -203,25 +235,17 @@ export const TableIndex: React.FC<TableIndexProps> = ({
disabled={readonly}
/>
</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}
readonly={readonly}
/>
</div>
) : null}
{!readonly ? (
{indexTypeOptions.length > 0 && (
<IndexTypeSelector
options={indexTypeOptions}
value={currentIndexType}
label={currentTypeLabel}
onChange={updateIndexType}
readonly={readonly}
t={t}
/>
)}
{!readonly && (
<>
<Separator orientation="horizontal" />
<Button
@@ -235,7 +259,7 @@ export const TableIndex: React.FC<TableIndexProps> = ({
)}
</Button>
</>
) : null}
)}
</div>
</PopoverContent>
</Popover>