Compare commits

...

1 Commits

Author SHA1 Message Date
johnnyfish
21ba816a6d feat: add array type support for PostgreSQL fields 2025-08-26 15:19:48 +03:00
8 changed files with 136 additions and 17 deletions

View File

@@ -143,6 +143,7 @@ export const en = {
title: 'Field Attributes',
unique: 'Unique',
auto_increment: 'Auto Increment',
array: 'Declare Array',
character_length: 'Max Length',
precision: 'Precision',
scale: 'Scale',

View File

@@ -165,3 +165,21 @@ export const supportsAutoIncrementDataType = (
'decimal',
].includes(dataTypeName.toLocaleLowerCase());
};
export const supportsArrayDataType = (dataTypeName: string): boolean => {
// Types that do NOT support arrays in PostgreSQL
const unsupportedTypes = [
'serial',
'bigserial',
'smallserial',
'serial2',
'serial4',
'serial8',
'xml',
'money',
];
// Check if the type is in the unsupported list
const normalizedType = dataTypeName.toLowerCase();
return !unsupportedTypes.includes(normalizedType);
};

View File

@@ -286,10 +286,14 @@ export function exportPostgreSQL({
}
}
// Handle array types (check if the type name ends with '[]')
if (typeName.endsWith('[]')) {
typeWithSize =
typeWithSize.replace('[]', '') + '[]';
// Handle array types (check if the field has array property or type name ends with '[]')
if (field.array || typeName.endsWith('[]')) {
if (!typeName.endsWith('[]')) {
typeWithSize = typeWithSize + '[]';
} else {
typeWithSize =
typeWithSize.replace('[]', '') + '[]';
}
}
const notNull = field.nullable ? '' : ' NOT NULL';

View File

@@ -29,6 +29,7 @@ export interface SQLColumn {
comment?: string;
default?: string;
increment?: boolean;
array?: boolean;
}
export interface SQLTable {
@@ -612,6 +613,7 @@ export function convertToChartDBDiagram(
default: column.default || '',
createdAt: Date.now(),
increment: column.increment,
array: column.array,
};
// Add type arguments if present

View File

@@ -373,6 +373,13 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] {
'SMALLSERIAL',
].includes(upperType.split('(')[0]);
// Check if it's an array type
let isArrayType = false;
if (columnType.endsWith('[]')) {
isArrayType = true;
columnType = columnType.slice(0, -2);
}
// Normalize the type
columnType = normalizePostgreSQLType(columnType);
@@ -395,6 +402,7 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] {
trimmedLine.includes('uuid_generate_v4()') ||
trimmedLine.includes('GENERATED ALWAYS AS IDENTITY') ||
trimmedLine.includes('GENERATED BY DEFAULT AS IDENTITY'),
array: isArrayType,
});
}
}
@@ -782,6 +790,16 @@ export async function fromPostgres(
normalizePostgreSQLType(rawDataType);
}
// Check if it's an array type
let isArrayType = false;
if (normalizedBaseType.endsWith('[]')) {
isArrayType = true;
normalizedBaseType = normalizedBaseType.slice(
0,
-2
);
}
// Now handle parameters - but skip for integer types that shouldn't have them
let finalDataType = normalizedBaseType;
@@ -874,6 +892,7 @@ export async function fromPostgres(
stmt.sql
.toUpperCase()
.includes('IDENTITY')),
array: isArrayType,
});
}
} else if (def.resource === 'constraint') {

View File

@@ -19,6 +19,7 @@ export interface DBField {
unique: boolean;
nullable: boolean;
increment?: boolean | null;
array?: boolean | null;
createdAt: number;
characterMaximumLength?: string | null;
precision?: number | null;
@@ -36,6 +37,7 @@ export const dbFieldSchema: z.ZodType<DBField> = z.object({
unique: z.boolean(),
nullable: z.boolean(),
increment: z.boolean().or(z.null()).optional(),
array: z.boolean().or(z.null()).optional(),
createdAt: z.number(),
characterMaximumLength: z.string().or(z.null()).optional(),
precision: z.number().or(z.null()).optional(),
@@ -71,13 +73,48 @@ export const createFieldsFromMetadata = ({
pk.column.trim()
);
return sortedColumns.map(
(col: ColumnInfo): DBField => ({
return sortedColumns.map((col: ColumnInfo): DBField => {
// Check if type is an array (ends with [])
const isArrayType = col.type.endsWith('[]');
let baseType = col.type;
// Extract base type and any parameters if it's an array
if (isArrayType) {
baseType = col.type.slice(0, -2); // Remove the [] suffix
}
// Extract parameters from types like "character varying(100)" or "numeric(10,2)"
let charMaxLength = col.character_maximum_length;
let precision = col.precision?.precision;
let scale = col.precision?.scale;
// Handle types with single parameter like varchar(100)
const singleParamMatch = baseType.match(/^(.+?)\((\d+)\)$/);
if (singleParamMatch) {
baseType = singleParamMatch[1];
if (!charMaxLength || charMaxLength === 'null') {
charMaxLength = singleParamMatch[2];
}
}
// Handle types with two parameters like numeric(10,2)
const twoParamMatch = baseType.match(/^(.+?)\((\d+),\s*(\d+)\)$/);
if (twoParamMatch) {
baseType = twoParamMatch[1];
if (!precision) {
precision = parseInt(twoParamMatch[2]);
}
if (!scale) {
scale = parseInt(twoParamMatch[3]);
}
}
return {
id: generateId(),
name: col.name,
type: {
id: col.type.split(' ').join('_').toLowerCase(),
name: col.type.toLowerCase(),
id: baseType.split(' ').join('_').toLowerCase(),
name: baseType.toLowerCase(),
},
primaryKey: tablePrimaryKeysColumns.includes(col.name),
unique: Object.values(aggregatedIndexes).some(
@@ -87,20 +124,18 @@ export const createFieldsFromMetadata = ({
idx.columns[0].name === col.name
),
nullable: Boolean(col.nullable),
...(col.character_maximum_length &&
col.character_maximum_length !== 'null'
? { characterMaximumLength: col.character_maximum_length }
...(isArrayType ? { array: true } : {}),
...(charMaxLength && charMaxLength !== 'null'
? { characterMaximumLength: charMaxLength }
: {}),
...(col.precision?.precision
? { precision: col.precision.precision }
: {}),
...(col.precision?.scale ? { scale: col.precision.scale } : {}),
...(precision ? { precision } : {}),
...(scale ? { scale } : {}),
...(col.default ? { default: col.default } : {}),
...(col.collation ? { collation: col.collation } : {}),
createdAt: Date.now(),
comments: col.comment ? col.comment : undefined,
})
);
};
});
};
export const generateDBFieldSuffix = (

View File

@@ -8,6 +8,7 @@ import type { FieldAttributeRange } from '@/lib/data/data-types/data-types';
import {
findDataTypeDataById,
supportsAutoIncrementDataType,
supportsArrayDataType,
} from '@/lib/data/data-types/data-types';
import {
Popover,
@@ -87,6 +88,7 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
unique: localField.unique,
default: localField.default,
increment: localField.increment,
array: localField.array,
});
}
prevFieldRef.current = localField;
@@ -102,6 +104,13 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
[field.type.name]
);
const supportsArray = useMemo(
() =>
databaseType === 'postgresql' &&
supportsArrayDataType(field.type.name),
[field.type.name, databaseType]
);
return (
<Popover
open={isOpen}
@@ -168,6 +177,27 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
/>
</div>
) : null}
{supportsArray ? (
<div className="flex items-center justify-between">
<Label
htmlFor="array"
className="text-subtitle"
>
{t(
'side_panel.tables_section.table.field_actions.array'
)}
</Label>
<Checkbox
checked={localField.array ?? false}
onCheckedChange={(value) =>
setLocalField((current) => ({
...current,
array: !!value,
}))
}
/>
</div>
) : null}
<div className="flex flex-col gap-2">
<Label htmlFor="default" className="text-subtitle">
{t(

View File

@@ -7,6 +7,7 @@ import type { DataTypeData } from '@/lib/data/data-types/data-types';
import {
dataTypeDataToDataType,
sortedDataTypeMap,
supportsArrayDataType,
} from '@/lib/data/data-types/data-types';
import {
Tooltip,
@@ -175,6 +176,13 @@ export const TableField: React.FC<TableFieldProps> = ({
}
}
// Check if the new type supports arrays - if not, clear the array property
const newTypeName = dataType?.name ?? (value as string);
const shouldClearArray =
databaseType === 'postgresql' &&
!supportsArrayDataType(newTypeName) &&
field.array;
updateField({
characterMaximumLength,
precision,
@@ -185,6 +193,7 @@ export const TableField: React.FC<TableFieldProps> = ({
name: value as string,
}
),
...(shouldClearArray ? { array: false } : {}),
});
},
[
@@ -193,6 +202,7 @@ export const TableField: React.FC<TableFieldProps> = ({
field.characterMaximumLength,
field.precision,
field.scale,
field.array,
]
);