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', title: 'Field Attributes',
unique: 'Unique', unique: 'Unique',
auto_increment: 'Auto Increment', auto_increment: 'Auto Increment',
array: 'Declare Array',
character_length: 'Max Length', character_length: 'Max Length',
precision: 'Precision', precision: 'Precision',
scale: 'Scale', scale: 'Scale',

View File

@@ -165,3 +165,21 @@ export const supportsAutoIncrementDataType = (
'decimal', 'decimal',
].includes(dataTypeName.toLocaleLowerCase()); ].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 '[]') // Handle array types (check if the field has array property or type name ends with '[]')
if (typeName.endsWith('[]')) { if (field.array || typeName.endsWith('[]')) {
typeWithSize = if (!typeName.endsWith('[]')) {
typeWithSize.replace('[]', '') + '[]'; typeWithSize = typeWithSize + '[]';
} else {
typeWithSize =
typeWithSize.replace('[]', '') + '[]';
}
} }
const notNull = field.nullable ? '' : ' NOT NULL'; const notNull = field.nullable ? '' : ' NOT NULL';

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ export interface DBField {
unique: boolean; unique: boolean;
nullable: boolean; nullable: boolean;
increment?: boolean | null; increment?: boolean | null;
array?: boolean | null;
createdAt: number; createdAt: number;
characterMaximumLength?: string | null; characterMaximumLength?: string | null;
precision?: number | null; precision?: number | null;
@@ -36,6 +37,7 @@ export const dbFieldSchema: z.ZodType<DBField> = z.object({
unique: z.boolean(), unique: z.boolean(),
nullable: z.boolean(), nullable: z.boolean(),
increment: z.boolean().or(z.null()).optional(), increment: z.boolean().or(z.null()).optional(),
array: z.boolean().or(z.null()).optional(),
createdAt: z.number(), createdAt: z.number(),
characterMaximumLength: z.string().or(z.null()).optional(), characterMaximumLength: z.string().or(z.null()).optional(),
precision: z.number().or(z.null()).optional(), precision: z.number().or(z.null()).optional(),
@@ -71,13 +73,48 @@ export const createFieldsFromMetadata = ({
pk.column.trim() pk.column.trim()
); );
return sortedColumns.map( return sortedColumns.map((col: ColumnInfo): DBField => {
(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(), id: generateId(),
name: col.name, name: col.name,
type: { type: {
id: col.type.split(' ').join('_').toLowerCase(), id: baseType.split(' ').join('_').toLowerCase(),
name: col.type.toLowerCase(), name: baseType.toLowerCase(),
}, },
primaryKey: tablePrimaryKeysColumns.includes(col.name), primaryKey: tablePrimaryKeysColumns.includes(col.name),
unique: Object.values(aggregatedIndexes).some( unique: Object.values(aggregatedIndexes).some(
@@ -87,20 +124,18 @@ export const createFieldsFromMetadata = ({
idx.columns[0].name === col.name idx.columns[0].name === col.name
), ),
nullable: Boolean(col.nullable), nullable: Boolean(col.nullable),
...(col.character_maximum_length && ...(isArrayType ? { array: true } : {}),
col.character_maximum_length !== 'null' ...(charMaxLength && charMaxLength !== 'null'
? { characterMaximumLength: col.character_maximum_length } ? { characterMaximumLength: charMaxLength }
: {}), : {}),
...(col.precision?.precision ...(precision ? { precision } : {}),
? { precision: col.precision.precision } ...(scale ? { scale } : {}),
: {}),
...(col.precision?.scale ? { scale: col.precision.scale } : {}),
...(col.default ? { default: col.default } : {}), ...(col.default ? { default: col.default } : {}),
...(col.collation ? { collation: col.collation } : {}), ...(col.collation ? { collation: col.collation } : {}),
createdAt: Date.now(), createdAt: Date.now(),
comments: col.comment ? col.comment : undefined, comments: col.comment ? col.comment : undefined,
}) };
); });
}; };
export const generateDBFieldSuffix = ( export const generateDBFieldSuffix = (

View File

@@ -8,6 +8,7 @@ import type { FieldAttributeRange } from '@/lib/data/data-types/data-types';
import { import {
findDataTypeDataById, findDataTypeDataById,
supportsAutoIncrementDataType, supportsAutoIncrementDataType,
supportsArrayDataType,
} from '@/lib/data/data-types/data-types'; } from '@/lib/data/data-types/data-types';
import { import {
Popover, Popover,
@@ -87,6 +88,7 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
unique: localField.unique, unique: localField.unique,
default: localField.default, default: localField.default,
increment: localField.increment, increment: localField.increment,
array: localField.array,
}); });
} }
prevFieldRef.current = localField; prevFieldRef.current = localField;
@@ -102,6 +104,13 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
[field.type.name] [field.type.name]
); );
const supportsArray = useMemo(
() =>
databaseType === 'postgresql' &&
supportsArrayDataType(field.type.name),
[field.type.name, databaseType]
);
return ( return (
<Popover <Popover
open={isOpen} open={isOpen}
@@ -168,6 +177,27 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
/> />
</div> </div>
) : null} ) : 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"> <div className="flex flex-col gap-2">
<Label htmlFor="default" className="text-subtitle"> <Label htmlFor="default" className="text-subtitle">
{t( {t(

View File

@@ -7,6 +7,7 @@ import type { DataTypeData } from '@/lib/data/data-types/data-types';
import { import {
dataTypeDataToDataType, dataTypeDataToDataType,
sortedDataTypeMap, sortedDataTypeMap,
supportsArrayDataType,
} from '@/lib/data/data-types/data-types'; } from '@/lib/data/data-types/data-types';
import { import {
Tooltip, 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({ updateField({
characterMaximumLength, characterMaximumLength,
precision, precision,
@@ -185,6 +193,7 @@ export const TableField: React.FC<TableFieldProps> = ({
name: value as string, name: value as string,
} }
), ),
...(shouldClearArray ? { array: false } : {}),
}); });
}, },
[ [
@@ -193,6 +202,7 @@ export const TableField: React.FC<TableFieldProps> = ({
field.characterMaximumLength, field.characterMaximumLength,
field.precision, field.precision,
field.scale, field.scale,
field.array,
] ]
); );