mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-01 20:43:49 +00:00
fix(custom-types): fetch directly via the smart-query the custom types (#729)
This commit is contained in:
@@ -165,7 +165,7 @@ cols AS (
|
||||
'","table":"', cols.table_name,
|
||||
'","name":"', cols.column_name,
|
||||
'","ordinal_position":', cols.ordinal_position,
|
||||
',"type":"', LOWER(replace(cols.data_type, '"', '')),
|
||||
',"type":"', case when LOWER(replace(cols.data_type, '"', '')) = 'user-defined' then pg_type.typname else LOWER(replace(cols.data_type, '"', '')) end,
|
||||
'","character_maximum_length":"', COALESCE(cols.character_maximum_length::text, 'null'),
|
||||
'","precision":',
|
||||
CASE
|
||||
@@ -184,8 +184,12 @@ cols AS (
|
||||
ON c.relname = cols.table_name
|
||||
JOIN pg_catalog.pg_namespace n
|
||||
ON n.oid = c.relnamespace AND n.nspname = cols.table_schema
|
||||
LEFT JOIN pg_catalog.pg_description dsc ON dsc.objoid = c.oid
|
||||
AND dsc.objsubid = cols.ordinal_position
|
||||
LEFT JOIN pg_catalog.pg_description dsc
|
||||
ON dsc.objoid = c.oid AND dsc.objsubid = cols.ordinal_position
|
||||
LEFT JOIN pg_catalog.pg_attribute attr
|
||||
ON attr.attrelid = c.oid AND attr.attname = cols.column_name
|
||||
LEFT JOIN pg_catalog.pg_type
|
||||
ON pg_type.oid = attr.atttypid
|
||||
WHERE cols.table_schema NOT IN ('information_schema', 'pg_catalog')${
|
||||
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
|
||||
? timescaleColFilter
|
||||
|
||||
@@ -58,6 +58,3 @@ export const customTypeKindToLabel: Record<DBCustomTypeKind, string> = {
|
||||
enum: 'Enum',
|
||||
composite: 'Composite',
|
||||
};
|
||||
|
||||
export const getCustomTypeId = (name: string) =>
|
||||
`custom-type-${name.toLowerCase().trim()}`;
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { PrimaryKeyInfo } from '../data/import-metadata/metadata-types/prim
|
||||
import type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
|
||||
import type { DBCustomTypeInfo } from '../data/import-metadata/metadata-types/custom-type-info';
|
||||
import { schemaNameToDomainSchemaName } from './db-schema';
|
||||
import { getCustomTypeId } from './db-custom-type';
|
||||
|
||||
export interface DBField {
|
||||
id: string;
|
||||
@@ -43,56 +42,12 @@ export const dbFieldSchema: z.ZodType<DBField> = z.object({
|
||||
comments: z.string().optional(),
|
||||
});
|
||||
|
||||
// Helper function to find the best matching custom type for a column
|
||||
const findMatchingCustomType = (
|
||||
columnName: string,
|
||||
customTypes: DBCustomTypeInfo[]
|
||||
): DBCustomTypeInfo | undefined => {
|
||||
// 1. Exact name match (highest priority)
|
||||
const exactMatch = customTypes.find((ct) => ct.type === columnName);
|
||||
if (exactMatch) return exactMatch;
|
||||
|
||||
// 2. Check if column name is the base of a custom type (e.g., 'role' matches 'role_enum')
|
||||
const prefixMatch = customTypes.find((ct) =>
|
||||
ct.type.startsWith(columnName + '_')
|
||||
);
|
||||
if (prefixMatch) return prefixMatch;
|
||||
|
||||
// 3. Check if a custom type name is the base of the column name (e.g., 'user_role' matches 'role_enum')
|
||||
const baseTypeMatch = customTypes.find((ct) => {
|
||||
// Extract base name by removing common suffixes
|
||||
const baseTypeName = ct.type.replace(/_enum$|_type$/, '');
|
||||
return (
|
||||
columnName.includes(baseTypeName) ||
|
||||
baseTypeName.includes(columnName)
|
||||
);
|
||||
});
|
||||
if (baseTypeMatch) return baseTypeMatch;
|
||||
|
||||
// 4. For composite types, check if any field matches the column name
|
||||
const compositeMatch = customTypes.find(
|
||||
(ct) =>
|
||||
ct.kind === 'composite' &&
|
||||
ct.fields?.some((f) => f.field === columnName)
|
||||
);
|
||||
if (compositeMatch) return compositeMatch;
|
||||
|
||||
// 5. Special case for name/fullname relationship which is common
|
||||
if (columnName === 'name') {
|
||||
const fullNameType = customTypes.find((ct) => ct.type === 'full_name');
|
||||
if (fullNameType) return fullNameType;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const createFieldsFromMetadata = ({
|
||||
columns,
|
||||
tableSchema,
|
||||
tableInfo,
|
||||
primaryKeys,
|
||||
aggregatedIndexes,
|
||||
customTypes = [],
|
||||
}: {
|
||||
columns: ColumnInfo[];
|
||||
tableSchema?: string;
|
||||
@@ -126,50 +81,14 @@ export const createFieldsFromMetadata = ({
|
||||
)
|
||||
.map((pk) => pk.column.trim());
|
||||
|
||||
// Create a mapping between column names and custom types
|
||||
const typeMap: Record<string, DBCustomTypeInfo> = {};
|
||||
|
||||
if (customTypes && customTypes.length > 0) {
|
||||
// Filter to custom types in this schema
|
||||
const schemaCustomTypes = customTypes.filter(
|
||||
(ct) => schemaNameToDomainSchemaName(ct.schema) === tableSchema
|
||||
);
|
||||
|
||||
// Process user-defined columns to find matching custom types
|
||||
for (const column of sortedColumns) {
|
||||
if (column.type.toLowerCase() === 'user-defined') {
|
||||
const matchingType = findMatchingCustomType(
|
||||
column.name,
|
||||
schemaCustomTypes
|
||||
);
|
||||
if (matchingType) {
|
||||
typeMap[column.name] = matchingType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sortedColumns.map((col: ColumnInfo): DBField => {
|
||||
let type: DataType;
|
||||
|
||||
// Use custom type if available, otherwise use standard type
|
||||
if (col.type.toLowerCase() === 'user-defined' && typeMap[col.name]) {
|
||||
const customType = typeMap[col.name];
|
||||
type = {
|
||||
id: getCustomTypeId(customType.type),
|
||||
name: customType.type,
|
||||
};
|
||||
} else {
|
||||
type = {
|
||||
id: col.type.split(' ').join('_').toLowerCase(),
|
||||
name: col.type.toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
return sortedColumns.map(
|
||||
(col: ColumnInfo): DBField => ({
|
||||
id: generateId(),
|
||||
name: col.name,
|
||||
type,
|
||||
type: {
|
||||
id: col.type.split(' ').join('_').toLowerCase(),
|
||||
name: col.type.toLowerCase(),
|
||||
},
|
||||
primaryKey: tablePrimaryKeys.includes(col.name),
|
||||
unique: Object.values(aggregatedIndexes).some(
|
||||
(idx) =>
|
||||
@@ -190,6 +109,6 @@ export const createFieldsFromMetadata = ({
|
||||
...(col.collation ? { collation: col.collation } : {}),
|
||||
createdAt: Date.now(),
|
||||
comments: col.comment ? col.comment : undefined,
|
||||
};
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -102,7 +102,6 @@ export const createTablesFromMetadata = ({
|
||||
columns,
|
||||
indexes,
|
||||
views: views,
|
||||
custom_types: customTypes,
|
||||
} = databaseMetadata;
|
||||
|
||||
return tableInfos.map((tableInfo: TableInfo) => {
|
||||
@@ -121,10 +120,6 @@ export const createTablesFromMetadata = ({
|
||||
primaryKeys,
|
||||
tableInfo,
|
||||
tableSchema,
|
||||
customTypes:
|
||||
databaseType === DatabaseType.POSTGRESQL
|
||||
? customTypes
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const dbIndexes = createIndexesFromMetadata({
|
||||
|
||||
@@ -55,7 +55,7 @@ export const CustomTypeCompositeFields: React.FC<
|
||||
const customDataTypes = useMemo<DataTypeData[]>(
|
||||
() =>
|
||||
customTypes.map<DataTypeData>((type) => ({
|
||||
id: `custom-type-${type.id}`,
|
||||
id: type.name,
|
||||
name: type.name,
|
||||
})),
|
||||
[customTypes]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { GripVertical, KeyRound } from 'lucide-react';
|
||||
import { Input } from '@/components/input/input';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
@@ -6,7 +6,6 @@ import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import {
|
||||
dataTypeDataToDataType,
|
||||
sortedDataTypeMap,
|
||||
type DataType,
|
||||
} from '@/lib/data/data-types/data-types';
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -35,98 +34,51 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
updateField,
|
||||
removeField,
|
||||
}) => {
|
||||
const { databaseType, customTypes } = useChartDB();
|
||||
const { databaseType } = useChartDB();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: field.id });
|
||||
|
||||
const dataFieldOptions = useMemo(() => {
|
||||
// Start with the built-in data types
|
||||
const standardTypes: SelectBoxOption[] = sortedDataTypeMap[
|
||||
databaseType
|
||||
].map((type) => ({
|
||||
label: type.name,
|
||||
value: type.id,
|
||||
regex: type.hasCharMaxLength
|
||||
? `^${type.name}\\(\\d+\\)$`
|
||||
: undefined,
|
||||
extractRegex: type.hasCharMaxLength ? /\((\d+)\)/ : undefined,
|
||||
group: 'Standard Types',
|
||||
}));
|
||||
|
||||
if (!customTypes?.length) {
|
||||
return standardTypes;
|
||||
}
|
||||
|
||||
// Add custom types as options
|
||||
const customTypeOptions: SelectBoxOption[] = customTypes.map(
|
||||
(type) => ({
|
||||
label: type.name,
|
||||
value: `custom-type-${type.name}`,
|
||||
// Add additional info to distinguish custom types
|
||||
description:
|
||||
type.kind === 'enum' ? `${type.values?.join(' | ')}` : '',
|
||||
// Group custom types together
|
||||
group: 'Custom Types',
|
||||
})
|
||||
);
|
||||
|
||||
return [...standardTypes, ...customTypeOptions];
|
||||
}, [databaseType, customTypes]);
|
||||
const dataFieldOptions: SelectBoxOption[] = sortedDataTypeMap[
|
||||
databaseType
|
||||
].map((type) => ({
|
||||
label: type.name,
|
||||
value: type.id,
|
||||
regex: type.hasCharMaxLength ? `^${type.name}\\(\\d+\\)$` : undefined,
|
||||
extractRegex: type.hasCharMaxLength ? /\((\d+)\)/ : undefined,
|
||||
}));
|
||||
|
||||
const onChangeDataType = useCallback<
|
||||
NonNullable<SelectBoxProps['onChange']>
|
||||
>(
|
||||
(value, regexMatches) => {
|
||||
// Check if this is a custom type
|
||||
const isCustomType =
|
||||
typeof value === 'string' && value.startsWith('custom-type-');
|
||||
|
||||
let newType: DataType;
|
||||
|
||||
if (isCustomType && typeof value === 'string') {
|
||||
// For custom types, create a custom DataType object
|
||||
const typeName = value.replace('custom-type-', '');
|
||||
newType = {
|
||||
id: value as string,
|
||||
name: typeName,
|
||||
};
|
||||
} else {
|
||||
// For standard types, use the existing logic
|
||||
const dataType = sortedDataTypeMap[databaseType].find(
|
||||
(v) => v.id === value
|
||||
) ?? {
|
||||
id: value as string,
|
||||
name: value as string,
|
||||
};
|
||||
|
||||
newType = dataTypeDataToDataType(dataType);
|
||||
}
|
||||
const dataType = sortedDataTypeMap[databaseType].find(
|
||||
(v) => v.id === value
|
||||
) ?? {
|
||||
id: value as string,
|
||||
name: value as string,
|
||||
};
|
||||
|
||||
let characterMaximumLength: string | undefined = undefined;
|
||||
|
||||
if (regexMatches?.length && !isCustomType) {
|
||||
const dataType = sortedDataTypeMap[databaseType].find(
|
||||
(v) => v.id === value
|
||||
);
|
||||
|
||||
if (dataType?.hasCharMaxLength) {
|
||||
characterMaximumLength = regexMatches[1];
|
||||
}
|
||||
} else if (field.characterMaximumLength && !isCustomType) {
|
||||
const dataType = sortedDataTypeMap[databaseType].find(
|
||||
(v) => v.id === value
|
||||
);
|
||||
|
||||
if (dataType?.hasCharMaxLength) {
|
||||
characterMaximumLength = field.characterMaximumLength;
|
||||
}
|
||||
if (regexMatches?.length && dataType?.hasCharMaxLength) {
|
||||
characterMaximumLength = regexMatches[1];
|
||||
} else if (
|
||||
field.characterMaximumLength &&
|
||||
dataType?.hasCharMaxLength
|
||||
) {
|
||||
characterMaximumLength = field.characterMaximumLength;
|
||||
}
|
||||
|
||||
updateField({
|
||||
characterMaximumLength,
|
||||
type: newType,
|
||||
type: dataTypeDataToDataType(
|
||||
dataType ?? {
|
||||
id: value as string,
|
||||
name: value as string,
|
||||
}
|
||||
),
|
||||
});
|
||||
},
|
||||
[updateField, databaseType, field.characterMaximumLength]
|
||||
@@ -137,21 +89,6 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
transition,
|
||||
};
|
||||
|
||||
// Helper to get the display value for custom types
|
||||
const getCustomTypeDisplayValue = () => {
|
||||
if (field.type.id.startsWith('custom-type-')) {
|
||||
const typeName = field.type.name;
|
||||
const customType = customTypes?.find((ct) => ct.name === typeName);
|
||||
|
||||
if (customType) {
|
||||
return typeName;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const customTypeDisplayValue = getCustomTypeDisplayValue();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-1 touch-none flex-row justify-between p-1"
|
||||
@@ -198,22 +135,11 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
)}
|
||||
value={field.type.id}
|
||||
valueSuffix={
|
||||
customTypeDisplayValue
|
||||
? ``
|
||||
: field.characterMaximumLength
|
||||
? `(${field.characterMaximumLength})`
|
||||
: ''
|
||||
field.characterMaximumLength
|
||||
? `(${field.characterMaximumLength})`
|
||||
: ''
|
||||
}
|
||||
optionSuffix={(option) => {
|
||||
// For custom types, we don't need to add a suffix
|
||||
if (
|
||||
option.value
|
||||
.toString()
|
||||
.startsWith('custom-type-')
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const type = sortedDataTypeMap[
|
||||
databaseType
|
||||
].find((v) => v.id === option.value);
|
||||
@@ -236,12 +162,10 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{customTypeDisplayValue ||
|
||||
`${field.type.name}${
|
||||
field.characterMaximumLength
|
||||
? `(${field.characterMaximumLength})`
|
||||
: ''
|
||||
}`}
|
||||
{field.type.name}
|
||||
{field.characterMaximumLength
|
||||
? `(${field.characterMaximumLength})`
|
||||
: ''}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user